Skip to content

Commit

Permalink
feat: add wallet_sendTransaction support
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Oct 16, 2024
1 parent 5c9ed2f commit a166b2f
Show file tree
Hide file tree
Showing 14 changed files with 90 additions and 57 deletions.
4 changes: 2 additions & 2 deletions site/pages/docs/actions/wallet/sendTransaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions site/pages/docs/contract/writeContract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions src/actions/getContract.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
2 changes: 1 addition & 1 deletion src/actions/wallet/prepareTransactionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export type PrepareTransactionRequestParameters<
chainOverride
> = PrepareTransactionRequestRequest<chain, chainOverride>,
> = request &
GetAccountParameter<account, accountOverride, false> &
GetAccountParameter<account, accountOverride, false, true> &
GetChainParameter<chain, chainOverride> &
GetTransactionRequestKzgParameter<request> & { chainId?: number | undefined }

Expand Down
20 changes: 20 additions & 0 deletions src/actions/wallet/sendTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -605,6 +606,25 @@ describe('args: chain', async () => {
Version: [email protected]]
`)
})

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', () => {
Expand Down
42 changes: 27 additions & 15 deletions src/actions/wallet/sendTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Address } from 'abitype'

import type { Account } from '../../accounts/types.js'
import {
type ParseAccountErrorType,
Expand Down Expand Up @@ -73,7 +75,7 @@ export type SendTransactionParameters<
chainOverride
> = SendTransactionRequest<chain, chainOverride>,
> = request &
GetAccountParameter<account> &
GetAccountParameter<account, Account | Address, true, true> &
GetChainParameter<chain, chainOverride> &
GetTransactionRequestKzgParameter<request>

Expand Down Expand Up @@ -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)
Expand All @@ -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')({})
Expand All @@ -207,15 +209,15 @@ 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,
authorizationList,
blobs,
chainId,
data,
from: account.address,
from: account?.address,
gas,
gasPrice,
maxFeePerBlobGas,
Expand All @@ -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,
Expand Down Expand Up @@ -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.',
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/actions/wallet/writeContract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Abi } from 'abitype'
import type { Abi, Address } from 'abitype'

import type { Account } from '../../accounts/types.js'
import {
Expand Down Expand Up @@ -71,7 +71,7 @@ export type WriteContractParameters<
> &
GetChainParameter<chain, chainOverride> &
Prettify<
GetAccountParameter<account> &
GetAccountParameter<account, Account | Address, true, true> &
GetMutabilityAwareValue<
abi,
'nonpayable' | 'payable',
Expand Down
2 changes: 1 addition & 1 deletion src/clients/createClient.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('extend', () => {

client.extend(() => ({
async sendTransaction(args) {
expectTypeOf(args.account).toEqualTypeOf<Address | Account>()
expectTypeOf(args.account).toEqualTypeOf<Address | Account | null>()
expectTypeOf(args.to).toEqualTypeOf<Address | null | undefined>()
expectTypeOf(args.value).toEqualTypeOf<bigint | undefined>()
return '0x'
Expand Down
2 changes: 1 addition & 1 deletion src/errors/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class TransactionExecutionError extends BaseError {
to,
value,
}: Omit<SendTransactionParameters, 'account' | 'chain'> & {
account: Account
account: Account | null
chain?: Chain | undefined
docsPath?: string | undefined
},
Expand Down
31 changes: 10 additions & 21 deletions src/experimental/eip5792/actions/getCapabilities.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<account>
export type GetCapabilitiesParameters = {
account?: Account | Address | undefined
}

export type GetCapabilitiesReturnType = Prettify<
WalletCapabilitiesRecord<WalletCapabilities, number>
Expand Down Expand Up @@ -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<Transport, chain, account>]
| [
client: Client<Transport, chain, account>,
parameters: GetCapabilitiesParameters<account>,
]
: [
client: Client<Transport, chain, account>,
parameters: GetCapabilitiesParameters<account>,
]
export async function getCapabilities<chain extends Chain | undefined>(
client: Client<Transport, chain>,
parameters: GetCapabilitiesParameters = {},
): Promise<GetCapabilitiesReturnType> {
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)
Expand Down
4 changes: 1 addition & 3 deletions src/experimental/eip5792/decorators/eip5792.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ export type Eip5792Actions<
* })
*/
getCapabilities: (
...parameters: account extends Account
? [] | [parameters: GetCapabilitiesParameters<account>]
: [parameters: GetCapabilitiesParameters<undefined>]
parameters?: GetCapabilitiesParameters,
) => Promise<GetCapabilitiesReturnType>
/**
* Requests the connected wallet to send a batch of calls.
Expand Down
23 changes: 17 additions & 6 deletions src/types/account.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<account> 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<account> extends true
? required extends true
? true
: false
: false
>

export type ParseAccount<
accountOrAddress extends Account | Address | undefined =
Expand Down
2 changes: 1 addition & 1 deletion src/utils/errors/getTransactionError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type GetTransactionErrorParameters = Omit<
SendTransactionParameters,
'account' | 'chain'
> & {
account: Account
account: Account | null
chain?: Chain | undefined
docsPath?: string | undefined
}
Expand Down
3 changes: 3 additions & 0 deletions test/src/anvil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ function defineAnvil<const chain extends Chain>(
],
},
]
if (method === 'wallet_sendTransaction') {
method = 'eth_sendTransaction'
}

return request({ method, params }, opts)
},
Expand Down

0 comments on commit a166b2f

Please sign in to comment.