Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for wallet_sendTransaction fallback #2875

Merged
merged 8 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smooth-kiwis-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added support for `wallet_sendTransaction`.
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
32 changes: 32 additions & 0 deletions src/actions/wallet/sendTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { deploy } from '../../../test/src/utils.js'
import { generatePrivateKey } from '../../accounts/generatePrivateKey.js'
import { createWalletClient } from '../../clients/createWalletClient.js'
import { http } from '../../clients/transports/http.js'
import { InvalidInputRpcError } from '../../errors/rpc.js'
import { signAuthorization } from '../../experimental/index.js'
import type { Hex } from '../../types/misc.js'
import type { TransactionSerializable } from '../../types/transaction.js'
Expand Down Expand Up @@ -605,6 +606,37 @@ describe('args: chain', async () => {
Version: [email protected]]
`)
})

test('behavior: 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: sourceAccount.address,
to: targetAccount.address,
value: 0n,
}),
).toBeDefined()
})

test('behavior: nullish account', async () => {
await setup()

expect(
await sendTransaction(client, {
account: null,
to: targetAccount.address,
value: 0n,
}),
).toBeDefined()
})
})

describe('local account', () => {
Expand Down
61 changes: 45 additions & 16 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 @@ -40,6 +42,7 @@
formatTransactionRequest,
} from '../../utils/formatters/transactionRequest.js'
import { getAction } from '../../utils/getAction.js'
import { LruMap } from '../../utils/lru.js'
import {
type AssertRequestErrorType,
type AssertRequestParameters,
Expand All @@ -56,6 +59,8 @@
sendRawTransaction,
} from './sendRawTransaction.js'

const supportsWalletNamespace = new LruMap<boolean>(128)

export type SendTransactionRequest<
chain extends Chain | undefined = Chain | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
Expand All @@ -73,7 +78,7 @@
chainOverride
> = SendTransactionRequest<chain, chainOverride>,
> = request &
GetAccountParameter<account> &
GetAccountParameter<account, Account | Address, true, true> &
GetChainParameter<chain, chainOverride> &
GetTransactionRequestKzgParameter<request>

Expand Down Expand Up @@ -166,11 +171,11 @@
...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 +199,7 @@
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 +212,15 @@
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 +230,40 @@
to,
value,
} as TransactionRequest)
return await client.request(
{
method: 'eth_sendTransaction',
params: [request],
},
{ retryCount: 0 },
)

const method = supportsWalletNamespace.get(client.uid)
? 'wallet_sendTransaction'

Check warning on line 235 in src/actions/wallet/sendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/wallet/sendTransaction.ts#L235

Added line #L235 was not covered by tests
: 'eth_sendTransaction'

try {
return await client.request({
method,
params: [params],
})
} catch (e) {
const error = e as BaseError
// If the transport does not support the method or input, attempt to use the
// `wallet_sendTransaction` method.
if (
error.name === 'InvalidInputRpcError' ||
error.name === 'InvalidParamsRpcError' ||
error.name === 'MethodNotFoundRpcError' ||
error.name === 'MethodNotSupportedRpcError'

Check warning on line 251 in src/actions/wallet/sendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/wallet/sendTransaction.ts#L251

Added line #L251 was not covered by tests
)
return await client
.request({
method: 'wallet_sendTransaction',
params: [params],
})
.then((hash) => {
supportsWalletNamespace.set(client.uid, true)
return hash

Check warning on line 260 in src/actions/wallet/sendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/wallet/sendTransaction.ts#L259-L260

Added lines #L259 - L260 were not covered by tests
})
throw error
}

Check warning on line 263 in src/actions/wallet/sendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/wallet/sendTransaction.ts#L262-L263

Added lines #L262 - L263 were not covered by tests
}

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 +302,7 @@
})
}

if (account.type === 'smart')
if (account?.type === 'smart')
throw new AccountTypeNotSupportedError({
metaMessages: [
'Consider using the `sendUserOperation` Action instead.',
Expand All @@ -284,7 +313,7 @@

throw new AccountTypeNotSupportedError({
docsPath: '/docs/actions/wallet/sendTransaction',
type: (account as { type: string }).type,
type: (account as any)?.type,

Check warning on line 316 in src/actions/wallet/sendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/wallet/sendTransaction.ts#L316

Added line #L316 was not covered by tests
})
} catch (err) {
if (err instanceof AccountTypeNotSupportedError) throw err
Expand Down
28 changes: 28 additions & 0 deletions src/actions/wallet/writeContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { optimism } from '../../chains/index.js'
import { createWalletClient } from '../../clients/createWalletClient.js'
import { walletActions } from '../../clients/decorators/wallet.js'
import { http } from '../../clients/transports/http.js'
import { InvalidInputRpcError } from '../../errors/rpc.js'
import { signAuthorization } from '../../experimental/index.js'
import { decodeEventLog, getAddress, parseEther } from '../../utils/index.js'
import { getBalance } from '../public/getBalance.js'
Expand Down Expand Up @@ -462,6 +463,33 @@ test('w/ simulateContract (client chain mismatch)', async () => {
`)
})

test('behavior: transport supports `wallet_sendTransaction`', async () => {
const request = client.request
client.request = (parameters: any) => {
if (parameters.method === 'eth_sendTransaction')
throw new InvalidInputRpcError(new Error())
return request(parameters)
}

expect(
await writeContract(client, {
...wagmiContractConfig,
account: accounts[0].address,
functionName: 'mint',
}),
).toBeDefined()
})

test('behavior: nullish account', async () => {
expect(
await writeContract(client, {
...wagmiContractConfig,
functionName: 'mint',
account: null,
}),
).toBeDefined()
})

describe('behavior: contract revert', () => {
test('revert', async () => {
const { contractAddress } = await deployErrorExample()
Expand Down
10 changes: 5 additions & 5 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 Expand Up @@ -181,11 +181,11 @@ export async function writeContract<
...request
} = parameters as WriteContractParameters

if (!account_)
if (typeof account_ === 'undefined')
throw new AccountNotFoundError({
docsPath: '/docs/contract/writeContract',
})
const account = parseAccount(account_)
const account = account_ ? parseAccount(account_) : null

const data = encodeFunctionData({
abi,
Expand All @@ -211,7 +211,7 @@ export async function writeContract<
args,
docsPath: '/docs/contract/writeContract',
functionName,
sender: account.address,
sender: account?.address,
})
}
}
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
Loading
Loading