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,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