diff --git a/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.test.ts b/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.test.ts index e84bef01..41e47884 100644 --- a/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.test.ts +++ b/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.test.ts @@ -6,7 +6,7 @@ import { baseSepolia } from 'viem/chains'; import { encodeFunctionData, keccak256 } from 'viem/utils'; import { beforeEach, describe, expect, vi } from 'vitest'; -import { createSmartAccount, sign, wrapSignature } from './createSmartAccount.js'; +import { createFactoryData, createSmartAccount, sign, wrapSignature } from './createSmartAccount.js'; const privateKey = '0x8d0ec8aa1f67f8c11db3c191d3d66408e148759acd617fa22ab5d5d677a234e9'; const signer = privateKeyToAccount(privateKey); @@ -505,3 +505,88 @@ describe('wrapSignature', () => { ); }); }); + +describe('createFactoryData', () => { + it('should create factory data for local account', () => { + const factoryData = createFactoryData(signer); + + // Verify that the factory data is properly encoded + expect(factoryData).toMatch(/^0x/); + expect(factoryData.length).toBeGreaterThan(10); // Should be a reasonable length + + // The factory data should start with the function selector for createAccount + // which is 0x3ffba36f (first 4 bytes of keccak256("createAccount(bytes[],uint256)")) + expect(factoryData.startsWith('0x3ffba36f')).toBe(true); + }); + + it('should create factory data for WebAuthn account', () => { + const webauthnOwner = toWebAuthnAccount({ + credential: { + id: 'test-id', + publicKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + }, + }); + + const factoryData = createFactoryData(webauthnOwner); + + // Verify that the factory data is properly encoded + expect(factoryData).toMatch(/^0x/); + expect(factoryData.length).toBeGreaterThan(10); + expect(factoryData.startsWith('0x3ffba36f')).toBe(true); + }); + + it('should throw error for WebAuthn account with invalid public key', () => { + const webauthnOwner = toWebAuthnAccount({ + credential: { + id: 'test-id', + publicKey: '0xinvalid' // Too short + }, + }); + + expect(() => createFactoryData(webauthnOwner)).toThrow('WebAuthn owner must have a valid 64-byte public key'); + }); + + it('should throw error for unsupported owner type', () => { + const unsupportedOwner = { + type: 'unsupported', + address: '0x1234567890123456789012345678901234567890', + } as any; + + expect(() => createFactoryData(unsupportedOwner)).toThrow('Unsupported owner type: unsupported'); + }); +}); + +describe('getFactoryArgs with generated factory data', () => { + it('should generate factory data when none is provided', async () => { + const account = await createSmartAccount({ + client, + owner: signer, + ownerIndex: 0, + address: '0xBb0c1d5E7f530e8e648150fc7Cf30912575523E8', + factoryData: undefined, // No factory data provided + }); + + const factoryArgs = await account.getFactoryArgsWithGeneration(); + expect(factoryArgs.factory).toBe('0xba5ed110efdba3d005bfc882d75358acbbb85842'); + expect(factoryArgs.factoryData).toBeDefined(); + expect(factoryArgs.factoryData).toMatch(/^0x/); + expect(factoryArgs.factoryData!.startsWith('0x3ffba36f')).toBe(true); + }); + + it('should use provided factory data when available', async () => { + const providedFactoryData = '0x3ffba36f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; + + const account = await createSmartAccount({ + client, + owner: signer, + ownerIndex: 0, + address: '0xBb0c1d5E7f530e8e648150fc7Cf30912575523E8', + factoryData: providedFactoryData, + }); + + const factoryArgs = await account.getFactoryArgsWithGeneration(); + + expect(factoryArgs.factory).toBe('0xba5ed110efdba3d005bfc882d75358acbbb85842'); + expect(factoryArgs.factoryData).toBe(providedFactoryData); + }); +}); diff --git a/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.ts b/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.ts index 5e24c1bb..58fef8b4 100644 --- a/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.ts +++ b/packages/account-sdk/src/sign/base-account/utils/createSmartAccount.ts @@ -19,6 +19,7 @@ import { encodePacked, hashMessage, hashTypedData, + pad, parseSignature, size, stringToHex, @@ -57,6 +58,7 @@ export type CoinbaseSmartAccountImplementation = Assign< { decodeCalls: NonNullable; sign: NonNullable; + getFactoryArgsWithGeneration: () => Promise<{ factory: Address; factoryData: Hex }>; } >; @@ -73,7 +75,7 @@ export type CoinbaseSmartAccountImplementation = Assign< * owner: privateKeyToAccount('0x...'), * ownerIndex: 0, * address: '0x...', - * factoryData: '0x...', + * factoryData: '0x...', // Optional: if not provided, will be generated automatically * }) */ export async function createSmartAccount( @@ -135,10 +137,12 @@ export async function createSmartAccount( return address; }, - async getFactoryArgs() { - if (factoryData) return { factory: factory.address, factoryData }; - // TODO: support creating factory data - return { factory: factory.address, factoryData }; + async getFactoryArgsWithGeneration() { + if (factoryData) return { factory: factoryAddress, factoryData }; + + // Generate factory data for owner initialization + const generatedFactoryData = createFactoryData(owner); + return { factory: factoryAddress, factoryData: generatedFactoryData }; }, async getStubSignature() { @@ -386,3 +390,49 @@ export function wrapSignature(parameters: { ownerIndex?: number | undefined; sig ] ); } + +/** + * @description Creates factory data for smart account initialization. + * + * This function generates the encoded data needed to initialize a smart account + * through the factory contract. It supports both local (private key) and WebAuthn + * account types. + * + * @param owner - The owner account to initialize the smart account with + * @returns Encoded factory data for the createAccount function call + * @throws {BaseError} If the owner type is unsupported or has invalid data + * + * @example + * + * const factoryData = createFactoryData(privateKeyToAccount('0x...')); + * // Returns: '0x3ffba36f...' (encoded createAccount call) + */ +export function createFactoryData(owner: OwnerAccount): Hex { + // Convert owner to the format expected by the factory + let ownerBytes: Hex; + + if (owner.type === 'webAuthn') { + // For WebAuthn accounts, we need to encode the public key + // The public key should be 64 bytes (32 bytes x + 32 bytes y) + if (!owner.publicKey || owner.publicKey.length !== 130) { + // 0x + 64 hex chars = 130 + throw new BaseError('WebAuthn owner must have a valid 64-byte public key'); + } + ownerBytes = owner.publicKey as Hex; + } else if (owner.type === 'local') { + // For local accounts (private key accounts), we need the address padded to 32 bytes + if (!owner.address) { + throw new BaseError('Local owner must have an address'); + } + ownerBytes = pad(owner.address); + } else { + throw new BaseError(`Unsupported owner type: ${owner.type}`); + } + + // Encode the createAccount function call with the owner and nonce 0 + return encodeFunctionData({ + abi: factoryAbi, + functionName: 'createAccount', + args: [[ownerBytes], BigInt(0)], + }); +}