diff --git a/package-lock.json b/package-lock.json index 42346f6..1dba0ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,6 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "vaul": "^0.9.3", - "viem": "^2.33.2", "wagmi": "^2.16.1", "zod": "^3.23.8" }, diff --git a/package.json b/package.json index 80e4193..484ae14 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "vaul": "^0.9.3", - "viem": "^2.33.2", "wagmi": "^2.16.1", "zod": "^3.23.8" }, diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d8da40d..752c98e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -15,7 +15,7 @@ const Header = () => { currentUser, isAuthenticated, verificationStatus, - verifyOrdinal, + verifyOwnership, delegateKey, isDelegationValid, delegationTimeRemaining, @@ -49,7 +49,7 @@ const Header = () => { }; const handleVerify = async () => { - await verifyOrdinal(); + await verifyOwnership(); }; const handleDelegateKey = async () => { @@ -246,11 +246,11 @@ const Header = () => { - {address?.slice(0, 5)}...{address?.slice(-4)} + {currentUser?.ensName || `${address?.slice(0, 5)}...${address?.slice(-4)}`} -

{address}

+

{currentUser?.ensName ? `${currentUser.ensName} (${address})` : address}

diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 00a1015..d074d4c 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -12,7 +12,7 @@ interface AuthContextType { isAuthenticated: boolean; isAuthenticating: boolean; verificationStatus: VerificationStatus; - verifyOrdinal: () => Promise; + verifyOwnership: () => Promise; delegateKey: () => Promise; isDelegationValid: () => boolean; delegationTimeRemaining: () => number; @@ -49,6 +49,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Create ref for AuthService so it persists between renders const authServiceRef = useRef(new AuthService()); + // Set AppKit accounts in AuthService + useEffect(() => { + authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount); + }, [bitcoinAccount, ethereumAccount]); + // Sync with AppKit wallet state useEffect(() => { if (isConnected && address) { @@ -58,33 +63,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (storedUser && storedUser.address === address) { // Use stored user data setCurrentUser(storedUser); - if ('ordinalOwnership' in storedUser) { - setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none'); - } else { - setVerificationStatus('unverified'); - } + setVerificationStatus(getVerificationStatus(storedUser)); } else { // Create new user from AppKit wallet const newUser: User = { address, walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', - ordinalOwnership: false, - delegationExpiry: null, verificationStatus: 'unverified', + lastChecked: Date.now(), }; + setCurrentUser(newUser); setVerificationStatus('unverified'); authServiceRef.current.saveUser(newUser); const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum'; - toast({ - title: "Wallet Connected", - description: `Connected to ${chainName} with address ${address.slice(0, 6)}...${address.slice(-4)}`, - }); + const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`; + toast({ + title: "Wallet Connected", + description: `Connected to ${chainName} with ${displayName}`, + }); + + const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership'; toast({ title: "Action Required", - description: "Please verify your Ordinal ownership and delegate a signing key for better UX.", + description: `Please verify your ${verificationType} and delegate a signing key for better UX.`, }); } } else { @@ -92,9 +96,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setCurrentUser(null); setVerificationStatus('unverified'); } - }, [isConnected, address, isBitcoinConnected, toast]); + }, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]); - const verifyOrdinal = async (): Promise => { + const getVerificationStatus = (user: User): VerificationStatus => { + if (user.walletType === 'bitcoin') { + return user.ordinalOwnership ? 'verified-owner' : 'verified-none'; + } else if (user.walletType === 'ethereum') { + return user.ensOwnership ? 'verified-owner' : 'verified-none'; + } + return 'unverified'; + }; + + const verifyOwnership = async (): Promise => { if (!currentUser || !currentUser.address) { toast({ title: "Not Connected", @@ -108,12 +121,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setVerificationStatus('verifying'); try { + const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS'; toast({ - title: "Verifying Ordinal", - description: "Checking your wallet for Ordinal Operators..." + title: `Verifying ${verificationType}`, + description: `Checking your wallet for ${verificationType} ownership...` }); - const result: AuthResult = await authServiceRef.current.verifyOrdinal(currentUser); + const result: AuthResult = await authServiceRef.current.verifyOwnership(currentUser); if (!result.success) { throw new Error(result.error); @@ -124,27 +138,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { authServiceRef.current.saveUser(updatedUser); // Update verification status - setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none'); + setVerificationStatus(getVerificationStatus(updatedUser)); - if (updatedUser.ordinalOwnership) { + if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) { toast({ title: "Ordinal Verified", description: "You now have full access. We recommend delegating a key for better UX.", }); + } else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) { + toast({ + title: "ENS Verified", + description: "You now have full access. We recommend delegating a key for better UX.", + }); } else { + const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain'; toast({ title: "Read-Only Access", - description: "No Ordinal Operators found. You have read-only access.", + description: `No ${verificationType} found. You have read-only access.`, variant: "default", }); } - return Boolean(updatedUser.ordinalOwnership); + return Boolean( + (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) || + (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) + ); } catch (error) { - console.error("Error verifying Ordinal:", error); + console.error("Error verifying ownership:", error); setVerificationStatus('unverified'); - let errorMessage = "Failed to verify Ordinal ownership. Please try again."; + let errorMessage = "Failed to verify ownership. Please try again."; if (error instanceof Error) { errorMessage = error.message; } @@ -203,24 +226,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { console.error("Error delegating key:", error); let errorMessage = "Failed to delegate key. Please try again."; - if (error instanceof Error) { - // Provide specific guidance based on error type - if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) { - errorMessage = "You declined the signature request. Key delegation is optional but improves your experience."; - } else if (error.message.includes("timeout")) { - errorMessage = "Wallet request timed out. Please try again and approve the signature promptly."; - } else if (error.message.includes("Failed to connect wallet")) { - errorMessage = "Unable to connect to wallet. Please ensure it's installed and unlocked, then try again."; - } else if (error.message.includes("Wallet is not connected")) { - errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again."; - } else { - errorMessage = error.message; - } + errorMessage = error.message; } toast({ - title: "Delegation Failed", + title: "Delegation Error", description: errorMessage, variant: "destructive", }); @@ -230,37 +241,43 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsAuthenticating(false); } }; - + const isDelegationValid = (): boolean => { return authServiceRef.current.isDelegationValid(); }; - + const delegationTimeRemaining = (): number => { return authServiceRef.current.getDelegationTimeRemaining(); }; const isWalletAvailable = (): boolean => { - return isConnected && !!address; + return isConnected; + }; + + const messageSigning = { + signMessage: async (message: OpchanMessage): Promise => { + return authServiceRef.current.signMessage(message); + }, + verifyMessage: (message: OpchanMessage): boolean => { + return authServiceRef.current.verifyMessage(message); + } + }; + + const value: AuthContextType = { + currentUser, + isAuthenticated: Boolean(currentUser && isConnected), + isAuthenticating, + verificationStatus, + verifyOwnership, + delegateKey, + isDelegationValid, + delegationTimeRemaining, + isWalletAvailable, + messageSigning }; return ( - authServiceRef.current.signMessage(message), - verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message), - }, - }} - > + {children} ); diff --git a/src/lib/identity/services/AuthService.ts b/src/lib/identity/services/AuthService.ts index e494ba8..040ab21 100644 --- a/src/lib/identity/services/AuthService.ts +++ b/src/lib/identity/services/AuthService.ts @@ -1,5 +1,5 @@ import { User } from '@/types'; -import { WalletService } from '../wallets'; +import { WalletService, AppKitAccount } from '../wallets/index'; import { OrdinalAPI } from '../ordinal'; import { MessageSigning } from '../signatures/message-signing'; import { OpchanMessage } from '@/types'; @@ -18,7 +18,14 @@ export class AuthService { constructor() { this.walletService = new WalletService(); this.ordinalApi = new OrdinalAPI(); - this.messageSigning = new MessageSigning(this.walletService['keyDelegation']); + this.messageSigning = new MessageSigning(this.walletService.getKeyDelegation()); + } + + /** + * Set AppKit accounts for wallet service + */ + setAccounts(bitcoinAccount: AppKitAccount, ethereumAccount: AppKitAccount) { + this.walletService.setAccounts(bitcoinAccount, ethereumAccount); } /** @@ -26,20 +33,27 @@ export class AuthService { */ async connectWallet(): Promise { try { - if (!this.walletService.isWalletAvailable('phantom')) { + const walletInfo = await this.walletService.getWalletInfo(); + if (!walletInfo) { return { success: false, - error: 'Phantom wallet not installed' + error: 'No wallet connected' }; } - const address = await this.walletService.connectWallet('phantom'); - const user: User = { - address, + address: walletInfo.address, + walletType: walletInfo.walletType, + verificationStatus: 'unverified', lastChecked: Date.now(), }; + // Add ENS info for Ethereum wallets + if (walletInfo.walletType === 'ethereum' && walletInfo.ensName) { + user.ensName = walletInfo.ensName; + user.ensOwnership = true; + } + return { success: true, user @@ -56,53 +70,93 @@ export class AuthService { * Disconnect wallet and clear user data */ async disconnectWallet(): Promise { - await this.walletService.disconnectWallet('phantom'); + const walletType = this.walletService.getActiveWalletType(); + if (walletType) { + await this.walletService.disconnectWallet(walletType); + } } /** - * Verify ordinal ownership for a user + * Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users */ - async verifyOrdinal(user: User): Promise { + async verifyOwnership(user: User): Promise { try { - // TODO: revert when the API is ready - // const response = await this.ordinalApi.getOperatorDetails(user.address); - // const hasOperators = response.has_operators; - const hasOperators = true; - - const updatedUser = { - ...user, - ordinalOwnership: hasOperators, - lastChecked: Date.now(), - }; - - return { - success: true, - user: updatedUser - }; + if (user.walletType === 'bitcoin') { + return await this.verifyBitcoinOrdinal(user); + } else if (user.walletType === 'ethereum') { + return await this.verifyEthereumENS(user); + } else { + return { + success: false, + error: 'Unknown wallet type' + }; + } } catch (error) { return { success: false, - error: error instanceof Error ? error.message : 'Failed to verify ordinal' + error: error instanceof Error ? error.message : 'Failed to verify ownership' }; } } + /** + * Verify Bitcoin Ordinal ownership + */ + private async verifyBitcoinOrdinal(user: User): Promise { + // TODO: revert when the API is ready + // const response = await this.ordinalApi.getOperatorDetails(user.address); + // const hasOperators = response.has_operators; + const hasOperators = true; + + const updatedUser = { + ...user, + ordinalOwnership: hasOperators, + lastChecked: Date.now(), + }; + + return { + success: true, + user: updatedUser + }; + } + + /** + * Verify Ethereum ENS ownership + */ + private async verifyEthereumENS(user: User): Promise { + const walletInfo = await this.walletService.getWalletInfo(); + const hasENS = walletInfo?.ensName && walletInfo.ensName.length > 0; + + const updatedUser = { + ...user, + ensOwnership: hasENS, + ensName: walletInfo?.ensName, + lastChecked: Date.now(), + }; + + return { + success: true, + user: updatedUser + }; + } + /** * Set up key delegation for the user */ async delegateKey(user: User): Promise { try { - const canConnect = await this.walletService.canConnectWallet('phantom'); + const walletType = user.walletType; + const canConnect = await this.walletService.canConnectWallet(walletType); if (!canConnect) { return { success: false, - error: 'Phantom wallet is not available or cannot be connected. Please ensure it is installed and unlocked.' + error: `${walletType} wallet is not available or cannot be connected. Please ensure it is installed and unlocked.` }; } const delegationInfo = await this.walletService.setupKeyDelegation( user.address, - 'phantom' + walletType ); const updatedUser = { @@ -155,8 +209,8 @@ export class AuthService { /** * Get current wallet info */ - getWalletInfo() { - return this.walletService.getWalletInfo(); + async getWalletInfo() { + return await this.walletService.getWalletInfo(); } /** diff --git a/src/lib/identity/wallets/ReOwnWalletService.ts b/src/lib/identity/wallets/ReOwnWalletService.ts new file mode 100644 index 0000000..1f00d57 --- /dev/null +++ b/src/lib/identity/wallets/ReOwnWalletService.ts @@ -0,0 +1,225 @@ +import { KeyDelegation } from '../signatures/key-delegation'; +import { bytesToHex } from '@/lib/utils'; +import { getEnsName } from '@wagmi/core'; +import { config } from './appkit'; +import { UseAppKitAccountReturn } from '@reown/appkit'; + + + +export interface WalletInfo { + address: string; + walletType: 'bitcoin' | 'ethereum'; + ensName?: string; + isConnected: boolean; +} + +export interface DelegationInfo { + browserPublicKey: string; + signature: string; + expiryTimestamp: number; +} + +export class ReOwnWalletService { + private keyDelegation: KeyDelegation; + private bitcoinAccount?: UseAppKitAccountReturn; + private ethereumAccount?: UseAppKitAccountReturn; + + constructor() { + this.keyDelegation = new KeyDelegation(); + } + + /** + * Set account references from AppKit hooks + */ + setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn) { + this.bitcoinAccount = bitcoinAccount; + this.ethereumAccount = ethereumAccount; + } + + /** + * Check if a wallet type is available and connected + */ + isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean { + if (walletType === 'bitcoin') { + return this.bitcoinAccount?.isConnected || false; + } else { + return this.ethereumAccount?.isConnected || false; + } + } + + /** + * Check if wallet can be connected + */ + async canConnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise { + // For ReOwn, we assume connection is always possible if AppKit is initialized + return true; + } + + /** + * Get wallet connection info with ENS resolution for Ethereum + */ + async getWalletInfo(): Promise { + if (this.bitcoinAccount?.isConnected) { + return { + address: this.bitcoinAccount.address, + walletType: 'bitcoin', + isConnected: true + }; + } else if (this.ethereumAccount?.isConnected) { + // Use Wagmi to resolve ENS name + let ensName: string | undefined; + try { + const resolvedName = await getEnsName(config, { + address: this.ethereumAccount.address as `0x${string}` + }); + ensName = resolvedName || undefined; + } catch (error) { + console.warn('Failed to resolve ENS name:', error); + // Continue without ENS name + } + + return { + address: this.ethereumAccount.address, + walletType: 'ethereum', + ensName, + isConnected: true + }; + } + return null; + } + + /** + * Get the active wallet address + */ + getActiveAddress(): string | null { + if (this.bitcoinAccount?.isConnected) { + return this.bitcoinAccount.address; + } else if (this.ethereumAccount?.isConnected) { + return this.ethereumAccount.address; + } + return null; + } + + /** + * Get the active wallet type + */ + getActiveWalletType(): 'bitcoin' | 'ethereum' | null { + if (this.bitcoinAccount?.isConnected) { + return 'bitcoin'; + } else if (this.ethereumAccount?.isConnected) { + return 'ethereum'; + } + return null; + } + + /** + * Setup key delegation for the connected wallet + */ + async setupKeyDelegation( + address: string, + walletType: 'bitcoin' | 'ethereum' + ): Promise { + // Generate browser keypair + const keypair = this.keyDelegation.generateKeypair(); + + // Create delegation message with chain-specific format + const expiryTimestamp = Date.now() + (24 * 60 * 60 * 1000); // 24 hours + const delegationMessage = this.createDelegationMessage( + keypair.publicKey, + address, + walletType, + expiryTimestamp + ); + + // Get the appropriate account for signing + const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; + if (!account?.isConnected) { + throw new Error(`${walletType} wallet is not connected`); + } + + // Sign the delegation message + const signature = await this.signMessage(delegationMessage, walletType); + + // Create and store delegation + const delegationInfo = this.keyDelegation.createDelegation( + address, + signature, + keypair.publicKey, + keypair.privateKey, + 24 + ); + + this.keyDelegation.storeDelegation(delegationInfo); + + return { + browserPublicKey: keypair.publicKey, + signature, + expiryTimestamp + }; + } + + /** + * Create chain-specific delegation message + */ + private createDelegationMessage( + browserPublicKey: string, + address: string, + walletType: 'bitcoin' | 'ethereum', + expiryTimestamp: number + ): string { + const chainName = walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum'; + const expiryDate = new Date(expiryTimestamp).toISOString(); + + return `I, ${address} (${chainName}), delegate authority to this pubkey: ${browserPublicKey} until ${expiryDate}`; + } + + /** + * Sign a message with the appropriate wallet + */ + private async signMessage(message: string, walletType: 'bitcoin' | 'ethereum'): Promise { + const account = walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; + if (!account?.isConnected) { + throw new Error(`${walletType} wallet is not connected`); + } + + // Convert message to bytes for signing + const messageBytes = new TextEncoder().encode(message); + + // Sign with the appropriate wallet + const signature = await account.signMessage({ message: messageBytes }); + + // Return hex-encoded signature + return bytesToHex(signature); + } + + /** + * Disconnect wallet (handled by AppKit) + */ + async disconnectWallet(walletType: 'bitcoin' | 'ethereum'): Promise { + // Clear stored delegation + this.keyDelegation.clearDelegation(); + + // Note: Actual disconnection is handled by AppKit's useDisconnect hook + } + + /** + * Check if delegation is valid + */ + isDelegationValid(): boolean { + return this.keyDelegation.isDelegationValid(); + } + + /** + * Get delegation time remaining + */ + getDelegationTimeRemaining(): number { + return this.keyDelegation.getDelegationTimeRemaining(); + } + + /** + * Get the key delegation instance + */ + getKeyDelegation(): KeyDelegation { + return this.keyDelegation; + } +} \ No newline at end of file diff --git a/src/lib/identity/wallets/index.ts b/src/lib/identity/wallets/index.ts new file mode 100644 index 0000000..3b5b7c3 --- /dev/null +++ b/src/lib/identity/wallets/index.ts @@ -0,0 +1,2 @@ +export { ReOwnWalletService as WalletService } from './ReOwnWalletService'; +export type { WalletInfo, DelegationInfo, AppKitAccount } from './ReOwnWalletService'; \ No newline at end of file diff --git a/src/lib/identity/wallets/types.ts b/src/lib/identity/wallets/types.ts deleted file mode 100644 index 3060d29..0000000 --- a/src/lib/identity/wallets/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum WalletConnectionStatus { - Connected = 'connected', - Disconnected = 'disconnected', - NotDetected = 'not-detected', - Connecting = 'connecting' - } - -export interface BtcAccount { - address: string; - addressType: "p2tr" | "p2wpkh" | "p2sh" | "p2pkh"; - publicKey: string; - purpose: "payment" | "ordinals"; - } - - export interface PhantomBitcoinProvider { - isPhantom?: boolean; - signMessage?: (address: string, message: Uint8Array) => Promise<{ signature: Uint8Array }>; - connect?: () => Promise<{ publicKey: string }>; - disconnect?: () => Promise; - on?: (event: string, callback: (arg: unknown) => void) => void; - off?: (event: string, callback: (arg: unknown) => void) => void; - publicKey?: string; - requestAccounts?: () => Promise; - } - - export interface PhantomWallet { - bitcoin?: PhantomBitcoinProvider; - } - - declare global { - interface Window { - phantom?: PhantomWallet; - } - } \ No newline at end of file