From 0404ee6500d7ff86dfb07c57d21b48274c34436c Mon Sep 17 00:00:00 2001 From: VGabriel45 Date: Thu, 22 Aug 2024 16:45:43 +0300 Subject: [PATCH 1/6] feat: eip 712 (draft) --- src/account/BaseSmartContractAccount.ts | 14 +- src/account/NexusSmartAccount.ts | 94 ++++-- src/account/abi/EIP1271Abi.ts | 33 ++ src/account/utils/Constants.ts | 3 + src/account/utils/Types.ts | 34 ++- src/account/utils/Utils.ts | 119 +++++++- tests/account/read.test.ts | 388 +++++++++++++----------- tests/utils.ts | 48 ++- 8 files changed, 468 insertions(+), 265 deletions(-) create mode 100644 src/account/abi/EIP1271Abi.ts diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts index 603f56f8..2e9928cf 100644 --- a/src/account/BaseSmartContractAccount.ts +++ b/src/account/BaseSmartContractAccount.ts @@ -7,7 +7,7 @@ import { type PublicClient, createPublicClient, getContract, - trim + trim, } from "viem" import { EntryPointAbi } from "./abi/EntryPointAbi.js" import { Logger, type SmartAccountSigner, getChain } from "./index.js" @@ -20,7 +20,6 @@ import type { BasSmartContractAccountProps, BatchUserOperationCallData, ISmartContractAccount, - SignTypedDataParams, Transaction } from "./utils/Types.js" import { wrapSignatureWith6492 } from "./utils/Utils.js" @@ -33,8 +32,7 @@ export enum DeploymentState { export abstract class BaseSmartContractAccount< TSigner extends SmartAccountSigner = SmartAccountSigner -> implements ISmartContractAccount -{ +> implements ISmartContractAccount { protected factoryAddress: Address protected deploymentState: DeploymentState = DeploymentState.UNDEFINED @@ -135,9 +133,7 @@ export abstract class BaseSmartContractAccount< * * @param _params -- Typed Data params to sign */ - async signTypedData(_params: SignTypedDataParams): Promise<`0x${string}`> { - throw new Error("signTypedData not supported") - } + abstract signTypedData(typedData: any): Promise<`0x${string}`> /** * This method should wrap the result of `signMessage` as per @@ -162,11 +158,11 @@ export abstract class BaseSmartContractAccount< * @param params -- Typed Data params to sign */ async signTypedDataWith6492( - params: SignTypedDataParams + typedData: any ): Promise<`0x${string}`> { const [isDeployed, signature] = await Promise.all([ this.isAccountDeployed(), - this.signTypedData(params) + this.signTypedData(typedData) ]) return this.create6492Signature(isDeployed, signature) diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index f4dff878..333311ab 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -20,7 +20,11 @@ import { keccak256, parseAbi, parseAbiParameters, - toBytes + toBytes, + getTypesForEIP712Domain, + validateTypedData, + type TypedDataDefinition, + hashTypedData } from "viem" import { Bundler, @@ -92,7 +96,9 @@ import type { WithdrawalRequest } from "./utils/Types.js" import { + accountMetadata, addressEquals, + eip712WrapHash, isNullOrUndefined, isValidRpcUrl, packUserOp @@ -106,14 +112,12 @@ export class NexusSmartAccount extends BaseSmartContractAccount { private chainId: number - private provider: PublicClient + publicClient: PublicClient paymaster?: IPaymaster bundler?: IBundler - publicClient!: PublicClient - private accountContract?: GetContractReturnType< typeof NexusAccountAbi, PublicClient | WalletClient @@ -168,10 +172,6 @@ export class NexusSmartAccount extends BaseSmartContractAccount { transport: http() }) - // this.implementationAddress = - // nexusSmartAccountConfig.implementationAddress ?? - // (BICONOMY_IMPLEMENTATION_ADDRESSES_BY_VERSION.V2_0_0 as Hex) - if (nexusSmartAccountConfig.paymasterUrl) { this.paymaster = new Paymaster({ paymasterUrl: nexusSmartAccountConfig.paymasterUrl @@ -203,14 +203,14 @@ export class NexusSmartAccount extends BaseSmartContractAccount { // biome-ignore lint/style/noNonNullAssertion: nexusSmartAccountConfig.activeValidationModule! - this.provider = createPublicClient({ + this.publicClient = createPublicClient({ chain: nexusSmartAccountConfig.viemChain ?? nexusSmartAccountConfig.customChain ?? getChain(nexusSmartAccountConfig.chainId), transport: http( nexusSmartAccountConfig.rpcUrl || - getChain(nexusSmartAccountConfig.chainId).rpcUrls.default.http[0] + getChain(nexusSmartAccountConfig.chainId).rpcUrls.default.http[0] ) }) @@ -501,7 +501,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { getContract({ address, abi: parseAbi(ERC20_ABI), - client: this.provider + client: this.publicClient }) ) @@ -527,7 +527,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { ) } - const balance = await this.provider.getBalance({ address: accountAddress }) + const balance = await this.publicClient.getBalance({ address: accountAddress }) result.push({ amount: balance, @@ -646,7 +646,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { // get eth balance if not present in withdrawal requests const nativeTokenAmountToWithdraw = nativeTokenRequest?.amount ?? - (await this.provider.getBalance({ address: accountAddress })) + (await this.publicClient.getBalance({ address: accountAddress })) txs.push({ to: (nativeTokenRequest?.recipient ?? defaultRecipient) as Hex, @@ -731,7 +731,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { this.accountContract = getContract({ address: await this.getAddress(), abi: NexusAccountAbi, - client: this.provider as PublicClient + client: this.publicClient as PublicClient }) } return this.accountContract @@ -823,7 +823,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { // address: ADDRESS_RESOLVER_ADDRESS, // abi: AccountResolverAbi, // client: { - // public: this.provider as PublicClient + // public: this.publicClient as PublicClient // } // }) // // Note: depending on moduleAddress and moduleSetupData passed call this. otherwise could call resolveAddresses() @@ -1321,15 +1321,15 @@ export class NexusSmartAccount extends BaseSmartContractAccount { const finalUserOp = userOp - // if neither user sent gas fee nor the bundler, estimate gas from provider + // if neither user sent gas fee nor the bundler, estimate gas from publicClient if (!userOp.maxFeePerGas && !userOp.maxPriorityFeePerGas) { - const feeData = await this.provider.estimateFeesPerGas() + const feeData = await this.publicClient.estimateFeesPerGas() if (feeData.maxFeePerGas?.toString()) { finalUserOp.maxFeePerGas = feeData.maxFeePerGas } else if (feeData.gasPrice?.toString()) { finalUserOp.maxFeePerGas = feeData.gasPrice } else { - finalUserOp.maxFeePerGas = await this.provider.getGasPrice() + finalUserOp.maxFeePerGas = await this.publicClient.getGasPrice() } if (feeData.maxPriorityFeePerGas?.toString()) { @@ -1337,7 +1337,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { } else if (feeData.gasPrice?.toString()) { finalUserOp.maxPriorityFeePerGas = feeData.gasPrice ?? 0n } else { - finalUserOp.maxPriorityFeePerGas = await this.provider.getGasPrice() + finalUserOp.maxPriorityFeePerGas = await this.publicClient.getGasPrice() } } @@ -1976,7 +1976,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { this.accountAddress ?? (await this.getAccountAddress()) // Check that the account has not already been deployed - const byteCode = await this.provider?.getBytecode({ + const byteCode = await this.publicClient?.getBytecode({ address: accountAddress as Hex }) if (byteCode !== undefined) { @@ -1985,7 +1985,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { // Check that the account has enough native token balance to deploy, if not using a paymaster if (!buildUseropDto?.paymasterServiceData?.mode) { - const nativeTokenBalance = await this.provider?.getBalance({ + const nativeTokenBalance = await this.publicClient?.getBalance({ address: accountAddress }) if (nativeTokenBalance === BigInt(0)) { @@ -2030,7 +2030,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { ["address", "bytes"], [ this.activeValidationModule.getAddress() ?? - this.defaultValidationModule.getAddress(), + this.defaultValidationModule.getAddress(), signature ] ) @@ -2062,6 +2062,44 @@ export class NexusSmartAccount extends BaseSmartContractAccount { return concat([abiEncodedMessage, MAGIC_BYTES]) } + /** + * If your contract supports signing and verifying typed data, + * you should implement this method. + * + * @param _params -- Typed Data params to sign + */ + async signTypedData(typedData: any): Promise<`0x${string}`> { + const types = { + EIP712Domain: getTypesForEIP712Domain({ + domain: typedData.domain + }), + ...typedData.types + } + + validateTypedData({ + domain: typedData.domain, + message: typedData.message, + primaryType: typedData.primaryType, + types: types + } as TypedDataDefinition) + + const typedHash = hashTypedData(typedData) + + const { name, chainId, version } = await accountMetadata( + this.publicClient, + await this.getAddress() + ) + + const wrappedMessageHash = await eip712WrapHash(typedHash, { + name, + chainId: Number(chainId), + version, + verifyingContract: await this.getAddress() + }) + + return await this.signMessage(wrappedMessageHash) + } + async getIsValidSignatureData( messageHash: Hex, signature: Hex @@ -2304,7 +2342,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { callData: (transactions[0].data ?? "0x") as Hex, value: BigInt(transactions[0].value ?? 0n) } - return await this.activeExecutionModule.execute( + return await this.activeExecutionModule.execute( execution, ownedAccountAddress ) @@ -2314,11 +2352,11 @@ export class NexusSmartAccount extends BaseSmartContractAccount { ) } - /** - * Checks if the account contract supports a specific execution mode. - * @param mode - The execution mode to check, represented as a viem Address. - * @returns A promise that resolves to a boolean indicating whether the execution mode is supported. - */ + /** + * Checks if the account contract supports a specific execution mode. + * @param mode - The execution mode to check, represented as a viem Address. + * @returns A promise that resolves to a boolean indicating whether the execution mode is supported. + */ async supportsExecutionMode(mode: Address): Promise { const accountContract = await this._getAccountContract() return (await accountContract.read.supportsExecutionMode([mode])) as boolean diff --git a/src/account/abi/EIP1271Abi.ts b/src/account/abi/EIP1271Abi.ts new file mode 100644 index 00000000..11bb06a3 --- /dev/null +++ b/src/account/abi/EIP1271Abi.ts @@ -0,0 +1,33 @@ +export const EIP1271Abi = [ + { + type: "function", + name: "eip712Domain", + inputs: [], + outputs: [ + { name: "fields", type: "bytes1", internalType: "bytes1" }, + { name: "name", type: "string", internalType: "string" }, + { name: "version", type: "string", internalType: "string" }, + { name: "chainId", type: "uint256", internalType: "uint256" }, + { + name: "verifyingContract", + type: "address", + internalType: "address" + }, + { name: "salt", type: "bytes32", internalType: "bytes32" }, + { name: "extensions", type: "uint256[]", internalType: "uint256[]" } + ], + stateMutability: "view" + }, + { + type: "function", + name: "isValidSignature", + inputs: [ + { name: "data", type: "bytes32", internalType: "bytes32" }, + { name: "signature", type: "bytes", internalType: "bytes" } + ], + outputs: [ + { name: "magicValue", type: "bytes4", internalType: "bytes4" } + ], + stateMutability: "view" + } +] as const diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index ac90a26e..0b1870aa 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -132,3 +132,6 @@ export const MODULE_ENABLE_MODE_TYPE_HASH = keccak256( export const MOCK_MULTI_MODULE_ADDRESS = "0x9C992f91E7Cd4697B81E137007f446E826b8378b" export const MODULE_TYPE_MULTI = 0 + +export const NEXUS_DOMAIN_NAME = "Nexus" +export const NEXUS_DOMAIN_VERSION = "1.0.0-beta" \ No newline at end of file diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index be3cb4c8..07e20f4d 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -179,9 +179,9 @@ export type NexusSmartAccountConfig = NexusSmartAccountConfigBaseProps & export type NexusSmartAccountConfigConstructorProps = NexusSmartAccountConfigBaseProps & - BaseSmartAccountConfig & - ResolvedBundlerProps & - ResolvedValidationProps + BaseSmartAccountConfig & + ResolvedBundlerProps & + ResolvedValidationProps /** * Represents options for building a user operation. @@ -442,13 +442,13 @@ export interface SmartAccountSigner { //#region UserOperationCallData export type UserOperationCallData = | { - /* the target of the call */ - target: Address - /* the data passed to the target */ - data: Hex - /* the amount of native token to send to the target (default: 0) */ - value?: bigint - } + /* the target of the call */ + target: Address + /* the data passed to the target */ + data: Hex + /* the amount of native token to send to the target (default: 0) */ + value?: bigint + } | Hex //#endregion UserOperationCallData @@ -537,10 +537,10 @@ export interface ISmartContractAccount< /** * Signs a typed data object as per ERC-712 * - * @param params - {@link SignTypedDataParams} + * @param typedData * @returns the signed hash for the message passed */ - signTypedData(params: SignTypedDataParams): Promise + signTypedData(typedData: any): Promise /** * If the account is not deployed, it will sign the message and then wrap it in 6492 format @@ -643,3 +643,13 @@ export enum CallType { CALLTYPE_STATIC = '0xFE', CALLTYPE_DELEGATECALL = '0xFF', } + +export type NEXUS_VERSION_TYPE = "1.0.0-beta" + +export type AccountMetadata = { + name: string + version: string + chainId: bigint +} + +export type WithRequired = Required> diff --git a/src/account/utils/Utils.ts b/src/account/utils/Utils.ts index c8b29d03..5fab87b3 100644 --- a/src/account/utils/Utils.ts +++ b/src/account/utils/Utils.ts @@ -1,13 +1,21 @@ import { type Address, + type Client, type Hash, type Hex, + type TypedDataDomain, + type TypedDataParameter, concat, + concatHex, + decodeFunctionResult, + domainSeparator, encodeAbiParameters, + encodeFunctionData, hexToBytes, keccak256, pad, parseAbiParameters, + publicActions, stringToBytes, toBytes, toHex @@ -16,13 +24,16 @@ import type { UserOperationStruct } from "../../account" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, + NEXUS_DOMAIN_NAME, + NEXUS_DOMAIN_VERSION, type SupportedSigner, convertSigner } from "../../account" import { extractChainIdFromBundlerUrl } from "../../bundler" import { extractChainIdFromPaymasterUrl } from "../../bundler" -import type { NexusSmartAccountConfig } from "./Types.js" +import type { AccountMetadata, NexusSmartAccountConfig, WithRequired } from "./Types.js" import { type ModuleType, moduleTypeIds } from "../../modules/index.js" +import { EIP1271Abi } from "../abi/EIP1271Abi.js" /** * pack the userOperation @@ -42,15 +53,15 @@ export function packUserOp( const hashedPaymasterAndData = keccak256( userOperation.paymaster ? concat([ - userOperation.paymaster, - pad(toHex(userOperation.paymasterVerificationGasLimit || BigInt(0)), { - size: 16 - }), - pad(toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), { - size: 16 - }), - userOperation.paymasterData || "0x" - ]) + userOperation.paymaster, + pad(toHex(userOperation.paymasterVerificationGasLimit || BigInt(0)), { + size: 16 + }), + pad(toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), { + size: 16 + }), + userOperation.paymasterData || "0x" + ]) : "0x" ) @@ -109,8 +120,8 @@ export const compareChainIds = async ( ? extractChainIdFromBundlerUrl(biconomySmartAccountConfig.bundlerUrl) : biconomySmartAccountConfig.bundler ? extractChainIdFromBundlerUrl( - biconomySmartAccountConfig.bundler.getBundlerUrl() - ) + biconomySmartAccountConfig.bundler.getBundlerUrl() + ) : undefined const chainIdFromPaymasterUrl = biconomySmartAccountConfig.paymasterUrl @@ -272,3 +283,87 @@ export function _hashTypedData( ]) ) } + +export function getTypesForEIP712Domain({ + domain, +}: { domain?: TypedDataDomain | undefined }): TypedDataParameter[] { + return [ + typeof domain?.name === 'string' && { name: 'name', type: 'string' }, + domain?.version && { name: 'version', type: 'string' }, + typeof domain?.chainId === 'number' && { + name: 'chainId', + type: 'uint256', + }, + domain?.verifyingContract && { + name: 'verifyingContract', + type: 'address', + }, + domain?.salt && { name: 'salt', type: 'bytes32' }, + ].filter(Boolean) as TypedDataParameter[] +} +export const accountMetadata = async ( + client: Client, + accountAddress: Address, +): Promise => { + try { + const domain = await client.request({ + method: "eth_call", + params: [ + { + to: accountAddress, + data: encodeFunctionData({ + abi: EIP1271Abi, + functionName: "eip712Domain" + }) + }, + "latest" + ] + }) + if (domain !== "0x") { + const decoded = decodeFunctionResult({ + abi: [...EIP1271Abi], + functionName: "eip712Domain", + data: domain + }) + return { + name: decoded[1], + version: decoded[2], + chainId: decoded[3] + } + } + } catch (error) { } + return { + name: NEXUS_DOMAIN_NAME, + version: NEXUS_DOMAIN_VERSION, + chainId: client.chain + ? BigInt(client.chain.id) + : BigInt(await client.extend(publicActions).getChainId()) + } +} + + +export const eip712WrapHash = async ( + messageHash: Hex, + domain: WithRequired< + TypedDataDomain, + "name" | "chainId" | "verifyingContract" | "version" + > +): Promise => { + const { name, version, chainId, verifyingContract } = domain + + const _domainSeparator = domainSeparator({ + domain: { + name, + version, + chainId, + verifyingContract + } + }) + + let finalMessageHash = messageHash + + const digest = keccak256( + concatHex(["0x1901", _domainSeparator, finalMessageHash]) + ) + return digest +} \ No newline at end of file diff --git a/tests/account/read.test.ts b/tests/account/read.test.ts index 2273f512..395acec3 100644 --- a/tests/account/read.test.ts +++ b/tests/account/read.test.ts @@ -8,14 +8,14 @@ import { createWalletClient, encodeAbiParameters, encodeFunctionData, - encodePacked, getContract, hashMessage, keccak256, parseAbi, parseAbiParameters, toBytes, - toHex + zeroAddress, + hashTypedData, } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { bsc, baseSepolia } from "viem/chains" @@ -40,6 +40,7 @@ import { getBundlerUrl, getConfig } from "../utils" +import { ethers } from "ethers" describe("Account:Read", () => { const eip1271MagicValue = "0x1626ba7e" @@ -87,11 +88,11 @@ describe("Account:Read", () => { }) ) ) - ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( - [smartAccount, smartAccountTwo].map((account) => - account.getAccountAddress() + ;[smartAccountAddress, smartAccountAddressTwo] = await Promise.all( + [smartAccount, smartAccountTwo].map((account) => + account.getAccountAddress() + ) ) - ) }) test.concurrent( @@ -216,93 +217,93 @@ describe("Account:Read", () => { } ) - test.concurrent( - "should pickup the rpcUrl from viem wallet and ethers", - async () => { - const newRpcUrl = "http://localhost:8545" - const defaultRpcUrl = chain.rpcUrls.default.http[0] //http://127.0.0.1:8545" - - const ethersProvider = new JsonRpcProvider(newRpcUrl) - const ethersSignerWithNewRpcUrl = new Wallet(privateKey, ethersProvider) - - const originalEthersProvider = new JsonRpcProvider( - chain.rpcUrls.default.http[0] - ) - const ethersSigner = new Wallet(privateKey, originalEthersProvider) - - const accountOne = privateKeyToAccount(`0x${privateKey}`) - const walletClientWithNewRpcUrl = createWalletClient({ - account: accountOne, - chain, - transport: http(newRpcUrl) - }) - const [ - smartAccountFromEthersWithNewRpc, - smartAccountFromViemWithNewRpc, - smartAccountFromEthersWithOldRpc, - smartAccountFromViemWithOldRpc - ] = await Promise.all([ - createSmartAccountClient({ - chainId, - signer: ethersSignerWithNewRpcUrl, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: newRpcUrl - }), - createSmartAccountClient({ - chainId, - signer: walletClientWithNewRpcUrl, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: newRpcUrl - }), - createSmartAccountClient({ - chainId, - signer: ethersSigner, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: chain.rpcUrls.default.http[0] - }), - createSmartAccountClient({ - chainId, - signer: walletClient, - bundlerUrl: getBundlerUrl(1337), - rpcUrl: chain.rpcUrls.default.http[0] - }) - ]) - - const [ - smartAccountFromEthersWithNewRpcAddress, - smartAccountFromViemWithNewRpcAddress, - smartAccountFromEthersWithOldRpcAddress, - smartAccountFromViemWithOldRpcAddress - ] = await Promise.all([ - smartAccountFromEthersWithNewRpc.getAccountAddress(), - smartAccountFromViemWithNewRpc.getAccountAddress(), - smartAccountFromEthersWithOldRpc.getAccountAddress(), - smartAccountFromViemWithOldRpc.getAccountAddress() - ]) - - expect( - [ - smartAccountFromEthersWithNewRpcAddress, - smartAccountFromViemWithNewRpcAddress, - smartAccountFromEthersWithOldRpcAddress, - smartAccountFromViemWithOldRpcAddress - ].every(Boolean) - ).toBeTruthy() - - expect(smartAccountFromEthersWithNewRpc.rpcProvider.transport.url).toBe( - newRpcUrl - ) - expect(smartAccountFromViemWithNewRpc.rpcProvider.transport.url).toBe( - newRpcUrl - ) - expect(smartAccountFromEthersWithOldRpc.rpcProvider.transport.url).toBe( - defaultRpcUrl - ) - expect(smartAccountFromViemWithOldRpc.rpcProvider.transport.url).toBe( - defaultRpcUrl - ) - } - ) + // test.concurrent( + // "should pickup the rpcUrl from viem wallet and ethers", + // async () => { + // const newRpcUrl = "http://localhost:8545" + // const defaultRpcUrl = chain.rpcUrls.default.http[0] //http://127.0.0.1:8545" + + // const ethersProvider = new JsonRpcProvider(newRpcUrl) + // const ethersSignerWithNewRpcUrl = new Wallet(privateKey, ethersProvider) + + // const originalEthersProvider = new JsonRpcProvider( + // chain.rpcUrls.default.http[0] + // ) + // const ethersSigner = new Wallet(privateKey, originalEthersProvider) + + // const accountOne = privateKeyToAccount(`0x${privateKey}`) + // const walletClientWithNewRpcUrl = createWalletClient({ + // account: accountOne, + // chain, + // transport: http(newRpcUrl) + // }) + // const [ + // smartAccountFromEthersWithNewRpc, + // smartAccountFromViemWithNewRpc, + // smartAccountFromEthersWithOldRpc, + // smartAccountFromViemWithOldRpc + // ] = await Promise.all([ + // createSmartAccountClient({ + // chainId, + // signer: ethersSignerWithNewRpcUrl, + // bundlerUrl: getBundlerUrl(1337), + // rpcUrl: newRpcUrl + // }), + // createSmartAccountClient({ + // chainId, + // signer: walletClientWithNewRpcUrl, + // bundlerUrl: getBundlerUrl(1337), + // rpcUrl: newRpcUrl + // }), + // createSmartAccountClient({ + // chainId, + // signer: ethersSigner, + // bundlerUrl: getBundlerUrl(1337), + // rpcUrl: chain.rpcUrls.default.http[0] + // }), + // createSmartAccountClient({ + // chainId, + // signer: walletClient, + // bundlerUrl: getBundlerUrl(1337), + // rpcUrl: chain.rpcUrls.default.http[0] + // }) + // ]) + + // const [ + // smartAccountFromEthersWithNewRpcAddress, + // smartAccountFromViemWithNewRpcAddress, + // smartAccountFromEthersWithOldRpcAddress, + // smartAccountFromViemWithOldRpcAddress + // ] = await Promise.all([ + // smartAccountFromEthersWithNewRpc.getAccountAddress(), + // smartAccountFromViemWithNewRpc.getAccountAddress(), + // smartAccountFromEthersWithOldRpc.getAccountAddress(), + // smartAccountFromViemWithOldRpc.getAccountAddress() + // ]) + + // expect( + // [ + // smartAccountFromEthersWithNewRpcAddress, + // smartAccountFromViemWithNewRpcAddress, + // smartAccountFromEthersWithOldRpcAddress, + // smartAccountFromViemWithOldRpcAddress + // ].every(Boolean) + // ).toBeTruthy() + + // expect(smartAccountFromEthersWithNewRpc.rpcProvider.transport.url).toBe( + // newRpcUrl + // ) + // expect(smartAccountFromViemWithNewRpc.rpcProvider.transport.url).toBe( + // newRpcUrl + // ) + // expect(smartAccountFromEthersWithOldRpc.rpcProvider.transport.url).toBe( + // defaultRpcUrl + // ) + // expect(smartAccountFromViemWithOldRpc.rpcProvider.transport.url).toBe( + // defaultRpcUrl + // ) + // } + // ) test.concurrent( "should read estimated user op gas values", @@ -483,19 +484,19 @@ describe("Account:Read", () => { test.concurrent("should have correct fields", async () => { const chainId = 1 const chain = getChain(chainId) - ;[ - "blockExplorers", - "contracts", - "fees", - "formatters", - "id", - "name", - "nativeCurrency", - "rpcUrls", - "serializers" - ].every((field) => { - expect(chain).toHaveProperty(field) - }) + ;[ + "blockExplorers", + "contracts", + "fees", + "formatters", + "id", + "name", + "nativeCurrency", + "rpcUrls", + "serializers" + ].every((field) => { + expect(chain).toHaveProperty(field) + }) }) test.concurrent("should throw an error, chain id not found", async () => { @@ -731,100 +732,135 @@ describe("Account:Read", () => { } ) - test.skip.concurrent( + test.concurrent( "should test isValidSignature EIP712Sign to be valid", async () => { if (await smartAccount.isAccountDeployed()) { - const data = keccak256("0x1234") + const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; + const data = "0x1234"; + const contents = ethers.keccak256(ethers.toUtf8Bytes(data)); - // Define constants as per the original Solidity function - const DOMAIN_NAME = "Nexus" - const DOMAIN_VERSION = "1.0.0-beta" - const DOMAIN_TYPEHASH = - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - const PARENT_TYPEHASH = - "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions) Contents(bytes32 stuff)" - const chainId = baseSepolia.id + const domainSeparator = (await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: parseAbi([ + "function DOMAIN_SEPARATOR() external view returns (bytes32)" + ]), + functionName: "DOMAIN_SEPARATOR" + })) + + const accountDomainStructFields = await getAccountDomainStructFields(publicClient, await smartAccount.getAddress()); + + const parentStructHash = ethers.keccak256( + ethers.solidityPacked(["bytes", "bytes"], [ + ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes32", "bytes32"], + [ethers.keccak256(ethers.toUtf8Bytes(PARENT_TYPEHASH)), contents] + ), + accountDomainStructFields + ]) + ); - // Calculate the domain separator - const domainSeparator = keccak256( - encodeAbiParameters( - parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"), - [ - keccak256(toBytes(DOMAIN_TYPEHASH)), - keccak256(toBytes(DOMAIN_NAME)), - keccak256(toBytes(DOMAIN_VERSION)), - BigInt(chainId), - smartAccountAddress - ] - ) - ) + const dataToSign = ethers.keccak256( + ethers.concat([ + '0x1901', + domainSeparator, + parentStructHash + ]) + ); - const encodedAccountDomainStructFields = - await getAccountDomainStructFields(publicClient, smartAccountAddress) + const signer = new Wallet(privateKey, new JsonRpcProvider(chain.rpcUrls.default.http[0])); + const signature = await signer.signMessage(ethers.getBytes(dataToSign)); // using viem signMessage to sing fails - // Calculate the parent struct hash - const parentStructHash = keccak256( - encodePacked( - ["bytes", "bytes"], - [ - encodeAbiParameters(parseAbiParameters("bytes32, bytes32"), [ - keccak256(toBytes(PARENT_TYPEHASH)), - hashMessage(data) - ]), - encodedAccountDomainStructFields - ] - ) - ) + const contentsType = ethers.toUtf8Bytes("Contents(bytes32 stuff)"); - const dataToSign: Hex = keccak256( - concat(["0x1901" as Hex, domainSeparator, parentStructHash]) - ) + const signatureData = ethers.concat([ + signature, + domainSeparator, + contents, + contentsType, + ethers.toBeHex(contentsType.length, 2) + ]); - let signature = await smartAccount.signMessage(dataToSign) - const contentsType: Hex = toHex("Contents(bytes32 stuff)") - signature = encodePacked( - ["bytes", "bytes", "bytes", "bytes", "uint"], - [ - signature, + const contentsHash = ethers.keccak256( + ethers.concat([ + '0x1901', domainSeparator, - hashMessage(data), - contentsType, - BigInt(contentsType.length) - ] - ) - - const finalSignature = encodePacked( - ["address", "bytes"], - [smartAccount.activeValidationModule.moduleAddress, signature] - ) + contents + ]) + ); - const contents = keccak256( - encodePacked( - ["bytes", "bytes", "bytes"], - ["0x1901", domainSeparator, hashMessage(data)] - ) - ) + const finalSignature = ethers.solidityPacked(["address", "bytes"], [ + K1_VALIDATOR, + signatureData + ]); const contractResponse = await publicClient.readContract({ address: await smartAccount.getAddress(), abi: NexusAccountAbi, functionName: "isValidSignature", - args: [contents, finalSignature] + args: [contentsHash, finalSignature] }) - const viemResponse = await publicClient.verifyMessage({ - address: smartAccountAddress, - message: data, - signature: finalSignature - }) + // const viemResponse = await publicClient.verifyMessage({ + // address: smartAccountAddress, + // message: contentsHash, + // signature: finalSignature as Hex + // }) expect(contractResponse).toBe(eip1271MagicValue) - expect(viemResponse).toBe(true) + // expect(viemResponse).toBe(true) } } ) + test("sign using signTypedData", async () => { + const domain = { + chainId: chain.id, + name: "Test", + verifyingContract: zeroAddress + } + const primaryType = "Test" + const types = { + Test: [ + { + name: "test", + type: "string" + } + ] + } + const message = { + test: "hello world" + } + const typedHash = hashTypedData({ + domain, + primaryType, + types, + message + }) + + const response = await smartAccount.signTypedData({ + domain, + primaryType, + types, + message + }) + + // const ethersSigner = new Wallet(privateKey, new JsonRpcProvider(chain.rpcUrls.default.http[0])) + // const sig = await ethersSigner.signMessage(ethers.getBytes("0x4c9c09d9309ecfd6ee71b194106607f610f209051ecae294f0e6837c3cbbdf72")); + // const finalSig = ethers.concat([K1_VALIDATOR, sig]); + // console.log(finalSig, "final sig from ethers"); + + const nexusResponse = await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: NexusAccountAbi, + functionName: "isValidSignature", + args: [typedHash, response] + }) + + console.log(nexusResponse, "nexusResponse"); + expect(nexusResponse).toEqual("0x1626ba7e") + }) + // test.concurrent("should call isValidSignature for deployed smart account", async () => { // const smartAccount = await createSmartAccountClient({ // signer: walletClient, diff --git a/tests/utils.ts b/tests/utils.ts index e166e22a..315a1fff 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -6,15 +6,14 @@ import { type PublicClient, createPublicClient, createWalletClient, - encodeAbiParameters, parseAbi, - parseAbiParameters } from "viem" import { privateKeyToAccount } from "viem/accounts" import { baseSepolia } from "viem/chains" import type { EIP712DomainReturn } from "../src" import { Logger } from "../src/account/utils/Logger" import { getChain } from "../src/account/utils/getChain" +import { ethers } from "ethers" export const getEnvVars = () => { const fields = [ @@ -29,8 +28,7 @@ export const getEnvVars = () => { const errorFields = fields.filter((field) => !process?.env?.[field]) if (errorFields.length) { throw new Error( - `Missing environment variable${ - errorFields.length > 1 ? "s" : "" + `Missing environment variable${errorFields.length > 1 ? "s" : "" }: ${errorFields.join(", ")}` ) } @@ -39,12 +37,10 @@ export const getEnvVars = () => { bundlerUrlTwo: getBundlerUrl(84532) || "", privateKey: process.env.E2E_PRIVATE_KEY_ONE || "", privateKeyTwo: process.env.E2E_PRIVATE_KEY_TWO || "", - paymasterUrl: `https://paymaster.biconomy.io/api/v1/11155111/${ - process.env.E2E_BICO_PAYMASTER_KEY_AMOY || "" - }`, - paymasterUrlTwo: `https://paymaster.biconomy.io/api/v1/84532/${ - process.env.E2E_BICO_PAYMASTER_KEY_BASE || "" - }`, + paymasterUrl: `https://paymaster.biconomy.io/api/v1/11155111/${process.env.E2E_BICO_PAYMASTER_KEY_AMOY || "" + }`, + paymasterUrlTwo: `https://paymaster.biconomy.io/api/v1/84532/${process.env.E2E_BICO_PAYMASTER_KEY_BASE || "" + }`, chainId: process.env.CHAIN_ID || "0" } } @@ -128,8 +124,7 @@ export const nonZeroBalance = async (address: Hex, tokenAddress?: Hex) => { const balance = await checkBalance(address, tokenAddress) if (balance > BigInt(0)) return throw new Error( - `Insufficient balance ${ - tokenAddress ? `of token ${tokenAddress}` : "of native token" + `Insufficient balance ${tokenAddress ? `of token ${tokenAddress}` : "of native token" } during test setup of owner: ${address}` ) } @@ -153,8 +148,7 @@ export const topUp = async ( if (balanceOfRecipient > amount) { Logger.log( - `balanceOfRecipient (${recipient}) already has enough ${ - token ?? "native token" + `balanceOfRecipient (${recipient}) already has enough ${token ?? "native token" } (${balanceOfRecipient}) during topUp` ) return await Promise.resolve() @@ -162,8 +156,7 @@ export const topUp = async ( if (balanceOfSender < amount) { throw new Error( - `Insufficient ${ - token ? token : "" + `Insufficient ${token ? token : "" }balance during test setup: ${balanceOfSender}` ) } @@ -211,19 +204,18 @@ export const getAccountDomainStructFields = async ( const [fields, name, version, chainId, verifyingContract, salt, extensions] = accountDomainStructFields - const params = parseAbiParameters( - "bytes1, string, string, uint256, address, bytes32, uint256[]" + return ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes1", "bytes32", "bytes32", "uint256", "address", "bytes32", "bytes32"], + [ + fields, + ethers.keccak256(ethers.toUtf8Bytes(name)), + ethers.keccak256(ethers.toUtf8Bytes(version)), + chainId, + verifyingContract, + salt, + ethers.keccak256(ethers.solidityPacked(["uint256[]"], [extensions])) + ] ) - - return encodeAbiParameters(params, [ - fields, - name, - version, - chainId, - verifyingContract, - salt, - extensions - ]) } export const getBundlerUrl = (chainId: number) => From 6a417d06c833d7eaef48e9c0f2c83f206b0c51cb Mon Sep 17 00:00:00 2001 From: VGabriel45 Date: Mon, 26 Aug 2024 16:07:08 +0300 Subject: [PATCH 2/6] feat: added signTypedData for Nexus (draft) --- src/account/NexusSmartAccount.ts | 55 ++++++++++- src/account/utils/Constants.ts | 4 +- src/account/utils/Types.ts | 9 ++ src/account/utils/Utils.ts | 17 +++- tests/account/read.test.ts | 156 +++++++++++++++++++++++++------ tests/utils.ts | 36 +++++++ 6 files changed, 240 insertions(+), 37 deletions(-) diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index 333311ab..974d67ec 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -24,7 +24,9 @@ import { getTypesForEIP712Domain, validateTypedData, type TypedDataDefinition, - hashTypedData + hashTypedData, + toHex, + domainSeparator } from "viem" import { Bundler, @@ -69,7 +71,8 @@ import { import { GENERIC_FALLBACK_SELECTOR, type MODE_MODULE_ENABLE, - MODE_VALIDATION + MODE_VALIDATION, + PARENT_TYPEHASH } from "./utils/Constants.js" import { ADDRESS_ZERO, @@ -101,8 +104,10 @@ import { eip712WrapHash, isNullOrUndefined, isValidRpcUrl, - packUserOp + packUserOp, + typeToString } from "./utils/Utils.js" +import { getAccountDomainStructFieldsViem } from "../../tests/utils.js" // type UserOperationKey = keyof UserOperationStruct export class NexusSmartAccount extends BaseSmartContractAccount { @@ -2090,14 +2095,54 @@ export class NexusSmartAccount extends BaseSmartContractAccount { await this.getAddress() ) - const wrappedMessageHash = await eip712WrapHash(typedHash, { + const accountDomainStructFields = await getAccountDomainStructFieldsViem(this.publicClient, await this.getAddress()); + + const parentStructHash = keccak256( + encodePacked(["bytes", "bytes"], [ + encodeAbiParameters( + parseAbiParameters(["bytes32, bytes32"]), + [keccak256(toBytes(PARENT_TYPEHASH)), typedHash] + ), + accountDomainStructFields + ]) + ); + + const wrappedTypedHash = await eip712WrapHash(parentStructHash, { name, chainId: Number(chainId), version, verifyingContract: await this.getAddress() }) - return await this.signMessage(wrappedMessageHash) + let signature = await this.activeValidationModule.signMessage(toBytes(wrappedTypedHash)) + + const contentsType = toBytes(typeToString(types)[1]); + const _domainSeparator = domainSeparator({ + domain: { + name, + version, + chainId: this.chainId, + verifyingContract: await this.getAddress() + } + }) + const signatureData = concatHex([ + signature, + _domainSeparator, + typedHash, + toHex(contentsType), + toHex(contentsType.length, { size: 2 }) + ]); + + signature = encodePacked( + ["address", "bytes"], + [ + this.activeValidationModule.getAddress() ?? + this.defaultValidationModule.getAddress(), + signatureData + ] + ) + + return signature } async getIsValidSignatureData( diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index 0b1870aa..64306cd9 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -134,4 +134,6 @@ export const MOCK_MULTI_MODULE_ADDRESS = export const MODULE_TYPE_MULTI = 0 export const NEXUS_DOMAIN_NAME = "Nexus" -export const NEXUS_DOMAIN_VERSION = "1.0.0-beta" \ No newline at end of file +export const NEXUS_DOMAIN_VERSION = "1.0.0-beta" + +export const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 07e20f4d..db9a9203 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -653,3 +653,12 @@ export type AccountMetadata = { } export type WithRequired = Required> + +export type TypeField = { + name: string; + type: string; +} + +export type TypeDefinition = { + [key: string]: TypeField[]; +} diff --git a/src/account/utils/Utils.ts b/src/account/utils/Utils.ts index 5fab87b3..7c49d4f5 100644 --- a/src/account/utils/Utils.ts +++ b/src/account/utils/Utils.ts @@ -31,7 +31,7 @@ import { } from "../../account" import { extractChainIdFromBundlerUrl } from "../../bundler" import { extractChainIdFromPaymasterUrl } from "../../bundler" -import type { AccountMetadata, NexusSmartAccountConfig, WithRequired } from "./Types.js" +import type { AccountMetadata, NexusSmartAccountConfig, TypeDefinition, WithRequired } from "./Types.js" import { type ModuleType, moduleTypeIds } from "../../modules/index.js" import { EIP1271Abi } from "../abi/EIP1271Abi.js" @@ -319,6 +319,7 @@ export const accountMetadata = async ( "latest" ] }) + if (domain !== "0x") { const decoded = decodeFunctionResult({ abi: [...EIP1271Abi], @@ -343,7 +344,7 @@ export const accountMetadata = async ( export const eip712WrapHash = async ( - messageHash: Hex, + typedHash: Hex, domain: WithRequired< TypedDataDomain, "name" | "chainId" | "verifyingContract" | "version" @@ -360,10 +361,16 @@ export const eip712WrapHash = async ( } }) - let finalMessageHash = messageHash - const digest = keccak256( - concatHex(["0x1901", _domainSeparator, finalMessageHash]) + concat(["0x1901", _domainSeparator, typedHash]) ) + return digest +} + +export function typeToString(typeDef: TypeDefinition): string[] { + return Object.entries(typeDef).map(([key, fields]) => { + const fieldStrings = fields.map(field => `${field.type} ${field.name}`).join(','); + return `${key}(${fieldStrings})`; + }); } \ No newline at end of file diff --git a/tests/account/read.test.ts b/tests/account/read.test.ts index 395acec3..7ce2fde8 100644 --- a/tests/account/read.test.ts +++ b/tests/account/read.test.ts @@ -14,8 +14,10 @@ import { parseAbi, parseAbiParameters, toBytes, - zeroAddress, hashTypedData, + encodePacked, + concatHex, + toHex, } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { bsc, baseSepolia } from "viem/chains" @@ -37,7 +39,7 @@ import { K1_VALIDATOR, createK1ValidatorModule } from "../../src/modules" import { checkBalance, getAccountDomainStructFields, - getBundlerUrl, + getAccountDomainStructFieldsViem, getConfig } from "../utils" import { ethers } from "ethers" @@ -733,7 +735,7 @@ describe("Account:Read", () => { ) test.concurrent( - "should test isValidSignature EIP712Sign to be valid", + "should test isValidSignature EIP712Sign to be valid with ethers", async () => { if (await smartAccount.isAccountDeployed()) { const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; @@ -801,63 +803,165 @@ describe("Account:Read", () => { args: [contentsHash, finalSignature] }) - // const viemResponse = await publicClient.verifyMessage({ - // address: smartAccountAddress, - // message: contentsHash, - // signature: finalSignature as Hex - // }) + expect(contractResponse).toBe(eip1271MagicValue) + } + } + ) + + test.concurrent( + "should test isValidSignature EIP712Sign to be valid with viem", + async () => { + if (await smartAccount.isAccountDeployed()) { + const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; + + const domain = { + chainId: baseSepolia.id, + name: "Contents", + verifyingContract: smartAccountAddress + } + + const primaryType = "Contents" + const types = { + Contents: [ + { + name: "contents", + type: "bytes32" + } + ] + } + const message = { + contents: keccak256(toBytes("test", { size: 32 })) + } + + const typedHash = hashTypedData({ + domain, + primaryType, + types, + message + }) + + const domainSeparator = (await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: parseAbi([ + "function DOMAIN_SEPARATOR() external view returns (bytes32)" + ]), + functionName: "DOMAIN_SEPARATOR" + })) + + const typedHashHashed = keccak256( + concat([ + '0x1901', + domainSeparator, + typedHash + ]) + ); + + const accountDomainStructFields = await getAccountDomainStructFieldsViem(publicClient, await smartAccount.getAddress()); + + const parentStructHash = keccak256( + encodePacked(["bytes", "bytes"], [ + encodeAbiParameters( + parseAbiParameters(["bytes32, bytes32"]), + [keccak256(toBytes(PARENT_TYPEHASH)), typedHash] + ), + accountDomainStructFields + ]) + ); + + const dataToSign = keccak256( + concat([ + '0x1901', + domainSeparator, + parentStructHash + ]) + ); + + const signature = await walletClient.signMessage({ message: { raw: toBytes(dataToSign) } }); + + const contentsType = toBytes("Contents(bytes32 stuff)"); + + const signatureData = concatHex([ + signature, + domainSeparator, + typedHash, + toHex(contentsType), + toHex(contentsType.length, { size: 2 }) + ]); + + const finalSignature = encodePacked(["address", "bytes"], [ + K1_VALIDATOR, + signatureData + ]); + + const contractResponse = await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: NexusAccountAbi, + functionName: "isValidSignature", + args: [typedHashHashed, finalSignature] + }) expect(contractResponse).toBe(eip1271MagicValue) - // expect(viemResponse).toBe(true) } } ) test("sign using signTypedData", async () => { const domain = { - chainId: chain.id, - name: "Test", - verifyingContract: zeroAddress + chainId: baseSepolia.id, + name: "Neuxs", + verifyingContract: smartAccountAddress } - const primaryType = "Test" + + const primaryType = "Contents" const types = { - Test: [ + Contents: [ { - name: "test", - type: "string" + name: "stuff", + type: "bytes32" } ] } const message = { - test: "hello world" + stuff: keccak256(toBytes("test", { size: 32 })) } - const typedHash = hashTypedData({ + + let typedHash = hashTypedData({ domain, primaryType, types, message }) - const response = await smartAccount.signTypedData({ + const domainSeparator = (await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: parseAbi([ + "function DOMAIN_SEPARATOR() external view returns (bytes32)" + ]), + functionName: "DOMAIN_SEPARATOR" + })) + + const typedHashHashed = keccak256( + concat([ + '0x1901', + domainSeparator, + typedHash + ]) + ); + + const finalSignature = await smartAccount.signTypedData({ domain, primaryType, types, message }) - // const ethersSigner = new Wallet(privateKey, new JsonRpcProvider(chain.rpcUrls.default.http[0])) - // const sig = await ethersSigner.signMessage(ethers.getBytes("0x4c9c09d9309ecfd6ee71b194106607f610f209051ecae294f0e6837c3cbbdf72")); - // const finalSig = ethers.concat([K1_VALIDATOR, sig]); - // console.log(finalSig, "final sig from ethers"); - const nexusResponse = await publicClient.readContract({ address: await smartAccount.getAddress(), abi: NexusAccountAbi, functionName: "isValidSignature", - args: [typedHash, response] + args: [typedHashHashed, finalSignature] }) - console.log(nexusResponse, "nexusResponse"); expect(nexusResponse).toEqual("0x1626ba7e") }) diff --git a/tests/utils.ts b/tests/utils.ts index 315a1fff..596b4e81 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -7,6 +7,11 @@ import { createPublicClient, createWalletClient, parseAbi, + parseAbiParameters, + keccak256, + toBytes, + encodePacked, + encodeAbiParameters, } from "viem" import { privateKeyToAccount } from "viem/accounts" import { baseSepolia } from "viem/chains" @@ -218,5 +223,36 @@ export const getAccountDomainStructFields = async ( ) } +export const getAccountDomainStructFieldsViem = async ( + publicClient: PublicClient, + accountAddress: Address +) => { + const accountDomainStructFields = (await publicClient.readContract({ + address: accountAddress, + abi: parseAbi([ + "function eip712Domain() public view returns (bytes1 fields, string memory name, string memory version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] memory extensions)" + ]), + functionName: "eip712Domain" + })) as EIP712DomainReturn + + const [fields, name, version, chainId, verifyingContract, salt, extensions] = + accountDomainStructFields + + const params = parseAbiParameters(["bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32"]); + + return encodeAbiParameters( + params, + [ + fields, + keccak256(toBytes(name)), + keccak256(toBytes(version)), + chainId, + verifyingContract, + salt, + keccak256(encodePacked(["uint256[]"], [extensions])) + ] + ) +} + export const getBundlerUrl = (chainId: number) => `https://bundler.biconomy.io/api/v2/${chainId}/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f14` From dbf664b86ec5b2c8274e718479b1c27299f94a5e Mon Sep 17 00:00:00 2001 From: VGabriel45 Date: Thu, 12 Sep 2024 17:06:07 +0300 Subject: [PATCH 3/6] feat: signTypedData + tests on baseSepolia testnet --- src/__contracts/addresses.ts | 6 +- src/account/NexusSmartAccount.ts | 42 +- src/account/utils/Utils.ts | 56 +- tests/account.read.test.ts | 625 +++++++----------- .../src/__contracts/abi/MockPermitTokenAbi.ts | 1 + tests/src/__contracts/abi/index.ts | 1 + tests/src/testUtils.ts | 37 +- 7 files changed, 333 insertions(+), 435 deletions(-) create mode 100644 tests/src/__contracts/abi/MockPermitTokenAbi.ts diff --git a/src/__contracts/addresses.ts b/src/__contracts/addresses.ts index 9b61a7b5..f4ae5505 100644 --- a/src/__contracts/addresses.ts +++ b/src/__contracts/addresses.ts @@ -2,9 +2,9 @@ import type { Hex } from "viem" export const addresses: Record = { - Nexus: "0x776d63154D2aa9256D72C420416c930F3B735464", - K1Validator: "0xd98238BBAeA4f91683d250003799EAd31d7F5c55", - K1ValidatorFactory: "0x8025afaD10209b8bEF3A3C94684AaE4D309c9996", + Nexus: "0x2ecd86799137FA35De834Da03D876bcc363ec0c3", + K1Validator: "0xBD654f9F8718840591A2964E2f0cA5b0bB743183", + K1ValidatorFactory: "0xB0D70f13903f3Eb5D378dD6A5aC4E755Fc13dC1b", UniActionPolicy: "0x28120dC008C36d95DE5fa0603526f219c1Ba80f6" } as const export default addresses diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index cc9d915e..c3b336ab 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -23,7 +23,6 @@ import { getTypesForEIP712Domain, validateTypedData, type TypedDataDefinition, - hashTypedData, toHex, domainSeparator } from "viem" @@ -98,11 +97,10 @@ import type { WithdrawalRequest } from "./utils/Types.js" import { - accountMetadata, eip712WrapHash, typeToString } from "./utils/Utils.js" -import { getAccountDomainStructFieldsViem } from "../../tests/utils.js" +import { getAccountDomainStructFields } from "./utils/Utils.js" import { addressEquals, isNullOrUndefined, packUserOp } from "./utils/Utils.js" export class NexusSmartAccount extends BaseSmartContractAccount { @@ -1778,47 +1776,37 @@ export class NexusSmartAccount extends BaseSmartContractAccount { types: types } as TypedDataDefinition) - const typedHash = hashTypedData(typedData) - - const { name, chainId, version } = await accountMetadata( - this.publicClient, - await this.getAddress() - ) + const appDomainSeparator = domainSeparator({ + domain: { + name: typedData.domain.name, + version: typedData.domain.version, + chainId: typedData.domain.chainId, + verifyingContract: typedData.domain.verifyingContract + } + }) - const accountDomainStructFields = await getAccountDomainStructFieldsViem(this.publicClient, await this.getAddress()); + const accountDomainStructFields = await getAccountDomainStructFields(this.publicClient, await this.getAddress()); const parentStructHash = keccak256( encodePacked(["bytes", "bytes"], [ encodeAbiParameters( parseAbiParameters(["bytes32, bytes32"]), - [keccak256(toBytes(PARENT_TYPEHASH)), typedHash] + [keccak256(toBytes(PARENT_TYPEHASH)), typedData.message.stuff] ), accountDomainStructFields ]) ); - const wrappedTypedHash = await eip712WrapHash(parentStructHash, { - name, - chainId: Number(chainId), - version, - verifyingContract: await this.getAddress() - }) + const wrappedTypedHash = await eip712WrapHash(parentStructHash, appDomainSeparator) let signature = await this.activeValidationModule.signMessage(toBytes(wrappedTypedHash)) const contentsType = toBytes(typeToString(types)[1]); - const _domainSeparator = domainSeparator({ - domain: { - name, - version, - chainId: this.chainId, - verifyingContract: await this.getAddress() - } - }) + const signatureData = concatHex([ signature, - _domainSeparator, - typedHash, + appDomainSeparator, + typedData.message.stuff, toHex(contentsType), toHex(contentsType.length, { size: 2 }) ]); diff --git a/src/account/utils/Utils.ts b/src/account/utils/Utils.ts index 3d7ecce9..80d37572 100644 --- a/src/account/utils/Utils.ts +++ b/src/account/utils/Utils.ts @@ -3,24 +3,25 @@ import { type Client, type Hash, type Hex, + PublicClient, type TypedDataDomain, type TypedDataParameter, concat, - concatHex, decodeFunctionResult, - domainSeparator, encodeAbiParameters, encodeFunctionData, + encodePacked, hexToBytes, keccak256, pad, + parseAbi, parseAbiParameters, publicActions, stringToBytes, toBytes, toHex } from "viem" -import type { AccountMetadata, TypeDefinition, UserOperationStruct, WithRequired } from "../../account" +import type { AccountMetadata, EIP712DomainReturn, TypeDefinition, UserOperationStruct, WithRequired } from "../../account" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, @@ -290,24 +291,10 @@ export const accountMetadata = async ( export const eip712WrapHash = async ( typedHash: Hex, - domain: WithRequired< - TypedDataDomain, - "name" | "chainId" | "verifyingContract" | "version" - > + appDomainSeparator: Hex ): Promise => { - const { name, version, chainId, verifyingContract } = domain - - const _domainSeparator = domainSeparator({ - domain: { - name, - version, - chainId, - verifyingContract - } - }) - const digest = keccak256( - concat(["0x1901", _domainSeparator, typedHash]) + concat(["0x1901", appDomainSeparator, typedHash]) ) return digest @@ -318,4 +305,35 @@ export function typeToString(typeDef: TypeDefinition): string[] { const fieldStrings = fields.map(field => `${field.type} ${field.name}`).join(','); return `${key}(${fieldStrings})`; }); +} + +export const getAccountDomainStructFields = async ( + publicClient: PublicClient, + accountAddress: Address +) => { + const accountDomainStructFields = (await publicClient.readContract({ + address: accountAddress, + abi: parseAbi([ + "function eip712Domain() public view returns (bytes1 fields, string memory name, string memory version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] memory extensions)" + ]), + functionName: "eip712Domain" + })) as EIP712DomainReturn + + const [fields, name, version, chainId, verifyingContract, salt, extensions] = + accountDomainStructFields + + const params = parseAbiParameters(["bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32"]); + + return encodeAbiParameters( + params, + [ + fields, + keccak256(toBytes(name)), + keccak256(toBytes(version)), + chainId, + verifyingContract, + salt, + keccak256(encodePacked(["uint256[]"], [extensions])) + ] + ) } \ No newline at end of file diff --git a/tests/account.read.test.ts b/tests/account.read.test.ts index 1533c32e..8ae91308 100644 --- a/tests/account.read.test.ts +++ b/tests/account.read.test.ts @@ -17,7 +17,12 @@ import { keccak256, parseAbiParameters, toBytes, - toHex + toHex, + parseAbi, + PublicClient, + concatHex, + parseEther, + domainSeparator, } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { baseSepolia } from "viem/chains" @@ -29,13 +34,12 @@ import { NATIVE_TOKEN_ALIAS, type NexusSmartAccount, type SupportedSigner, - type Transaction, createSmartAccountClient, eip1271MagicValue, getChain, makeInstallDataAndHash } from "../src/account" -import { CounterAbi } from "./src/__contracts/abi" +import { CounterAbi, MockPermitTokenAbi } from "./src/__contracts/abi" import mockAddresses from "./src/__contracts/mockAddresses" import { type TestFileNetworkType, toNetwork } from "./src/testSetup" import { @@ -51,10 +55,9 @@ import { import type { MasterClient, NetworkConfig, - NetworkConfigWithBundler } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "COMMON_LOCALHOST" +const NETWORK_TYPE: TestFileNetworkType = "PUBLIC_TESTNET" describe("account.read", () => { let network: NetworkConfig @@ -62,6 +65,7 @@ describe("account.read", () => { let chain: Chain let bundlerUrl: string let walletClient: WalletClient + let publicClient: PublicClient // Test utils let testClient: MasterClient @@ -79,6 +83,11 @@ describe("account.read", () => { account = getTestAccount(0) recipientAccount = getTestAccount(3) + publicClient = createPublicClient({ + chain, + transport: http() + }) + walletClient = createWalletClient({ account, chain, @@ -99,13 +108,46 @@ describe("account.read", () => { await killNetwork([network?.rpcPort, network?.bundlerPort]) }) + test("should deploy smart account if not deployed", async () => { + const isDeployed = await smartAccount.isAccountDeployed() + + if (!isDeployed) { + console.log("Smart account not deployed. Deploying...") + + // Fund the account first + await topUp(testClient, smartAccountAddress, parseEther("0.01")) + + // Create a dummy transaction to trigger deployment + const dummyTx = { + to: smartAccountAddress, + value: 0n, + data: "0x" + } + + const userOp = await smartAccount.sendTransaction([dummyTx]) + await userOp.wait() + + const isNowDeployed = await smartAccount.isAccountDeployed() + expect(isNowDeployed).toBe(true) + + console.log("Smart account deployed successfully") + } else { + console.log("Smart account already deployed") + } + + // Verify the account is now deployed + const finalDeploymentStatus = await smartAccount.isAccountDeployed() + expect(finalDeploymentStatus).toBe(true) + }) + test("should fund the smart account", async () => { - await topUp(testClient, smartAccountAddress) + await topUp(testClient, smartAccountAddress, parseEther("0.01")) const [balance] = await smartAccount.getBalances() expect(balance.amount > 0) }) - test("should have account addresses", async () => { + // @note @todo this test is only valid for anvil + test.skip("should have account addresses", async () => { const addresses = await Promise.all([ account.address, smartAccount.getAddress() @@ -117,7 +159,7 @@ describe("account.read", () => { ]) }) - test.skip("should estimate gas for minting an NFT", async () => { + test("should estimate gas for minting an NFT", async () => { const encodedCall = encodeFunctionData({ abi: CounterAbi, functionName: "incrementNumber" @@ -138,7 +180,7 @@ describe("account.read", () => { expect(increasingGasExpenditure).toBeTruthy() }, 60000) - test.skip("should throw if PrivateKeyAccount is used as signer and rpcUrl is not provided", async () => { + test("should throw if PrivateKeyAccount is used as signer and rpcUrl is not provided", async () => { const createSmartAccount = createSmartAccountClient({ chain, signer: account as SupportedSigner, @@ -150,7 +192,7 @@ describe("account.read", () => { ) }, 50000) - test.skip("should get all modules", async () => { + test("should get all modules", async () => { const modules = smartAccount.getInstalledModules() if (await smartAccount.isAccountDeployed()) { expect(modules).resolves @@ -159,7 +201,7 @@ describe("account.read", () => { } }, 30000) - test.skip("should check if module is enabled on the smart account", async () => { + test("should check if module is enabled on the smart account", async () => { const isEnabled = smartAccount.isModuleInstalled({ type: "validator", moduleAddress: addresses.K1Validator @@ -171,7 +213,7 @@ describe("account.read", () => { } }, 30000) - test.skip("enable mode", async () => { + test("enable mode", async () => { const result = makeInstallDataAndHash(account.address, [ { moduleType: "validator", @@ -181,7 +223,7 @@ describe("account.read", () => { expect(result).toBeTruthy() }, 30000) - test.skip("should create a smartAccountClient from an ethers signer", async () => { + test("should create a smartAccountClient from an ethers signer", async () => { const ethersProvider = new JsonRpcProvider(chain.rpcUrls.default.http[0]) const ethersSigner = new Wallet(pKey, ethersProvider) @@ -191,6 +233,8 @@ describe("account.read", () => { bundlerUrl, rpcUrl: chain.rpcUrls.default.http[0] }) + + expect(smartAccount).toBeTruthy(); }) test.skip("should pickup the rpcUrl from viem wallet and ethers", async () => { @@ -277,7 +321,7 @@ describe("account.read", () => { ) }) - test.skip("should read estimated user op gas values", async () => { + test("should read estimated user op gas values", async () => { const tx = { to: recipientAccount.address, data: "0x" @@ -293,12 +337,11 @@ describe("account.read", () => { expect(estimatedGas.preVerificationGas).toBeTruthy() }, 30000) - test.skip("should have an active validation module", async () => { + test("should have an active validation module", async () => { const module = smartAccount.activeValidationModule expect(module).toBeTruthy() }) - // @note Ignored untill we implement Paymaster // test.skip( // "should create a smart account with paymaster by creating instance", // async () => { @@ -314,7 +357,7 @@ describe("account.read", () => { // } // ) - test.skip("should fail to create a smartAccountClient from a walletClient without an account", async () => { + test("should fail to create a smartAccountClient from a walletClient without an account", async () => { const viemWalletNoAccount = createWalletClient({ transport: http(chain.rpcUrls.default.http[0]) }) @@ -329,105 +372,45 @@ describe("account.read", () => { ).rejects.toThrow("Cannot consume a viem wallet without an account") }) - // test.skip( - // "should create a smart account with paymaster with an api key", - // async () => { - // const paymaster = smartAccount.paymaster - // expect(paymaster).not.toBeNull() - // expect(paymaster).not.toBeUndefined() - // } - // ) - - // test.skip("should not throw and error, chain ids match", async () => { - // const mockBundlerUrl = - // "https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44" - // const mockPaymasterUrl = - // "https://paymaster.biconomy.io/api/v1/84532/-RObQRX9ei.fc6918eb-c582-4417-9d5a-0507b17cfe71" - - // const config: NexusSmartAccountConfig = { - // signer: walletClient, - // bundlerUrl: mockBundlerUrl, - // paymasterUrl: mockPaymasterUrl - // } - - // await expect( - // compareChainIds(walletClient, config, false) - // ).resolves.not.toThrow() - // }) - - // test.skip( - // "should throw and error, bundlerUrl chain id and paymaster url chain id does not match with validation module", - // async () => { - // const mockPaymasterUrl = - // "https://paymaster.biconomy.io/api/v1/1337/-RObQRX9ei.fc6918eb-c582-4417-9d5a-0507b17cfe71" - - // const k1ValidationModule = await createK1ValidatorModule( - // smartAccount.getSigner() - // ) - - // const config: NexusSmartAccountConfig = { - // chain, - // defaultValidationModule: k1ValidationModule, - // activeValidationModule: k1ValidationModule, - // bundlerUrl, - // paymasterUrl: mockPaymasterUrl - // } - - // } - // ) - - // test.skip( - // "should throw and error, signer has chain id (56) and paymasterUrl has chain id (11155111)", - // async () => { - // const mockPaymasterUrl = - // "https://paymaster.biconomy.io/api/v1/11155111/-RObQRX9ei.fc6918eb-c582-4417-9d5a-0507b17cfe71" - - // const walletClientBsc = createWalletClient({ - // account: walletClient.account, - // chain: bsc, - // transport: http(bsc.rpcUrls.default.http[0]) - // }) - - // const config: NexusSmartAccountConfig = { - // chain, - // signer: walletClientBsc, - // bundlerUrl, - // paymasterUrl: mockPaymasterUrl - // } - - // } - // ) + test.skip( + "should create a smart account with paymaster with an api key", + async () => { + const paymaster = smartAccount.paymaster + expect(paymaster).not.toBeNull() + expect(paymaster).not.toBeUndefined() + } + ) - test.skip("should return chain object for chain id 1", async () => { + test("should return chain object for chain id 1", async () => { const chainId = 1 const chain = getChain(chainId) expect(chain.id).toBe(chainId) }) - test.skip("should have correct fields", async () => { + test("should have correct fields", async () => { const chainId = 1 const chain = getChain(chainId) - ;[ - "blockExplorers", - "contracts", - "fees", - "formatters", - "id", - "name", - "nativeCurrency", - "rpcUrls", - "serializers" - ].every((field) => { - expect(chain).toHaveProperty(field) - }) + ;[ + "blockExplorers", + "contracts", + "fees", + "formatters", + "id", + "name", + "nativeCurrency", + "rpcUrls", + "serializers" + ].every((field) => { + expect(chain).toHaveProperty(field) + }) }) - test.skip("should throw an error, chain id not found", async () => { + test("should throw an error, chain id not found", async () => { const chainId = 0 expect(() => getChain(chainId)).toThrow(ERROR_MESSAGES.CHAIN_NOT_FOUND) }) - test.skip("should have matching counterFactual address from the contracts with smartAccount.getAddress()", async () => { + test("should have matching counterFactual address from the contracts with smartAccount.getAddress()", async () => { const client = createWalletClient({ account, chain, @@ -442,11 +425,6 @@ describe("account.read", () => { const smartAccountAddressFromSDK = await smartAccount.getAccountAddress() - const publicClient = createPublicClient({ - chain, - transport: http() - }) - const factoryContract = getContract({ address: addresses.K1ValidatorFactory, abi: K1ValidatorFactoryAbi, @@ -464,7 +442,7 @@ describe("account.read", () => { expect(smartAccountAddressFromSDK).toBe(smartAccountAddressFromContracts) }) - test.skip("should be deployed to counterfactual address", async () => { + test("should be deployed to counterfactual address", async () => { const accountAddress = await smartAccount.getAccountAddress() const byteCode = await testClient.getBytecode({ address: accountAddress as Hex @@ -476,7 +454,7 @@ describe("account.read", () => { } }, 10000) - test.skip("should check if ecdsaOwnershipModule is enabled", async () => { + test("should check if K1Validator is enabled", async () => { const ecdsaOwnershipModule = addresses.K1Validator expect(ecdsaOwnershipModule).toBe( @@ -484,7 +462,7 @@ describe("account.read", () => { ) }) - test.skip("should fail to deploy a smart account if no native token balance or paymaster", async () => { + test("should fail to deploy a smart account if no native token balance or paymaster", async () => { const newPrivateKey = generatePrivateKey() const newAccount = privateKeyToAccount(newPrivateKey) @@ -505,7 +483,7 @@ describe("account.read", () => { ) }) - test.skip("should fail to deploy a smart account if already deployed", async () => { + test("should fail to deploy a smart account if already deployed", async () => { if (await smartAccount.isAccountDeployed()) { expect(async () => smartAccount.deploy()).rejects.toThrow( ERROR_MESSAGES.ACCOUNT_ALREADY_DEPLOYED @@ -529,7 +507,7 @@ describe("account.read", () => { expect(tokenBalanceBefore).toBe(tokenBalanceFromSmartAccount.amount) }) - test.skip("should error if no recipient exists", async () => { + test("should error if no recipient exists", async () => { const token: Hex = "0x69835C1f31ed0721A05d5711C1d669C10802a3E1" const txs = [ @@ -542,13 +520,13 @@ describe("account.read", () => { ) }) - test.skip("should error when withdraw all of native token is attempted without an amount explicitly set", async () => { + test("should error when withdraw all of native token is attempted without an amount explicitly set", async () => { expect(async () => smartAccount.withdraw(null, account.address) ).rejects.toThrow(ERROR_MESSAGES.NATIVE_TOKEN_WITHDRAWAL_WITHOUT_AMOUNT) }, 6000) - test.skip("should check native token balance and more token info for smartAccount", async () => { + test("should check native token balance and more token info for smartAccount", async () => { const [ethBalanceFromSmartAccount] = await smartAccount.getBalances() expect(ethBalanceFromSmartAccount.amount).toBeGreaterThan(0n) @@ -557,22 +535,21 @@ describe("account.read", () => { expect(ethBalanceFromSmartAccount.decimals).toBe(18) }, 60000) - // @note Skip until we implement the Paymaster - // test.skip( - // "should check balance of supported token", - // async () => { - // const tokens = await smartAccount.getSupportedTokens() - // const [firstToken] = tokens - - // expect(tokens.length).toBeGreaterThan(0) - // expect(tokens[0]).toHaveProperty("balance") - // expect(firstToken.balance.amount).toBeGreaterThanOrEqual(0n) - // }, - // 60000 - // ) + test.skip( + "should check balance of supported token", + async () => { + const tokens = await smartAccount.getSupportedTokens() + const [firstToken] = tokens + + expect(tokens.length).toBeGreaterThan(0) + expect(tokens[0]).toHaveProperty("balance") + expect(firstToken.balance.amount).toBeGreaterThanOrEqual(0n) + }, + 60000 + ) // @note Nexus SA signature needs to contain the validator module address in the first 20 bytes - test.skip("should test isValidSignature PersonalSign to be valid", async () => { + test("should test isValidSignature PersonalSign to be valid", async () => { if (await smartAccount.isAccountDeployed()) { const data = hashMessage("0x1234") @@ -631,97 +608,6 @@ describe("account.read", () => { } }) - test.skip("should test isValidSignature EIP712Sign to be valid", async () => { - if (await smartAccount.isAccountDeployed()) { - const data = keccak256("0x1234") - - // Define constants as per the original Solidity function - const DOMAIN_NAME = "Nexus" - const DOMAIN_VERSION = "1.0.0-beta" - const DOMAIN_TYPEHASH = - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - const PARENT_TYPEHASH = - "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions) Contents(bytes32 stuff)" - const chainId = baseSepolia.id - - // Calculate the domain separator - const domainSeparator = keccak256( - encodeAbiParameters( - parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"), - [ - keccak256(toBytes(DOMAIN_TYPEHASH)), - keccak256(toBytes(DOMAIN_NAME)), - keccak256(toBytes(DOMAIN_VERSION)), - BigInt(chainId), - smartAccountAddress - ] - ) - ) - - const encodedAccountDomainStructFields = - await getAccountDomainStructFields(testClient, smartAccountAddress) - - // Calculate the parent struct hash - const parentStructHash = keccak256( - encodePacked( - ["bytes", "bytes"], - [ - encodeAbiParameters(parseAbiParameters("bytes32, bytes32"), [ - keccak256(toBytes(PARENT_TYPEHASH)), - hashMessage(data) - ]), - encodedAccountDomainStructFields - ] - ) - ) - - const dataToSign: Hex = keccak256( - concat(["0x1901" as Hex, domainSeparator, parentStructHash]) - ) - - let signature = await smartAccount.signMessage(dataToSign) - const contentsType: Hex = toHex("Contents(bytes32 stuff)") - signature = encodePacked( - ["bytes", "bytes", "bytes", "bytes", "uint"], - [ - signature, - domainSeparator, - hashMessage(data), - contentsType, - BigInt(contentsType.length) - ] - ) - - const finalSignature = encodePacked( - ["address", "bytes"], - [smartAccount.activeValidationModule.moduleAddress, signature] - ) - - const contents = keccak256( - encodePacked( - ["bytes", "bytes", "bytes"], - ["0x1901", domainSeparator, hashMessage(data)] - ) - ) - - const contractResponse = await testClient.readContract({ - address: await smartAccount.getAddress(), - abi: NexusAbi, - functionName: "isValidSignature", - args: [contents, finalSignature] - }) - - const viemResponse = await testClient.verifyMessage({ - address: smartAccountAddress, - message: data, - signature: finalSignature - }) - - expect(contractResponse).toBe(eip1271MagicValue) - expect(viemResponse).toBe(true) - } - }) - test("should have consistent behaviour between ethers.AbiCoder.defaultAbiCoder() and viem.encodeAbiParameters()", async () => { const expectedResult = "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b90600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" @@ -772,165 +658,168 @@ describe("account.read", () => { expect(executionCalldataPrepWithViem).toBe(expectedResult) }) - // test.skip("should call isValidSignature for deployed smart account", async () => { - // const smartAccount = await createSmartAccountClient({ - // signer: walletClient, - // bundlerUrl - // }) - - // const message = "hello world" - // const signature = await smartAccount.signMessage(message) - - // const isVerified = await publicClient.readContract({ - // address: await smartAccount.getAddress(), - // abi: NexusAccountAbi, - // functionName: "isValidSignature", - // args: [hashMessage(message), signature] - // }) - - // expect(isVerified).toBe(eip1271MagicValue) - // }) - - // test.skip("should verifySignature of not deployed", async () => { - // const undeployedSmartAccount = await createSmartAccountClient({ - // signer: walletClient, - // bundlerUrl, - // index: 99n - // }) - // const isDeployed = await undeployedSmartAccount.isAccountDeployed() - // if (!isDeployed) { - // const message = "hello world" - - // const signature = await smartAccount.signMessage(message) - // // OR - // // const signature = await smartAccount.signMessageWith6492(message) - - // const isVerified = await publicClient.readContract({ - // address: await smartAccount.getAddress(), - // abi: NexusAccountAbi, - // functionName: "isValidSignature", - // args: [hashMessage(message), signature] - // }) - - // expect(isVerified).toBe(eip1271MagicValue) - // } - // }) - - // test.skip("should verifySignature using viem", async () => { - // const isDeployed = await smartAccount.isAccountDeployed() - // if (isDeployed) { - // const message = "0x123" - - // const signature = await smartAccount.signMessage(message) - - // console.log(signature, 'signature'); - // console.log(hashMessage(message), 'hashMessage(message)'); - - // // const isVerified = await verifyMessage(publicClient, { - // // address: await smartAccount.getAddress(), - // // message, - // // signature, - // // }) + test.concurrent( + "should test isValidSignature EIP712Sign to be valid with viem", + async () => { + if (await smartAccount.isAccountDeployed()) { + const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; + + const message = { + contents: keccak256(toBytes("test", { size: 32 })) + } + + const domainSeparator = (await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: parseAbi([ + "function DOMAIN_SEPARATOR() external view returns (bytes32)" + ]), + functionName: "DOMAIN_SEPARATOR" + })) + + const typedHashHashed = keccak256( + concat([ + '0x1901', + domainSeparator, + message.contents + ]) + ); + + const accountDomainStructFields = await getAccountDomainStructFields(testClient, await smartAccount.getAddress()); + + const parentStructHash = keccak256( + encodePacked(["bytes", "bytes"], [ + encodeAbiParameters( + parseAbiParameters(["bytes32, bytes32"]), + [keccak256(toBytes(PARENT_TYPEHASH)), message.contents] + ), + accountDomainStructFields + ]) + ); + + const dataToSign = keccak256( + concat([ + '0x1901', + domainSeparator, + parentStructHash + ]) + ); + + const signature = await walletClient.signMessage({ message: { raw: toBytes(dataToSign) }, account }); + + const contentsType = toBytes("Contents(bytes32 stuff)"); + + const signatureData = concatHex([ + signature, + domainSeparator, + message.contents, + toHex(contentsType), + toHex(contentsType.length, { size: 2 }) + ]); + + const finalSignature = encodePacked(["address", "bytes"], [ + addresses.K1Validator, + signatureData + ]); + + const contractResponse = await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: NexusAbi, + functionName: "isValidSignature", + args: [typedHashHashed, finalSignature] + }) + + expect(contractResponse).toBe(eip1271MagicValue) + } else { + throw new Error("Smart account is not deployed") + } + } + ) + + test("should sign using signTypedData SDK method", async () => { + const permitTestTokenAddress = "0xd8a978B9a0e1Af5579314E626D77fc1C9fF76c7D" as Hex; + const appDomain = { + chainId: network.chain.id, + name: "TestToken", + verifyingContract: permitTestTokenAddress, + version: "1" + } - // const isVerified = await publicClient.readContract({ - // address: await smartAccount.getAddress(), - // abi: NexusAccountAbi, - // functionName: "isValidSignature", - // args: [hashMessage(message), signature] - // }) + const primaryType = "Contents" + const types = { + Contents: [ + { + name: "stuff", + type: "bytes32" + } + ] + } - // console.log(isVerified, "isVerified"); + const permitTypehash = keccak256(toBytes("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")); + const nonce = await publicClient.readContract({ + address: permitTestTokenAddress, + abi: MockPermitTokenAbi, + functionName: "nonces", + args: [smartAccountAddress] + }) as bigint; - // expect(isVerified).toBe(eip1271MagicValue) - // } - // }) + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now - // @note Removed untill we implement the Bundler (Pimlico's bundler does no behave as expected in this test) - // test.skip( - // "should simulate a user operation execution, expecting to fail", - // async () => { - // const smartAccount = await createSmartAccountClient({ - // signer: walletClient, - // bundlerUrl - // }) + const message = { + stuff: keccak256( + encodeAbiParameters( + parseAbiParameters("bytes32, address, address, uint256, uint256, uint256"), + [permitTypehash, smartAccountAddress, smartAccountAddress, parseEther("2"), nonce, deadline] + ) + ) + }; - // const balances = await smartAccount.getBalances() - // expect(balances[0].amount).toBeGreaterThan(0n) + const appDomainSeparator = domainSeparator( + { + domain: appDomain + } + ) - // const encodedCall = encodeFunctionData({ - // abi: parseAbi(["function deposit()"]), - // functionName: "deposit" - // }) + const contentsHash = keccak256( + concat([ + '0x1901', + appDomainSeparator, + message.stuff + ]) + ); - // const amoyTestContract = "0x59Dbe91FBa486CA10E4ad589688Fe547a48bd62A" - - // // fail if value is not bigger than 1 - // // the contract call requires a deposit of at least 1 wei - // const tx1 = { - // to: amoyTestContract as Hex, - // data: encodedCall, - // value: 0 - // } - // const tx2 = { - // to: amoyTestContract as Hex, - // data: encodedCall, - // value: 2 - // } - - // await expect(smartAccount.buildUserOp([tx1, tx2])).rejects.toThrow() - // } - // ) + const finalSignature = await smartAccount.signTypedData({ + domain: appDomain, + primaryType, + types, + message + }) - // @note Removed untill we implement the Bundler (Pimlico's bundler does no behave as expected in this test) - // test.skip( - // "should simulate a user operation execution, expecting to pass execution", - // async () => { - // const smartAccount = await createSmartAccountClient({ - // signer: walletClient, - // bundlerUrl - // }) + const nexusResponse = await publicClient.readContract({ + address: await smartAccount.getAddress(), + abi: NexusAbi, + functionName: "isValidSignature", + args: [contentsHash, finalSignature] + }) - // const balances = await smartAccount.getBalances() - // expect(balances[0].amount).toBeGreaterThan(0n) + const permitTokenResponse = await walletClient.writeContract({ + account: account, + address: permitTestTokenAddress, + abi: MockPermitTokenAbi, + functionName: "permitWith1271", + chain: network.chain, + args: [await smartAccount.getAddress(), await smartAccount.getAddress(), parseEther("2"), deadline, finalSignature] + }) - // const encodedCall = encodeFunctionData({ - // abi: parseAbi(["function deposit()"]), - // functionName: "deposit" - // }) + await publicClient.waitForTransactionReceipt({ hash: permitTokenResponse }); - // const amoyTestContract = "0x59Dbe91FBa486CA10E4ad589688Fe547a48bd62A" - - // // fail if value is not bigger than 1 - // // the contract call requires a deposit of at least 1 wei - // const tx1 = { - // to: amoyTestContract as Hex, - // data: encodedCall, - // value: 2 - // } - // const tx2 = { - // to: amoyTestContract as Hex, - // data: encodedCall, - // value: 2 - // } - - // await expect(smartAccount.buildUserOp([tx1, tx2])).resolves.toBeTruthy() - // } - // ) + const allowance = await publicClient.readContract({ + address: permitTestTokenAddress, + abi: MockPermitTokenAbi, + functionName: "allowance", + args: [await smartAccount.getAddress(), await smartAccount.getAddress()] + }) - // test.skip("Should verify supported modes", async () => { - // expect( - // await smartAccount.supportsExecutionMode(ACCOUNT_MODES.DEFAULT_SINGLE) - // ).to.be.true - // expect( - // await smartAccount.supportsExecutionMode(ACCOUNT_MODES.DEFAULT_BATCH) - // ).to.be.true - // expect(await smartAccount.supportsExecutionMode(ACCOUNT_MODES.TRY_BATCH)).to - // .be.true - // expect(await smartAccount.supportsExecutionMode(ACCOUNT_MODES.TRY_SINGLE)) - // .to.be.true - - // expect( - // await smartAccount.supportsExecutionMode(ACCOUNT_MODES.DELEGATE_SINGLE) - // ).to.be.false - // }) + expect(allowance).toEqual(parseEther("2")); + expect(nexusResponse).toEqual("0x1626ba7e") + }) }) diff --git a/tests/src/__contracts/abi/MockPermitTokenAbi.ts b/tests/src/__contracts/abi/MockPermitTokenAbi.ts new file mode 100644 index 00000000..d28d9133 --- /dev/null +++ b/tests/src/__contracts/abi/MockPermitTokenAbi.ts @@ -0,0 +1 @@ +export const MockPermitTokenAbi = [{ "inputs": [{ "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "symbol", "type": "string" }], "stateMutability": "nonpayable", "type": "constructor" }, { "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": "address", "name": "signer", "type": "address" }], "name": "ERC1271InvalidSigner", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "allowance", "type": "uint256" }, { "internalType": "uint256", "name": "needed", "type": "uint256" }], "name": "ERC20InsufficientAllowance", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "uint256", "name": "balance", "type": "uint256" }, { "internalType": "uint256", "name": "needed", "type": "uint256" }], "name": "ERC20InsufficientBalance", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "approver", "type": "address" }], "name": "ERC20InvalidApprover", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }], "name": "ERC20InvalidReceiver", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], "name": "ERC20InvalidSender", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }], "name": "ERC20InvalidSpender", "type": "error" }, { "inputs": [{ "internalType": "uint256", "name": "deadline", "type": "uint256" }], "name": "ERC2612ExpiredSignature", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "signer", "type": "address" }, { "internalType": "address", "name": "owner", "type": "address" }], "name": "ERC2612InvalidSigner", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "currentNonce", "type": "uint256" }], "name": "InvalidAccountNonce", "type": "error" }, { "inputs": [], "name": "InvalidShortString", "type": "error" }, { "inputs": [{ "internalType": "string", "name": "str", "type": "string" }], "name": "StringTooLong", "type": "error" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [], "name": "EIP712DomainChanged", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "inputs": [], "name": "DOMAIN_SEPARATOR", "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "PERMIT_TYPEHASH_LOCAL", "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], "name": "balanceOf", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "eip712Domain", "outputs": [{ "internalType": "bytes1", "name": "fields", "type": "bytes1" }, { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "version", "type": "string" }, { "internalType": "uint256", "name": "chainId", "type": "uint256" }, { "internalType": "address", "name": "verifyingContract", "type": "address" }, { "internalType": "bytes32", "name": "salt", "type": "bytes32" }, { "internalType": "uint256[]", "name": "extensions", "type": "uint256[]" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "mint", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }], "name": "nonces", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }, { "internalType": "uint8", "name": "v", "type": "uint8" }, { "internalType": "bytes32", "name": "r", "type": "bytes32" }, { "internalType": "bytes32", "name": "s", "type": "bytes32" }], "name": "permit", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }, { "internalType": "bytes", "name": "signature", "type": "bytes" }], "name": "permitWith1271", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalSupply", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transfer", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }] \ No newline at end of file diff --git a/tests/src/__contracts/abi/index.ts b/tests/src/__contracts/abi/index.ts index 0b57abb4..954178d2 100644 --- a/tests/src/__contracts/abi/index.ts +++ b/tests/src/__contracts/abi/index.ts @@ -10,3 +10,4 @@ export * from "./MockRegistryAbi" export * from "./MockHandlerAbi" export * from "./BootstrapAbi" export * from "./MockExecutorAbi" +export * from "./MockPermitTokenAbi" \ No newline at end of file diff --git a/tests/src/testUtils.ts b/tests/src/testUtils.ts index 4463d4c7..ad90f8e7 100644 --- a/tests/src/testUtils.ts +++ b/tests/src/testUtils.ts @@ -15,7 +15,10 @@ import { parseAbi, parseAbiParameters, publicActions, - walletActions + walletActions, + keccak256, + toBytes, + encodePacked } from "viem" import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts" import { @@ -260,8 +263,7 @@ export const nonZeroBalance = async ( const balance = await checkBalance(testClient, address, tokenAddress) if (balance > BigInt(0)) return throw new Error( - `Insufficient balance ${ - tokenAddress ? `of token ${tokenAddress}` : "of native token" + `Insufficient balance ${tokenAddress ? `of token ${tokenAddress}` : "of native token" } during test setup of owner: ${address}` ) } @@ -367,8 +369,7 @@ export const topUp = async ( if (balanceOfRecipient > amount) { Logger.log( - `balanceOfRecipient (${recipient}) already has enough ${ - token ?? "native token" + `balanceOfRecipient (${recipient}) already has enough ${token ?? "native token" } (${balanceOfRecipient}) during safeTopUp` ) return await Promise.resolve() @@ -394,7 +395,6 @@ export const topUp = async ( return testClient.waitForTransactionReceipt({ hash }) } -// Returns the encoded EIP-712 domain struct fields. export const getAccountDomainStructFields = async ( testClient: MasterClient, accountAddress: Address @@ -410,19 +410,20 @@ export const getAccountDomainStructFields = async ( const [fields, name, version, chainId, verifyingContract, salt, extensions] = accountDomainStructFields - const params = parseAbiParameters( - "bytes1, string, string, uint256, address, bytes32, uint256[]" + const params = parseAbiParameters(["bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32"]); + + return encodeAbiParameters( + params, + [ + fields, + keccak256(toBytes(name)), + keccak256(toBytes(version)), + chainId, + verifyingContract, + salt, + keccak256(encodePacked(["uint256[]"], [extensions])) + ] ) - - return encodeAbiParameters(params, [ - fields, - name, - version, - chainId, - verifyingContract, - salt, - extensions - ]) } export const getBundlerUrl = (chainId: number) => From ffd06b31e2820ea46665b4babcdf77b243f95dfd Mon Sep 17 00:00:00 2001 From: VGabriel45 Date: Thu, 12 Sep 2024 17:11:07 +0300 Subject: [PATCH 4/6] refactor: fix build errors --- src/account/NexusSmartAccount.ts | 2 -- src/account/utils/Utils.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index c3b336ab..034b640f 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -108,8 +108,6 @@ export class NexusSmartAccount extends BaseSmartContractAccount { private chainId: number - publicClient: PublicClient - paymaster?: IPaymaster bundler?: IBundler diff --git a/src/account/utils/Utils.ts b/src/account/utils/Utils.ts index 80d37572..9bfc01c4 100644 --- a/src/account/utils/Utils.ts +++ b/src/account/utils/Utils.ts @@ -3,7 +3,7 @@ import { type Client, type Hash, type Hex, - PublicClient, + type PublicClient, type TypedDataDomain, type TypedDataParameter, concat, @@ -21,7 +21,7 @@ import { toBytes, toHex } from "viem" -import type { AccountMetadata, EIP712DomainReturn, TypeDefinition, UserOperationStruct, WithRequired } from "../../account" +import type { AccountMetadata, EIP712DomainReturn, TypeDefinition, UserOperationStruct } from "../../account" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, From 8409032039a804452c22f787913269702a7cf2d3 Mon Sep 17 00:00:00 2001 From: VGabriel45 Date: Thu, 12 Sep 2024 18:20:51 +0300 Subject: [PATCH 5/6] feat: updated to latest nexus contracts --- bun.lockb | Bin 352626 -> 352659 bytes package.json | 4 +- scripts/fetch:deployment.ts | 2 +- src/__contracts/abi/EIP1271Abi.ts | 60 +- src/__contracts/abi/K1ValidatorAbi.ts | 76 +++ src/__contracts/abi/NexusAbi.ts | 67 +- src/__contracts/addresses.ts | 6 +- src/account/BaseSmartContractAccount.ts | 9 +- src/account/NexusSmartAccount.ts | 54 +- src/account/utils/Constants.ts | 3 +- src/account/utils/Types.ts | 30 +- src/account/utils/Utils.ts | 89 +-- tests/account.read.test.ts | 190 +++--- tests/src/__contracts/abi/MockHookAbi.ts | 6 +- .../src/__contracts/abi/MockPermitTokenAbi.ts | 1 - tests/src/__contracts/abi/MockValidatorAbi.ts | 71 ++ .../src/__contracts/abi/TokenWithPermitAbi.ts | 611 ++++++++++++++++++ tests/src/__contracts/abi/index.ts | 2 +- tests/src/__contracts/mockAddresses.ts | 25 +- tests/src/testUtils.ts | 39 +- 20 files changed, 1089 insertions(+), 256 deletions(-) delete mode 100644 tests/src/__contracts/abi/MockPermitTokenAbi.ts create mode 100644 tests/src/__contracts/abi/TokenWithPermitAbi.ts diff --git a/bun.lockb b/bun.lockb index f13bf97af633eafe847c7d91826756e9d8973153..951562036dad3a1f86819f65ad7d3516879c1305 100755 GIT binary patch delta 42669 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 delta 42520 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

BEj?>Wkz|4pI!gA~Ms8@3*} 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 = { - Nexus: "0x2ecd86799137FA35De834Da03D876bcc363ec0c3", - K1Validator: "0xBD654f9F8718840591A2964E2f0cA5b0bB743183", - K1ValidatorFactory: "0xB0D70f13903f3Eb5D378dD6A5aC4E755Fc13dC1b", + Nexus: "0x21f4C007C9f091B93B7C1C6911E13ACcd3DAd403", + K1Validator: "0x6854688d3D9A87a33Addd5f4deB5cea1B97fa5b7", + K1ValidatorFactory: "0x976869CF9c5Dd5046b41963EF1bBcE62b5366869", UniActionPolicy: "0x28120dC008C36d95DE5fa0603526f219c1Ba80f6" } as const export default addresses diff --git a/src/account/BaseSmartContractAccount.ts b/src/account/BaseSmartContractAccount.ts index 8ef89c2e..92d77413 100644 --- a/src/account/BaseSmartContractAccount.ts +++ b/src/account/BaseSmartContractAccount.ts @@ -7,7 +7,7 @@ import { type PublicClient, createPublicClient, getContract, - trim, + trim } from "viem" import { EntrypointAbi } from "../__contracts/abi/EntryPointABI.js" import contracts from "../__contracts/index.js" @@ -29,7 +29,8 @@ export enum DeploymentState { export abstract class BaseSmartContractAccount< TSigner extends SmartAccountSigner = SmartAccountSigner -> implements ISmartContractAccount { +> implements ISmartContractAccount +{ protected factoryAddress: Address protected deploymentState: DeploymentState = DeploymentState.UNDEFINED @@ -152,9 +153,7 @@ export abstract class BaseSmartContractAccount< * * @param params -- Typed Data params to sign */ - async signTypedDataWith6492( - typedData: any - ): Promise<`0x${string}`> { + async signTypedDataWith6492(typedData: any): Promise<`0x${string}`> { const [isDeployed, signature] = await Promise.all([ this.isAccountDeployed(), this.signTypedData(typedData) diff --git a/src/account/NexusSmartAccount.ts b/src/account/NexusSmartAccount.ts index 034b640f..9d3ad3ff 100644 --- a/src/account/NexusSmartAccount.ts +++ b/src/account/NexusSmartAccount.ts @@ -5,26 +5,26 @@ import { type Hash, type Hex, type PublicClient, + type TypedDataDefinition, type WalletClient, concat, concatHex, decodeFunctionData, + domainSeparator, encodeAbiParameters, encodeFunctionData, encodePacked, formatUnits, getAddress, getContract, + getTypesForEIP712Domain, keccak256, pad, parseAbi, parseAbiParameters, toBytes, - getTypesForEIP712Domain, - validateTypedData, - type TypedDataDefinition, toHex, - domainSeparator + validateTypedData } from "viem" import contracts from "../__contracts" import { NexusAbi } from "../__contracts/abi" @@ -96,10 +96,7 @@ import type { TransferOwnershipCompatibleModule, WithdrawalRequest } from "./utils/Types.js" -import { - eip712WrapHash, - typeToString -} from "./utils/Utils.js" +import { eip712WrapHash, typeToString } from "./utils/Utils.js" import { getAccountDomainStructFields } from "./utils/Utils.js" import { addressEquals, isNullOrUndefined, packUserOp } from "./utils/Utils.js" @@ -1721,7 +1718,7 @@ export class NexusSmartAccount extends BaseSmartContractAccount { ["address", "bytes"], [ this.activeValidationModule.getAddress() ?? - this.defaultValidationModule.getAddress(), + this.defaultValidationModule.getAddress(), signature ] ) @@ -1783,23 +1780,34 @@ export class NexusSmartAccount extends BaseSmartContractAccount { } }) - const accountDomainStructFields = await getAccountDomainStructFields(this.publicClient, await this.getAddress()); + const accountDomainStructFields = await getAccountDomainStructFields( + this.publicClient, + await this.getAddress() + ) const parentStructHash = keccak256( - encodePacked(["bytes", "bytes"], [ - encodeAbiParameters( - parseAbiParameters(["bytes32, bytes32"]), - [keccak256(toBytes(PARENT_TYPEHASH)), typedData.message.stuff] - ), - accountDomainStructFields - ]) - ); + encodePacked( + ["bytes", "bytes"], + [ + encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ + keccak256(toBytes(PARENT_TYPEHASH)), + typedData.message.stuff + ]), + accountDomainStructFields + ] + ) + ) - const wrappedTypedHash = await eip712WrapHash(parentStructHash, appDomainSeparator) + const wrappedTypedHash = await eip712WrapHash( + parentStructHash, + appDomainSeparator + ) - let signature = await this.activeValidationModule.signMessage(toBytes(wrappedTypedHash)) + let signature = await this.activeValidationModule.signMessage( + toBytes(wrappedTypedHash) + ) - const contentsType = toBytes(typeToString(types)[1]); + const contentsType = toBytes(typeToString(types)[1]) const signatureData = concatHex([ signature, @@ -1807,13 +1815,13 @@ export class NexusSmartAccount extends BaseSmartContractAccount { typedData.message.stuff, toHex(contentsType), toHex(contentsType.length, { size: 2 }) - ]); + ]) signature = encodePacked( ["address", "bytes"], [ this.activeValidationModule.getAddress() ?? - this.defaultValidationModule.getAddress(), + this.defaultValidationModule.getAddress(), signatureData ] ) diff --git a/src/account/utils/Constants.ts b/src/account/utils/Constants.ts index 45cc1035..d42eda82 100644 --- a/src/account/utils/Constants.ts +++ b/src/account/utils/Constants.ts @@ -83,5 +83,6 @@ export const MODULE_TYPE_MULTI = 0 export const NEXUS_DOMAIN_NAME = "Nexus" export const NEXUS_DOMAIN_VERSION = "1.0.0-beta" -export const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; +export const PARENT_TYPEHASH = + "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)" export const eip1271MagicValue: Hex = "0x1626ba7e" diff --git a/src/account/utils/Types.ts b/src/account/utils/Types.ts index 882ade2a..d65e619e 100644 --- a/src/account/utils/Types.ts +++ b/src/account/utils/Types.ts @@ -180,10 +180,10 @@ export type NexusSmartAccountConfig = NexusSmartAccountConfigBaseProps & export type NexusSmartAccountConfigConstructorProps = NexusSmartAccountConfigBaseProps & - BaseSmartAccountConfig & - ResolvedBundlerProps & - ResolvedValidationProps & - ConfigurationAddresses + BaseSmartAccountConfig & + ResolvedBundlerProps & + ResolvedValidationProps & + ConfigurationAddresses /** * Represents options for building a user operation. @@ -442,13 +442,13 @@ export interface SmartAccountSigner { //#region UserOperationCallData export type UserOperationCallData = | { - /* the target of the call */ - target: Address - /* the data passed to the target */ - data: Hex - /* the amount of native token to send to the target (default: 0) */ - value?: bigint - } + /* the target of the call */ + target: Address + /* the data passed to the target */ + data: Hex + /* the amount of native token to send to the target (default: 0) */ + value?: bigint + } | Hex //#endregion UserOperationCallData @@ -535,7 +535,7 @@ export interface ISmartContractAccount< /** * Signs a typed data object as per ERC-712 * - * @param typedData + * @param typedData * @returns the signed hash for the message passed */ signTypedData(typedData: any): Promise @@ -652,10 +652,10 @@ export type AccountMetadata = { export type WithRequired = Required> export type TypeField = { - name: string; - type: string; + name: string + type: string } export type TypeDefinition = { - [key: string]: TypeField[]; + [key: string]: TypeField[] } diff --git a/src/account/utils/Utils.ts b/src/account/utils/Utils.ts index 9bfc01c4..09511380 100644 --- a/src/account/utils/Utils.ts +++ b/src/account/utils/Utils.ts @@ -21,7 +21,13 @@ import { toBytes, toHex } from "viem" -import type { AccountMetadata, EIP712DomainReturn, TypeDefinition, UserOperationStruct } from "../../account" +import { EIP1271Abi } from "../../__contracts/abi/EIP1271Abi" +import type { + AccountMetadata, + EIP712DomainReturn, + TypeDefinition, + UserOperationStruct +} from "../../account" import { MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, @@ -29,7 +35,6 @@ import { NEXUS_DOMAIN_VERSION } from "../../account" import { type ModuleType, moduleTypeIds } from "../../modules/utils/Types" -import { EIP1271Abi } from "../../__contracts/abi/EIP1271Abi" /** * pack the userOperation @@ -49,15 +54,15 @@ export function packUserOp( const hashedPaymasterAndData = keccak256( userOperation.paymaster ? concat([ - userOperation.paymaster, - pad(toHex(userOperation.paymasterVerificationGasLimit || BigInt(0)), { - size: 16 - }), - pad(toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), { - size: 16 - }), - userOperation.paymasterData || "0x" - ]) + userOperation.paymaster, + pad(toHex(userOperation.paymasterVerificationGasLimit || BigInt(0)), { + size: 16 + }), + pad(toHex(userOperation.paymasterPostOpGasLimit || BigInt(0)), { + size: 16 + }), + userOperation.paymasterData || "0x" + ]) : "0x" ) @@ -231,25 +236,25 @@ export function _hashTypedData( } export function getTypesForEIP712Domain({ - domain, + domain }: { domain?: TypedDataDomain | undefined }): TypedDataParameter[] { return [ - typeof domain?.name === 'string' && { name: 'name', type: 'string' }, - domain?.version && { name: 'version', type: 'string' }, - typeof domain?.chainId === 'number' && { - name: 'chainId', - type: 'uint256', + typeof domain?.name === "string" && { name: "name", type: "string" }, + domain?.version && { name: "version", type: "string" }, + typeof domain?.chainId === "number" && { + name: "chainId", + type: "uint256" }, domain?.verifyingContract && { - name: 'verifyingContract', - type: 'address', + name: "verifyingContract", + type: "address" }, - domain?.salt && { name: 'salt', type: 'bytes32' }, + domain?.salt && { name: "salt", type: "bytes32" } ].filter(Boolean) as TypedDataParameter[] } export const accountMetadata = async ( client: Client, - accountAddress: Address, + accountAddress: Address ): Promise => { try { const domain = await client.request({ @@ -278,7 +283,7 @@ export const accountMetadata = async ( chainId: decoded[3] } } - } catch (error) { } + } catch (error) {} return { name: NEXUS_DOMAIN_NAME, version: NEXUS_DOMAIN_VERSION, @@ -288,23 +293,22 @@ export const accountMetadata = async ( } } - export const eip712WrapHash = async ( typedHash: Hex, appDomainSeparator: Hex ): Promise => { - const digest = keccak256( - concat(["0x1901", appDomainSeparator, typedHash]) - ) + const digest = keccak256(concat(["0x1901", appDomainSeparator, typedHash])) return digest } export function typeToString(typeDef: TypeDefinition): string[] { return Object.entries(typeDef).map(([key, fields]) => { - const fieldStrings = fields.map(field => `${field.type} ${field.name}`).join(','); - return `${key}(${fieldStrings})`; - }); + const fieldStrings = fields + .map((field) => `${field.type} ${field.name}`) + .join(",") + return `${key}(${fieldStrings})` + }) } export const getAccountDomainStructFields = async ( @@ -322,18 +326,17 @@ export const getAccountDomainStructFields = async ( const [fields, name, version, chainId, verifyingContract, salt, extensions] = accountDomainStructFields - const params = parseAbiParameters(["bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32"]); + const params = parseAbiParameters([ + "bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32" + ]) - return encodeAbiParameters( - params, - [ - fields, - keccak256(toBytes(name)), - keccak256(toBytes(version)), - chainId, - verifyingContract, - salt, - keccak256(encodePacked(["uint256[]"], [extensions])) - ] - ) -} \ No newline at end of file + return encodeAbiParameters(params, [ + fields, + keccak256(toBytes(name)), + keccak256(toBytes(version)), + chainId, + verifyingContract, + salt, + keccak256(encodePacked(["uint256[]"], [extensions])) + ]) +} diff --git a/tests/account.read.test.ts b/tests/account.read.test.ts index 8ae91308..0dee25b0 100644 --- a/tests/account.read.test.ts +++ b/tests/account.read.test.ts @@ -5,24 +5,24 @@ import { type Account, type Chain, type Hex, + type PublicClient, type WalletClient, concat, + concatHex, createPublicClient, createWalletClient, + domainSeparator, encodeAbiParameters, encodeFunctionData, encodePacked, getContract, hashMessage, keccak256, - parseAbiParameters, - toBytes, - toHex, parseAbi, - PublicClient, - concatHex, + parseAbiParameters, parseEther, - domainSeparator, + toBytes, + toHex } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { baseSepolia } from "viem/chains" @@ -39,7 +39,7 @@ import { getChain, makeInstallDataAndHash } from "../src/account" -import { CounterAbi, MockPermitTokenAbi } from "./src/__contracts/abi" +import { CounterAbi, TokenWithPermitAbi } from "./src/__contracts/abi" import mockAddresses from "./src/__contracts/mockAddresses" import { type TestFileNetworkType, toNetwork } from "./src/testSetup" import { @@ -52,12 +52,9 @@ import { toTestClient, topUp } from "./src/testUtils" -import type { - MasterClient, - NetworkConfig, -} from "./src/testUtils" +import type { MasterClient, NetworkConfig } from "./src/testUtils" -const NETWORK_TYPE: TestFileNetworkType = "PUBLIC_TESTNET" +const NETWORK_TYPE: TestFileNetworkType = "FILE_LOCALHOST" describe("account.read", () => { let network: NetworkConfig @@ -146,7 +143,7 @@ describe("account.read", () => { expect(balance.amount > 0) }) - // @note @todo this test is only valid for anvil + // @note @todo this test is only valid for anvil test.skip("should have account addresses", async () => { const addresses = await Promise.all([ account.address, @@ -234,7 +231,7 @@ describe("account.read", () => { rpcUrl: chain.rpcUrls.default.http[0] }) - expect(smartAccount).toBeTruthy(); + expect(smartAccount).toBeTruthy() }) test.skip("should pickup the rpcUrl from viem wallet and ethers", async () => { @@ -372,14 +369,11 @@ describe("account.read", () => { ).rejects.toThrow("Cannot consume a viem wallet without an account") }) - test.skip( - "should create a smart account with paymaster with an api key", - async () => { - const paymaster = smartAccount.paymaster - expect(paymaster).not.toBeNull() - expect(paymaster).not.toBeUndefined() - } - ) + test.skip("should create a smart account with paymaster with an api key", async () => { + const paymaster = smartAccount.paymaster + expect(paymaster).not.toBeNull() + expect(paymaster).not.toBeUndefined() + }) test("should return chain object for chain id 1", async () => { const chainId = 1 @@ -535,18 +529,14 @@ describe("account.read", () => { expect(ethBalanceFromSmartAccount.decimals).toBe(18) }, 60000) - test.skip( - "should check balance of supported token", - async () => { - const tokens = await smartAccount.getSupportedTokens() - const [firstToken] = tokens - - expect(tokens.length).toBeGreaterThan(0) - expect(tokens[0]).toHaveProperty("balance") - expect(firstToken.balance.amount).toBeGreaterThanOrEqual(0n) - }, - 60000 - ) + test.skip("should check balance of supported token", async () => { + const tokens = await smartAccount.getSupportedTokens() + const [firstToken] = tokens + + expect(tokens.length).toBeGreaterThan(0) + expect(tokens[0]).toHaveProperty("balance") + expect(firstToken.balance.amount).toBeGreaterThanOrEqual(0n) + }, 60000) // @note Nexus SA signature needs to contain the validator module address in the first 20 bytes test("should test isValidSignature PersonalSign to be valid", async () => { @@ -559,7 +549,6 @@ describe("account.read", () => { const DOMAIN_TYPEHASH = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" const PARENT_TYPEHASH = "PersonalSign(bytes prefixed)" - const chainId = baseSepolia.id // Calculate the domain separator const domainSeparator = keccak256( @@ -569,7 +558,7 @@ describe("account.read", () => { keccak256(toBytes(DOMAIN_TYPEHASH)), keccak256(toBytes(DOMAIN_NAME)), keccak256(toBytes(DOMAIN_VERSION)), - BigInt(chainId), + BigInt(chain.id), smartAccountAddress ] ) @@ -662,51 +651,53 @@ describe("account.read", () => { "should test isValidSignature EIP712Sign to be valid with viem", async () => { if (await smartAccount.isAccountDeployed()) { - const PARENT_TYPEHASH = "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)"; + const PARENT_TYPEHASH = + "TypedDataSign(Contents contents,bytes1 fields,string name,string version,uint256 chainId,address verifyingContract,bytes32 salt,uint256[] extensions)Contents(bytes32 stuff)" const message = { contents: keccak256(toBytes("test", { size: 32 })) } - const domainSeparator = (await publicClient.readContract({ + const domainSeparator = await publicClient.readContract({ address: await smartAccount.getAddress(), abi: parseAbi([ "function DOMAIN_SEPARATOR() external view returns (bytes32)" ]), functionName: "DOMAIN_SEPARATOR" - })) + }) const typedHashHashed = keccak256( - concat([ - '0x1901', - domainSeparator, - message.contents - ]) - ); + concat(["0x1901", domainSeparator, message.contents]) + ) - const accountDomainStructFields = await getAccountDomainStructFields(testClient, await smartAccount.getAddress()); + const accountDomainStructFields = await getAccountDomainStructFields( + testClient, + await smartAccount.getAddress() + ) const parentStructHash = keccak256( - encodePacked(["bytes", "bytes"], [ - encodeAbiParameters( - parseAbiParameters(["bytes32, bytes32"]), - [keccak256(toBytes(PARENT_TYPEHASH)), message.contents] - ), - accountDomainStructFields - ]) - ); + encodePacked( + ["bytes", "bytes"], + [ + encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ + keccak256(toBytes(PARENT_TYPEHASH)), + message.contents + ]), + accountDomainStructFields + ] + ) + ) const dataToSign = keccak256( - concat([ - '0x1901', - domainSeparator, - parentStructHash - ]) - ); + concat(["0x1901", domainSeparator, parentStructHash]) + ) - const signature = await walletClient.signMessage({ message: { raw: toBytes(dataToSign) }, account }); + const signature = await walletClient.signMessage({ + message: { raw: toBytes(dataToSign) }, + account + }) - const contentsType = toBytes("Contents(bytes32 stuff)"); + const contentsType = toBytes("Contents(bytes32 stuff)") const signatureData = concatHex([ signature, @@ -714,12 +705,12 @@ describe("account.read", () => { message.contents, toHex(contentsType), toHex(contentsType.length, { size: 2 }) - ]); + ]) - const finalSignature = encodePacked(["address", "bytes"], [ - addresses.K1Validator, - signatureData - ]); + const finalSignature = encodePacked( + ["address", "bytes"], + [addresses.K1Validator, signatureData] + ) const contractResponse = await publicClient.readContract({ address: await smartAccount.getAddress(), @@ -736,10 +727,10 @@ describe("account.read", () => { ) test("should sign using signTypedData SDK method", async () => { - const permitTestTokenAddress = "0xd8a978B9a0e1Af5579314E626D77fc1C9fF76c7D" as Hex; + const permitTestTokenAddress = mockAddresses.TokenWithPermit; const appDomain = { chainId: network.chain.id, - name: "TestToken", + name: "TokenWithPermit", verifyingContract: permitTestTokenAddress, version: "1" } @@ -754,38 +745,45 @@ describe("account.read", () => { ] } - const permitTypehash = keccak256(toBytes("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")); - const nonce = await publicClient.readContract({ + const permitTypehash = keccak256( + toBytes( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ) + ) + const nonce = (await publicClient.readContract({ address: permitTestTokenAddress, - abi: MockPermitTokenAbi, + abi: TokenWithPermitAbi, functionName: "nonces", args: [smartAccountAddress] - }) as bigint; + })) as bigint - const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now const message = { stuff: keccak256( encodeAbiParameters( - parseAbiParameters("bytes32, address, address, uint256, uint256, uint256"), - [permitTypehash, smartAccountAddress, smartAccountAddress, parseEther("2"), nonce, deadline] + parseAbiParameters( + "bytes32, address, address, uint256, uint256, uint256" + ), + [ + permitTypehash, + smartAccountAddress, + smartAccountAddress, + parseEther("2"), + nonce, + deadline + ] ) ) - }; + } - const appDomainSeparator = domainSeparator( - { - domain: appDomain - } - ) + const appDomainSeparator = domainSeparator({ + domain: appDomain + }) const contentsHash = keccak256( - concat([ - '0x1901', - appDomainSeparator, - message.stuff - ]) - ); + concat(["0x1901", appDomainSeparator, message.stuff]) + ) const finalSignature = await smartAccount.signTypedData({ domain: appDomain, @@ -804,22 +802,28 @@ describe("account.read", () => { const permitTokenResponse = await walletClient.writeContract({ account: account, address: permitTestTokenAddress, - abi: MockPermitTokenAbi, + abi: TokenWithPermitAbi, functionName: "permitWith1271", chain: network.chain, - args: [await smartAccount.getAddress(), await smartAccount.getAddress(), parseEther("2"), deadline, finalSignature] + args: [ + await smartAccount.getAddress(), + await smartAccount.getAddress(), + parseEther("2"), + deadline, + finalSignature + ] }) - await publicClient.waitForTransactionReceipt({ hash: permitTokenResponse }); + await publicClient.waitForTransactionReceipt({ hash: permitTokenResponse }) const allowance = await publicClient.readContract({ address: permitTestTokenAddress, - abi: MockPermitTokenAbi, + abi: TokenWithPermitAbi, functionName: "allowance", args: [await smartAccount.getAddress(), await smartAccount.getAddress()] }) - expect(allowance).toEqual(parseEther("2")); + expect(allowance).toEqual(parseEther("2")) expect(nexusResponse).toEqual("0x1626ba7e") }) }) diff --git a/tests/src/__contracts/abi/MockHookAbi.ts b/tests/src/__contracts/abi/MockHookAbi.ts index 11b57fab..cfc2f634 100644 --- a/tests/src/__contracts/abi/MockHookAbi.ts +++ b/tests/src/__contracts/abi/MockHookAbi.ts @@ -105,17 +105,17 @@ export const MockHookAbi = [ inputs: [ { internalType: "address", - name: "", + name: "sender", type: "address" }, { internalType: "uint256", - name: "", + name: "value", type: "uint256" }, { internalType: "bytes", - name: "", + name: "data", type: "bytes" } ], diff --git a/tests/src/__contracts/abi/MockPermitTokenAbi.ts b/tests/src/__contracts/abi/MockPermitTokenAbi.ts deleted file mode 100644 index d28d9133..00000000 --- a/tests/src/__contracts/abi/MockPermitTokenAbi.ts +++ /dev/null @@ -1 +0,0 @@ -export const MockPermitTokenAbi = [{ "inputs": [{ "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "symbol", "type": "string" }], "stateMutability": "nonpayable", "type": "constructor" }, { "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": "address", "name": "signer", "type": "address" }], "name": "ERC1271InvalidSigner", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "allowance", "type": "uint256" }, { "internalType": "uint256", "name": "needed", "type": "uint256" }], "name": "ERC20InsufficientAllowance", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "uint256", "name": "balance", "type": "uint256" }, { "internalType": "uint256", "name": "needed", "type": "uint256" }], "name": "ERC20InsufficientBalance", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "approver", "type": "address" }], "name": "ERC20InvalidApprover", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }], "name": "ERC20InvalidReceiver", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], "name": "ERC20InvalidSender", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }], "name": "ERC20InvalidSpender", "type": "error" }, { "inputs": [{ "internalType": "uint256", "name": "deadline", "type": "uint256" }], "name": "ERC2612ExpiredSignature", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "signer", "type": "address" }, { "internalType": "address", "name": "owner", "type": "address" }], "name": "ERC2612InvalidSigner", "type": "error" }, { "inputs": [{ "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "currentNonce", "type": "uint256" }], "name": "InvalidAccountNonce", "type": "error" }, { "inputs": [], "name": "InvalidShortString", "type": "error" }, { "inputs": [{ "internalType": "string", "name": "str", "type": "string" }], "name": "StringTooLong", "type": "error" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [], "name": "EIP712DomainChanged", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "inputs": [], "name": "DOMAIN_SEPARATOR", "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "PERMIT_TYPEHASH_LOCAL", "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }], "name": "allowance", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], "name": "balanceOf", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "eip712Domain", "outputs": [{ "internalType": "bytes1", "name": "fields", "type": "bytes1" }, { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "version", "type": "string" }, { "internalType": "uint256", "name": "chainId", "type": "uint256" }, { "internalType": "address", "name": "verifyingContract", "type": "address" }, { "internalType": "bytes32", "name": "salt", "type": "bytes32" }, { "internalType": "uint256[]", "name": "extensions", "type": "uint256[]" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" }], "name": "mint", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }], "name": "nonces", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }, { "internalType": "uint8", "name": "v", "type": "uint8" }, { "internalType": "bytes32", "name": "r", "type": "bytes32" }, { "internalType": "bytes32", "name": "s", "type": "bytes32" }], "name": "permit", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }, { "internalType": "bytes", "name": "signature", "type": "bytes" }], "name": "permitWith1271", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalSupply", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transfer", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }] \ No newline at end of file diff --git a/tests/src/__contracts/abi/MockValidatorAbi.ts b/tests/src/__contracts/abi/MockValidatorAbi.ts index d3ae3504..68ed5ed7 100644 --- a/tests/src/__contracts/abi/MockValidatorAbi.ts +++ b/tests/src/__contracts/abi/MockValidatorAbi.ts @@ -109,6 +109,35 @@ export const MockValidatorAbi = [ stateMutability: "view", type: "function" }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address" + }, + { + internalType: "bytes32", + name: "hash", + type: "bytes32" + }, + { + internalType: "bytes", + name: "signature", + type: "bytes" + } + ], + name: "isValidSignatureWithSenderLegacy", + outputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4" + } + ], + stateMutability: "view", + type: "function" + }, { inputs: [ { @@ -154,6 +183,48 @@ export const MockValidatorAbi = [ stateMutability: "view", type: "function" }, + { + inputs: [], + name: "supportsNestedTypedDataSign", + outputs: [ + { + internalType: "bytes32", + name: "result", + type: "bytes32" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "bytes32", + name: "hash", + type: "bytes32" + }, + { + internalType: "bytes", + name: "sig", + type: "bytes" + }, + { + internalType: "bytes", + name: "data", + type: "bytes" + } + ], + name: "validateSignatureWithData", + outputs: [ + { + internalType: "bool", + name: "validSig", + type: "bool" + } + ], + stateMutability: "view", + type: "function" + }, { inputs: [ { diff --git a/tests/src/__contracts/abi/TokenWithPermitAbi.ts b/tests/src/__contracts/abi/TokenWithPermitAbi.ts new file mode 100644 index 00000000..2360d7b3 --- /dev/null +++ b/tests/src/__contracts/abi/TokenWithPermitAbi.ts @@ -0,0 +1,611 @@ +export const TokenWithPermitAbi = [ + { + inputs: [ + { + internalType: "string", + name: "name", + type: "string" + }, + { + internalType: "string", + name: "symbol", + type: "string" + } + ], + stateMutability: "nonpayable", + type: "constructor" + }, + { + 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: "address", + name: "signer", + type: "address" + } + ], + name: "ERC1271InvalidSigner", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "allowance", + type: "uint256" + }, + { + internalType: "uint256", + name: "needed", + type: "uint256" + } + ], + name: "ERC20InsufficientAllowance", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + }, + { + internalType: "uint256", + name: "balance", + type: "uint256" + }, + { + internalType: "uint256", + name: "needed", + type: "uint256" + } + ], + name: "ERC20InsufficientBalance", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "approver", + type: "address" + } + ], + name: "ERC20InvalidApprover", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "receiver", + type: "address" + } + ], + name: "ERC20InvalidReceiver", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + } + ], + name: "ERC20InvalidSender", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address" + } + ], + name: "ERC20InvalidSpender", + type: "error" + }, + { + inputs: [ + { + internalType: "uint256", + name: "deadline", + type: "uint256" + } + ], + name: "ERC2612ExpiredSignature", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "signer", + type: "address" + }, + { + internalType: "address", + name: "owner", + type: "address" + } + ], + name: "ERC2612InvalidSigner", + type: "error" + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address" + }, + { + internalType: "uint256", + name: "currentNonce", + type: "uint256" + } + ], + name: "InvalidAccountNonce", + type: "error" + }, + { + inputs: [], + name: "InvalidShortString", + type: "error" + }, + { + inputs: [ + { + internalType: "string", + name: "str", + type: "string" + } + ], + name: "StringTooLong", + type: "error" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address" + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address" + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256" + } + ], + name: "Approval", + type: "event" + }, + { + anonymous: false, + inputs: [], + name: "EIP712DomainChanged", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address" + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address" + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256" + } + ], + name: "Transfer", + type: "event" + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "PERMIT_TYPEHASH_LOCAL", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "address", + name: "spender", + type: "address" + } + ], + name: "allowance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } + ], + name: "approve", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address" + } + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "decimals", + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { + internalType: "bytes1", + name: "fields", + type: "bytes1" + }, + { + internalType: "string", + name: "name", + type: "string" + }, + { + internalType: "string", + name: "version", + type: "string" + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256" + }, + { + internalType: "address", + name: "verifyingContract", + type: "address" + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32" + }, + { + internalType: "uint256[]", + name: "extensions", + type: "uint256[]" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address" + }, + { + internalType: "uint256", + name: "amount", + type: "uint256" + } + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + } + ], + name: "nonces", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256" + }, + { + internalType: "uint8", + name: "v", + type: "uint8" + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32" + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32" + } + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "address", + name: "spender", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256" + }, + { + internalType: "bytes", + name: "signature", + type: "bytes" + } + ], + name: "permitWith1271", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [], + name: "symbol", + outputs: [ + { + internalType: "string", + name: "", + type: "string" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "totalSupply", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } + ], + name: "transfer", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address" + }, + { + internalType: "address", + name: "to", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + } + ], + name: "transferFrom", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool" + } + ], + stateMutability: "nonpayable", + type: "function" + } +] as const diff --git a/tests/src/__contracts/abi/index.ts b/tests/src/__contracts/abi/index.ts index 954178d2..50290d37 100644 --- a/tests/src/__contracts/abi/index.ts +++ b/tests/src/__contracts/abi/index.ts @@ -8,6 +8,6 @@ export * from "./MockTokenAbi" export * from "./BootstrapLibAbi" export * from "./MockRegistryAbi" export * from "./MockHandlerAbi" +export * from "./TokenWithPermitAbi" export * from "./BootstrapAbi" export * from "./MockExecutorAbi" -export * from "./MockPermitTokenAbi" \ No newline at end of file diff --git a/tests/src/__contracts/mockAddresses.ts b/tests/src/__contracts/mockAddresses.ts index 64cc26a3..b6dfb220 100644 --- a/tests/src/__contracts/mockAddresses.ts +++ b/tests/src/__contracts/mockAddresses.ts @@ -2,17 +2,18 @@ import type { Hex } from "viem" export const mockAddresses: Record = { - MockHook: "0xAB9733982E5b98bdDc4f00314E8EA4911A9D1BA5", - Stakeable: "0xc60F4C65a698C0FE5eddACfB71661B580D15BDaa", - NexusAccountFactory: "0x609e47C5404758D83102AB4fe58dbeD4AC39Ae78", - BiconomyMetaFactory: "0x98C8792cf50A93900d575842eDAFf3Ccc2C2902b", - Counter: "0x36023f0abe27eC68fD2c6a489A3e21772A08E120", - MockValidator: "0xa5ab9E06eB79805e6b20586e16285f10cA3274fB", - MockToken: "0x56623d18E54cBbCae340EC449E3c5D1DC0bF60cd", - BootstrapLib: "0x0c66e850AB4aB7e748bf48698a257aaB87d9a899", - MockRegistry: "0x25D55884BFA6380B0fCDc9E924c495C44Aa46415", - MockHandler: "0xBE52B87DA68EC967e977191bE125584b98c1Ea04", - Bootstrap: "0xC952c381C006D14395047A8aeE7F67fB59D38A10", - MockExecutor: "0x940C64D0c650615d3Af0053a4CD32AbAbD3F04e7" + MockHook: "0x4445134442b717275c06034D0e6A589C258a7CfC", + Stakeable: "0x251a414Fa6a2a719Cf79D370F9aEb2e0817FA086", + NexusAccountFactory: "0xA2cE66fB6bBA1828DbcE2114682286c82434e59E", + BiconomyMetaFactory: "0xDdb351881419426F0942921db482690Da5a47d22", + Counter: "0x61f70428b61864B38D9B45b7B032c700B960acCD", + MockValidator: "0x4713E68A85A2ED5FF28e61DD76f17E8Cd94f4992", + MockToken: "0xe22ed8281e84aeAA770eE04f0E07dAf6A028117F", + BootstrapLib: "0x15a93E4a5221e9aDe5B3E6A7A70060080cc464f0", + MockRegistry: "0xD270D6A18B9C6063dE7B92EBd25B11f74e8B75eB", + MockHandler: "0x67D49D2345A96890c2A986bE294e8808868C5057", + TokenWithPermit: "0xfD6345760Ff39EC6B19506a7b8486EcA74B3F82A", + Bootstrap: "0xF53a824Bb0508FBa3a599FaF5BD37CE70b0867Fc", + MockExecutor: "0x9A75Bc1C839164196Eb5668b38337712994A2c8A" } as const export default mockAddresses diff --git a/tests/src/testUtils.ts b/tests/src/testUtils.ts index ad90f8e7..95b9e44a 100644 --- a/tests/src/testUtils.ts +++ b/tests/src/testUtils.ts @@ -12,13 +12,13 @@ import { createTestClient, createWalletClient, encodeAbiParameters, + encodePacked, + keccak256, parseAbi, parseAbiParameters, publicActions, - walletActions, - keccak256, toBytes, - encodePacked + walletActions } from "viem" import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts" import { @@ -263,7 +263,8 @@ export const nonZeroBalance = async ( const balance = await checkBalance(testClient, address, tokenAddress) if (balance > BigInt(0)) return throw new Error( - `Insufficient balance ${tokenAddress ? `of token ${tokenAddress}` : "of native token" + `Insufficient balance ${ + tokenAddress ? `of token ${tokenAddress}` : "of native token" } during test setup of owner: ${address}` ) } @@ -369,7 +370,8 @@ export const topUp = async ( if (balanceOfRecipient > amount) { Logger.log( - `balanceOfRecipient (${recipient}) already has enough ${token ?? "native token" + `balanceOfRecipient (${recipient}) already has enough ${ + token ?? "native token" } (${balanceOfRecipient}) during safeTopUp` ) return await Promise.resolve() @@ -410,20 +412,19 @@ export const getAccountDomainStructFields = async ( const [fields, name, version, chainId, verifyingContract, salt, extensions] = accountDomainStructFields - const params = parseAbiParameters(["bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32"]); - - return encodeAbiParameters( - params, - [ - fields, - keccak256(toBytes(name)), - keccak256(toBytes(version)), - chainId, - verifyingContract, - salt, - keccak256(encodePacked(["uint256[]"], [extensions])) - ] - ) + const params = parseAbiParameters([ + "bytes1, bytes32, bytes32, uint256, address, bytes32, bytes32" + ]) + + return encodeAbiParameters(params, [ + fields, + keccak256(toBytes(name)), + keccak256(toBytes(version)), + chainId, + verifyingContract, + salt, + keccak256(encodePacked(["uint256[]"], [extensions])) + ]) } export const getBundlerUrl = (chainId: number) => From d0359407fa9b55e5476d1c87f239d5f3462d22b3 Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:01:30 +0400 Subject: [PATCH 6/6] fix tests --- tests/account.read.test.ts | 2 +- tests/account.write.test.ts | 2 +- tests/modules.k1Validator.write.test.ts | 2 +- tests/modules.ownableExecutor.read.test.ts | 2 +- tests/modules.ownableExecutor.write.test.ts | 2 +- tests/modules.ownableValidator.install.write.test.ts | 2 +- tests/modules.ownableValidator.uninstall.write.test.ts | 2 +- tests/smart.sessions.test.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/account.read.test.ts b/tests/account.read.test.ts index 0dee25b0..60434435 100644 --- a/tests/account.read.test.ts +++ b/tests/account.read.test.ts @@ -152,7 +152,7 @@ describe("account.read", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/account.write.test.ts b/tests/account.write.test.ts index 065ff4de..8af5a0fb 100644 --- a/tests/account.write.test.ts +++ b/tests/account.write.test.ts @@ -80,7 +80,7 @@ describe("account.write", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/modules.k1Validator.write.test.ts b/tests/modules.k1Validator.write.test.ts index 7995e845..d267c342 100644 --- a/tests/modules.k1Validator.write.test.ts +++ b/tests/modules.k1Validator.write.test.ts @@ -87,7 +87,7 @@ describe("modules.k1Validator.write", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/modules.ownableExecutor.read.test.ts b/tests/modules.ownableExecutor.read.test.ts index 042e12c5..9e9a1362 100644 --- a/tests/modules.ownableExecutor.read.test.ts +++ b/tests/modules.ownableExecutor.read.test.ts @@ -82,7 +82,7 @@ describe("modules.ownable.executor.read", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/modules.ownableExecutor.write.test.ts b/tests/modules.ownableExecutor.write.test.ts index 41522182..0094e708 100644 --- a/tests/modules.ownableExecutor.write.test.ts +++ b/tests/modules.ownableExecutor.write.test.ts @@ -80,7 +80,7 @@ describe("modules.ownable.executor.write", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/modules.ownableValidator.install.write.test.ts b/tests/modules.ownableValidator.install.write.test.ts index 680b4ca8..6ccbf096 100644 --- a/tests/modules.ownableValidator.install.write.test.ts +++ b/tests/modules.ownableValidator.install.write.test.ts @@ -80,7 +80,7 @@ describe("modules.ownable.validator.install.write", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/modules.ownableValidator.uninstall.write.test.ts b/tests/modules.ownableValidator.uninstall.write.test.ts index 09fbd4a7..136e2b34 100644 --- a/tests/modules.ownableValidator.uninstall.write.test.ts +++ b/tests/modules.ownableValidator.uninstall.write.test.ts @@ -80,7 +80,7 @@ describe("modules.ownable.validator.uninstall.write", () => { expect(addresses.every(Boolean)).to.be.true expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) }) diff --git a/tests/smart.sessions.test.ts b/tests/smart.sessions.test.ts index ce60eaf0..5c7308c3 100644 --- a/tests/smart.sessions.test.ts +++ b/tests/smart.sessions.test.ts @@ -87,7 +87,7 @@ describe("smart.sessions", () => { expect(addresses.every(Boolean)).toBeTruthy() expect(addresses).toStrictEqual([ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "0xa3962DB24D3cAb711e18d5A508591C6dB82a0f54" // Sender smart account + "0x9faF274EB7cc2D342d786Ad0995dB3c0d641446d" // Sender smart account ]) })