import { Address, hashTypedData, Hex, isAddressEqual, toHex } from 'viem'; import { signByPasskey } from '../core/web-authn'; import { encodeSignatureBytes } from '@prex0/prex-structs'; import { SharedWalletList } from '../types'; import { querySharedWallets } from '../graph/wallets'; import { ProviderEventEmitter, ProviderInterface, RequestArguments, } from './base-provider'; import { PrexApiService } from '../api'; /** * PasskeyProvider is a provider that uses passkey to sign messages. * It implements the EIP1193Provider interface. */ export class PasskeyProvider extends ProviderEventEmitter implements ProviderInterface { private sharedWallets?: SharedWalletList; constructor( private readonly apiService: PrexApiService, private readonly myAddress: Address, private readonly index: number, private readonly passkeyIds?: string[], private readonly fallbackProvider?: string ) { super(); } async request(request: RequestArguments): Promise { const { method, params } = request; switch (method) { case 'eth_requestAccounts': return this.requestAccounts(); case 'eth_accounts': return [this.myAddress]; case 'eth_chainId': return toHex(this.apiService.chainId); case 'eth_sign': if (Array.isArray(params)) { return this.sign(params[0], params[1]); } else { throw new Error('invalid params'); } case 'eth_signTypedData_v4': if (Array.isArray(params)) { return this.signTypedData( params[0], typeof params[1] === 'string' ? JSON.parse(params[1]) : params[1] ); } else { throw new Error('invalid params'); } default: return this.handleFallback(request); } } async disconnect() { this.emit('disconnect'); } public async requestAccounts() { return [this.myAddress]; } /** * Sign a message with the passkey * @param address - The address to sign the message for * @param hash - The hash of the message to sign * @returns The signature of the message */ private async sign(address: Address, hash: Hex): Promise { if (isAddressEqual(this.myAddress, address)) { const { signature } = await signByPasskey(hash, this.passkeyIds); return this.encodeSignature(this.index, signature); } // shared wallet const { signature } = await signByPasskey( createReplaySafeHash(this.apiService.chainId, hash, this.myAddress), this.passkeyIds ); // If the address is not the owner's address, find the shared wallet const sharedWallets = await this.fetchSharedWallets(); const sharedWallet = sharedWallets.sharedWallets.find((wallet) => isAddressEqual(wallet.address, address) ); if (!sharedWallet) { // throw new Error('shared wallet not found'); // shared wallet creation case // SharedWalletの作成時は、作成者がindex=0である必要がある return this.encodeSignature( 0, this.encodeSignature(this.index, signature) ); } return this.encodeSignature( sharedWallet.index, this.encodeSignature(this.index, signature) ); } private encodeSignature(index: number, signature: Hex): Hex { return encodeSignatureBytes(BigInt(index), signature); } private async signTypedData(address: Address, data: any): Promise { const hash = hashTypedData(data); const messageHash = createReplaySafeHash( this.apiService.chainId, hash, address ); return this.sign(address, messageHash); } // fallback to other provider if fallbackProvider is set private async handleFallback(request: RequestArguments): Promise { if (this.fallbackProvider) { return fetchRPCRequest(request, this.fallbackProvider); } throw new Error('unknown method'); } private async fetchSharedWallets() { if (this.sharedWallets) { return this.sharedWallets; } const sharedWallets = await querySharedWallets( this.apiService, this.myAddress ); this.sharedWallets = sharedWallets; return sharedWallets; } } async function fetchRPCRequest( request: RequestArguments, rpcUrl: string ): Promise { const requestBody = { ...request, jsonrpc: '2.0', id: crypto.randomUUID(), }; const response = await fetch(rpcUrl, { method: 'POST', body: JSON.stringify(requestBody), mode: 'cors', headers: { 'Content-Type': 'application/json' }, }); const { result, error } = await response.json(); if (error) throw error; return result; } export function createReplaySafeHash( chainId: number, hash: Hex, address: Address ) { return hashTypedData({ domain: { name: 'Prex Smart Wallet', version: '1', chainId: chainId, verifyingContract: address as Address, }, types: { PrexWalletMessage: [{ name: 'hash', type: 'bytes32' }], }, message: { hash, }, primaryType: 'PrexWalletMessage', }); }