diff --git a/.changeset/sync-transaction-actions.md b/.changeset/sync-transaction-actions.md new file mode 100644 index 00000000..60ebe8df --- /dev/null +++ b/.changeset/sync-transaction-actions.md @@ -0,0 +1,5 @@ +--- +"@abstract-foundation/agw-client": minor +--- + +Add `sendTransactionSync` and `writeContractSync` to `AbstractClient` and `SessionClient` for EIP-7966 support diff --git a/packages/agw-client/src/actions/sendTransactionForSessionSync.ts b/packages/agw-client/src/actions/sendTransactionForSessionSync.ts new file mode 100644 index 00000000..3cfcdbfa --- /dev/null +++ b/packages/agw-client/src/actions/sendTransactionForSessionSync.ts @@ -0,0 +1,95 @@ +import { + type Account, + BaseError, + type Client, + type Hex, + type PublicClient, + type SendTransactionRequest, + type Transport, + type WalletClient, +} from 'viem'; +import { + type SendTransactionSyncReturnType, + sendRawTransactionSync, +} from 'viem/actions'; +import { getAction } from 'viem/utils'; +import { + type ChainEIP712, + type SendEip712TransactionParameters, +} from 'viem/zksync'; + +import { SESSION_KEY_VALIDATOR_ADDRESS } from '../constants.js'; +import { + encodeSessionWithPeriodIds, + getPeriodIdsForTransaction, + type SessionConfig, +} from '../sessions.js'; +import type { CustomPaymasterHandler } from '../types/customPaymaster.js'; +import { sendTransactionInternal } from './sendTransactionInternal.js'; +import type { SendEip712TransactionSyncParameters } from './sendTransactionSync.js'; + +export async function sendTransactionForSessionSync< + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + const request extends SendTransactionRequest< + chain, + chainOverride + > = SendTransactionRequest, +>( + client: Client, + signerClient: WalletClient, + publicClient: PublicClient, + parameters: SendEip712TransactionSyncParameters< + chain, + account, + chainOverride, + request + >, + session: SessionConfig, + customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, +): Promise> { + const { throwOnReceiptRevert, timeout, ...txParameters } = parameters; + + const selector: Hex | undefined = txParameters.data + ? `0x${txParameters.data.slice(2, 10)}` + : undefined; + + if (!txParameters.to) { + throw new BaseError('Transaction to field is not specified'); + } + return sendTransactionInternal( + client, + signerClient, + publicClient, + txParameters as SendEip712TransactionParameters< + chain, + account, + chainOverride, + request + >, + SESSION_KEY_VALIDATOR_ADDRESS, + { + [SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds( + session, + getPeriodIdsForTransaction({ + sessionConfig: session, + target: txParameters.to, + selector, + timestamp: BigInt(Math.floor(Date.now() / 1000)), + }), + ), + }, + customPaymasterHandler, + (serializedTransaction) => + getAction( + client, + sendRawTransactionSync, + 'sendRawTransactionSync', + )({ + serializedTransaction, + throwOnReceiptRevert, + timeout, + }), + ); +} diff --git a/packages/agw-client/src/actions/sendTransactionInternal.ts b/packages/agw-client/src/actions/sendTransactionInternal.ts index a76c4576..3512cdad 100644 --- a/packages/agw-client/src/actions/sendTransactionInternal.ts +++ b/packages/agw-client/src/actions/sendTransactionInternal.ts @@ -36,6 +36,7 @@ export async function sendTransactionInternal< chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, account extends Account | undefined = Account | undefined, chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + TReturnType = SendEip712TransactionReturnType, >( client: Client, signerClient: WalletClient, @@ -49,7 +50,10 @@ export async function sendTransactionInternal< validator: Address, validationHookData: Record = {}, customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, -): Promise { + sendSerializedTransaction?: ( + serializedTransaction: `0x${string}`, + ) => Promise, +): Promise { const { chain = client.chain } = parameters; if (!signerClient.account) @@ -97,13 +101,16 @@ export async function sendTransactionInternal< validationHookData, customPaymasterHandler, ); - return await getAction( + if (sendSerializedTransaction) { + return await sendSerializedTransaction(serializedTransaction); + } + return (await getAction( client, sendRawTransaction, 'sendRawTransaction', )({ serializedTransaction, - }); + })) as TReturnType; } catch (err) { if ( err instanceof Error && diff --git a/packages/agw-client/src/actions/sendTransactionSync.ts b/packages/agw-client/src/actions/sendTransactionSync.ts new file mode 100644 index 00000000..3b2ff86d --- /dev/null +++ b/packages/agw-client/src/actions/sendTransactionSync.ts @@ -0,0 +1,143 @@ +import { + type Account, + BaseError, + type Client, + type PublicClient, + type SendTransactionRequest, + type Transport, + type WalletClient, +} from 'viem'; +import { + type SendTransactionSyncParameters, + type SendTransactionSyncReturnType, + sendRawTransactionSync, +} from 'viem/actions'; +import { + type GetTransactionErrorParameters, + getAction, + getTransactionError, + parseAccount, +} from 'viem/utils'; +import { + type ChainEIP712, + type SendEip712TransactionParameters, +} from 'viem/zksync'; + +import { + EOA_VALIDATOR_ADDRESS, + INSUFFICIENT_BALANCE_SELECTOR, +} from '../constants.js'; +import { InsufficientBalanceError } from '../errors/insufficientBalance.js'; +import type { + CustomPaymasterHandler, + PaymasterArgs, +} from '../types/customPaymaster.js'; +import { signPrivyTransaction } from './sendPrivyTransaction.js'; +import { sendTransactionInternal } from './sendTransactionInternal.js'; + +export type SendEip712TransactionSyncParameters< + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + request extends SendTransactionRequest< + chain, + chainOverride + > = SendTransactionRequest, +> = SendEip712TransactionParameters & + Pick< + SendTransactionSyncParameters, + 'throwOnReceiptRevert' | 'timeout' + >; + +export async function sendTransactionSync< + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + const request extends SendTransactionRequest< + chain, + chainOverride + > = SendTransactionRequest, +>( + client: Client, + signerClient: WalletClient, + publicClient: PublicClient, + parameters: SendEip712TransactionSyncParameters< + chain, + account, + chainOverride, + request + >, + isPrivyCrossApp = false, + customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, +): Promise> { + const { throwOnReceiptRevert, timeout, ...txParameters } = parameters; + + if (isPrivyCrossApp) { + try { + let paymasterData: Partial = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestAsAny = txParameters as any; + if ( + customPaymasterHandler && + !requestAsAny.paymaster && + !requestAsAny.paymasterInput + ) { + paymasterData = await customPaymasterHandler({ + ...(txParameters as any), + from: client.account.address, + chainId: txParameters.chain?.id ?? client.chain.id, + }); + } + + const updatedParameters = { + ...txParameters, + ...(paymasterData as any), + }; + const signedTx = await signPrivyTransaction(client, updatedParameters); + return await sendRawTransactionSync(publicClient, { + serializedTransaction: signedTx, + throwOnReceiptRevert, + timeout, + }); + } catch (err) { + if ( + err instanceof Error && + err.message.includes(INSUFFICIENT_BALANCE_SELECTOR) + ) { + throw new InsufficientBalanceError(); + } + throw getTransactionError(err as BaseError, { + ...(txParameters as GetTransactionErrorParameters), + account: txParameters.account + ? parseAccount(txParameters.account) + : null, + chain: txParameters.chain ?? undefined, + }); + } + } + + return sendTransactionInternal( + client, + signerClient, + publicClient, + txParameters as SendEip712TransactionParameters< + chain, + account, + chainOverride, + request + >, + EOA_VALIDATOR_ADDRESS, + {}, + customPaymasterHandler, + (serializedTransaction) => + getAction( + client, + sendRawTransactionSync, + 'sendRawTransactionSync', + )({ + serializedTransaction, + throwOnReceiptRevert, + timeout, + }), + ); +} diff --git a/packages/agw-client/src/actions/writeContractForSessionSync.ts b/packages/agw-client/src/actions/writeContractForSessionSync.ts new file mode 100644 index 00000000..a9300b19 --- /dev/null +++ b/packages/agw-client/src/actions/writeContractForSessionSync.ts @@ -0,0 +1,102 @@ +import { + type Abi, + type Account, + BaseError, + type Client, + type ContractFunctionArgs, + type ContractFunctionName, + type EncodeFunctionDataParameters, + encodeFunctionData, + type PublicClient, + type Transport, + type WalletClient, +} from 'viem'; +import { + type WriteContractSyncParameters, + type WriteContractSyncReturnType, +} from 'viem/actions'; +import { getContractError, parseAccount } from 'viem/utils'; +import { type ChainEIP712 } from 'viem/zksync'; + +import { AccountNotFoundError } from '../errors/account.js'; +import type { SessionConfig } from '../sessions.js'; +import type { CustomPaymasterHandler } from '../types/customPaymaster.js'; +import { sendTransactionForSessionSync } from './sendTransactionForSessionSync.js'; + +export async function writeContractForSessionSync< + chain extends ChainEIP712 | undefined, + account extends Account | undefined, + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + args extends ContractFunctionArgs< + abi, + 'nonpayable' | 'payable', + functionName + >, + chainOverride extends ChainEIP712 | undefined, +>( + client: Client, + signerClient: WalletClient, + publicClient: PublicClient, + parameters: WriteContractSyncParameters< + abi, + functionName, + args, + chain, + account, + chainOverride + >, + session: SessionConfig, + customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, +): Promise> { + const { + abi, + account: account_ = client.account, + address, + args, + dataSuffix, + functionName, + throwOnReceiptRevert, + timeout, + ...request + } = parameters as WriteContractSyncParameters; + + if (!account_) + throw new AccountNotFoundError({ + docsPath: '/docs/contract/writeContract', + }); + const account = parseAccount(account_); + + const data = encodeFunctionData({ + abi, + args, + functionName, + } as EncodeFunctionDataParameters); + + try { + return await sendTransactionForSessionSync( + client, + signerClient, + publicClient, + { + data: `${data}${dataSuffix ? dataSuffix.replace('0x', '') : ''}`, + to: address, + account, + throwOnReceiptRevert, + timeout, + ...request, + }, + session, + customPaymasterHandler, + ); + } catch (error) { + throw getContractError(error as BaseError, { + abi, + address, + args, + docsPath: '/docs/contract/writeContract', + functionName, + sender: account.address, + }); + } +} diff --git a/packages/agw-client/src/actions/writeContractSync.ts b/packages/agw-client/src/actions/writeContractSync.ts new file mode 100644 index 00000000..c788c36c --- /dev/null +++ b/packages/agw-client/src/actions/writeContractSync.ts @@ -0,0 +1,101 @@ +import { + type Abi, + type Account, + BaseError, + type Client, + type ContractFunctionArgs, + type ContractFunctionName, + type EncodeFunctionDataParameters, + encodeFunctionData, + type PublicClient, + type Transport, + type WalletClient, +} from 'viem'; +import { + type WriteContractSyncParameters, + type WriteContractSyncReturnType, +} from 'viem/actions'; +import { getContractError, parseAccount } from 'viem/utils'; +import { type ChainEIP712 } from 'viem/zksync'; + +import { AccountNotFoundError } from '../errors/account.js'; +import type { CustomPaymasterHandler } from '../types/customPaymaster.js'; +import { sendTransactionSync } from './sendTransactionSync.js'; + +export async function writeContractSync< + chain extends ChainEIP712 | undefined, + account extends Account | undefined, + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + args extends ContractFunctionArgs< + abi, + 'nonpayable' | 'payable', + functionName + >, + chainOverride extends ChainEIP712 | undefined, +>( + client: Client, + signerClient: WalletClient, + publicClient: PublicClient, + parameters: WriteContractSyncParameters< + abi, + functionName, + args, + chain, + account, + chainOverride + >, + isPrivyCrossApp = false, + customPaymasterHandler: CustomPaymasterHandler | undefined = undefined, +): Promise> { + const { + abi, + account: account_ = client.account, + address, + args, + dataSuffix, + functionName, + throwOnReceiptRevert, + timeout, + ...request + } = parameters as WriteContractSyncParameters; + + if (!account_) + throw new AccountNotFoundError({ + docsPath: '/docs/contract/writeContract', + }); + const account = parseAccount(account_); + + const data = encodeFunctionData({ + abi, + args, + functionName, + } as EncodeFunctionDataParameters); + + try { + return await sendTransactionSync( + client, + signerClient, + publicClient, + { + data: `${data}${dataSuffix ? dataSuffix.replace('0x', '') : ''}`, + to: address, + account, + throwOnReceiptRevert, + timeout, + ...request, + }, + isPrivyCrossApp, + customPaymasterHandler, + ); + } catch (error) { + throw getContractError(error as BaseError, { + abi, + address, + args, + docsPath: '/docs/contract/writeContract', + functionName, + sender: account.address, + }); + } +} diff --git a/packages/agw-client/src/clients/decorators/abstract.ts b/packages/agw-client/src/clients/decorators/abstract.ts index f3b19ace..ce85059e 100644 --- a/packages/agw-client/src/clients/decorators/abstract.ts +++ b/packages/agw-client/src/clients/decorators/abstract.ts @@ -4,6 +4,8 @@ import { type Address, type Chain, type Client, + type ContractFunctionArgs, + type ContractFunctionName, type GetChainIdReturnType, type Hash, type PrepareTransactionRequestReturnType, @@ -19,7 +21,12 @@ import { type WriteContractParameters, } from 'viem'; import { parseAccount } from 'viem/accounts'; -import { getChainId } from 'viem/actions'; +import { + getChainId, + type SendTransactionSyncReturnType, + type WriteContractSyncParameters, + type WriteContractSyncReturnType, +} from 'viem/actions'; import { type ChainEIP712, type Eip712WalletActions, @@ -58,11 +65,14 @@ import { import { sendCalls } from '../../actions/sendCalls.js'; import { sendTransaction } from '../../actions/sendTransaction.js'; import { sendTransactionBatch } from '../../actions/sendTransactionBatch.js'; +import type { SendEip712TransactionSyncParameters } from '../../actions/sendTransactionSync.js'; +import { sendTransactionSync } from '../../actions/sendTransactionSync.js'; import { signMessage } from '../../actions/signMessage.js'; import { signTransaction } from '../../actions/signTransaction.js'; import { signTransactionBatch } from '../../actions/signTransactionBatch.js'; import { signTypedData } from '../../actions/signTypedData.js'; import { writeContract } from '../../actions/writeContract.js'; +import { writeContractSync } from '../../actions/writeContractSync.js'; import { EOA_VALIDATOR_ADDRESS } from '../../constants.js'; import type { SessionConfig, SessionStatus } from '../../sessions.js'; import type { CustomPaymasterHandler } from '../../types/customPaymaster.js'; @@ -134,6 +144,30 @@ export type AbstractWalletActions< request >, ) => Promise; + sendTransactionSync: < + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + >( + args: SendEip712TransactionSyncParameters, + ) => Promise>; + writeContractSync: < + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + args extends ContractFunctionArgs< + abi, + 'nonpayable' | 'payable', + functionName + >, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + >( + args: WriteContractSyncParameters< + abi, + functionName, + args, + chain, + account, + chainOverride + >, + ) => Promise>; toSessionClient: (signer: Account, session: SessionConfig) => SessionClient; getSessionStatus: ( sessionHashOrConfig: Hash | SessionConfig, @@ -241,6 +275,24 @@ export function globalWalletActions< >, isPrivyCrossApp, ), + sendTransactionSync: (args) => + sendTransactionSync( + client, + signerClient, + publicClient, + args, + isPrivyCrossApp, + customPaymasterHandler, + ), + writeContractSync: (args) => + writeContractSync( + client, + signerClient, + publicClient, + args, + isPrivyCrossApp, + customPaymasterHandler, + ), toSessionClient: (signer, session) => toSessionClient({ client: client as AbstractClient, diff --git a/packages/agw-client/src/clients/decorators/session.ts b/packages/agw-client/src/clients/decorators/session.ts index e612226c..d0b67cd4 100644 --- a/packages/agw-client/src/clients/decorators/session.ts +++ b/packages/agw-client/src/clients/decorators/session.ts @@ -1,7 +1,10 @@ import { + type Abi, type Account, type Chain, type Client, + type ContractFunctionArgs, + type ContractFunctionName, type PublicClient, type SendTransactionRequest, type SendTransactionReturnType, @@ -10,6 +13,11 @@ import { type WalletClient, } from 'viem'; import { parseAccount, type SignTransactionReturnType } from 'viem/accounts'; +import { + type SendTransactionSyncReturnType, + type WriteContractSyncParameters, + type WriteContractSyncReturnType, +} from 'viem/actions'; import { type ChainEIP712, @@ -18,9 +26,12 @@ import { } from 'viem/zksync'; import { getSessionStatus } from '../../actions/getSessionStatus.js'; import { sendTransactionForSession } from '../../actions/sendTransactionForSession.js'; +import { sendTransactionForSessionSync } from '../../actions/sendTransactionForSessionSync.js'; +import type { SendEip712TransactionSyncParameters } from '../../actions/sendTransactionSync.js'; import { signTransactionForSession } from '../../actions/signTransactionForSession.js'; import { signTypedDataForSession } from '../../actions/signTypedData.js'; import { writeContractForSession } from '../../actions/writeContractForSession.js'; +import { writeContractForSessionSync } from '../../actions/writeContractForSessionSync.js'; import type { SessionConfig, SessionStatus } from '../../sessions.js'; import type { CustomPaymasterHandler } from '../../types/customPaymaster.js'; @@ -45,6 +56,36 @@ export type SessionClientActions< args: SignEip712TransactionParameters, ) => Promise; writeContract: WalletActions['writeContract']; + sendTransactionSync: < + const request extends SendTransactionRequest, + chainOverride extends ChainEIP712 | undefined = undefined, + >( + args: SendEip712TransactionSyncParameters< + chain, + account, + chainOverride, + request + >, + ) => Promise>; + writeContractSync: < + const abi extends Abi | readonly unknown[], + functionName extends ContractFunctionName, + args extends ContractFunctionArgs< + abi, + 'nonpayable' | 'payable', + functionName + >, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + >( + args: WriteContractSyncParameters< + abi, + functionName, + args, + chain, + account, + chainOverride + >, + ) => Promise>; signTypedData: WalletActions['signTypedData']; getSessionStatus: () => Promise; }; @@ -76,6 +117,24 @@ export function sessionWalletActions( session, paymasterHandler, ), + sendTransactionSync: (args) => + sendTransactionForSessionSync( + client, + signerClient, + publicClient, + args, + session, + paymasterHandler, + ), + writeContractSync: (args) => + writeContractForSessionSync( + client, + signerClient, + publicClient, + args, + session, + paymasterHandler, + ), signTransaction: (args) => signTransactionForSession( client, diff --git a/packages/agw-client/src/exports/actions.ts b/packages/agw-client/src/exports/actions.ts index 445ee9ae..cf4619a5 100644 --- a/packages/agw-client/src/exports/actions.ts +++ b/packages/agw-client/src/exports/actions.ts @@ -32,3 +32,7 @@ export { type SendTransactionBatchParameters, type SendTransactionBatchReturnType, } from '../actions/sendTransactionBatch.js'; +export { sendTransactionForSessionSync } from '../actions/sendTransactionForSessionSync.js'; +export { sendTransactionSync } from '../actions/sendTransactionSync.js'; +export { writeContractForSessionSync } from '../actions/writeContractForSessionSync.js'; +export { writeContractSync } from '../actions/writeContractSync.js'; diff --git a/packages/agw-client/test/src/actions/sendTransactionForSessionSync.test.ts b/packages/agw-client/test/src/actions/sendTransactionForSessionSync.test.ts new file mode 100644 index 00000000..bf463b8c --- /dev/null +++ b/packages/agw-client/test/src/actions/sendTransactionForSessionSync.test.ts @@ -0,0 +1,241 @@ +import { + createClient, + createPublicClient, + createWalletClient, + http, + parseEther, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712, ZksyncTransactionRequestEIP712 } from 'viem/zksync'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { SESSION_KEY_VALIDATOR_ADDRESS } from '../../../src/constants.js'; +import { isSmartAccountDeployed } from '../../../src/utils.js'; +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; + +vi.mock('../../../src/utils.js'); +vi.mock('../../../src/actions/sendTransactionInternal', () => ({ + sendTransactionInternal: vi.fn().mockResolvedValue({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', + }), +})); +vi.mock('viem', async (importOriginal) => { + const original = await importOriginal(); + return { + ...(original as any), + encodeFunctionData: vi.fn().mockReturnValue('0xmockedEncodedData'), + }; +}); + +import { sendTransactionForSessionSync } from '../../../src/actions/sendTransactionForSessionSync.js'; +import { sendTransactionInternal } from '../../../src/actions/sendTransactionInternal.js'; +import { + encodeSessionWithPeriodIds, + getPeriodIdsForTransaction, + LimitType, + LimitZero, + SessionConfig, +} from '../../../src/sessions.js'; + +const mockReceipt = { + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', +}; + +// Client setup +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const signerClient = createWalletClient({ + account: toAccount(address.sessionSignerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +const publicClient = createPublicClient({ + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const transaction1: ZksyncTransactionRequestEIP712 = { + to: '0x5432100000000000000000000000000000000000', + from: '0x0000000000000000000000000000000000000000', + data: '0x01234578', + paymaster: '0x5407B5040dec3D339A9247f3654E59EEccbb6391', + paymasterInput: '0xabc', +}; + +const transaction2: ZksyncTransactionRequestEIP712 = { + to: '0x0000000000000000000000000000000000000101', + from: '0x0000000000000000000000000000000000000000', + value: 1000n, +}; + +const session: SessionConfig = { + signer: signerClient.account.address, + expiresAt: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7), + feeLimit: { + limit: parseEther('1'), + limitType: LimitType.Lifetime, + period: 0n, + }, + callPolicies: [ + { + selector: '0x01234578', + target: '0x5432100000000000000000000000000000000000', + constraints: [], + maxValuePerUse: 0n, + valueLimit: LimitZero, + }, + ], + transferPolicies: [], +}; + +const session2: SessionConfig = { + signer: signerClient.account.address, + expiresAt: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7), + feeLimit: { + limit: parseEther('1'), + limitType: LimitType.Lifetime, + period: 0n, + }, + callPolicies: [], + transferPolicies: [ + { + maxValuePerUse: parseEther('1'), + target: transaction2.to, + valueLimit: { + limit: parseEther('1'), + limitType: LimitType.Lifetime, + period: 0n, + }, + }, + ], +}; + +describe('sendTransactionForSessionSync', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(sendTransactionInternal).mockResolvedValue(mockReceipt); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('should call sendTransactionInternal with correct arguments and sync callback (function call)', async () => { + vi.mocked(isSmartAccountDeployed).mockResolvedValue(true); + await sendTransactionForSessionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction1, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + }, + session, + ); + expect(sendTransactionInternal).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + { + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + to: transaction1.to, + from: transaction1.from, + data: transaction1.data, + type: 'eip712', + paymaster: '0x5407B5040dec3D339A9247f3654E59EEccbb6391', + paymasterInput: '0xabc', + }, + SESSION_KEY_VALIDATOR_ADDRESS, + { + [SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds( + session, + getPeriodIdsForTransaction({ + sessionConfig: session, + target: transaction1.to, + selector: transaction1.data, + timestamp: BigInt(Math.floor(Date.now() / 1000)), + }), + ), + }, + undefined, + expect.any(Function), + ); + }); + + it('should call sendTransactionInternal with correct arguments (simple transfer)', async () => { + vi.mocked(isSmartAccountDeployed).mockResolvedValue(true); + await sendTransactionForSessionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction2, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + }, + session2, + ); + expect(sendTransactionInternal).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + { + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + to: transaction2.to, + from: transaction2.from, + value: transaction2.value, + type: 'eip712', + }, + SESSION_KEY_VALIDATOR_ADDRESS, + { + [SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds( + session2, + getPeriodIdsForTransaction({ + sessionConfig: session2, + target: transaction2.to, + selector: transaction2.data, + timestamp: BigInt(Math.floor(Date.now() / 1000)), + }), + ), + }, + undefined, + expect.any(Function), + ); + }); + + it('should throw if to field is not set', async () => { + vi.mocked(isSmartAccountDeployed).mockResolvedValue(true); + await expect( + sendTransactionForSessionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction1, + to: undefined, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + }, + session, + ), + ).rejects.toThrow('Transaction to field is not specified'); + }); +}); diff --git a/packages/agw-client/test/src/actions/sendTransactionInternal.test.ts b/packages/agw-client/test/src/actions/sendTransactionInternal.test.ts index d32ecc36..5fa583fe 100644 --- a/packages/agw-client/test/src/actions/sendTransactionInternal.test.ts +++ b/packages/agw-client/test/src/actions/sendTransactionInternal.test.ts @@ -193,6 +193,42 @@ describe('sendTransactionInternal', () => { ); }); +test('sendTransactionInternal with custom sendSerializedTransaction callback', async () => { + vi.mocked(isSmartAccountDeployed).mockResolvedValue(true); + baseClientRequestSpy.mockClear(); + const mockReceipt = { + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: MOCK_TRANSACTION_HASH, + status: 'success' as const, + }; + const customCallback = vi.fn().mockResolvedValue(mockReceipt); + + const result = await sendTransactionInternal( + baseClient, + signerClient, + publicClient, + { + ...transaction, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + } as any, + EOA_VALIDATOR_ADDRESS, + {}, + undefined, + customCallback, + ); + + expect(result).toBe(mockReceipt); + expect(customCallback).toHaveBeenCalledWith(MOCK_TRANSACTION_HASH); + // Verify that sendRawTransaction was NOT called + const sendRawTransactionCall = baseClientRequestSpy.mock.calls.find( + (call) => call[0].method === 'eth_sendRawTransaction', + ); + expect(sendRawTransactionCall).toBeUndefined(); +}); + test('sendTransactionInternal with mismatched chain', async () => { const invalidChain = mainnet; expect( diff --git a/packages/agw-client/test/src/actions/sendTransactionSync.test.ts b/packages/agw-client/test/src/actions/sendTransactionSync.test.ts new file mode 100644 index 00000000..28531b5e --- /dev/null +++ b/packages/agw-client/test/src/actions/sendTransactionSync.test.ts @@ -0,0 +1,279 @@ +import { + createClient, + createPublicClient, + createWalletClient, + http, + zeroAddress, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712, ZksyncTransactionRequestEIP712 } from 'viem/zksync'; +import { describe, expect, test, vi } from 'vitest'; + +import { sendTransactionSync } from '../../../src/actions/sendTransactionSync.js'; +import { EOA_VALIDATOR_ADDRESS } from '../../../src/constants.js'; +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; + +vi.mock('../../../src/actions/sendTransactionInternal'); +vi.mock('../../../src/actions/sendPrivyTransaction'); +vi.mock('viem/actions', async (importOriginal) => { + const original = await importOriginal(); + return { + ...(original as any), + sendRawTransactionSync: vi.fn().mockResolvedValue({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedSyncHash', + status: 'success', + }), + }; +}); + +import { sendRawTransactionSync } from 'viem/actions'; +import { signPrivyTransaction } from '../../../src/actions/sendPrivyTransaction.js'; +import { sendTransactionInternal } from '../../../src/actions/sendTransactionInternal.js'; + +const mockReceipt = { + blockHash: '0x1234' as `0x${string}`, + blockNumber: 1n, + contractAddress: null, + cumulativeGasUsed: 21000n, + effectiveGasPrice: 1000000000n, + from: address.smartAccountAddress, + gasUsed: 21000n, + logs: [], + logsBloom: '0x' as `0x${string}`, + status: 'success' as const, + to: '0x5432100000000000000000000000000000000000' as `0x${string}`, + transactionHash: '0xmockedTransactionHash' as `0x${string}`, + transactionIndex: 0, + type: 'eip1559' as const, +}; + +// Client setup +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const signerClient = createWalletClient({ + account: toAccount(address.signerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +const publicClient = createPublicClient({ + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const transaction: ZksyncTransactionRequestEIP712 = { + to: '0x5432100000000000000000000000000000000000', + from: '0x0000000000000000000000000000000000000000', + data: '0x1234', + paymaster: '0x5407B5040dec3D339A9247f3654E59EEccbb6391', + paymasterInput: '0xabc', +}; + +describe('sendTransactionSync', () => { + test('calls sendTransactionInternal with sync callback', async () => { + vi.mocked(sendTransactionInternal).mockResolvedValue(mockReceipt); + const receipt = await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + } as any, + false, + ); + + expect(sendTransactionInternal).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + expect.objectContaining({ + from: zeroAddress, + to: transaction.to, + data: transaction.data, + type: 'eip712', + paymaster: '0x5407B5040dec3D339A9247f3654E59EEccbb6391', + paymasterInput: '0xabc', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + }), + EOA_VALIDATOR_ADDRESS, + {}, + undefined, + expect.any(Function), + ); + expect(receipt).toBe(mockReceipt); + }); + + test('passes throwOnReceiptRevert and timeout via callback', async () => { + vi.mocked(sendTransactionInternal).mockResolvedValue(mockReceipt); + await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + throwOnReceiptRevert: false, + timeout: 5000, + } as any, + false, + ); + + // The sendTransactionInternal should receive parameters WITHOUT throwOnReceiptRevert/timeout + const callArgs = vi.mocked(sendTransactionInternal).mock.calls[0]; + const txParams = callArgs[3] as any; + expect(txParams.throwOnReceiptRevert).toBeUndefined(); + expect(txParams.timeout).toBeUndefined(); + // But the callback (8th arg) should exist + expect(callArgs[7]).toBeInstanceOf(Function); + }); + + test('calls customPaymasterHandler and uses its output in Privy cross-app flow', async () => { + const transactionWithoutPaymaster: ZksyncTransactionRequestEIP712 = { + to: '0x5432100000000000000000000000000000000000', + from: zeroAddress, + data: '0x1234', + }; + + const mockPaymasterHandler = vi.fn().mockResolvedValue({ + paymaster: '0xCustomPaymaster0000000000000000000000000', + paymasterInput: '0xdeadbeef', + }); + + vi.mocked(signPrivyTransaction).mockResolvedValue('0xsigned'); + vi.mocked(sendRawTransactionSync).mockResolvedValue(mockReceipt as any); + + await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transactionWithoutPaymaster, + type: 'eip712', + } as any, + true, + mockPaymasterHandler, + ); + + expect(mockPaymasterHandler).toHaveBeenCalledOnce(); + expect(mockPaymasterHandler).toHaveBeenCalledWith( + expect.objectContaining({ + to: transactionWithoutPaymaster.to, + data: transactionWithoutPaymaster.data, + from: baseClient.account.address, + chainId: baseClient.chain.id, + }), + ); + + // The paymaster output should be merged into the params passed to signPrivyTransaction + expect(signPrivyTransaction).toHaveBeenCalledWith( + baseClient, + expect.objectContaining({ + paymaster: '0xCustomPaymaster0000000000000000000000000', + paymasterInput: '0xdeadbeef', + }), + ); + }); + + test('skips customPaymasterHandler when paymaster already set in Privy cross-app flow', async () => { + const mockPaymasterHandler = vi.fn(); + + vi.mocked(signPrivyTransaction).mockResolvedValue('0xsigned'); + vi.mocked(sendRawTransactionSync).mockResolvedValue(mockReceipt as any); + + await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction, // already has paymaster + paymasterInput + type: 'eip712', + } as any, + true, + mockPaymasterHandler, + ); + + expect(mockPaymasterHandler).not.toHaveBeenCalled(); + }); + + test('forwards customPaymasterHandler to sendTransactionInternal in non-Privy flow', async () => { + vi.mocked(sendTransactionInternal).mockResolvedValue(mockReceipt); + + const mockPaymasterHandler = vi.fn(); + + await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + } as any, + false, + mockPaymasterHandler, + ); + + expect(sendTransactionInternal).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + expect.any(Object), + EOA_VALIDATOR_ADDRESS, + {}, + mockPaymasterHandler, + expect.any(Function), + ); + }); + + test('calls signPrivyTransaction for Privy cross-app flow', async () => { + vi.mocked(signPrivyTransaction).mockResolvedValue('0x01abab'); + vi.mocked(sendRawTransactionSync).mockResolvedValue({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedSyncHash', + status: 'success', + } as any); + + const receipt = await sendTransactionSync( + baseClient, + signerClient, + publicClient, + { + ...transaction, + type: 'eip712', + } as any, + true, + ); + + expect(signPrivyTransaction).toHaveBeenCalledWith( + baseClient, + expect.objectContaining(transaction), + ); + expect(sendRawTransactionSync).toHaveBeenCalledWith( + publicClient, + expect.objectContaining({ + serializedTransaction: '0x01abab', + }), + ); + expect(receipt).toEqual({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedSyncHash', + status: 'success', + }); + }); +}); diff --git a/packages/agw-client/test/src/actions/writeContractForSessionSync.test.ts b/packages/agw-client/test/src/actions/writeContractForSessionSync.test.ts new file mode 100644 index 00000000..98bf9172 --- /dev/null +++ b/packages/agw-client/test/src/actions/writeContractForSessionSync.test.ts @@ -0,0 +1,147 @@ +import { + createClient, + createPublicClient, + createWalletClient, + encodeFunctionData, + http, + parseEther, + toFunctionSelector, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712 } from 'viem/zksync'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; + +vi.mock('../../../src/utils.js'); +vi.mock('../../../src/actions/sendTransactionForSessionSync', () => ({ + sendTransactionForSessionSync: vi.fn().mockResolvedValue({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', + }), +})); + +import { sendTransactionForSessionSync } from '../../../src/actions/sendTransactionForSessionSync.js'; +import { writeContractForSessionSync } from '../../../src/actions/writeContractForSessionSync.js'; +import { LimitType, LimitZero, SessionConfig } from '../../../src/sessions.js'; +import { isSmartAccountDeployed } from '../../../src/utils.js'; + +const mockReceipt = { + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', +}; + +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const signerClient = createWalletClient({ + account: toAccount(address.sessionSignerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +const publicClient = createPublicClient({ + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const TestTokenABI = [ + { + inputs: [ + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'uint256', + name: 'qty', + type: 'uint256', + }, + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; + +const MOCK_CONTRACT_ADDRESS = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; + +const session: SessionConfig = { + signer: signerClient.account.address, + expiresAt: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7), + feeLimit: { + limit: parseEther('1'), + limitType: LimitType.Lifetime, + period: 0n, + }, + callPolicies: [ + { + selector: toFunctionSelector('function mint(address,uint256)'), + target: MOCK_CONTRACT_ADDRESS, + constraints: [], + maxValuePerUse: 0n, + valueLimit: LimitZero, + }, + ], + transferPolicies: [], +}; + +beforeEach(() => { + vi.resetAllMocks(); +}); + +test('writeContractForSessionSync', async () => { + vi.mocked(isSmartAccountDeployed).mockResolvedValue(true); + vi.mocked(sendTransactionForSessionSync).mockResolvedValue( + mockReceipt as any, + ); + + const expectedData = encodeFunctionData({ + abi: TestTokenABI, + args: [address.signerAddress, 1n], + functionName: 'mint', + }); + + const receipt = await writeContractForSessionSync( + baseClient, + signerClient, + publicClient, + { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + }, + session, + ); + + expect(receipt).toBe(mockReceipt); + + expect(sendTransactionForSessionSync).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + { + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + data: expectedData, + to: MOCK_CONTRACT_ADDRESS, + throwOnReceiptRevert: undefined, + timeout: undefined, + }, + session, + undefined, + ); +}); diff --git a/packages/agw-client/test/src/actions/writeContractSync.test.ts b/packages/agw-client/test/src/actions/writeContractSync.test.ts new file mode 100644 index 00000000..8032d234 --- /dev/null +++ b/packages/agw-client/test/src/actions/writeContractSync.test.ts @@ -0,0 +1,257 @@ +import { + createClient, + createPublicClient, + createWalletClient, + encodeFunctionData, + http, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712 } from 'viem/zksync'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import { writeContractSync } from '../../../src/actions/writeContractSync.js'; +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; + +vi.mock('../../../src/actions/sendTransactionSync', () => ({ + sendTransactionSync: vi.fn().mockResolvedValue({ + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', + }), +})); + +import { sendTransactionSync } from '../../../src/actions/sendTransactionSync.js'; + +vi.mock('viem/utils', async (importOriginal) => { + const original = await importOriginal(); + return { + ...(original as any), + getContractError: vi + .fn() + .mockReturnValue(new Error('Mocked getContractError')), + }; +}); + +import { getContractError } from 'viem/utils'; + +const mockReceipt = { + blockHash: '0x1234', + blockNumber: 1n, + transactionHash: '0xmockedTransactionHash', + status: 'success', +}; + +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const signerClient = createWalletClient({ + account: toAccount(address.signerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +const publicClient = createPublicClient({ + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +const TestTokenABI = [ + { + inputs: [ + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { + internalType: 'uint256', + name: 'qty', + type: 'uint256', + }, + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; + +const MOCK_CONTRACT_ADDRESS = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; + +beforeEach(() => { + vi.resetAllMocks(); +}); + +test('basic', async () => { + vi.mocked(sendTransactionSync).mockResolvedValue(mockReceipt as any); + + const expectedData = encodeFunctionData({ + abi: TestTokenABI, + args: [address.signerAddress, 1n], + functionName: 'mint', + }); + + const receipt = await writeContractSync( + baseClient, + signerClient, + publicClient, + { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + }, + ); + + expect(receipt).toBe(mockReceipt); + + expect(sendTransactionSync).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + { + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + data: expectedData, + to: MOCK_CONTRACT_ADDRESS, + throwOnReceiptRevert: undefined, + timeout: undefined, + }, + false, + undefined, + ); +}); + +test('passes sync-specific params', async () => { + vi.mocked(sendTransactionSync).mockResolvedValue(mockReceipt as any); + + await writeContractSync(baseClient, signerClient, publicClient, { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + throwOnReceiptRevert: false, + timeout: 5000, + }); + + expect(sendTransactionSync).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + expect.objectContaining({ + throwOnReceiptRevert: false, + timeout: 5000, + }), + false, + undefined, + ); +}); + +test('forwards customPaymasterHandler to sendTransactionSync', async () => { + vi.mocked(sendTransactionSync).mockResolvedValue(mockReceipt as any); + + const mockPaymasterHandler = vi.fn().mockResolvedValue({ + paymaster: '0x5407B5040dec3D339A9247f3654E59EEccbb6391', + paymasterInput: '0xabc', + }); + + const expectedData = encodeFunctionData({ + abi: TestTokenABI, + args: [address.signerAddress, 1n], + functionName: 'mint', + }); + + const receipt = await writeContractSync( + baseClient, + signerClient, + publicClient, + { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + }, + false, + mockPaymasterHandler, + ); + + expect(receipt).toBe(mockReceipt); + + expect(sendTransactionSync).toHaveBeenCalledWith( + baseClient, + signerClient, + publicClient, + { + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + data: expectedData, + to: MOCK_CONTRACT_ADDRESS, + throwOnReceiptRevert: undefined, + timeout: undefined, + }, + false, + mockPaymasterHandler, + ); +}); + +test('getContractError', async () => { + vi.mocked(sendTransactionSync).mockRejectedValue( + new Error('Send transaction failed'), + ); + + await expect( + writeContractSync(baseClient, signerClient, publicClient, { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + }), + ).rejects.toThrowError('Mocked getContractError'); + + expect(getContractError).toHaveBeenCalledWith( + new Error('Send transaction failed'), + { + abi: TestTokenABI, + address: MOCK_CONTRACT_ADDRESS, + args: [address.signerAddress, 1n], + docsPath: '/docs/contract/writeContract', + functionName: 'mint', + sender: baseClient.account.address, + }, + ); +}); + +test('account not found', async () => { + const originalAccount = baseClient.account; + (baseClient as any).account = undefined; + + await expect( + writeContractSync(baseClient, signerClient, publicClient, { + abi: TestTokenABI, + account: baseClient.account, + address: MOCK_CONTRACT_ADDRESS, + chain: anvilAbstractTestnet.chain as ChainEIP712, + functionName: 'mint', + args: [address.signerAddress, 1n], + }), + ).rejects.toThrowError( + 'Could not find an Account to execute with this Action', + ); + + expect(sendTransactionSync).not.toHaveBeenCalled(); + + (baseClient as any).account = originalAccount; +});