Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
encodePacked,
hashMessage,
hashTypedData,
pad,
parseSignature,
size,
stringToHex,
Expand Down Expand Up @@ -57,6 +58,7 @@ export type CoinbaseSmartAccountImplementation = Assign<
{
decodeCalls: NonNullable<SmartAccountImplementation['decodeCalls']>;
sign: NonNullable<SmartAccountImplementation['sign']>;
getFactoryArgsWithGeneration: () => Promise<{ factory: Address; factoryData: Hex }>;
}
>;

Expand All @@ -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(
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)],
});
}