FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,27 @@
// TODO(v3): Remove this in favor of `ox/WebAuthnP256` entirely.
import * as PublicKey from 'ox/PublicKey'
import * as WebAuthnP256 from 'ox/WebAuthnP256'
import type { Hex } from '../../types/misc.js'
export type P256Credential = {
id: WebAuthnP256.P256Credential['id']
publicKey: Hex
raw: WebAuthnP256.P256Credential['raw']
}
export type CreateWebAuthnCredentialParameters =
WebAuthnP256.createCredential.Options
export type CreateWebAuthnCredentialReturnType = P256Credential
export async function createWebAuthnCredential(
parameters: CreateWebAuthnCredentialParameters,
): Promise<CreateWebAuthnCredentialReturnType> {
const credential = await WebAuthnP256.createCredential(parameters)
return {
id: credential.id,
publicKey: PublicKey.toHex(credential.publicKey, { includePrefix: false }),
raw: credential.raw,
}
}

View File

@@ -0,0 +1,879 @@
import type { Address, TypedData } from 'abitype'
import * as Signature from 'ox/Signature'
import type * as WebAuthnP256 from 'ox/WebAuthnP256'
import type { LocalAccount } from '../../../accounts/types.js'
import { readContract } from '../../../actions/public/readContract.js'
import { BaseError } from '../../../errors/base.js'
import type { Hash, Hex } from '../../../types/misc.js'
import type { TypedDataDefinition } from '../../../types/typedData.js'
import type { Assign, OneOf, Prettify } from '../../../types/utils.js'
import { decodeFunctionData } from '../../../utils/abi/decodeFunctionData.js'
import { encodeAbiParameters } from '../../../utils/abi/encodeAbiParameters.js'
import { encodeFunctionData } from '../../../utils/abi/encodeFunctionData.js'
import { encodePacked } from '../../../utils/abi/encodePacked.js'
import { pad } from '../../../utils/data/pad.js'
import { size } from '../../../utils/data/size.js'
import { stringToHex } from '../../../utils/encoding/toHex.js'
import { hashMessage } from '../../../utils/signature/hashMessage.js'
import { hashTypedData } from '../../../utils/signature/hashTypedData.js'
import { parseSignature } from '../../../utils/signature/parseSignature.js'
import { entryPoint06Abi } from '../../constants/abis.js'
import { entryPoint06Address } from '../../constants/address.js'
import type { UserOperation } from '../../types/userOperation.js'
import { getUserOperationHash } from '../../utils/userOperation/getUserOperationHash.js'
import { toSmartAccount } from '../toSmartAccount.js'
import type {
SmartAccount,
SmartAccountImplementation,
WebAuthnAccount,
} from '../types.js'
export type ToCoinbaseSmartAccountParameters = {
address?: Address | undefined
client: CoinbaseSmartAccountImplementation['client']
ownerIndex?: number | undefined
owners: readonly (Address | OneOf<LocalAccount | WebAuthnAccount>)[]
nonce?: bigint | undefined
version: '1.1' | '1'
}
export type ToCoinbaseSmartAccountReturnType = 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']>
}
>
const factoryAddress = {
'1.1': '0xba5ed110efdba3d005bfc882d75358acbbb85842',
'1': '0x0ba5ed0c6aa8c49038f819e587e2633c4a9f428a',
} as const
/**
* @description Create a Coinbase Smart Account.
*
* @param parameters - {@link ToCoinbaseSmartAccountParameters}
* @returns Coinbase Smart Account. {@link ToCoinbaseSmartAccountReturnType}
*
* @example
* import { toCoinbaseSmartAccount } from 'viem/account-abstraction'
* import { privateKeyToAccount } from 'viem/accounts'
* import { client } from './client.js'
*
* const account = toCoinbaseSmartAccount({
* client,
* owners: [privateKeyToAccount('0x...')],
* version: '1.1',
* })
*/
export async function toCoinbaseSmartAccount(
parameters: ToCoinbaseSmartAccountParameters,
): Promise<ToCoinbaseSmartAccountReturnType> {
const {
client,
ownerIndex = 0,
owners,
nonce = 0n,
version = '1',
} = parameters
let address = parameters.address
const entryPoint = {
abi: entryPoint06Abi,
address: entryPoint06Address,
version: '0.6',
} as const
const factory = {
abi: factoryAbi,
address: factoryAddress[version],
} as const
const owners_bytes = owners.map((owner) => {
if (typeof owner === 'string') return pad(owner)
if (owner.type === 'webAuthn') return owner.publicKey
if (owner.type === 'local') return pad(owner.address)
throw new BaseError('invalid owner type')
})
const owner = (() => {
const owner = owners[ownerIndex] ?? owners[0]
if (typeof owner === 'string')
return { address: owner, type: 'address' } as const
return owner
})()
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 ?? 0n, calls[0].data ?? '0x'],
})
return encodeFunctionData({
abi,
functionName: 'executeBatch',
args: [
calls.map((call) => ({
data: call.data ?? '0x',
target: call.to,
value: call.value ?? 0n,
})),
],
})
},
async getAddress() {
address ??= await readContract(client, {
...factory,
functionName: 'getAddress',
args: [owners_bytes, nonce],
})
return address
},
async getFactoryArgs() {
const factoryData = encodeFunctionData({
abi: factory.abi,
functionName: 'createAccount',
args: [owners_bytes, nonce],
})
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 typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: parameters.hash,
})
if (owner.type === 'address') throw new Error('owner cannot sign')
const signature = await signTypedData({ owner, typedData })
return wrapSignature({
ownerIndex,
signature,
})
},
async signMessage(parameters) {
const { message } = parameters
const address = await this.getAddress()
const typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: hashMessage(message),
})
if (owner.type === 'address') throw new Error('owner cannot sign')
const signature = await signTypedData({ owner, typedData })
return wrapSignature({
ownerIndex,
signature,
})
},
async signTypedData(parameters) {
const { domain, types, primaryType, message } =
parameters as TypedDataDefinition<TypedData, string>
const address = await this.getAddress()
const typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: hashTypedData({
domain,
message,
primaryType,
types,
}),
})
if (owner.type === 'address') throw new Error('owner cannot sign')
const signature = await signTypedData({ owner, typedData })
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,
},
})
if (owner.type === 'address') throw new Error('owner cannot sign')
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 ?? 0n), 800_000),
),
}
},
},
})
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
/////////////////////////////////////////////////////////////////////////////////////////////
/** @internal */
export async function signTypedData({
typedData,
owner,
}: {
typedData: TypedDataDefinition
owner: OneOf<LocalAccount | WebAuthnAccount>
}) {
if (owner.type === 'local' && owner.signTypedData)
return owner.signTypedData(typedData)
const hash = hashTypedData(typedData)
return sign({ hash, owner })
}
/** @internal */
export async function sign({
hash,
owner,
}: {
hash: Hash
owner: OneOf<LocalAccount | WebAuthnAccount>
}) {
// 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 toReplaySafeTypedData({
address,
chainId,
hash,
}: {
address: Address
chainId: number
hash: Hash
}) {
return {
domain: {
chainId,
name: 'Coinbase Smart Wallet',
verifyingContract: address,
version: '1',
},
types: {
CoinbaseSmartWalletMessage: [
{
name: 'hash',
type: 'bytes32',
},
],
},
primaryType: 'CoinbaseSmartWalletMessage',
message: {
hash,
},
} as const
}
/** @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 ?? 0n),
typeIndex: BigInt(webauthn.typeIndex ?? 0n),
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,
},
],
)
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Constants
/////////////////////////////////////////////////////////////////////////////////////////////
const abi = [
{ inputs: [], stateMutability: 'nonpayable', type: 'constructor' },
{
inputs: [{ name: 'owner', type: 'bytes' }],
name: 'AlreadyOwner',
type: 'error',
},
{ inputs: [], name: 'Initialized', type: 'error' },
{
inputs: [{ name: 'owner', type: 'bytes' }],
name: 'InvalidEthereumAddressOwner',
type: 'error',
},
{
inputs: [{ name: 'key', type: 'uint256' }],
name: 'InvalidNonceKey',
type: 'error',
},
{
inputs: [{ name: 'owner', type: 'bytes' }],
name: 'InvalidOwnerBytesLength',
type: 'error',
},
{ inputs: [], name: 'LastOwner', type: 'error' },
{
inputs: [{ name: 'index', type: 'uint256' }],
name: 'NoOwnerAtIndex',
type: 'error',
},
{
inputs: [{ name: 'ownersRemaining', type: 'uint256' }],
name: 'NotLastOwner',
type: 'error',
},
{
inputs: [{ name: 'selector', type: 'bytes4' }],
name: 'SelectorNotAllowed',
type: 'error',
},
{ inputs: [], name: 'Unauthorized', type: 'error' },
{ inputs: [], name: 'UnauthorizedCallContext', type: 'error' },
{ inputs: [], name: 'UpgradeFailed', type: 'error' },
{
inputs: [
{ name: 'index', type: 'uint256' },
{ name: 'expectedOwner', type: 'bytes' },
{ name: 'actualOwner', type: 'bytes' },
],
name: 'WrongOwnerAtIndex',
type: 'error',
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'index',
type: 'uint256',
},
{ indexed: false, name: 'owner', type: 'bytes' },
],
name: 'AddOwner',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'index',
type: 'uint256',
},
{ indexed: false, name: 'owner', type: 'bytes' },
],
name: 'RemoveOwner',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'implementation',
type: 'address',
},
],
name: 'Upgraded',
type: 'event',
},
{ stateMutability: 'payable', type: 'fallback' },
{
inputs: [],
name: 'REPLAYABLE_NONCE_KEY',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'owner', type: 'address' }],
name: 'addOwnerAddress',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'x', type: 'bytes32' },
{ name: 'y', type: 'bytes32' },
],
name: 'addOwnerPublicKey',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'functionSelector', type: 'bytes4' }],
name: 'canSkipChainIdValidation',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'pure',
type: 'function',
},
{
inputs: [],
name: 'domainSeparator',
outputs: [{ name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'eip712Domain',
outputs: [
{ name: 'fields', type: 'bytes1' },
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
{ name: 'salt', type: 'bytes32' },
{ name: 'extensions', type: 'uint256[]' },
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'entryPoint',
outputs: [{ name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'target', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
name: 'execute',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
components: [
{ name: 'target', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
name: 'calls',
type: 'tuple[]',
},
],
name: 'executeBatch',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [{ name: 'calls', type: 'bytes[]' }],
name: 'executeWithoutChainIdValidation',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
components: [
{ name: 'sender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'initCode', type: 'bytes' },
{ name: 'callData', type: 'bytes' },
{ name: 'callGasLimit', type: 'uint256' },
{
name: 'verificationGasLimit',
type: 'uint256',
},
{
name: 'preVerificationGas',
type: 'uint256',
},
{ name: 'maxFeePerGas', type: 'uint256' },
{
name: 'maxPriorityFeePerGas',
type: 'uint256',
},
{ name: 'paymasterAndData', type: 'bytes' },
{ name: 'signature', type: 'bytes' },
],
name: 'userOp',
type: 'tuple',
},
],
name: 'getUserOpHashWithoutChainId',
outputs: [{ name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'implementation',
outputs: [{ name: '$', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'owners', type: 'bytes[]' }],
name: 'initialize',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [{ name: 'account', type: 'address' }],
name: 'isOwnerAddress',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'account', type: 'bytes' }],
name: 'isOwnerBytes',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'x', type: 'bytes32' },
{ name: 'y', type: 'bytes32' },
],
name: 'isOwnerPublicKey',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'hash', type: 'bytes32' },
{ name: 'signature', type: 'bytes' },
],
name: 'isValidSignature',
outputs: [{ name: 'result', type: 'bytes4' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'nextOwnerIndex',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'index', type: 'uint256' }],
name: 'ownerAtIndex',
outputs: [{ name: '', type: 'bytes' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'ownerCount',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'proxiableUUID',
outputs: [{ name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'index', type: 'uint256' },
{ name: 'owner', type: 'bytes' },
],
name: 'removeLastOwner',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'index', type: 'uint256' },
{ name: 'owner', type: 'bytes' },
],
name: 'removeOwnerAtIndex',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'removedOwnersCount',
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'hash', type: 'bytes32' }],
name: 'replaySafeHash',
outputs: [{ name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ name: 'newImplementation', type: 'address' },
{ name: 'data', type: 'bytes' },
],
name: 'upgradeToAndCall',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
components: [
{ name: 'sender', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'initCode', type: 'bytes' },
{ name: 'callData', type: 'bytes' },
{ name: 'callGasLimit', type: 'uint256' },
{
name: 'verificationGasLimit',
type: 'uint256',
},
{
name: 'preVerificationGas',
type: 'uint256',
},
{ name: 'maxFeePerGas', type: 'uint256' },
{
name: 'maxPriorityFeePerGas',
type: 'uint256',
},
{ name: 'paymasterAndData', type: 'bytes' },
{ name: 'signature', type: 'bytes' },
],
name: 'userOp',
type: 'tuple',
},
{ name: 'userOpHash', type: 'bytes32' },
{ name: 'missingAccountFunds', type: 'uint256' },
],
name: 'validateUserOp',
outputs: [{ name: 'validationData', type: 'uint256' }],
stateMutability: 'nonpayable',
type: 'function',
},
{ stateMutability: 'payable', type: 'receive' },
] as const
const factoryAbi = [
{
inputs: [{ name: 'implementation_', type: 'address' }],
stateMutability: 'payable',
type: 'constructor',
},
{ inputs: [], name: 'OwnerRequired', type: 'error' },
{
inputs: [
{ name: 'owners', type: 'bytes[]' },
{ name: 'nonce', type: 'uint256' },
],
name: 'createAccount',
outputs: [
{
name: 'account',
type: 'address',
},
],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ name: 'owners', type: 'bytes[]' },
{ name: 'nonce', type: 'uint256' },
],
name: 'getAddress',
outputs: [{ name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'implementation',
outputs: [{ name: '', type: 'address' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'initCodeHash',
outputs: [{ name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
] as const

View File

@@ -0,0 +1,363 @@
import type { Abi, Address, TypedData } from 'abitype'
import type { PrivateKeyAccount } from '../../../accounts/types.js'
import { BaseError } from '../../../errors/base.js'
import type { TypedDataDefinition } from '../../../types/typedData.js'
import type { Prettify } from '../../../types/utils.js'
import { decodeFunctionData } from '../../../utils/abi/decodeFunctionData.js'
import { encodeFunctionData } from '../../../utils/abi/encodeFunctionData.js'
import { entryPoint08Abi, entryPoint09Abi } from '../../constants/abis.js'
import {
entryPoint08Address,
entryPoint09Address,
} from '../../constants/address.js'
import type { EntryPointVersion } from '../../types/entryPointVersion.js'
import { getUserOperationTypedData } from '../../utils/userOperation/getUserOperationTypedData.js'
import { toSmartAccount } from '../toSmartAccount.js'
import type { SmartAccount, SmartAccountImplementation } from '../types.js'
type EntryPoint =
| '0.8'
| '0.9'
| {
abi: Abi
address: Address
version: EntryPointVersion
}
export type ToSimple7702SmartAccountParameters<
entryPoint extends EntryPoint = '0.8',
> = {
client: Simple7702SmartAccountImplementation['client']
entryPoint?: entryPoint | EntryPoint | undefined
implementation?: Address | undefined
getNonce?: SmartAccountImplementation['getNonce'] | undefined
owner: PrivateKeyAccount
}
export type ToSimple7702SmartAccountReturnType<
entryPoint extends EntryPoint = '0.8',
> = Prettify<SmartAccount<Simple7702SmartAccountImplementation<entryPoint>>>
export type Simple7702SmartAccountImplementation<
entryPoint extends EntryPoint = '0.8',
> = SmartAccountImplementation<
entryPoint extends { abi: infer abi }
? abi
: entryPoint extends '0.9'
? typeof entryPoint09Abi
: typeof entryPoint08Abi,
entryPoint extends string
? entryPoint
: entryPoint extends { version: infer version }
? version
: EntryPointVersion,
{
abi: entryPoint extends { abi: infer abi }
? abi
: entryPoint extends '0.9'
? typeof entryPoint09Abi
: typeof entryPoint08Abi
owner: PrivateKeyAccount
},
true
>
/**
* @description Create a Simple7702 Smart Account based off [eth-infinitism's `Simple7702Account.sol`](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/accounts/Simple7702Account.sol).
*
* @param parameters - {@link ToSimple7702SmartAccountParameters}
* @returns Simple7702 Smart Account. {@link ToSimple7702SmartAccountReturnType}
*
* @example
* import { toSimple7702SmartAccount } from 'viem/account-abstraction'
* import { client } from './client.js'
*
* const implementation = toSimple7702SmartAccount({
* client,
* owner: '0x...',
* })
*/
export async function toSimple7702SmartAccount<
entryPoint extends EntryPoint = '0.8',
>(
parameters: ToSimple7702SmartAccountParameters<entryPoint>,
): Promise<ToSimple7702SmartAccountReturnType<entryPoint>> {
const { client, getNonce, owner } = parameters
const entryPoint = (() => {
if (parameters.entryPoint === '0.9')
return {
abi: entryPoint09Abi,
address: entryPoint09Address,
version: '0.9',
} as const
if (typeof parameters.entryPoint === 'object') return parameters.entryPoint
return {
abi: entryPoint08Abi,
address: entryPoint08Address,
version: '0.8',
} as const
})()
const implementation = (() => {
if (parameters.implementation) return parameters.implementation
if (parameters.entryPoint === '0.9')
return '0xa46cc63eBF4Bd77888AA327837d20b23A63a56B5'
return '0xe6Cae83BdE06E4c305530e199D7217f42808555B'
})()
return toSmartAccount({
authorization: { account: owner, address: implementation },
abi,
client,
extend: { abi, owner }, // not removing abi from here as this will be a breaking change
entryPoint,
getNonce,
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 ?? 0n, calls[0].data ?? '0x'],
})
return encodeFunctionData({
abi,
functionName: 'executeBatch',
args: [
calls.map((call) => ({
data: call.data ?? '0x',
target: call.to,
value: call.value ?? 0n,
})),
],
})
},
async getAddress() {
return owner.address
},
async getFactoryArgs() {
return { factory: '0x7702', factoryData: '0x' }
},
async getStubSignature() {
return '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'
},
async signMessage(parameters) {
const { message } = parameters
return await owner.signMessage({ message })
},
async signTypedData(parameters) {
const { domain, types, primaryType, message } =
parameters as TypedDataDefinition<TypedData, string>
return await owner.signTypedData({
domain,
message,
primaryType,
types,
})
},
async signUserOperation(parameters) {
const { chainId = client.chain!.id, ...userOperation } = parameters
const address = await this.getAddress()
const typedData = getUserOperationTypedData({
chainId,
entryPointAddress: entryPoint.address,
userOperation: {
...userOperation,
sender: address,
},
})
return await owner.signTypedData(typedData)
},
}) as unknown as ToSimple7702SmartAccountReturnType<entryPoint>
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Constants
const abi = [
{ inputs: [], name: 'ECDSAInvalidSignature', type: 'error' },
{
inputs: [{ internalType: 'uint256', name: 'length', type: 'uint256' }],
name: 'ECDSAInvalidSignatureLength',
type: 'error',
},
{
inputs: [{ internalType: 'bytes32', name: 's', type: 'bytes32' }],
name: 'ECDSAInvalidSignatureS',
type: 'error',
},
{
inputs: [
{ internalType: 'uint256', name: 'index', type: 'uint256' },
{ internalType: 'bytes', name: 'error', type: 'bytes' },
],
name: 'ExecuteError',
type: 'error',
},
{ stateMutability: 'payable', type: 'fallback' },
{
inputs: [],
name: 'entryPoint',
outputs: [
{ internalType: 'contract IEntryPoint', name: '', type: 'address' },
],
stateMutability: 'pure',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'target', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
name: 'execute',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
components: [
{ internalType: 'address', name: 'target', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
internalType: 'struct BaseAccount.Call[]',
name: 'calls',
type: 'tuple[]',
},
],
name: 'executeBatch',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'getNonce',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'bytes32', name: 'hash', type: 'bytes32' },
{ internalType: 'bytes', name: 'signature', type: 'bytes' },
],
name: 'isValidSignature',
outputs: [{ internalType: 'bytes4', name: 'magicValue', type: 'bytes4' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'uint256[]', name: '', type: 'uint256[]' },
{ internalType: 'uint256[]', name: '', type: 'uint256[]' },
{ internalType: 'bytes', name: '', type: 'bytes' },
],
name: 'onERC1155BatchReceived',
outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'uint256', name: '', type: 'uint256' },
{ internalType: 'uint256', name: '', type: 'uint256' },
{ internalType: 'bytes', name: '', type: 'bytes' },
],
name: 'onERC1155Received',
outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'uint256', name: '', type: 'uint256' },
{ internalType: 'bytes', name: '', type: 'bytes' },
],
name: 'onERC721Received',
outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'bytes4', name: 'id', type: 'bytes4' }],
name: 'supportsInterface',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'pure',
type: 'function',
},
{
inputs: [
{
components: [
{ internalType: 'address', name: 'sender', type: 'address' },
{ internalType: 'uint256', name: 'nonce', type: 'uint256' },
{ internalType: 'bytes', name: 'initCode', type: 'bytes' },
{ internalType: 'bytes', name: 'callData', type: 'bytes' },
{
internalType: 'bytes32',
name: 'accountGasLimits',
type: 'bytes32',
},
{
internalType: 'uint256',
name: 'preVerificationGas',
type: 'uint256',
},
{ internalType: 'bytes32', name: 'gasFees', type: 'bytes32' },
{ internalType: 'bytes', name: 'paymasterAndData', type: 'bytes' },
{ internalType: 'bytes', name: 'signature', type: 'bytes' },
],
internalType: 'struct PackedUserOperation',
name: 'userOp',
type: 'tuple',
},
{ internalType: 'bytes32', name: 'userOpHash', type: 'bytes32' },
{ internalType: 'uint256', name: 'missingAccountFunds', type: 'uint256' },
],
name: 'validateUserOp',
outputs: [
{ internalType: 'uint256', name: 'validationData', type: 'uint256' },
],
stateMutability: 'nonpayable',
type: 'function',
},
{ stateMutability: 'payable', type: 'receive' },
] as const

View File

@@ -0,0 +1,796 @@
import type { Abi, Address, TypedData } from 'abitype'
import { parseAccount } from '../../../accounts/utils/parseAccount.js'
import { readContract } from '../../../actions/public/readContract.js'
import { signMessage as signMessage_ } from '../../../actions/wallet/signMessage.js'
import { BaseError } from '../../../errors/base.js'
import { signMessage } from '../../../experimental/erc7739/actions/signMessage.js'
import { signTypedData } from '../../../experimental/erc7739/actions/signTypedData.js'
import type { Account } from '../../../types/account.js'
import type { Hex } from '../../../types/misc.js'
import type { TypedDataDefinition } from '../../../types/typedData.js'
import type { Prettify } from '../../../types/utils.js'
import { decodeFunctionData } from '../../../utils/abi/decodeFunctionData.js'
import { encodeFunctionData } from '../../../utils/abi/encodeFunctionData.js'
import { pad } from '../../../utils/data/pad.js'
import { getAction } from '../../../utils/getAction.js'
import { entryPoint07Abi } from '../../constants/abis.js'
import { entryPoint07Address } from '../../constants/address.js'
import type { EntryPointVersion } from '../../types/entryPointVersion.js'
import { getUserOperationHash } from '../../utils/userOperation/getUserOperationHash.js'
import { toSmartAccount } from '../toSmartAccount.js'
import type { SmartAccount, SmartAccountImplementation } from '../types.js'
export type ToSoladySmartAccountParameters<
entryPointAbi extends Abi = Abi,
entryPointVersion extends EntryPointVersion = EntryPointVersion,
> = {
address?: Address | undefined
client: SoladySmartAccountImplementation['client']
entryPoint?:
| {
abi: entryPointAbi
address: Address
version: entryPointVersion | EntryPointVersion
}
| undefined
factoryAddress?: Address | undefined
getNonce?: SmartAccountImplementation['getNonce'] | undefined
owner: Address | Account
salt?: Hex | undefined
}
export type ToSoladySmartAccountReturnType<
entryPointAbi extends Abi = Abi,
entryPointVersion extends EntryPointVersion = EntryPointVersion,
> = Prettify<
SmartAccount<
SoladySmartAccountImplementation<entryPointAbi, entryPointVersion>
>
>
export type SoladySmartAccountImplementation<
entryPointAbi extends Abi = Abi,
entryPointVersion extends EntryPointVersion = EntryPointVersion,
> = SmartAccountImplementation<
entryPointAbi,
entryPointVersion,
{ abi: typeof abi; factory: { abi: typeof factoryAbi; address: Address } }
>
/**
* @description Create a Solady Smart Account based off [Solady's `ERC4337.sol`](https://github.com/Vectorized/solady/blob/main/src/accounts/ERC4337.sol).
*
* @param parameters - {@link ToSoladySmartAccountParameters}
* @returns Solady Smart Account. {@link ToSoladySmartAccountReturnType}
*
* @example
* import { toSoladySmartAccount } from 'viem/account-abstraction'
* import { client } from './client.js'
*
* const implementation = toSoladySmartAccount({
* client,
* owner: '0x...',
* })
*/
export async function toSoladySmartAccount<
entryPointAbi extends Abi = typeof entryPoint07Abi,
entryPointVersion extends EntryPointVersion = '0.7',
>(
parameters: ToSoladySmartAccountParameters<entryPointAbi, entryPointVersion>,
): Promise<ToSoladySmartAccountReturnType<entryPointAbi, entryPointVersion>> {
const {
address,
client,
entryPoint: entryPoint_ = {
abi: entryPoint07Abi,
address: entryPoint07Address,
version: '0.7',
},
factoryAddress = '0x5d82735936c6Cd5DE57cC3c1A799f6B2E6F933Df',
getNonce,
salt = '0x0',
} = parameters
const entryPoint = {
abi: entryPoint_.abi as entryPointAbi,
address: entryPoint_.address,
version: entryPoint_.version as entryPointVersion,
} as const
const factory = {
abi: factoryAbi,
address: factoryAddress,
} as const
const owner = parseAccount(parameters.owner)
return toSmartAccount({
client,
entryPoint,
getNonce,
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 ?? 0n, calls[0].data ?? '0x'],
})
return encodeFunctionData({
abi,
functionName: 'executeBatch',
args: [
calls.map((call) => ({
data: call.data ?? '0x',
target: call.to,
value: call.value ?? 0n,
})),
],
})
},
async getAddress() {
if (address) return address
return await readContract(client, {
...factory,
functionName: 'getAddress',
args: [pad(salt)],
})
},
async getFactoryArgs() {
const factoryData = encodeFunctionData({
abi: factory.abi,
functionName: 'createAccount',
args: [owner.address, pad(salt)],
})
return { factory: factory.address, factoryData }
},
async getStubSignature() {
return '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'
},
async signMessage(parameters) {
const { message } = parameters
const [address, { factory, factoryData }] = await Promise.all([
this.getAddress(),
this.getFactoryArgs(),
])
return await signMessage(client, {
account: owner,
factory,
factoryData,
message,
verifier: address,
})
},
async signTypedData(parameters) {
const { domain, types, primaryType, message } =
parameters as TypedDataDefinition<TypedData, string>
const [address, { factory, factoryData }] = await Promise.all([
this.getAddress(),
this.getFactoryArgs(),
])
return await signTypedData(client, {
account: owner,
domain,
message,
factory,
factoryData,
primaryType,
types,
verifier: address,
})
},
async signUserOperation(parameters) {
const { chainId = client.chain!.id, ...userOperation } = parameters
const address = await this.getAddress()
const userOpHash = getUserOperationHash({
chainId,
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
userOperation: {
...(userOperation as any),
sender: address,
},
})
const signature = await getAction(
client,
signMessage_,
'signMessage',
)({
account: owner,
message: {
raw: userOpHash,
},
})
return signature
},
})
}
/////////////////////////////////////////////////////////////////////////////////////////////
// Constants
const abi = [
{
type: 'fallback',
stateMutability: 'payable',
},
{
type: 'receive',
stateMutability: 'payable',
},
{
type: 'function',
name: 'addDeposit',
inputs: [],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'cancelOwnershipHandover',
inputs: [],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'completeOwnershipHandover',
inputs: [
{
name: 'pendingOwner',
type: 'address',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'delegateExecute',
inputs: [
{
name: 'delegate',
type: 'address',
},
{
name: 'data',
type: 'bytes',
},
],
outputs: [
{
name: 'result',
type: 'bytes',
},
],
stateMutability: 'payable',
},
{
type: 'function',
name: 'eip712Domain',
inputs: [],
outputs: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'address',
},
{
name: 'salt',
type: 'bytes32',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'entryPoint',
inputs: [],
outputs: [
{
name: '',
type: 'address',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'execute',
inputs: [
{
name: 'target',
type: 'address',
},
{
name: 'value',
type: 'uint256',
},
{
name: 'data',
type: 'bytes',
},
],
outputs: [
{
name: 'result',
type: 'bytes',
},
],
stateMutability: 'payable',
},
{
type: 'function',
name: 'executeBatch',
inputs: [
{
name: 'calls',
type: 'tuple[]',
components: [
{
name: 'target',
type: 'address',
},
{
name: 'value',
type: 'uint256',
},
{
name: 'data',
type: 'bytes',
},
],
},
],
outputs: [
{
name: 'results',
type: 'bytes[]',
},
],
stateMutability: 'payable',
},
{
type: 'function',
name: 'getDeposit',
inputs: [],
outputs: [
{
name: 'result',
type: 'uint256',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'initialize',
inputs: [
{
name: 'newOwner',
type: 'address',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'isValidSignature',
inputs: [
{
name: 'hash',
type: 'bytes32',
},
{
name: 'signature',
type: 'bytes',
},
],
outputs: [
{
name: 'result',
type: 'bytes4',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'owner',
inputs: [],
outputs: [
{
name: 'result',
type: 'address',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'ownershipHandoverExpiresAt',
inputs: [
{
name: 'pendingOwner',
type: 'address',
},
],
outputs: [
{
name: 'result',
type: 'uint256',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'proxiableUUID',
inputs: [],
outputs: [
{
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'renounceOwnership',
inputs: [],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'requestOwnershipHandover',
inputs: [],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'storageLoad',
inputs: [
{
name: 'storageSlot',
type: 'bytes32',
},
],
outputs: [
{
name: 'result',
type: 'bytes32',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'storageStore',
inputs: [
{
name: 'storageSlot',
type: 'bytes32',
},
{
name: 'storageValue',
type: 'bytes32',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'transferOwnership',
inputs: [
{
name: 'newOwner',
type: 'address',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'upgradeToAndCall',
inputs: [
{
name: 'newImplementation',
type: 'address',
},
{
name: 'data',
type: 'bytes',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'validateUserOp',
inputs: [
{
name: 'userOp',
type: 'tuple',
components: [
{
name: 'sender',
type: 'address',
},
{
name: 'nonce',
type: 'uint256',
},
{
name: 'initCode',
type: 'bytes',
},
{
name: 'callData',
type: 'bytes',
},
{
name: 'accountGasLimits',
type: 'bytes32',
},
{
name: 'preVerificationGas',
type: 'uint256',
},
{
name: 'gasFees',
type: 'bytes32',
},
{
name: 'paymasterAndData',
type: 'bytes',
},
{
name: 'signature',
type: 'bytes',
},
],
},
{
name: 'userOpHash',
type: 'bytes32',
},
{
name: 'missingAccountFunds',
type: 'uint256',
},
],
outputs: [
{
name: 'validationData',
type: 'uint256',
},
],
stateMutability: 'payable',
},
{
type: 'function',
name: 'withdrawDepositTo',
inputs: [
{
name: 'to',
type: 'address',
},
{
name: 'amount',
type: 'uint256',
},
],
outputs: [],
stateMutability: 'payable',
},
{
type: 'event',
name: 'OwnershipHandoverCanceled',
inputs: [
{
name: 'pendingOwner',
type: 'address',
indexed: true,
},
],
anonymous: false,
},
{
type: 'event',
name: 'OwnershipHandoverRequested',
inputs: [
{
name: 'pendingOwner',
type: 'address',
indexed: true,
},
],
anonymous: false,
},
{
type: 'event',
name: 'OwnershipTransferred',
inputs: [
{
name: 'oldOwner',
type: 'address',
indexed: true,
},
{
name: 'newOwner',
type: 'address',
indexed: true,
},
],
anonymous: false,
},
{
type: 'event',
name: 'Upgraded',
inputs: [
{
name: 'implementation',
type: 'address',
indexed: true,
},
],
anonymous: false,
},
{
type: 'error',
name: 'AlreadyInitialized',
inputs: [],
},
{
type: 'error',
name: 'FnSelectorNotRecognized',
inputs: [],
},
{
type: 'error',
name: 'NewOwnerIsZeroAddress',
inputs: [],
},
{
type: 'error',
name: 'NoHandoverRequest',
inputs: [],
},
{
type: 'error',
name: 'Unauthorized',
inputs: [],
},
{
type: 'error',
name: 'UnauthorizedCallContext',
inputs: [],
},
{
type: 'error',
name: 'UpgradeFailed',
inputs: [],
},
] as const
const factoryAbi = [
{
type: 'constructor',
inputs: [
{
name: 'erc4337',
type: 'address',
},
],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'createAccount',
inputs: [
{
name: 'owner',
type: 'address',
},
{
name: 'salt',
type: 'bytes32',
},
],
outputs: [
{
name: '',
type: 'address',
},
],
stateMutability: 'payable',
},
{
type: 'function',
name: 'getAddress',
inputs: [
{
name: 'salt',
type: 'bytes32',
},
],
outputs: [
{
name: '',
type: 'address',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'implementation',
inputs: [],
outputs: [
{
name: '',
type: 'address',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'initCodeHash',
inputs: [],
outputs: [
{
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
},
] as const

View File

@@ -0,0 +1,141 @@
import { type Abi, parseAbi } from 'abitype'
import { getCode } from '../../actions/public/getCode.js'
import { readContract } from '../../actions/public/readContract.js'
import type { Prettify } from '../../types/utils.js'
import { getAction } from '../../utils/getAction.js'
import { createNonceManager } from '../../utils/nonceManager.js'
import { serializeErc6492Signature } from '../../utils/signature/serializeErc6492Signature.js'
import type { EntryPointVersion } from '../types/entryPointVersion.js'
import type { SmartAccount, SmartAccountImplementation } from './types.js'
export type ToSmartAccountParameters<
entryPointAbi extends Abi | readonly unknown[] = Abi,
entryPointVersion extends EntryPointVersion = EntryPointVersion,
extend extends object = object,
> = SmartAccountImplementation<entryPointAbi, entryPointVersion, extend>
export type ToSmartAccountReturnType<
implementation extends
SmartAccountImplementation = SmartAccountImplementation,
> = Prettify<SmartAccount<implementation>>
/**
* @description Creates a Smart Account with a provided account implementation.
*
* @param parameters - {@link ToSmartAccountParameters}
* @returns A Smart Account. {@link ToSmartAccountReturnType}
*/
export async function toSmartAccount<
implementation extends SmartAccountImplementation,
>(
implementation: implementation,
): Promise<ToSmartAccountReturnType<implementation>> {
const {
extend,
nonceKeyManager = createNonceManager({
source: {
get() {
return Date.now()
},
set() {},
},
}),
...rest
} = implementation
let deployed = false
const address = await implementation.getAddress()
return {
...extend,
...rest,
address,
async getFactoryArgs() {
if ('isDeployed' in this && (await this.isDeployed()))
return { factory: undefined, factoryData: undefined }
return implementation.getFactoryArgs()
},
async getNonce(parameters) {
const key =
parameters?.key ??
BigInt(
await nonceKeyManager.consume({
address,
chainId: implementation.client.chain!.id!,
client: implementation.client,
}),
)
if (implementation.getNonce)
return await implementation.getNonce({ ...parameters, key })
const nonce = await readContract(implementation.client, {
abi: parseAbi([
'function getNonce(address, uint192) pure returns (uint256)',
]),
address: implementation.entryPoint.address,
functionName: 'getNonce',
args: [address, key],
})
return nonce
},
async isDeployed() {
if (deployed) return true
const code = await getAction(
implementation.client,
getCode,
'getCode',
)({
address,
})
deployed = Boolean(code)
return deployed
},
...(implementation.sign
? {
async sign(parameters) {
const [{ factory, factoryData }, signature] = await Promise.all([
this.getFactoryArgs(),
implementation.sign!(parameters),
])
if (factory && factoryData)
return serializeErc6492Signature({
address: factory,
data: factoryData,
signature,
})
return signature
},
}
: {}),
async signMessage(parameters) {
const [{ factory, factoryData }, signature] = await Promise.all([
this.getFactoryArgs(),
implementation.signMessage(parameters),
])
if (factory && factoryData && factory !== '0x7702')
return serializeErc6492Signature({
address: factory,
data: factoryData,
signature,
})
return signature
},
async signTypedData(parameters) {
const [{ factory, factoryData }, signature] = await Promise.all([
this.getFactoryArgs(),
implementation.signTypedData(parameters),
])
if (factory && factoryData && factory !== '0x7702')
return serializeErc6492Signature({
address: factory,
data: factoryData,
signature,
})
return signature
},
type: 'smart',
} as ToSmartAccountReturnType<implementation>
}

View File

@@ -0,0 +1,69 @@
import * as Signature from 'ox/Signature'
import * as WebAuthnP256 from 'ox/WebAuthnP256'
import type { ErrorType } from '../../errors/utils.js'
import { hashMessage } from '../../utils/signature/hashMessage.js'
import { hashTypedData } from '../../utils/signature/hashTypedData.js'
import type { P256Credential } from './createWebAuthnCredential.js'
import type { WebAuthnAccount } from './types.js'
export type ToWebAuthnAccountParameters = {
/**
* The WebAuthn P256 credential to use.
*/
credential: {
id: P256Credential['id']
publicKey: P256Credential['publicKey']
}
/**
* Credential request function. Useful for environments that do not support
* the WebAuthn API natively (i.e. React Native or testing environments).
*
* @default window.navigator.credentials.get
*/
getFn?: WebAuthnP256.sign.Options['getFn'] | undefined
/**
* The relying party identifier to use.
*/
rpId?: WebAuthnP256.sign.Options['rpId'] | undefined
}
export type ToWebAuthnAccountReturnType = WebAuthnAccount
export type ToWebAuthnAccountErrorType = ErrorType
/**
* @description Creates an Account from a WebAuthn Credential.
*
* @returns A WebAuthn Account.
*/
export function toWebAuthnAccount(
parameters: ToWebAuthnAccountParameters,
): WebAuthnAccount {
const { getFn, rpId } = parameters
const { id, publicKey } = parameters.credential
return {
id,
publicKey,
async sign({ hash }) {
const { metadata, raw, signature } = await WebAuthnP256.sign({
credentialId: id,
getFn,
challenge: hash,
rpId,
})
return {
signature: Signature.toHex(signature),
raw,
webauthn: metadata,
}
},
async signMessage({ message }) {
return this.sign({ hash: hashMessage(message) })
},
async signTypedData(parameters) {
return this.sign({ hash: hashTypedData(parameters) })
},
type: 'webAuthn',
}
}

269
node_modules/viem/account-abstraction/accounts/types.ts generated vendored Normal file
View File

@@ -0,0 +1,269 @@
import type { Abi, Address, TypedData } from 'abitype'
import type * as WebAuthnP256 from 'ox/WebAuthnP256'
import type {
JsonRpcAccount,
LocalAccount,
PrivateKeyAccount,
} from '../../accounts/types.js'
import type { Client } from '../../clients/createClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import type { Chain } from '../../types/chain.js'
import type { Hash, Hex, SignableMessage } from '../../types/misc.js'
import type { TypedDataDefinition } from '../../types/typedData.js'
import type { Assign, ExactPartial, UnionPartialBy } from '../../types/utils.js'
import type { NonceManager } from '../../utils/nonceManager.js'
import type { EntryPointVersion } from '../types/entryPointVersion.js'
import type {
EstimateUserOperationGasReturnType,
UserOperation,
UserOperationRequest,
} from '../types/userOperation.js'
type Call = {
to: Hex
data?: Hex | undefined
value?: bigint | undefined
}
export type SmartAccountImplementation<
entryPointAbi extends Abi | readonly unknown[] = Abi,
entryPointVersion extends EntryPointVersion = EntryPointVersion,
extend extends object = object,
eip7702 extends boolean = boolean,
> = {
/** Client used to retrieve Smart Account data, and perform signing (if owner is a JSON-RPC Account). */
client: Client<
Transport,
Chain | undefined,
JsonRpcAccount | LocalAccount | undefined
>
/** Compatible EntryPoint of the Smart Account. */
entryPoint: {
/** Compatible EntryPoint ABI. */
abi: entryPointAbi
/** Compatible EntryPoint address. */
address: Address
/** Compatible EntryPoint version. */
version: entryPointVersion
}
/** Extend the Smart Account with custom properties. */
extend?: extend | undefined
/**
* Retrieves the Smart Account's address.
*
* @example
* ```ts
* const address = await account.getAddress()
* // '0x...'
* ```
*/
getAddress: () => Promise<Address>
/**
* Decodes calldata into structured calls.
*
* @example
* ```ts
* const calls = await account.decodeCalls('0x...')
* // [{ to: '0x...', data: '0x...', value: 100n }, ...]
* ```
*/
decodeCalls?: ((data: Hex) => Promise<readonly Call[]>) | undefined
/**
* Encodes the calls into calldata for executing a User Operation.
*
* @example
* ```ts
* const callData = await account.encodeCalls([
* { to: '0x...', data: '0x...' },
* { to: '0x...', data: '0x...', value: 100n },
* ])
* // '0x...'
* ```
*/
encodeCalls: (calls: readonly Call[]) => Promise<Hex>
/**
* Retrieves the calldata for factory call to deploy a Smart Account.
* If the Smart Account has already been deployed, this will return undefined values.
*
* @example Counterfactual account
* ```ts
* const { factory, factoryData } = await account.getFactoryArgs()
* // { factory: '0x...', factoryData: '0x...' }
* ```
*
* @example Deployed account
* ```ts
* const { factory, factoryData } = await account.getFactoryArgs()
* // { factory: undefined, factoryData: undefined }
* ```
*/
getFactoryArgs: () => Promise<{
factory?: Address | undefined
factoryData?: Hex | undefined
}>
/**
* Retrieves the nonce of the Account.
*
* @example
* ```ts
* const nonce = await account.getNonce()
* // 1n
* ```
*/
getNonce?:
| ((
parameters?: { key?: bigint | undefined } | undefined,
) => Promise<bigint>)
| undefined
/**
* Retrieves the User Operation "stub" signature for gas estimation.
*
* ```ts
* const signature = await account.getStubSignature()
* // '0x...'
* ```
*/
getStubSignature: (
parameters?: UserOperationRequest | undefined,
) => Promise<Hex>
/** Custom nonce key manager. */
nonceKeyManager?: NonceManager | undefined
/**
* Signs a hash via the Smart Account's owner.
*
* @example
* ```ts
* const signature = await account.sign({
* hash: '0x...'
* })
* // '0x...'
* ```
*/
sign?: ((parameters: { hash: Hash }) => Promise<Hex>) | undefined
/**
* Signs a [EIP-191 Personal Sign message](https://eips.ethereum.org/EIPS/eip-191).
*
* @example
* ```ts
* const signature = await account.signMessage({
* message: 'Hello, World!'
* })
* // '0x...'
* ```
*/
signMessage: (parameters: { message: SignableMessage }) => Promise<Hex>
/**
* Signs [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712).
*
* @example
* ```ts
* const signature = await account.signTypedData({
* domain,
* types,
* primaryType: 'Mail',
* message,
* })
* ```
*/
signTypedData: <
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData,
>(
parameters: TypedDataDefinition<typedData, primaryType>,
) => Promise<Hex>
/**
* Signs the User Operation.
*
* @example
* ```ts
* const signature = await account.signUserOperation({
* chainId: 1,
* userOperation,
* })
* ```
*/
signUserOperation: (
parameters: UnionPartialBy<UserOperation, 'sender'> & {
chainId?: number | undefined
},
) => Promise<Hex>
/** User Operation configuration properties. */
userOperation?:
| {
/** Prepares gas properties for the User Operation request. */
estimateGas?:
| ((
userOperation: UserOperationRequest,
) => Promise<
ExactPartial<EstimateUserOperationGasReturnType> | undefined
>)
| undefined
}
| undefined
} & (eip7702 extends true
? {
/** EIP-7702 authorization properties, if applicable. */
authorization: {
/** EOA to delegate to. */
account: PrivateKeyAccount
/** Delegation address. */
address: Address
}
}
: {
authorization?: undefined
})
export type SmartAccount<
implementation extends
SmartAccountImplementation = SmartAccountImplementation,
> = Assign<
implementation['extend'],
Assign<
implementation,
{
/** Address of the Smart Account. */
address: Address
/**
* Retrieves the nonce of the Account.
*
* @example
* ```ts
* const nonce = await account.getNonce()
* // 1n
* ```
*/
getNonce: NonNullable<SmartAccountImplementation['getNonce']>
/** Whether or not the Smart Account has been deployed. */
isDeployed: () => Promise<boolean>
/** Type of account. */
type: 'smart'
}
>
>
// TODO(v3): Remove this in favor of `WebAuthnP256.sign.ReturnType` from Ox.
export type WebAuthnSignReturnType = {
signature: Hex
webauthn: WebAuthnP256.SignMetadata
raw: WebAuthnP256.sign.ReturnType['raw']
}
export type WebAuthnAccount = {
id: string
publicKey: Hex
sign: ({ hash }: { hash: Hash }) => Promise<WebAuthnSignReturnType>
signMessage: ({
message,
}: {
message: SignableMessage
}) => Promise<WebAuthnSignReturnType>
signTypedData: <
const typedData extends TypedDataDefinition | Record<string, unknown>,
primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData,
>(
typedDataDefinition: TypedDataDefinition<typedData, primaryType>,
) => Promise<WebAuthnSignReturnType>
type: 'webAuthn'
}