UNPKG

@lukasrosario/account

Version:

Base Account SDK

389 lines (353 loc) 10.6 kB
import type { Address, TypedData } from 'abitype'; import * as Signature from 'ox/Signature'; import type * as WebAuthnP256 from 'ox/WebAuthnP256'; import { Assign, BaseError, Chain, type Client, type Hash, type Hex, JsonRpcAccount, LocalAccount, type Prettify, Transport, type TypedDataDefinition, decodeFunctionData, encodeAbiParameters, encodeFunctionData, encodePacked, hashMessage, hashTypedData, parseSignature, size, stringToHex, } from 'viem'; import { type SmartAccount, type SmartAccountImplementation, type UserOperation, entryPoint06Abi, entryPoint06Address, getUserOperationHash, toSmartAccount, } from 'viem/account-abstraction'; import { OwnerAccount } from ':core/type/index.js'; import { abi, factoryAbi, factoryAddress } from './constants.js'; export type CreateSmartAccountParameters = { address: Address; client: Client<Transport, Chain | undefined, JsonRpcAccount | LocalAccount | undefined>; factoryData: Hex | undefined; ownerIndex: number; owner: OwnerAccount; }; export type CreateSmartAccountReturnType = Prettify< SmartAccount<CoinbaseSmartAccountImplementation> >; export type CoinbaseSmartAccountImplementation = Assign< SmartAccountImplementation< typeof entryPoint06Abi, '0.6', { abi: typeof abi; factory: { abi: typeof factoryAbi; address: Address } } >, { decodeCalls: NonNullable<SmartAccountImplementation['decodeCalls']>; sign: NonNullable<SmartAccountImplementation['sign']>; } >; /** * @description Create a Coinbase Smart Account. * * @param parameters - {@link CreateSmartAccountParameters} * @returns Coinbase Smart Account. {@link CreateSmartAccountReturnType} * * @example * * const account = createSmartAccount({ * client, * owner: privateKeyToAccount('0x...'), * ownerIndex: 0, * address: '0x...', * factoryData: '0x...', * }) */ export async function createSmartAccount( parameters: CreateSmartAccountParameters ): Promise<CreateSmartAccountReturnType> { const { owner, ownerIndex, address, client, factoryData } = parameters; const entryPoint = { abi: entryPoint06Abi, address: entryPoint06Address, version: '0.6', } as const; const factory = { abi: factoryAbi, address: factoryAddress, } as const; return toSmartAccount({ client, entryPoint, extend: { abi, factory }, async decodeCalls(data) { const result = decodeFunctionData({ abi, data, }); if (result.functionName === 'execute') return [{ to: result.args[0], value: result.args[1], data: result.args[2] }]; if (result.functionName === 'executeBatch') return result.args[0].map((arg) => ({ to: arg.target, value: arg.value, data: arg.data, })); throw new BaseError(`unable to decode calls for "${result.functionName}"`); }, async encodeCalls(calls) { if (calls.length === 1) { return encodeFunctionData({ abi, functionName: 'execute', args: [calls[0].to, calls[0].value ?? BigInt(0), calls[0].data ?? '0x'], }); } return encodeFunctionData({ abi, functionName: 'executeBatch', args: [ calls.map((call) => ({ data: call.data ?? '0x', target: call.to, value: call.value ?? BigInt(0), })), ], }); }, async getAddress() { return address; }, async getFactoryArgs() { if (factoryData) return { factory: factory.address, factoryData }; // TODO: support creating factory data return { factory: factory.address, factoryData }; }, async getStubSignature() { if (owner.type === 'webAuthn') return '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000001949fc7c88032b9fcb5f6efc7a7b8c63668eae9871b765e23123bb473ff57aa831a7c0d9276168ebcc29f2875a0239cffdf2a9cd1c2007c5c77c071db9264df1d000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2273496a396e6164474850596759334b7156384f7a4a666c726275504b474f716d59576f4d57516869467773222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000'; return wrapSignature({ ownerIndex, signature: '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c', }); }, async sign(parameters) { const address = await this.getAddress(); const hash = toReplaySafeHash({ address, chainId: client.chain!.id, hash: parameters.hash, }); const signature = await sign({ hash, owner }); return wrapSignature({ ownerIndex, signature, }); }, async signMessage(parameters) { const { message } = parameters; const address = await this.getAddress(); const hash = toReplaySafeHash({ address, chainId: client.chain!.id, hash: hashMessage(message), }); const signature = await sign({ hash, owner }); return wrapSignature({ ownerIndex, signature, }); }, async signTypedData(parameters) { const { domain, types, primaryType, message } = parameters as TypedDataDefinition< TypedData, string >; const address = await this.getAddress(); const hash = toReplaySafeHash({ address, chainId: client.chain!.id, hash: hashTypedData({ domain, message, primaryType, types, }), }); const signature = await sign({ hash, owner }); return wrapSignature({ ownerIndex, signature, }); }, async signUserOperation(parameters) { const { chainId = client.chain!.id, ...userOperation } = parameters; const address = await this.getAddress(); const hash = getUserOperationHash({ chainId, entryPointAddress: entryPoint.address, entryPointVersion: entryPoint.version, userOperation: { ...(userOperation as unknown as UserOperation), sender: address, }, }); const signature = await sign({ hash, owner }); return wrapSignature({ ownerIndex, signature, }); }, userOperation: { async estimateGas(userOperation) { if (owner.type !== 'webAuthn') return; // Accounts with WebAuthn owner require a minimum verification gas limit of 800,000. return { verificationGasLimit: BigInt( Math.max(Number(userOperation.verificationGasLimit ?? BigInt(0)), 800_000) ), }; }, }, }); } ///////////////////////////////////////////////////////////////////////////////////////////// // Utilities ///////////////////////////////////////////////////////////////////////////////////////////// /** @internal */ export async function sign({ hash, owner, }: { hash: Hash; owner: OwnerAccount; }) { // WebAuthn Account (Passkey) if (owner.type === 'webAuthn') { const { signature, webauthn } = await owner.sign({ hash, }); return toWebAuthnSignature({ signature, webauthn }); } if (owner.sign) return owner.sign({ hash }); throw new BaseError('`owner` does not support raw sign.'); } /** @internal */ export function toReplaySafeHash({ address, chainId, hash, }: { address: Address; chainId: number; hash: Hash; }) { return hashTypedData({ domain: { chainId, name: 'Coinbase Smart Wallet', verifyingContract: address, version: '1', }, types: { CoinbaseSmartWalletMessage: [ { name: 'hash', type: 'bytes32', }, ], }, primaryType: 'CoinbaseSmartWalletMessage', message: { hash, }, }); } /** @internal */ export function toWebAuthnSignature({ webauthn, signature, }: { webauthn: WebAuthnP256.SignMetadata; signature: Hex; }) { const { r, s } = Signature.fromHex(signature); return encodeAbiParameters( [ { components: [ { name: 'authenticatorData', type: 'bytes', }, { name: 'clientDataJSON', type: 'bytes' }, { name: 'challengeIndex', type: 'uint256' }, { name: 'typeIndex', type: 'uint256' }, { name: 'r', type: 'uint256', }, { name: 's', type: 'uint256', }, ], type: 'tuple', }, ], [ { authenticatorData: webauthn.authenticatorData, clientDataJSON: stringToHex(webauthn.clientDataJSON), challengeIndex: BigInt(webauthn.challengeIndex), typeIndex: BigInt(webauthn.typeIndex), r, s, }, ] ); } /** @internal */ export function wrapSignature(parameters: { ownerIndex?: number | undefined; signature: Hex }) { const { ownerIndex = 0 } = parameters; const signatureData = (() => { if (size(parameters.signature) !== 65) return parameters.signature; const signature = parseSignature(parameters.signature); return encodePacked( ['bytes32', 'bytes32', 'uint8'], [signature.r, signature.s, signature.yParity === 0 ? 27 : 28] ); })(); return encodeAbiParameters( [ { components: [ { name: 'ownerIndex', type: 'uint8', }, { name: 'signatureData', type: 'bytes', }, ], type: 'tuple', }, ], [ { ownerIndex, signatureData, }, ] ); }