From a166b2f7b2916338272127fa94eb3088f5bcab27 Mon Sep 17 00:00:00 2001 From: jxom Date: Thu, 17 Oct 2024 09:47:54 +1100 Subject: [PATCH] feat: add wallet_sendTransaction support --- .../docs/actions/wallet/sendTransaction.md | 4 +- site/pages/docs/contract/writeContract.md | 4 +- src/actions/getContract.test-d.ts | 4 +- .../wallet/prepareTransactionRequest.ts | 2 +- src/actions/wallet/sendTransaction.test.ts | 20 +++++++++ src/actions/wallet/sendTransaction.ts | 42 ++++++++++++------- src/actions/wallet/writeContract.ts | 4 +- src/clients/createClient.test-d.ts | 2 +- src/errors/transaction.ts | 2 +- .../eip5792/actions/getCapabilities.ts | 31 +++++--------- .../eip5792/decorators/eip5792.ts | 4 +- src/types/account.ts | 23 +++++++--- src/utils/errors/getTransactionError.ts | 2 +- test/src/anvil.ts | 3 ++ 14 files changed, 90 insertions(+), 57 deletions(-) diff --git a/site/pages/docs/actions/wallet/sendTransaction.md b/site/pages/docs/actions/wallet/sendTransaction.md index 8e7b2c9a5c..b51b5f3b36 100644 --- a/site/pages/docs/actions/wallet/sendTransaction.md +++ b/site/pages/docs/actions/wallet/sendTransaction.md @@ -87,11 +87,11 @@ The [Transaction](/docs/glossary/terms#transaction) hash. ### account -- **Type:** `Account | Address` +- **Type:** `Account | Address | null` The Account to send the transaction from. -Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). If set to `null`, it is assumed that the transport will handle filling the sender of the transaction. ```ts twoslash // [!include ~/snippets/walletClient.ts] diff --git a/site/pages/docs/contract/writeContract.md b/site/pages/docs/contract/writeContract.md index 4e4e16d2f0..a668d68fe8 100644 --- a/site/pages/docs/contract/writeContract.md +++ b/site/pages/docs/contract/writeContract.md @@ -248,11 +248,11 @@ await walletClient.writeContract({ ### account -- **Type:** `Account | Address` +- **Type:** `Account | Address | null` The Account to write to the contract from. -Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). If set to `null`, it is assumed that the transport will handle the filling the sender of the transaction. ```ts await walletClient.writeContract({ diff --git a/src/actions/getContract.test-d.ts b/src/actions/getContract.test-d.ts index a3b873ea42..8f047ae115 100644 --- a/src/actions/getContract.test-d.ts +++ b/src/actions/getContract.test-d.ts @@ -328,12 +328,12 @@ test('with and without wallet client `account`', () => { expectTypeOf(contractWithAccount.write.approve) .parameter(1) - .extract<{ account?: Account | Address | undefined }>() + .extract<{ account?: Account | Address | null | undefined }>() // @ts-expect-error .toBeNever() expectTypeOf(contractWithoutAccount.write.approve) .parameter(1) - .extract<{ account: Account | Address }>() + .extract<{ account: Account | Address | null }>() // @ts-expect-error .toBeNever() }) diff --git a/src/actions/wallet/prepareTransactionRequest.ts b/src/actions/wallet/prepareTransactionRequest.ts index c7ddf07605..530a38ab86 100644 --- a/src/actions/wallet/prepareTransactionRequest.ts +++ b/src/actions/wallet/prepareTransactionRequest.ts @@ -122,7 +122,7 @@ export type PrepareTransactionRequestParameters< chainOverride > = PrepareTransactionRequestRequest, > = request & - GetAccountParameter & + GetAccountParameter & GetChainParameter & GetTransactionRequestKzgParameter & { chainId?: number | undefined } diff --git a/src/actions/wallet/sendTransaction.test.ts b/src/actions/wallet/sendTransaction.test.ts index 6392a498f1..4dc86e4266 100644 --- a/src/actions/wallet/sendTransaction.test.ts +++ b/src/actions/wallet/sendTransaction.test.ts @@ -40,6 +40,7 @@ import { reset } from '../test/reset.js' import { setBalance } from '../test/setBalance.js' import { setNextBlockBaseFeePerGas } from '../test/setNextBlockBaseFeePerGas.js' import { sendTransaction } from './sendTransaction.js' +import { InvalidInputRpcError } from '../../errors/rpc.js' const client = anvilMainnet.getClient() const clientWithAccount = anvilMainnet.getClient({ @@ -605,6 +606,25 @@ describe('args: chain', async () => { Version: viem@x.y.z] `) }) + + test('behavior: nullish account, transport supports `wallet_sendTransaction`', async () => { + await setup() + + const request = client.request + client.request = (parameters: any) => { + if (parameters.method === 'eth_sendTransaction') + throw new InvalidInputRpcError(new Error()) + return request(parameters) + } + + expect( + await sendTransaction(client, { + account: null, + to: targetAccount.address, + value: 0n, + }), + ).toBeDefined() + }) }) describe('local account', () => { diff --git a/src/actions/wallet/sendTransaction.ts b/src/actions/wallet/sendTransaction.ts index 9862354ec5..6bd6ea4c39 100644 --- a/src/actions/wallet/sendTransaction.ts +++ b/src/actions/wallet/sendTransaction.ts @@ -1,3 +1,5 @@ +import type { Address } from 'abitype' + import type { Account } from '../../accounts/types.js' import { type ParseAccountErrorType, @@ -73,7 +75,7 @@ export type SendTransactionParameters< chainOverride > = SendTransactionRequest, > = request & - GetAccountParameter & + GetAccountParameter & GetChainParameter & GetTransactionRequestKzgParameter @@ -166,11 +168,11 @@ export async function sendTransaction< ...rest } = parameters - if (!account_) + if (typeof account_ === 'undefined') throw new AccountNotFoundError({ docsPath: '/docs/actions/wallet/sendTransaction', }) - const account = parseAccount(account_) + const account = account_ ? parseAccount(account_) : null try { assertRequest(parameters as AssertRequestParameters) @@ -194,7 +196,7 @@ export async function sendTransaction< return undefined })() - if (account.type === 'json-rpc') { + if (account?.type === 'json-rpc' || account === null) { let chainId: number | undefined if (chain !== null) { chainId = await getAction(client, getChainId, 'getChainId')({}) @@ -207,7 +209,7 @@ export async function sendTransaction< const chainFormat = client.chain?.formatters?.transactionRequest?.format const format = chainFormat || formatTransactionRequest - const request = format({ + const params = format({ // Pick out extra data that might exist on the chain's transaction request type. ...extract(rest, { format: chainFormat }), accessList, @@ -215,7 +217,7 @@ export async function sendTransaction< blobs, chainId, data, - from: account.address, + from: account?.address, gas, gasPrice, maxFeePerBlobGas, @@ -225,16 +227,26 @@ export async function sendTransaction< to, value, } as TransactionRequest) - return await client.request( - { + + try { + return await client.request({ method: 'eth_sendTransaction', - params: [request], - }, - { retryCount: 0 }, - ) + params: [params], + }) + } catch (e) { + const error = e as BaseError + // If the transport does not support the input, attempt to use the + // `wallet_sendTransaction` method. + if (error.name === 'InvalidInputRpcError') + return await client.request({ + method: 'wallet_sendTransaction', + params: [params], + }) + throw error + } } - if (account.type === 'local') { + if (account?.type === 'local') { // Prepare the request for signing (assign appropriate fees, etc.) const request = await getAction( client, @@ -273,7 +285,7 @@ export async function sendTransaction< }) } - if (account.type === 'smart') + if (account?.type === 'smart') throw new AccountTypeNotSupportedError({ metaMessages: [ 'Consider using the `sendUserOperation` Action instead.', @@ -284,7 +296,7 @@ export async function sendTransaction< throw new AccountTypeNotSupportedError({ docsPath: '/docs/actions/wallet/sendTransaction', - type: (account as { type: string }).type, + type: (account as any)?.type, }) } catch (err) { if (err instanceof AccountTypeNotSupportedError) throw err diff --git a/src/actions/wallet/writeContract.ts b/src/actions/wallet/writeContract.ts index af8f212019..5bb5de8124 100644 --- a/src/actions/wallet/writeContract.ts +++ b/src/actions/wallet/writeContract.ts @@ -1,4 +1,4 @@ -import type { Abi } from 'abitype' +import type { Abi, Address } from 'abitype' import type { Account } from '../../accounts/types.js' import { @@ -71,7 +71,7 @@ export type WriteContractParameters< > & GetChainParameter & Prettify< - GetAccountParameter & + GetAccountParameter & GetMutabilityAwareValue< abi, 'nonpayable' | 'payable', diff --git a/src/clients/createClient.test-d.ts b/src/clients/createClient.test-d.ts index 5f80e80bb7..3f55b4aeab 100644 --- a/src/clients/createClient.test-d.ts +++ b/src/clients/createClient.test-d.ts @@ -88,7 +88,7 @@ describe('extend', () => { client.extend(() => ({ async sendTransaction(args) { - expectTypeOf(args.account).toEqualTypeOf
() + expectTypeOf(args.account).toEqualTypeOf
() expectTypeOf(args.to).toEqualTypeOf
() expectTypeOf(args.value).toEqualTypeOf() return '0x' diff --git a/src/errors/transaction.ts b/src/errors/transaction.ts index bfbe12afe2..2c55ae25a2 100644 --- a/src/errors/transaction.ts +++ b/src/errors/transaction.ts @@ -160,7 +160,7 @@ export class TransactionExecutionError extends BaseError { to, value, }: Omit & { - account: Account + account: Account | null chain?: Chain | undefined docsPath?: string | undefined }, diff --git a/src/experimental/eip5792/actions/getCapabilities.ts b/src/experimental/eip5792/actions/getCapabilities.ts index 525ec39fa4..960c923741 100644 --- a/src/experimental/eip5792/actions/getCapabilities.ts +++ b/src/experimental/eip5792/actions/getCapabilities.ts @@ -1,9 +1,11 @@ +import type { Address } from 'abitype' + import { parseAccount } from '../../../accounts/utils/parseAccount.js' import type { Client } from '../../../clients/createClient.js' import type { Transport } from '../../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../../errors/account.js' import type { ErrorType } from '../../../errors/utils.js' -import type { Account, GetAccountParameter } from '../../../types/account.js' +import type { Account } from '../../../types/account.js' import type { Chain } from '../../../types/chain.js' import type { WalletCapabilities, @@ -12,9 +14,9 @@ import type { import type { Prettify } from '../../../types/utils.js' import type { RequestErrorType } from '../../../utils/buildRequest.js' -export type GetCapabilitiesParameters< - account extends Account | undefined = Account | undefined, -> = GetAccountParameter +export type GetCapabilitiesParameters = { + account?: Account | Address | undefined +} export type GetCapabilitiesReturnType = Prettify< WalletCapabilitiesRecord @@ -42,24 +44,11 @@ export type GetCapabilitiesErrorType = RequestErrorType | ErrorType * }) * const capabilities = await getCapabilities(client) */ -export async function getCapabilities< - chain extends Chain | undefined, - account extends Account | undefined = undefined, ->( - ...parameters: account extends Account - ? - | [client: Client] - | [ - client: Client, - parameters: GetCapabilitiesParameters, - ] - : [ - client: Client, - parameters: GetCapabilitiesParameters, - ] +export async function getCapabilities( + client: Client, + parameters: GetCapabilitiesParameters = {}, ): Promise { - const [client, args] = parameters - const account_raw = args?.account ?? client.account + const account_raw = parameters?.account ?? client.account if (!account_raw) throw new AccountNotFoundError() const account = parseAccount(account_raw) diff --git a/src/experimental/eip5792/decorators/eip5792.ts b/src/experimental/eip5792/decorators/eip5792.ts index 07e88a4573..5117817070 100644 --- a/src/experimental/eip5792/decorators/eip5792.ts +++ b/src/experimental/eip5792/decorators/eip5792.ts @@ -80,9 +80,7 @@ export type Eip5792Actions< * }) */ getCapabilities: ( - ...parameters: account extends Account - ? [] | [parameters: GetCapabilitiesParameters] - : [parameters: GetCapabilitiesParameters] + parameters?: GetCapabilitiesParameters, ) => Promise /** * Requests the connected wallet to send a batch of calls. diff --git a/src/types/account.ts b/src/types/account.ts index 0c45ea4041..9c24d4e192 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -1,7 +1,7 @@ import type { Address } from 'abitype' import type { Account, JsonRpcAccount } from '../accounts/types.js' -import type { IsUndefined, Prettify } from './utils.js' +import type { IsUndefined, MaybeRequired, Prettify } from './utils.js' export type DeriveAccount< account extends Account | undefined, @@ -12,11 +12,22 @@ export type GetAccountParameter< account extends Account | undefined = Account | undefined, accountOverride extends Account | Address | undefined = Account | Address, required extends boolean = true, -> = IsUndefined extends true - ? required extends true - ? { account: accountOverride | Account | Address } - : { account?: accountOverride | Account | Address | undefined } - : { account?: accountOverride | Account | Address | undefined } + nullish extends boolean = false, +> = MaybeRequired< + { + account?: + | accountOverride + | Account + | Address + | (nullish extends true ? null : never) + | undefined + }, + IsUndefined extends true + ? required extends true + ? true + : false + : false +> export type ParseAccount< accountOrAddress extends Account | Address | undefined = diff --git a/src/utils/errors/getTransactionError.ts b/src/utils/errors/getTransactionError.ts index 87d1fc9908..e95f0db1b7 100644 --- a/src/utils/errors/getTransactionError.ts +++ b/src/utils/errors/getTransactionError.ts @@ -19,7 +19,7 @@ export type GetTransactionErrorParameters = Omit< SendTransactionParameters, 'account' | 'chain' > & { - account: Account + account: Account | null chain?: Chain | undefined docsPath?: string | undefined } diff --git a/test/src/anvil.ts b/test/src/anvil.ts index de8d23abbd..869b5cb9fa 100644 --- a/test/src/anvil.ts +++ b/test/src/anvil.ts @@ -213,6 +213,9 @@ function defineAnvil( ], }, ] + if (method === 'wallet_sendTransaction') { + method = 'eth_sendTransaction' + } return request({ method, params }, opts) },