From 533b59006b3344b947485ef372f0c78b111f6dc3 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 28 Aug 2023 07:40:51 +1000 Subject: [PATCH] feat: `signTransaction`, `sendRawTransaction`, `prepareTransactionRequest` (#1058) * feat: signTranasction, prepareTransactionRequest, sendRawTransaction actions * refactor * coverage * docs * changeset * docs --- .changeset/brave-dryers-hope.md | 5 + site/.vitepress/sidebar.ts | 18 +- .../wallet/prepareTransactionRequest.md | 289 ++++++++++++++ .../docs/actions/wallet/sendRawTransaction.md | 71 ++++ site/docs/actions/wallet/signTransaction.md | 260 +++++++++++- src/actions/index.test.ts | 3 + src/actions/index.ts | 15 + src/actions/public/estimateFeesPerGas.test.ts | 17 + src/actions/public/estimateFeesPerGas.ts | 9 +- src/actions/public/estimateGas.ts | 10 +- .../public/estimateMaxPriorityFeePerGas.ts | 4 +- src/actions/public/getBlock.test.ts | 15 - src/actions/public/getTransaction.test.ts | 9 - src/actions/public/watchContractEvent.ts | 15 +- .../wallet/prepareTransactionRequest.test.ts} | 189 +++++---- .../wallet/prepareTransactionRequest.ts} | 116 ++++-- src/actions/wallet/sendRawTransaction.test.ts | 18 + src/actions/wallet/sendRawTransaction.ts | 48 +++ src/actions/wallet/sendTransaction.ts | 12 +- src/actions/wallet/signTransaction.test.ts | 377 ++++++++++++++++++ src/actions/wallet/signTransaction.ts | 137 +++++++ src/chains/celo/formatters.test-d.ts | 187 +++++++-- src/chains/celo/formatters.test.ts | 31 ++ src/chains/celo/formatters.ts | 23 +- src/chains/celo/types.ts | 116 ++++-- src/chains/utils.ts | 8 +- src/clients/createClient.test.ts | 2 + src/clients/createPublicClient.test.ts | 11 + src/clients/createTestClient.test.ts | 3 + src/clients/createWalletClient.test.ts | 21 + src/clients/decorators/public.test.ts | 27 ++ src/clients/decorators/public.ts | 81 ++++ src/clients/decorators/wallet.test.ts | 46 +++ src/clients/decorators/wallet.ts | 133 ++++++ src/index.test.ts | 2 +- src/index.ts | 8 +- src/types/chain.ts | 4 +- src/types/eip1193.ts | 2 +- src/utils/data/isBytesEqual.ts | 5 +- src/utils/ens/encodeLabelhash.ts | 2 +- src/utils/ens/encodedLabelToLabelhash.ts | 4 +- .../formatters/transactionRequest.test.ts | 27 +- src/utils/formatters/transactionRequest.ts | 10 + src/utils/index.test.ts | 2 +- src/utils/index.ts | 5 +- 45 files changed, 2153 insertions(+), 244 deletions(-) create mode 100644 .changeset/brave-dryers-hope.md create mode 100644 site/docs/actions/wallet/prepareTransactionRequest.md create mode 100644 site/docs/actions/wallet/sendRawTransaction.md rename src/{utils/transaction/prepareRequest.test.ts => actions/wallet/prepareTransactionRequest.test.ts} (79%) rename src/{utils/transaction/prepareRequest.ts => actions/wallet/prepareTransactionRequest.ts} (52%) create mode 100644 src/actions/wallet/sendRawTransaction.test.ts create mode 100644 src/actions/wallet/sendRawTransaction.ts create mode 100644 src/actions/wallet/signTransaction.test.ts create mode 100644 src/actions/wallet/signTransaction.ts diff --git a/.changeset/brave-dryers-hope.md b/.changeset/brave-dryers-hope.md new file mode 100644 index 0000000000..adf2bd74ac --- /dev/null +++ b/.changeset/brave-dryers-hope.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added `prepareTransactionRequest`, `signTransaction`, and `sendRawTransaction`. diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index 2d5a468fc4..983a3bbf80 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -172,6 +172,10 @@ export const sidebar: DefaultTheme.Sidebar = { text: 'Transaction', items: [ { text: 'call', link: '/docs/actions/public/call' }, + { + text: 'prepareTransactionRequest', + link: '/docs/actions/public/prepareTransactionRequest', + }, { text: 'getTransaction', link: '/docs/actions/public/getTransaction', @@ -184,6 +188,10 @@ export const sidebar: DefaultTheme.Sidebar = { text: 'getTransactionReceipt', link: '/docs/actions/public/getTransactionReceipt', }, + { + text: 'sendRawTransaction', + link: '/docs/actions/public/sendRawTransaction', + }, { text: 'waitForTransactionReceipt', link: '/docs/actions/public/waitForTransactionReceipt', @@ -265,12 +273,20 @@ export const sidebar: DefaultTheme.Sidebar = { { text: 'Transaction', items: [ + { + text: 'prepareTransactionRequest', + link: '/docs/actions/wallet/prepareTransactionRequest', + }, + { + text: 'sendRawTransaction', + link: '/docs/actions/wallet/sendRawTransaction', + }, { text: 'sendTransaction', link: '/docs/actions/wallet/sendTransaction', }, { - text: 'signTransaction 🚧', + text: 'signTransaction', link: '/docs/actions/wallet/signTransaction', }, ], diff --git a/site/docs/actions/wallet/prepareTransactionRequest.md b/site/docs/actions/wallet/prepareTransactionRequest.md new file mode 100644 index 0000000000..bc65fca44d --- /dev/null +++ b/site/docs/actions/wallet/prepareTransactionRequest.md @@ -0,0 +1,289 @@ +--- +head: + - - meta + - property: og:title + content: prepareTransactionRequest + - - meta + - name: description + content: Prepares a transaction request for signing. + - - meta + - property: og:description + content: Prepares a transaction request for signing. + +--- + +# prepareTransactionRequest + +Prepares a transaction request for signing by populating a nonce, gas limit, fee values, and a transaction type. + +## Usage + +::: code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ // [!code focus:16] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +/** + * { + * account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + * to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', + * maxFeePerGas: 150000000000n, + * maxPriorityFeePerGas: 1000000000n, + * nonce: 69, + * type: 'eip1559', + * value: 1000000000000000000n + * } + */ + +const signature = await walletClient.signTransaction(request) +const hash = await walletClient.sendRawTransaction(signature) +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum) +}) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `prepareTransactionRequest`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet.html#account). + +::: code-group + +```ts [example.ts] +import { walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ // [!code focus:16] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +/** + * { + * account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + * to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', + * maxFeePerGas: 150000000000n, + * maxPriorityFeePerGas: 1000000000n, + * nonce: 69, + * type: 'eip1559', + * value: 1000000000000000000n + * } + */ + +const signature = await walletClient.signTransaction(request) +const hash = await walletClient.sendRawTransaction(signature) +``` + +```ts {4-6,9} [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' + +// Retrieve Account from an EIP-1193 Provider. +const [account] = await window.ethereum.request({ + method: 'eth_requestAccounts' +}) + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) +}) +``` + +```ts {5} [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), + transport: custom(window.ethereum) +}) +``` + +::: + +## Returns + +[`TransactionRequest`](/docs/glossary/types#transactionrequest) + +The transaction request. + +## Parameters + +### account + +- **Type:** `Account | Address` + +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). + +```ts +const request = await walletClient.prepareSendTransaction({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### to + +- **Type:** `0x${string}` + +The transaction recipient or contract address. + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: 1000000000000000000n, + nonce: 69 +}) +``` + +### accessList (optional) + +- **Type:** [`AccessList`](/docs/glossary/types#accesslist) + +The access list. + +```ts +const request = await publicClient.prepareSendTransaction({ + accessList: [ // [!code focus:6] + { + address: '0x1', + storageKeys: ['0x1'], + }, + ], + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +The chain is also used to infer its request type (e.g. the Celo chain has a `gatewayFee` that you can pass through to `prepareSendTransaction`). + +```ts +import { optimism } from 'viem/chains' // [!code focus] + +const request = await walletClient.prepareSendTransaction({ + chain: optimism, // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### data (optional) + +- **Type:** `0x${string}` + +A contract hashed method call with encoded args. + +```ts +const request = await walletClient.prepareSendTransaction({ + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction). + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + gasPrice: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### maxFeePerGas (optional) + +- **Type:** `bigint` + +Total fee per gas (in wei), inclusive of `maxPriorityFeePerGas`. Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction) + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + maxFeePerGas: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### maxPriorityFeePerGas (optional) + +- **Type:** `bigint` + +Max priority fee per gas (in wei). Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction) + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + maxFeePerGas: parseGwei('20'), + maxPriorityFeePerGas: parseGwei('2'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### nonce (optional) + +- **Type:** `number` + +Unique number identifying this transaction. + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + nonce: 69 // [!code focus] +}) +``` + +### value (optional) + +- **Type:** `bigint` + +Value in wei sent with this transaction. + +```ts +const request = await walletClient.prepareSendTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1'), // [!code focus] + nonce: 69 +}) +``` diff --git a/site/docs/actions/wallet/sendRawTransaction.md b/site/docs/actions/wallet/sendRawTransaction.md new file mode 100644 index 0000000000..343ea1f2d2 --- /dev/null +++ b/site/docs/actions/wallet/sendRawTransaction.md @@ -0,0 +1,71 @@ +--- +head: + - - meta + - property: og:title + content: sendRawTransaction + - - meta + - name: description + content: Sends a signed transaction to the network + - - meta + - property: og:description + content: Sends a signed transaction to the network + +--- + +# sendRawTransaction + +Sends a **signed** transaction to the network. + +## Usage + +::: code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) + +const signature = await walletClient.signTranasction(request) + +const hash = await walletClient.sendRawTransaction(signature) // [!code focus] +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum) +}) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +## Returns + +[`Hash`](/docs/glossary/types#hash) + +The [Transaction](/docs/glossary/terms#transaction) hash. + +## Parameters + +### serializedTransaction + +- **Type:** `Hex` + +The signed serialized transaction. + +```ts +const signature = await walletClient.signTransaction({ + serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' // [!code focus] +}) +``` diff --git a/site/docs/actions/wallet/signTransaction.md b/site/docs/actions/wallet/signTransaction.md index f376de35aa..040e1b698a 100644 --- a/site/docs/actions/wallet/signTransaction.md +++ b/site/docs/actions/wallet/signTransaction.md @@ -5,15 +5,267 @@ head: content: signTransaction - - meta - name: description - content: Signs a transaction with the Account's private key. + content: Signs a transaction. - - meta - property: og:description - content: Signs a transaction with the Account's private key. + content: Signs a transaction. --- # signTransaction -Coming soon. +Signs a transaction. -See [Tracking](https://github.com/orgs/wagmi-dev/projects/3/views/1?pane=issue&itemId=21304943). \ No newline at end of file +## Usage + +::: code-group + +```ts [example.ts] +import { account, walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) + +const signature = await walletClient.signTransaction(request) // [!code focus:2] +// 0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33 + +const hash = await walletClient.sendRawTransaction(signature) +``` + +```ts [config.ts] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum) +}) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `prepareTransactionRequest`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet.html#account). + +::: code-group + +```ts [example.ts] +import { walletClient } from './config' + +const request = await walletClient.prepareTransactionRequest({ + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) + +const signature = await walletClient.signTransaction(request) // [!code focus:2] +// 0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33 + +const hash = await walletClient.sendRawTransaction(signature) +``` + +```ts {4-6,9} [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' + +// Retrieve Account from an EIP-1193 Provider. +const [account] = await window.ethereum.request({ + method: 'eth_requestAccounts' +}) + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) +}) +``` + +```ts {5} [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), + transport: custom(window.ethereum) +}) +``` + +::: + +## Returns + +[`Hex`](/docs/glossary/types#hex) + +The signed serialized transaction. + +## Parameters + +### account + +- **Type:** `Account | Address` + +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). + +```ts +const signature = await walletClient.signTransaction({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### to + +- **Type:** `0x${string}` + +The transaction recipient or contract address. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: 1000000000000000000n, + nonce: 69 +}) +``` + +### accessList (optional) + +- **Type:** [`AccessList`](/docs/glossary/types#accesslist) + +The access list. + +```ts +const signature = await publicClient.signTransaction({ + accessList: [ // [!code focus:6] + { + address: '0x1', + storageKeys: ['0x1'], + }, + ], + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +The chain is also used to infer its request type (e.g. the Celo chain has a `gatewayFee` that you can pass through to `signTransaction`). + +```ts +import { optimism } from 'viem/chains' // [!code focus] + +const signature = await walletClient.signTransaction({ + chain: optimism, // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### data (optional) + +- **Type:** `0x${string}` + +A contract hashed method call with encoded args. + +```ts +const signature = await walletClient.signTransaction({ + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // [!code focus] + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. Only applies to [Legacy Transactions](/docs/glossary/terms#legacy-transaction). + +```ts +const signature = await walletClient.signTransaction({ + account, + gasPrice: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### maxFeePerGas (optional) + +- **Type:** `bigint` + +Total fee per gas (in wei), inclusive of `maxPriorityFeePerGas`. Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction) + +```ts +const signature = await walletClient.signTransaction({ + account, + maxFeePerGas: parseGwei('20'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### maxPriorityFeePerGas (optional) + +- **Type:** `bigint` + +Max priority fee per gas (in wei). Only applies to [EIP-1559 Transactions](/docs/glossary/terms#eip-1559-transaction) + +```ts +const signature = await walletClient.signTransaction({ + account, + maxFeePerGas: parseGwei('20'), + maxPriorityFeePerGas: parseGwei('2'), // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') +}) +``` + +### nonce (optional) + +- **Type:** `number` + +Unique number identifying this transaction. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + nonce: 69 // [!code focus] +}) +``` + +### value (optional) + +- **Type:** `bigint` + +Value in wei sent with this transaction. + +```ts +const signature = await walletClient.signTransaction({ + account, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1'), // [!code focus] + nonce: 69 +}) +``` diff --git a/src/actions/index.test.ts b/src/actions/index.test.ts index f7dce69282..7f7d845b2c 100644 --- a/src/actions/index.test.ts +++ b/src/actions/index.test.ts @@ -43,11 +43,13 @@ test('exports actions', () => { "inspectTxpool": [Function], "mine": [Function], "multicall": [Function], + "prepareTransactionRequest": [Function], "readContract": [Function], "requestAddresses": [Function], "requestPermissions": [Function], "reset": [Function], "revert": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "sendUnsignedTransaction": [Function], "setAutomine": [Function], @@ -63,6 +65,7 @@ test('exports actions', () => { "setNonce": [Function], "setStorageAt": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "simulateContract": [Function], "snapshot": [Function], diff --git a/src/actions/index.ts b/src/actions/index.ts index c569a5c811..1708890e52 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -224,11 +224,26 @@ export { } from './test/inspectTxpool.js' export { type ResetParameters, reset } from './test/reset.js' export { type RevertParameters, revert } from './test/revert.js' +export { + type PrepareTransactionRequestParameters, + type PrepareTransactionRequestReturnType, + prepareTransactionRequest, +} from './wallet/prepareTransactionRequest.js' export { type SendTransactionParameters, type SendTransactionReturnType, sendTransaction, } from './wallet/sendTransaction.js' +export { + type SignTransactionParameters, + type SignTransactionReturnType, + signTransaction, +} from './wallet/signTransaction.js' +export { + type SendRawTransactionParameters, + type SendRawTransactionReturnType, + sendRawTransaction, +} from './wallet/sendRawTransaction.js' export { type SendUnsignedTransactionParameters, type SendUnsignedTransactionReturnType, diff --git a/src/actions/public/estimateFeesPerGas.test.ts b/src/actions/public/estimateFeesPerGas.test.ts index 1b24fd3327..8ffaa608b3 100644 --- a/src/actions/public/estimateFeesPerGas.test.ts +++ b/src/actions/public/estimateFeesPerGas.test.ts @@ -229,4 +229,21 @@ describe('internal_estimateFeesPerGas', () => { (baseFeePerGas * 120n) / 100n + maxPriorityFeePerGas, ) }) + + test('maxFeePerGas request args', async () => { + const maxFeePerGas_ = 69n + const { maxFeePerGas } = await internal_estimateFeesPerGas(publicClient, { + request: { maxFeePerGas: maxFeePerGas_ } as any, + }) + expect(maxFeePerGas).toBe(maxFeePerGas_) + }) + + test('gasPrice request args', async () => { + const gasPrice_ = 69n + const { gasPrice } = await internal_estimateFeesPerGas(publicClient, { + request: { gasPrice: gasPrice_ } as any, + type: 'legacy', + }) + expect(gasPrice).toBe(gasPrice_) + }) }) diff --git a/src/actions/public/estimateFeesPerGas.ts b/src/actions/public/estimateFeesPerGas.ts index 4985fea5f2..3a608776be 100644 --- a/src/actions/public/estimateFeesPerGas.ts +++ b/src/actions/public/estimateFeesPerGas.ts @@ -16,7 +16,7 @@ import type { FeeValuesLegacy, FeeValuesType, } from '../../types/fee.js' -import type { PrepareRequestParameters } from '../../utils/transaction/prepareRequest.js' +import type { PrepareTransactionRequestParameters } from '../wallet/prepareTransactionRequest.js' import { internal_estimateMaxPriorityFeePerGas } from './estimateMaxPriorityFeePerGas.js' import { getBlock } from './getBlock.js' import { getGasPrice } from './getGasPrice.js' @@ -85,7 +85,7 @@ export async function internal_estimateFeesPerGas< client: Client, args: EstimateFeesPerGasParameters & { block?: Block - request?: PrepareRequestParameters + request?: PrepareTransactionRequestParameters }, ): Promise> { const { @@ -138,7 +138,8 @@ export async function internal_estimateFeesPerGas< ) const baseFeePerGas = multiply(block.baseFeePerGas) - const maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas + const maxFeePerGas = + request?.maxFeePerGas ?? baseFeePerGas + maxPriorityFeePerGas return { maxFeePerGas, @@ -146,7 +147,7 @@ export async function internal_estimateFeesPerGas< } as EstimateFeesPerGasReturnType } - const gasPrice = multiply(await getGasPrice(client)) + const gasPrice = request?.gasPrice ?? multiply(await getGasPrice(client)) return { gasPrice, } as EstimateFeesPerGasReturnType diff --git a/src/actions/public/estimateGas.ts b/src/actions/public/estimateGas.ts index c71174d7a9..f711cd2a94 100644 --- a/src/actions/public/estimateGas.ts +++ b/src/actions/public/estimateGas.ts @@ -21,9 +21,9 @@ import { assertRequest, } from '../../utils/transaction/assertRequest.js' import { - type PrepareRequestParameters, - prepareRequest, -} from '../../utils/transaction/prepareRequest.js' + type PrepareTransactionRequestParameters, + prepareTransactionRequest, +} from '../wallet/prepareTransactionRequest.js' export type FormattedEstimateGas< TChain extends Chain | undefined = Chain | undefined, @@ -107,9 +107,9 @@ export async function estimateGas< ...rest } = account.type === 'local' - ? ((await prepareRequest( + ? ((await prepareTransactionRequest( client, - args as PrepareRequestParameters, + args as PrepareTransactionRequestParameters, )) as EstimateGasParameters) : args diff --git a/src/actions/public/estimateMaxPriorityFeePerGas.ts b/src/actions/public/estimateMaxPriorityFeePerGas.ts index 9e9ba08026..f97d07c3bd 100644 --- a/src/actions/public/estimateMaxPriorityFeePerGas.ts +++ b/src/actions/public/estimateMaxPriorityFeePerGas.ts @@ -6,7 +6,7 @@ import type { Block } from '../../types/block.js' import type { Chain, ChainFeesFnParameters } from '../../types/chain.js' import type { GetChain } from '../../types/chain.js' import { hexToBigInt } from '../../utils/encoding/fromHex.js' -import type { PrepareRequestParameters } from '../../utils/transaction/prepareRequest.js' +import type { PrepareTransactionRequestParameters } from '../wallet/prepareTransactionRequest.js' import { getBlock } from './getBlock.js' import { getGasPrice } from './getGasPrice.js' @@ -55,7 +55,7 @@ export async function internal_estimateMaxPriorityFeePerGas< client: Client, args: EstimateMaxPriorityFeePerGasParameters & { block?: Block - request?: PrepareRequestParameters< + request?: PrepareTransactionRequestParameters< chain, Account | undefined, chainOverride diff --git a/src/actions/public/getBlock.test.ts b/src/actions/public/getBlock.test.ts index 594a2e3efe..987a7f6788 100644 --- a/src/actions/public/getBlock.test.ts +++ b/src/actions/public/getBlock.test.ts @@ -1,4 +1,3 @@ -import type { Address } from 'abitype' import { assertType, describe, expect, test } from 'vitest' import { forkBlockNumber } from '../../_test/constants.js' @@ -7,8 +6,6 @@ import { celo } from '../../chains/index.js' import { createPublicClient } from '../../clients/createPublicClient.js' import { http } from '../../clients/transports/http.js' import type { Block } from '../../types/block.js' -import type { Hash, Hex } from '../../types/misc.js' -import type { Transaction } from '../../types/transaction.js' import { getBlock } from './getBlock.js' test('gets latest block', async () => { @@ -53,18 +50,6 @@ test('chain w/ custom block type', async () => { includeTransactions: true, }) - assertType< - Omit & { - randomness: { committed: Hex; revealed: Hex } - transactions: - | Hash[] - | (Transaction & { - feeCurrency: Address | null - gatewayFee: bigint | null - gatewayFeeRecipient: Address | null - })[] - } - >(block) const { extraData: _extraData, transactions, ...rest } = block expect(transactions[0]).toMatchInlineSnapshot(` { diff --git a/src/actions/public/getTransaction.test.ts b/src/actions/public/getTransaction.test.ts index 3073194f6e..3bad038e0d 100644 --- a/src/actions/public/getTransaction.test.ts +++ b/src/actions/public/getTransaction.test.ts @@ -1,5 +1,3 @@ -import type { Address } from 'abitype' - import { assertType, describe, expect, test } from 'vitest' import { accounts, forkBlockNumber } from '../../_test/constants.js' @@ -142,13 +140,6 @@ test('chain w/ custom block type', async () => { index: 0, }) - assertType< - Transaction & { - feeCurrency: Address | null - gatewayFee: bigint | null - gatewayFeeRecipient: Address | null - } - >(transaction) expect(transaction).toMatchInlineSnapshot(` { "blockHash": "0x740371d30b3cee9d687f72e3409ba6447eceda7de86bc38b0fa84493114b510b", diff --git a/src/actions/public/watchContractEvent.ts b/src/actions/public/watchContractEvent.ts index 823660e2a0..125bd99d69 100644 --- a/src/actions/public/watchContractEvent.ts +++ b/src/actions/public/watchContractEvent.ts @@ -2,7 +2,6 @@ import type { Abi, AbiEvent, Address, ExtractAbiEvent } from 'abitype' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' -import type { EncodeEventTopicsParameters, LogTopic } from '../../index.js' import type { Chain } from '../../types/chain.js' import type { GetEventArgs, InferEventName } from '../../types/contract.js' import type { Filter } from '../../types/filter.js' @@ -17,14 +16,18 @@ import { observe } from '../../utils/observe.js' import { poll } from '../../utils/poll.js' import { stringify } from '../../utils/stringify.js' -import { DecodeLogDataMismatch } from '../../errors/abi.js' import { + DecodeLogDataMismatch, DecodeLogTopicsMismatch, - InvalidInputRpcError, - decodeEventLog, +} from '../../errors/abi.js' +import { InvalidInputRpcError } from '../../errors/rpc.js' +import type { LogTopic } from '../../types/misc.js' +import { decodeEventLog } from '../../utils/abi/decodeEventLog.js' +import { + type EncodeEventTopicsParameters, encodeEventTopics, - formatLog, -} from '../../index.js' +} from '../../utils/abi/encodeEventTopics.js' +import { formatLog } from '../../utils/formatters/log.js' import { type CreateContractEventFilterParameters, createContractEventFilter, diff --git a/src/utils/transaction/prepareRequest.test.ts b/src/actions/wallet/prepareTransactionRequest.test.ts similarity index 79% rename from src/utils/transaction/prepareRequest.test.ts rename to src/actions/wallet/prepareTransactionRequest.test.ts index e4506df030..d95806020e 100644 --- a/src/utils/transaction/prepareRequest.test.ts +++ b/src/actions/wallet/prepareTransactionRequest.test.ts @@ -12,11 +12,11 @@ import * as getBlock from '../../actions/public/getBlock.js' import { mine } from '../../actions/test/mine.js' import { setBalance } from '../../actions/test/setBalance.js' import { setNextBlockBaseFeePerGas } from '../../actions/test/setNextBlockBaseFeePerGas.js' -import { parseEther } from '../unit/parseEther.js' -import { parseGwei } from '../unit/parseGwei.js' +import { parseEther } from '../../utils/unit/parseEther.js' +import { parseGwei } from '../../utils/unit/parseGwei.js' import { createWalletClient, http } from '../../index.js' -import { prepareRequest } from './prepareRequest.js' +import { prepareTransactionRequest } from './prepareTransactionRequest.js' const sourceAccount = accounts[0] const targetAccount = accounts[1] @@ -36,7 +36,7 @@ async function setup() { await mine(testClient, { blocks: 1 }) } -describe('prepareRequest', () => { +describe('prepareTransactionRequest', () => { test('default', async () => { await setup() @@ -46,7 +46,7 @@ describe('prepareRequest', () => { maxPriorityFeePerGas, nonce: _nonce, ...rest - } = await prepareRequest(walletClient, { + } = await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), @@ -68,6 +68,7 @@ describe('prepareRequest', () => { "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "gas": 21000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -80,11 +81,14 @@ describe('prepareRequest', () => { baseFeePerGas: undefined, } as any) - const { nonce: _nonce, ...request } = await prepareRequest(walletClient, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }, + ) expect(request).toMatchInlineSnapshot(` { "account": { @@ -100,6 +104,7 @@ describe('prepareRequest', () => { "gas": 21000n, "gasPrice": 11700000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", "value": 1000000000000000000n, } `) @@ -112,7 +117,7 @@ describe('prepareRequest', () => { maxFeePerGas: _maxFeePerGas, nonce: _nonce, ...rest - } = await prepareRequest(walletClient, { + } = await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), @@ -132,6 +137,7 @@ describe('prepareRequest', () => { "gas": 21000n, "maxPriorityFeePerGas": 18500000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -140,15 +146,13 @@ describe('prepareRequest', () => { test('args: nonce', async () => { await setup() - const { maxFeePerGas: _maxFeePerGas, ...rest } = await prepareRequest( - walletClient, - { + const { maxFeePerGas: _maxFeePerGas, ...rest } = + await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, nonce: 5, value: parseEther('1'), - }, - ) + }) expect(rest).toMatchInlineSnapshot(` { "account": { @@ -165,6 +169,7 @@ describe('prepareRequest', () => { "maxPriorityFeePerGas": 18500000000n, "nonce": 5, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -175,12 +180,15 @@ describe('prepareRequest', () => { vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({} as any) - const { nonce: _nonce, ...request } = await prepareRequest(walletClient, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - value: parseEther('1'), - }) + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + value: parseEther('1'), + }, + ) expect(request).toMatchInlineSnapshot(` { "account": { @@ -196,6 +204,7 @@ describe('prepareRequest', () => { "gas": 21000n, "gasPrice": 10000000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", "value": 1000000000000000000n, } `) @@ -205,7 +214,7 @@ describe('prepareRequest', () => { await setup() expect( - await prepareRequest(walletClient, { + await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, gasPrice: parseGwei('10'), @@ -227,6 +236,7 @@ describe('prepareRequest', () => { "gasPrice": 10000000000n, "nonce": 375, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", "value": 1000000000000000000n, } `) @@ -235,12 +245,15 @@ describe('prepareRequest', () => { test('args: maxFeePerGas', async () => { await setup() - const { nonce: _nonce, ...rest } = await prepareRequest(walletClient, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxFeePerGas: parseGwei('100'), - value: parseEther('1'), - }) + const { nonce: _nonce, ...rest } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('100'), + value: parseEther('1'), + }, + ) expect(rest).toMatchInlineSnapshot(` { "account": { @@ -257,6 +270,7 @@ describe('prepareRequest', () => { "maxFeePerGas": 100000000000n, "maxPriorityFeePerGas": 18500000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -266,7 +280,7 @@ describe('prepareRequest', () => { await setup() await expect(() => - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, maxFeePerGas: parseGwei('1'), @@ -287,7 +301,7 @@ describe('prepareRequest', () => { } as any) await expect(() => - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, maxFeePerGas: parseGwei('10'), @@ -303,12 +317,15 @@ describe('prepareRequest', () => { test('args: maxPriorityFeePerGas', async () => { await setup() - const { nonce: _nonce, ...rest } = await prepareRequest(walletClient, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxPriorityFeePerGas: parseGwei('5'), - value: parseEther('1'), - }) + const { nonce: _nonce, ...rest } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxPriorityFeePerGas: parseGwei('5'), + value: parseEther('1'), + }, + ) expect(rest).toMatchInlineSnapshot(` { "account": { @@ -325,6 +342,7 @@ describe('prepareRequest', () => { "maxFeePerGas": 17000000000n, "maxPriorityFeePerGas": 5000000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -338,7 +356,7 @@ describe('prepareRequest', () => { } as any) await expect(() => - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, maxFeePerGas: parseGwei('5'), @@ -354,13 +372,16 @@ describe('prepareRequest', () => { test('args: maxFeePerGas + maxPriorityFeePerGas', async () => { await setup() - const { nonce: _nonce, ...rest } = await prepareRequest(walletClient, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxFeePerGas: parseGwei('10'), - maxPriorityFeePerGas: parseGwei('5'), - value: parseEther('1'), - }) + const { nonce: _nonce, ...rest } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('10'), + maxPriorityFeePerGas: parseGwei('5'), + value: parseEther('1'), + }, + ) expect(rest).toMatchInlineSnapshot(` { "account": { @@ -377,6 +398,7 @@ describe('prepareRequest', () => { "maxFeePerGas": 10000000000n, "maxPriorityFeePerGas": 5000000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", "value": 1000000000000000000n, } `) @@ -387,7 +409,7 @@ describe('prepareRequest', () => { await expect(() => // @ts-expect-error - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, gasPrice: parseGwei('10'), @@ -399,12 +421,13 @@ describe('prepareRequest', () => { Use \`maxFeePerGas\`/\`maxPriorityFeePerGas\` for EIP-1559 compatible networks, and \`gasPrice\` for others. Estimate Gas Arguments: - from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 - to: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 - value: 1 ETH - gasPrice: 10 gwei - maxFeePerGas: 20 gwei - nonce: 375 + from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + to: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 + value: 1 ETH + gasPrice: 10 gwei + maxFeePerGas: 20 gwei + maxPriorityFeePerGas: 18.5 gwei + nonce: 375 Version: viem@1.0.2" `) @@ -415,29 +438,55 @@ describe('prepareRequest', () => { await expect(() => // @ts-expect-error - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, gasPrice: parseGwei('10'), maxPriorityFeePerGas: parseGwei('20'), + type: 'legacy', value: parseEther('1'), }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Cannot specify both a \`gasPrice\` and a \`maxFeePerGas\`/\`maxPriorityFeePerGas\`. - Use \`maxFeePerGas\`/\`maxPriorityFeePerGas\` for EIP-1559 compatible networks, and \`gasPrice\` for others. - - Estimate Gas Arguments: - from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 - to: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 - value: 1 ETH - gasPrice: 10 gwei - maxPriorityFeePerGas: 20 gwei - nonce: 375 + "Chain does not support EIP-1559 fees. Version: viem@1.0.2" `) }) + test('args: type', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest( + walletClient, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + type: 'eip1559', + value: parseEther('1'), + }, + ) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 30500000000n, + "maxPriorityFeePerGas": 18500000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + test('chain default priority fee', async () => { await setup() @@ -453,7 +502,7 @@ describe('prepareRequest', () => { }, transport: http(), }) - const request_1 = await prepareRequest(client_1, { + const request_1 = await prepareTransactionRequest(client_1, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), @@ -472,7 +521,7 @@ describe('prepareRequest', () => { }, transport: http(), }) - const request_2 = await prepareRequest(client_2, { + const request_2 = await prepareTransactionRequest(client_2, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), @@ -491,7 +540,7 @@ describe('prepareRequest', () => { }, transport: http(), }) - const request_3 = await prepareRequest(client_3, { + const request_3 = await prepareTransactionRequest(client_3, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), @@ -501,7 +550,7 @@ describe('prepareRequest', () => { ) // chain override (bigint) - const request_4 = await prepareRequest(walletClient, { + const request_4 = await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), chain: { ...anvilChain, @@ -517,7 +566,7 @@ describe('prepareRequest', () => { ) // chain override (async) - const request_5 = await prepareRequest(walletClient, { + const request_5 = await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), chain: { ...anvilChain, @@ -533,7 +582,7 @@ describe('prepareRequest', () => { ) // chain override (bigint) - const request_6 = await prepareRequest(walletClient, { + const request_6 = await prepareTransactionRequest(walletClient, { account: privateKeyToAccount(sourceAccount.privateKey), chain: { ...anvilChain, @@ -554,7 +603,7 @@ describe('prepareRequest', () => { await expect(() => // @ts-expect-error - prepareRequest(walletClient, { + prepareTransactionRequest(walletClient, { to: targetAccount.address, value: parseEther('1'), }), diff --git a/src/utils/transaction/prepareRequest.ts b/src/actions/wallet/prepareTransactionRequest.ts similarity index 52% rename from src/utils/transaction/prepareRequest.ts rename to src/actions/wallet/prepareTransactionRequest.ts index 487f78e98d..ac866ab012 100644 --- a/src/utils/transaction/prepareRequest.ts +++ b/src/actions/wallet/prepareTransactionRequest.ts @@ -1,5 +1,3 @@ -import type { Address } from 'abitype' - import type { Account } from '../../accounts/types.js' import { parseAccount } from '../../accounts/utils/parseAccount.js' import { internal_estimateFeesPerGas } from '../../actions/public/estimateFeesPerGas.js' @@ -9,7 +7,6 @@ import { } from '../../actions/public/estimateGas.js' import { getBlock } from '../../actions/public/getBlock.js' import { getTransactionCount } from '../../actions/public/getTransactionCount.js' -import type { SendTransactionParameters } from '../../actions/wallet/sendTransaction.js' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../errors/account.js' @@ -20,11 +17,14 @@ import { import type { GetAccountParameter } from '../../types/account.js' import type { Chain } from '../../types/chain.js' import type { GetChain } from '../../types/chain.js' +import type { TransactionSerializable } from '../../types/transaction.js' import type { UnionOmit } from '../../types/utils.js' -import { type FormattedTransactionRequest } from '../index.js' -import { type AssertRequestParameters, assertRequest } from './assertRequest.js' +import type { FormattedTransactionRequest } from '../../utils/formatters/transactionRequest.js' +import type { AssertRequestParameters } from '../../utils/transaction/assertRequest.js' +import { assertRequest } from '../../utils/transaction/assertRequest.js' +import { getTransactionType } from '../../utils/transaction/getTransactionType.js' -export type PrepareRequestParameters< +export type PrepareTransactionRequestParameters< TChain extends Chain | undefined = Chain | undefined, TAccount extends Account | undefined = Account | undefined, TChainOverride extends Chain | undefined = Chain | undefined, @@ -37,34 +37,70 @@ export type PrepareRequestParameters< GetAccountParameter & GetChain -export type PrepareRequestReturnType< +export type PrepareTransactionRequestReturnType< TChain extends Chain | undefined = Chain | undefined, TAccount extends Account | undefined = Account | undefined, TChainOverride extends Chain | undefined = Chain | undefined, - TArgs extends PrepareRequestParameters< - TChain, - TAccount, - TChainOverride - > = PrepareRequestParameters, -> = TArgs & { - from: Address - gas: SendTransactionParameters['gas'] - gasPrice?: SendTransactionParameters['gasPrice'] - maxFeePerGas?: SendTransactionParameters['maxFeePerGas'] - maxPriorityFeePerGas?: SendTransactionParameters['maxPriorityFeePerGas'] - nonce: SendTransactionParameters['nonce'] -} +> = UnionOmit< + FormattedTransactionRequest< + TChainOverride extends Chain ? TChainOverride : TChain + >, + 'from' +> & + GetAccountParameter & + GetChain -export async function prepareRequest< +/** + * Prepares a transaction request for signing. + * + * - Docs: https://viem.sh/docs/actions/wallet/prepareTransactionRequest.html + * + * @param args - {@link PrepareTransactionRequestParameters} + * @returns The transaction request. {@link PrepareTransactionRequestReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { prepareTransactionRequest } from 'viem/actions' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await prepareTransactionRequest(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { prepareTransactionRequest } from 'viem/actions' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await prepareTransactionRequest(client, { + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ +export async function prepareTransactionRequest< TChain extends Chain | undefined, TAccount extends Account | undefined, TChainOverride extends Chain | undefined, - TArgs extends PrepareRequestParameters, >( client: Client, - args: TArgs, -): Promise> { - const { account: account_, chain, gas, gasPrice, nonce } = args + args: PrepareTransactionRequestParameters, +): Promise< + PrepareTransactionRequestReturnType +> { + const { account: account_ = client.account, chain, gas, nonce, type } = args if (!account_) throw new AccountNotFoundError() const account = parseAccount(account_) @@ -78,16 +114,25 @@ export async function prepareRequest< blockTag: 'pending', }) - if ( - typeof block.baseFeePerGas === 'bigint' && - typeof gasPrice === 'undefined' - ) { + if (typeof type === 'undefined') { + try { + request.type = getTransactionType( + request as TransactionSerializable, + ) as any + } catch { + // infer type from block + request.type = + typeof block.baseFeePerGas === 'bigint' ? 'eip1559' : 'legacy' + } + } + + if (request.type === 'eip1559') { // EIP-1559 fees const { maxFeePerGas, maxPriorityFeePerGas } = await internal_estimateFeesPerGas(client, { block, chain, - request: request as PrepareRequestParameters, + request: request as PrepareTransactionRequestParameters, }) if ( @@ -100,8 +145,8 @@ export async function prepareRequest< }) request.maxPriorityFeePerGas = maxPriorityFeePerGas - request.maxFeePerGas = args.maxFeePerGas ?? maxFeePerGas - } else if (typeof gasPrice === 'undefined') { + request.maxFeePerGas = maxFeePerGas + } else { // Legacy fees if ( typeof args.maxFeePerGas !== 'undefined' || @@ -112,7 +157,7 @@ export async function prepareRequest< const { gasPrice: gasPrice_ } = await internal_estimateFeesPerGas(client, { block, chain, - request: request as PrepareRequestParameters, + request: request as PrepareTransactionRequestParameters, type: 'legacy', }) request.gasPrice = gasPrice_ @@ -126,10 +171,9 @@ export async function prepareRequest< assertRequest(request as AssertRequestParameters) - return request as PrepareRequestReturnType< + return request as PrepareTransactionRequestReturnType< TChain, TAccount, - TChainOverride, - TArgs + TChainOverride > } diff --git a/src/actions/wallet/sendRawTransaction.test.ts b/src/actions/wallet/sendRawTransaction.test.ts new file mode 100644 index 0000000000..45e73b0576 --- /dev/null +++ b/src/actions/wallet/sendRawTransaction.test.ts @@ -0,0 +1,18 @@ +import { accounts } from '../../_test/constants.js' +import { walletClient } from '../../_test/utils.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { prepareTransactionRequest } from './prepareTransactionRequest.js' +import { sendRawTransaction } from './sendRawTransaction.js' +import { signTransaction } from './signTransaction.js' +import { expect, test } from 'vitest' + +test('default', async () => { + const request = await prepareTransactionRequest(walletClient, { + account: privateKeyToAccount(accounts[0].privateKey), + to: accounts[1].address, + value: 1n, + }) + const serializedTransaction = await signTransaction(walletClient, request) + const hash = await sendRawTransaction(walletClient, { serializedTransaction }) + expect(hash).toBeDefined() +}) diff --git a/src/actions/wallet/sendRawTransaction.ts b/src/actions/wallet/sendRawTransaction.ts new file mode 100644 index 0000000000..2db8a85ff4 --- /dev/null +++ b/src/actions/wallet/sendRawTransaction.ts @@ -0,0 +1,48 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { Chain } from '../../types/chain.js' +import type { Hash } from '../../types/misc.js' +import type { TransactionSerialized } from '../../types/transaction.js' + +export type SendRawTransactionParameters = { + /** + * The signed serialized tranasction. + */ + serializedTransaction: TransactionSerialized +} + +export type SendRawTransactionReturnType = Hash + +/** + * Sends a **signed** transaction to the network + * + * - Docs: https://viem.sh/docs/actions/wallet/sendRawTransaction.html + * - JSON-RPC Method: [`eth_sendRawTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * + * @param client - Client to use + * @param parameters - {@link SendRawTransactionParameters} + * @returns The transaction hash. {@link SendRawTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { sendRawTransaction } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * + * const hash = await sendRawTransaction(client, { + * serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' + * }) + */ +export async function sendRawTransaction( + client: Client, + { serializedTransaction }: SendRawTransactionParameters, +): Promise { + return client.request({ + method: 'eth_sendRawTransaction', + params: [serializedTransaction], + }) +} diff --git a/src/actions/wallet/sendTransaction.ts b/src/actions/wallet/sendTransaction.ts index 8ab2f9e0a8..8145e216ba 100644 --- a/src/actions/wallet/sendTransaction.ts +++ b/src/actions/wallet/sendTransaction.ts @@ -24,8 +24,9 @@ import { type AssertRequestParameters, assertRequest, } from '../../utils/transaction/assertRequest.js' -import { prepareRequest } from '../../utils/transaction/prepareRequest.js' import { getChainId } from '../public/getChainId.js' +import { prepareTransactionRequest } from './prepareTransactionRequest.js' +import { sendRawTransaction } from './sendRawTransaction.js' export type SendTransactionParameters< TChain extends Chain | undefined = Chain | undefined, @@ -130,7 +131,7 @@ export async function sendTransaction< if (account.type === 'local') { // Prepare the request for signing (assign appropriate fees, etc.) - const request = await prepareRequest(client, { + const request = await prepareTransactionRequest(client, { account, accessList, chain, @@ -148,16 +149,15 @@ export async function sendTransaction< if (!chainId) chainId = await getChainId(client) const serializer = chain?.serializers?.transaction - const signedRequest = (await account.signTransaction( + const serializedTransaction = (await account.signTransaction( { ...request, chainId, } as TransactionSerializable, { serializer }, )) as Hash - return await client.request({ - method: 'eth_sendRawTransaction', - params: [signedRequest], + return await sendRawTransaction(client, { + serializedTransaction, }) } diff --git a/src/actions/wallet/signTransaction.test.ts b/src/actions/wallet/signTransaction.test.ts new file mode 100644 index 0000000000..7a371da002 --- /dev/null +++ b/src/actions/wallet/signTransaction.test.ts @@ -0,0 +1,377 @@ +import { describe, expect, test } from 'vitest' + +import { accounts, localHttpUrl } from '../../_test/constants.js' +import { walletClient } from '../../_test/utils.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { celo, mainnet } from '../../chains/index.js' +import { + type TransactionRequestBase, + type TransactionRequestEIP1559, + type TransactionRequestEIP2930, + type TransactionRequestLegacy, + createWalletClient, + http, + parseGwei, +} from '../../index.js' +import { prepareTransactionRequest } from './prepareTransactionRequest.js' +import { signTransaction } from './signTransaction.js' + +const sourceAccount = accounts[0] + +const base = { + from: '0x0000000000000000000000000000000000000000', + gas: 21000n, + nonce: 785, +} satisfies TransactionRequestBase + +describe('eip1559', () => { + const baseEip1559 = { + ...base, + type: 'eip1559', + } as const satisfies TransactionRequestEIP1559 + + // TODO: Anvil seems to be broken with JSON-RPC signing. This test will fail when + // it is fixed upstream. + test('default: json-rpc', async () => { + await expect(() => + signTransaction(walletClient, { + account: sourceAccount.address, + ...baseEip1559, + }), + ).rejects.toThrowError() + }) + + test('default: local', async () => { + const signature = await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip1559, + }) + expect(signature).toMatchInlineSnapshot( + '"0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33"', + ) + }) + + test('w/ prepareTransactionRequest', async () => { + const request_1 = await prepareTransactionRequest(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + value: 1n, + }) + const signature_1 = await signTransaction(walletClient, request_1) + expect(signature_1.match(/^0x02/)).toBeTruthy() + + const request_2 = await prepareTransactionRequest(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + maxFeePerGas: parseGwei('30'), + value: 1n, + }) + const signature_2 = await signTransaction(walletClient, request_2) + expect(signature_2.match(/^0x02/)).toBeTruthy() + }) + + describe('minimal', () => { + test('default (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + type: 'eip1559', + }), + ).toMatchInlineSnapshot( + '"0x02f84c0180808080808080c001a0db5b8a12b90b68aeb786379ac14219ac85934e833082dee6cf03fd912809224da06902cc208e3b14a056dca2005c96f59eae33118b899f642551a58cff09044c9a"', + ) + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: mainnet, + type: 'eip1559', + }), + ).toMatchInlineSnapshot( + '"0x02f84c0180808080808080c001a0db5b8a12b90b68aeb786379ac14219ac85934e833082dee6cf03fd912809224da06902cc208e3b14a056dca2005c96f59eae33118b899f642551a58cff09044c9a"', + ) + }) + + test('w/ maxFeePerGas (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + maxFeePerGas: parseGwei('2'), + }), + ).toMatchInlineSnapshot( + '"0x02f850018080847735940080808080c001a09926ae69535547075ec96eb3b7a9dd658ea58defa9c93a76e5a93288f3ce1d33a020baea54c020452a789fa0a0ea9c7249e25163929193ca5f8861f4e70935e631"', + ) + }) + + test('w/ type (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + type: 'eip1559', + }), + ).toMatchInlineSnapshot( + '"0x02f84c0180808080808080c001a0db5b8a12b90b68aeb786379ac14219ac85934e833082dee6cf03fd912809224da06902cc208e3b14a056dca2005c96f59eae33118b899f642551a58cff09044c9a"', + ) + }) + }) + + describe('args', () => { + test('accessList (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip1559, + accessList: [ + { + address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', + ], + }, + ], + }), + ).toMatchInlineSnapshot( + '"0x02f8ac018203118080825208808080f85bf85994f39fd6e51aad88f6f4ce6ab8827279cfffb92266f842a00000000000000000000000000000000000000000000000000000000000000001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe80a0df4810a25d0e147163b03e392bf083dc852702715b9ba848eb9821c70ce2c92ea00b6d11209ef326abaf83aa2443ba61851c7c8ca0813e347a04b501f584b03024"', + ) + }) + + test('data (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip1559, + data: '0x1234', + }), + ).toMatchInlineSnapshot( + '"0x02f8520182031180808252088080821234c001a054d552c58a162c9003633c20871d8e381ef7a3c35d1c8a79c7c12d5cf09a0914a03c5d6241f8c4fcf8b35262de038d3ab1940feb1a70b934ae5d40ea6bce912e2d"', + ) + }) + + test('maxFeePerGas/maxPriorityFeePerGas', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip1559, + maxFeePerGas: parseGwei('20'), + maxPriorityFeePerGas: parseGwei('2'), + }), + ).toMatchInlineSnapshot( + '"0x02f8590182031184773594008504a817c800825208808080c001a06ea33b188b30a5f5d0d1cec62b2bac7203ff428a49048766596727737689043fa0255b74c8e704e3692497a29cd246ffc4344b4107457ce1c914fe2b4e09993859"', + ) + }) + }) +}) + +describe('eip2930', () => { + const baseEip2930 = { + ...base, + type: 'eip2930', + } as const satisfies TransactionRequestEIP2930 + + test.skip('default: json-rpc', async () => { + const signature = await signTransaction(walletClient, { + account: sourceAccount.address, + ...baseEip2930, + }) + expect(signature).toMatchInlineSnapshot( + '"0x01f84f0182031180825208808080c080a089cebce5c7f728febd1060b55837c894ec2a79dd7854350abce252fc2de96b5da039f2782c70b92f4b1916aa8db91453c7229f33458bd091b3e10a40f9a7e443d2"', + ) + }) + + test('default: local', async () => { + const signature = await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip2930, + }) + expect(signature).toMatchInlineSnapshot( + '"0x01f84f0182031180825208808080c080a089cebce5c7f728febd1060b55837c894ec2a79dd7854350abce252fc2de96b5da039f2782c70b92f4b1916aa8db91453c7229f33458bd091b3e10a40f9a7e443d2"', + ) + }) + + test('w/ prepareTransactionRequest', async () => { + const request = await prepareTransactionRequest(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + value: 1n, + type: 'eip2930', + }) + const signature = await signTransaction(walletClient, request) + expect(signature.match(/^0x01/)).toBeTruthy() + }) + + describe('minimal', () => { + test('w/ type (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + type: 'eip2930', + }), + ).toMatchInlineSnapshot( + '"0x01f84b01808080808080c080a0cfac15d0507fbcdff8c8b489a85704f856f0b0803cacbbe9aa2a0fd34fd9c260a0571039b719e1c24b410bd6407b22539817c385d99dd9e07858fc973704564d5c"', + ) + }) + }) + + describe('args', () => { + test('accessList (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip2930, + accessList: [ + { + address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', + ], + }, + ], + }), + ).toMatchInlineSnapshot( + '"0x01f8ab0182031180825208808080f85bf85994f39fd6e51aad88f6f4ce6ab8827279cfffb92266f842a00000000000000000000000000000000000000000000000000000000000000001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe01a041dbfa79cb11e5049b7c64e29c8484a2b43e664dcaea31ba1f2c9887c62f76b7a002964169f5c366e21a440c006a06f121eca1aafa2275d7c1f165891eb3d31e54"', + ) + }) + + test('data (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip2930, + data: '0x1234', + }), + ).toMatchInlineSnapshot( + '"0x01f85101820311808252088080821234c080a084fdcea5fe55ce8378aa94a8d4a9c01545d59922f1edcdd89a71ebf740dc0bf5a0539a4ab61a42509a6b4c35c85099d8b7b8e815967f0c832c868327caca6307cb"', + ) + }) + + test('gasPrice (local)', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseEip2930, + gasPrice: parseGwei('20'), + }), + ).toMatchInlineSnapshot( + '"0x01f854018203118504a817c800825208808080c080a058e29913bc928a79e0536fc588e8fe372464d1ff4feff691c344c0163280c97ea037780b5c99301a67aaacfbe98c83139fd026e30925fc103b7898b53af9cb0658"', + ) + }) + }) +}) + +describe('legacy', () => { + const baseLegacy = { + ...base, + gasPrice: parseGwei('2'), + type: 'legacy', + } as const satisfies TransactionRequestLegacy + + test('default', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseLegacy, + }), + ).toMatchInlineSnapshot( + '"0xf851820311847735940082520880808025a0c1dc31893c8b13bc2dca5e650f68373ea0b8f3c182b516453faf217c53123527a0353f95bc1dab45198fde8cd20c597cb83ea7cf5a6d49586f0c2eaf150356aa49"', + ) + }) + + test('w/ prepareTransactionRequest', async () => { + const request = await prepareTransactionRequest(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + value: 1n, + type: 'legacy', + }) + const signature = await signTransaction(walletClient, request) + expect(signature.match(/^0xf8/)).toBeTruthy() + }) + + describe('minimal', () => { + test('w/ type', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + type: 'legacy', + }), + ).toMatchInlineSnapshot( + '"0xf84980808080808026a0ea35a1957cb3b2df609b111365df2bc937dfb054f18e4eaa0d0d74c0aa17de21a04aeb73911649e88c65b3b47581731ba1a0e2f810175823f9565da0d54ee45f16"', + ) + }) + + describe('args', () => { + test('data', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseLegacy, + data: '0x1234', + }), + ).toMatchInlineSnapshot( + '"0xf8538203118477359400825208808082123425a0d2c01222db75967c3c0e6e24898bf2123e567b693a2e8c469aba19be0e72403ea0119701066fc18f95dc1c54d0fff88d2eb5883e861341b7b5115db0ae1439969f"', + ) + }) + + test('gas', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseLegacy, + gas: 21000n, + }), + ).toMatchInlineSnapshot( + '"0xf851820311847735940082520880808025a0c1dc31893c8b13bc2dca5e650f68373ea0b8f3c182b516453faf217c53123527a0353f95bc1dab45198fde8cd20c597cb83ea7cf5a6d49586f0c2eaf150356aa49"', + ) + }) + + test('gasPrice', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + ...baseLegacy, + gasPrice: parseGwei('20'), + }), + ).toMatchInlineSnapshot( + '"0xf8518203118504a817c800825208808080259f672886c0db7d3a2710b5bb8b64fd516ff4fede94f34e76b22f451e5ff92ecfa0627283bbafb0600cd997c8dbfa6f4173c5376e2c4c57967541e171fb110b6d64"', + ) + }) + }) + }) +}) + +describe('custom (cip42)', () => { + const walletClient = createWalletClient({ + chain: celo, + transport: http(localHttpUrl), + }) + + test('default', async () => { + expect( + await signTransaction(walletClient, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: null, + ...base, + feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a', + maxFeePerGas: parseGwei('20'), + maxPriorityFeePerGas: parseGwei('2'), + type: 'cip42', + }), + ).toMatchInlineSnapshot( + '"0x7cf8700182031184773594008504a817c80082520894765de816845861e75a25fca122bb6898b8b1282a8080808080c080a05076fb030517e3243dd38850c21923f4343ed429db3ab032178f2d8702cad17fa07e860ca7bc7c2f34ece898c3e6912dcef4608ad3fc0b76bf0d760dcb608a71b2"', + ) + }) +}) + +describe('errors', () => { + test('no account', async () => { + await expect(() => + // @ts-expect-error + signTransaction(walletClient, { + type: 'eip1559', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the WalletClient. + + Docs: https://viem.sh/docs/actions/wallet/signTransaction.html#account + Version: viem@1.0.2" + `) + }) +}) diff --git a/src/actions/wallet/signTransaction.ts b/src/actions/wallet/signTransaction.ts new file mode 100644 index 0000000000..7abd1e0e29 --- /dev/null +++ b/src/actions/wallet/signTransaction.ts @@ -0,0 +1,137 @@ +import type { Account } from '../../accounts/types.js' +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 { GetAccountParameter } from '../../types/account.js' +import type { Chain, GetChain } from '../../types/chain.js' +import { type RpcTransactionRequest } from '../../types/rpc.js' +import type { + TransactionRequest, + TransactionSerializable, + TransactionSerialized, +} from '../../types/transaction.js' +import type { UnionOmit } from '../../types/utils.js' +import { assertCurrentChain } from '../../utils/chain.js' +import { + type FormattedTransactionRequest, + formatTransactionRequest, +} from '../../utils/formatters/transactionRequest.js' +import { numberToHex } from '../../utils/index.js' +import { assertRequest } from '../../utils/transaction/assertRequest.js' +import { getChainId } from '../public/getChainId.js' + +export type SignTransactionParameters< + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined, + TChainOverride extends Chain | undefined = Chain | undefined, +> = UnionOmit< + FormattedTransactionRequest< + TChainOverride extends Chain ? TChainOverride : TChain + >, + 'from' +> & + GetAccountParameter & + GetChain + +export type SignTransactionReturnType = TransactionSerialized + +/** + * Signs a transaction. + * + * - Docs: https://viem.sh/docs/actions/wallet/signTransaction.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param args - {@link SignTransactionParameters} + * @returns The signed serialized tranasction. {@link SignTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { signTransaction } from 'viem/actions' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await signTransaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { signTransaction } from 'viem/actions' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await signTransaction(client, { + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ +export async function signTransaction< + TChain extends Chain | undefined, + TAccount extends Account | undefined, + TChainOverride extends Chain | undefined, +>( + client: Client, + args: SignTransactionParameters, +): Promise { + const { + account: account_ = client.account, + chain = client.chain, + ...transaction + } = args + + if (!account_) + throw new AccountNotFoundError({ + docsPath: '/docs/actions/wallet/signTransaction', + }) + const account = parseAccount(account_) + + assertRequest({ + account, + ...args, + }) + + const chainId = await getChainId(client) + if (chain !== null) + assertCurrentChain({ + currentChainId: chainId, + chain, + }) + + const formatters = chain?.formatters || client.chain?.formatters + const format = + formatters?.transactionRequest?.format || formatTransactionRequest + + if (account.type === 'local') + return account.signTransaction( + { + chainId, + ...transaction, + } as unknown as TransactionSerializable, + { serializer: client.chain?.serializers?.transaction }, + ) as Promise + + return await client.request({ + method: 'eth_signTransaction', + params: [ + { + ...format(transaction as unknown as TransactionRequest), + chainId: numberToHex(chainId), + from: account.address, + } as unknown as RpcTransactionRequest, + ], + }) +} diff --git a/src/chains/celo/formatters.test-d.ts b/src/chains/celo/formatters.test-d.ts index 8be16d89b8..349c99606f 100644 --- a/src/chains/celo/formatters.test-d.ts +++ b/src/chains/celo/formatters.test-d.ts @@ -3,32 +3,32 @@ import { describe, expectTypeOf, test } from 'vitest' import { getBlock } from '../../actions/public/getBlock.js' import { getTransaction } from '../../actions/public/getTransaction.js' import { getTransactionReceipt } from '../../actions/public/getTransactionReceipt.js' +import { prepareTransactionRequest } from '../../actions/wallet/prepareTransactionRequest.js' +import { signTransaction } from '../../actions/wallet/signTransaction.js' import { createPublicClient } from '../../clients/createPublicClient.js' import { createWalletClient } from '../../clients/createWalletClient.js' import { http } from '../../clients/transports/http.js' import type { Hash } from '../../types/misc.js' -import type { - RpcBlock, - RpcTransaction, - RpcTransactionReceipt, -} from '../../types/rpc.js' +import type { RpcBlock, RpcTransactionReceipt } from '../../types/rpc.js' import type { TransactionRequest } from '../../types/transaction.js' +import type { Assign } from '../../types/utils.js' import { sendTransaction } from '../../wallet.js' import { celo } from '../index.js' import { formattersCelo } from './formatters.js' +import type { + CeloBlockOverrides, + CeloRpcTransaction, + CeloTransactionRequest, +} from './types.js' describe('block', () => { expectTypeOf(formattersCelo.block.format).parameter(0).toEqualTypeOf< - Partial & { - randomness: { committed: `0x${string}`; revealed: `0x${string}` } - transactions: - | `0x${string}`[] - | (RpcTransaction & { - feeCurrency: `0x${string}` | null - gatewayFee: `0x${string}` | null - gatewayFeeRecipient: `0x${string}` | null - })[] - } + Assign< + Partial, + CeloBlockOverrides & { + transactions: `0x${string}`[] | CeloRpcTransaction[] + } + > >() expectTypeOf< ReturnType['difficulty'] @@ -54,13 +54,6 @@ describe('block', () => { }) describe('transaction', () => { - expectTypeOf(formattersCelo.transaction.format).parameter(0).toEqualTypeOf< - Partial & { - feeCurrency: `0x${string}` | null - gatewayFee: `0x${string}` | null - gatewayFeeRecipient: `0x${string}` | null - } - >() expectTypeOf< ReturnType['feeCurrency'] >().toEqualTypeOf<`0x${string}` | null>() @@ -99,11 +92,7 @@ describe('transactionRequest', () => { expectTypeOf(formattersCelo.transactionRequest.format) .parameter(0) .toEqualTypeOf< - Partial & { - feeCurrency?: `0x${string}` | undefined - gatewayFee?: bigint | undefined - gatewayFeeRecipient?: `0x${string}` | undefined - } + Assign, CeloTransactionRequest> >() expectTypeOf< ReturnType['feeCurrency'] @@ -185,6 +174,9 @@ describe('smoke', () => { expectTypeOf(transaction.gatewayFeeRecipient).toEqualTypeOf< `0x${string}` | null >() + expectTypeOf(transaction.type).toEqualTypeOf< + 'legacy' | 'eip2930' | 'eip1559' | 'cip42' + >() }) test('transactionReceipt', async () => { @@ -204,7 +196,67 @@ describe('smoke', () => { >() }) - test('transactionRequest', async () => { + test('transactionRequest (prepareTransactionRequest)', async () => { + const client = createWalletClient({ + account: '0x', + chain: celo, + transport: http(), + }) + + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + }) + + // @ts-expect-error `gasPrice` is not defined + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + maxFeePerGas: 0n, + maxPriorityFeePerGas: 0n, + }) + + // @ts-expect-error `gasPrice` is not defined + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'cip42', + }) + + // @ts-expect-error `gasPrice` is not defined + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'eip1559', + }) + + // @ts-expect-error `type` cannot be "legacy" + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + maxFeePerGas: 0n, + type: 'legacy', + }) + + // @ts-expect-error `type` cannot be "eip2930" + prepareTransactionRequest(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + maxFeePerGas: 0n, + type: 'eip2930', + }) + }) + + test('transactionRequest (sendTransaction)', async () => { const client = createWalletClient({ account: '0x', chain: celo, @@ -227,6 +279,15 @@ describe('smoke', () => { maxPriorityFeePerGas: 0n, }) + // @ts-expect-error `gasPrice` is not defined + sendTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'cip42', + }) + // @ts-expect-error `gasPrice` is not defined sendTransaction(client, { feeCurrency: '0x', @@ -255,6 +316,66 @@ describe('smoke', () => { }) }) + test('transactionRequest (signTransaction)', async () => { + const client = createWalletClient({ + account: '0x', + chain: celo, + transport: http(), + }) + + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + }) + + // @ts-expect-error `gasPrice` is not defined + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + maxFeePerGas: 0n, + maxPriorityFeePerGas: 0n, + }) + + // @ts-expect-error `gasPrice` is not defined + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'cip42', + }) + + // @ts-expect-error `gasPrice` is not defined + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'eip1559', + }) + + // @ts-expect-error `type` cannot be "legacy" + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + maxFeePerGas: 0n, + type: 'legacy', + }) + + // @ts-expect-error `type` cannot be "eip2930" + signTransaction(client, { + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + maxFeePerGas: 0n, + type: 'eip2930', + }) + }) + test('transactionRequest (chain on action)', async () => { const client = createWalletClient({ account: '0x', @@ -279,6 +400,16 @@ describe('smoke', () => { maxPriorityFeePerGas: 0n, }) + // @ts-expect-error `gasPrice` is not defined + sendTransaction(client, { + chain: celo, + feeCurrency: '0x', + gatewayFee: 0n, + gatewayFeeRecipient: '0x', + gasPrice: 0n, + type: 'cip42', + }) + // @ts-expect-error `gasPrice` is not defined sendTransaction(client, { chain: celo, diff --git a/src/chains/celo/formatters.test.ts b/src/chains/celo/formatters.test.ts index 9b00272f9b..06e1e39a5e 100644 --- a/src/chains/celo/formatters.test.ts +++ b/src/chains/celo/formatters.test.ts @@ -777,6 +777,7 @@ describe('transactionRequest', () => { "maxFeePerGas": "0x2", "maxPriorityFeePerGas": "0x1", "nonce": "0x1", + "type": undefined, "value": "0x1", } `) @@ -803,6 +804,36 @@ describe('transactionRequest', () => { "maxFeePerGas": "0x2", "maxPriorityFeePerGas": "0x1", "nonce": "0x1", + "type": undefined, + "value": "0x1", + } + `) + + expect( + transactionRequest.format({ + feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + gas: 1n, + gatewayFee: 4n, + gatewayFeeRecipient: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + maxFeePerGas: 2n, + maxPriorityFeePerGas: 1n, + nonce: 1, + type: 'cip42', + value: 1n, + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": "0x1", + "gasPrice": undefined, + "gatewayFee": "0x4", + "gatewayFeeRecipient": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x1", + "nonce": "0x1", + "type": "0x7c", "value": "0x1", } `) diff --git a/src/chains/celo/formatters.ts b/src/chains/celo/formatters.ts index b60516cc4b..04e2c7467a 100644 --- a/src/chains/celo/formatters.ts +++ b/src/chains/celo/formatters.ts @@ -1,5 +1,6 @@ import { type ChainFormatters } from '../../types/chain.js' import type { Hash } from '../../types/misc.js' +import type { RpcTransaction } from '../../types/rpc.js' import { hexToBigInt } from '../../utils/encoding/fromHex.js' import { numberToHex } from '../../utils/encoding/toHex.js' import { defineBlock } from '../../utils/formatters/block.js' @@ -12,13 +13,11 @@ import { defineTransactionRequest } from '../../utils/formatters/transactionRequ import type { CeloBlockOverrides, CeloRpcTransaction, - CeloRpcTransactionOverrides, CeloRpcTransactionReceiptOverrides, - CeloRpcTransactionRequestOverrides, + CeloRpcTransactionRequest, CeloTransaction, - CeloTransactionOverrides, CeloTransactionReceiptOverrides, - CeloTransactionRequestOverrides, + CeloTransactionRequest, } from './types.js' export const formattersCelo = { @@ -34,7 +33,7 @@ export const formattersCelo = { const transactions = args.transactions?.map((transaction) => { if (typeof transaction === 'string') return transaction return { - ...formatTransaction(transaction), + ...formatTransaction(transaction as RpcTransaction), feeCurrency: transaction.feeCurrency, gatewayFee: transaction.gatewayFee ? hexToBigInt(transaction.gatewayFee) @@ -49,12 +48,12 @@ export const formattersCelo = { }, }), transaction: /*#__PURE__*/ defineTransaction({ - format(args: CeloRpcTransactionOverrides): CeloTransactionOverrides { + format(args: CeloRpcTransaction): CeloTransaction { return { feeCurrency: args.feeCurrency, gatewayFee: args.gatewayFee ? hexToBigInt(args.gatewayFee) : null, gatewayFeeRecipient: args.gatewayFeeRecipient, - } + } as CeloTransaction }, }), transactionReceipt: /*#__PURE__*/ defineTransactionReceipt({ @@ -69,17 +68,17 @@ export const formattersCelo = { }, }), transactionRequest: /*#__PURE__*/ defineTransactionRequest({ - format( - args: CeloTransactionRequestOverrides, - ): CeloRpcTransactionRequestOverrides { - return { + format(args: CeloTransactionRequest): CeloRpcTransactionRequest { + const request = { feeCurrency: args.feeCurrency, gatewayFee: typeof args.gatewayFee !== 'undefined' ? numberToHex(args.gatewayFee) : undefined, gatewayFeeRecipient: args.gatewayFeeRecipient, - } + } as CeloRpcTransactionRequest + if (args.type === 'cip42') request.type = '0x7c' + return request }, }), } as const satisfies ChainFormatters diff --git a/src/chains/celo/types.ts b/src/chains/celo/types.ts index 03876cee95..81854a36c0 100644 --- a/src/chains/celo/types.ts +++ b/src/chains/celo/types.ts @@ -4,17 +4,21 @@ import type { Block, BlockTag } from '../../types/block.js' import type { FeeValuesEIP1559 } from '../../types/fee.js' import type { Hex } from '../../types/misc.js' import type { + Index, + Quantity, RpcBlock, - RpcTransaction, + RpcTransaction as RpcTransaction_, RpcTransactionReceipt, - RpcTransactionRequest, + RpcTransactionRequest as RpcTransactionRequest_, TransactionType, } from '../../types/rpc.js' import type { AccessList, - Transaction, + Transaction as Transaction_, + TransactionBase, TransactionReceipt, - TransactionRequest, + TransactionRequest as TransactionRequest_, + TransactionRequestBase, TransactionSerializable, TransactionSerializableBase, TransactionSerialized, @@ -68,13 +72,9 @@ export type CeloRpcBlock< > & CeloRpcBlockOverrides -export type CeloRpcTransactionOverrides = { - feeCurrency: Address | null - gatewayFee: Hex | null - gatewayFeeRecipient: Address | null -} export type CeloRpcTransaction = - RpcTransaction & CeloRpcTransactionOverrides + | RpcTransaction + | RpcTransactionCIP42 export type CeloRpcTransactionReceiptOverrides = { feeCurrency: Address | null @@ -84,21 +84,13 @@ export type CeloRpcTransactionReceiptOverrides = { export type CeloRpcTransactionReceipt = RpcTransactionReceipt & CeloRpcTransactionReceiptOverrides -export type CeloRpcTransactionRequestOverrides = { - feeCurrency?: Address - gatewayFee?: Hex - gatewayFeeRecipient?: Address -} -export type CeloRpcTransactionRequest = RpcTransactionRequest & - CeloRpcTransactionRequestOverrides +export type CeloRpcTransactionRequest = + | RpcTransactionRequest + | RpcTransactionRequestCIP42 -export type CeloTransactionOverrides = { - feeCurrency: Address | null - gatewayFee: bigint | null - gatewayFeeRecipient: Address | null -} export type CeloTransaction = - Transaction & CeloTransactionOverrides + | Transaction + | TransactionCIP42 export type CeloTransactionReceiptOverrides = { feeCurrency: Address | null @@ -108,13 +100,9 @@ export type CeloTransactionReceiptOverrides = { export type CeloTransactionReceipt = TransactionReceipt & CeloTransactionReceiptOverrides -export type CeloTransactionRequestOverrides = { - feeCurrency?: Address - gatewayFee?: bigint - gatewayFeeRecipient?: Address -} -export type CeloTransactionRequest = TransactionRequest & - CeloTransactionRequestOverrides +export type CeloTransactionRequest = + | TransactionRequest + | TransactionRequestCIP42 export type CeloTransactionSerializable = | TransactionSerializableCIP42 @@ -126,6 +114,74 @@ export type CeloTransactionSerialized< export type CeloTransactionType = TransactionType | 'cip42' +type RpcTransaction = + RpcTransaction_ & { + feeCurrency: Address | null + gatewayFee: Hex | null + gatewayFeeRecipient: Address | null + } + +type RpcTransactionRequest = RpcTransactionRequest_ & { + feeCurrency?: Address + gatewayFee?: Hex + gatewayFeeRecipient?: Address +} + +export type RpcTransactionCIP42 = + TransactionBase & + FeeValuesEIP1559 & { + feeCurrency: Address | null + gatewayFee: Hex | null + gatewayFeeRecipient: Address | null + type: '0x7c' + } + +export type RpcTransactionRequestCIP42 = TransactionRequestBase< + Quantity, + Index +> & + Partial> & { + accessList?: AccessList + feeCurrency?: Address + gatewayFee?: Hex + gatewayFeeRecipient?: Address + type?: '0x7c' + } + +type Transaction = Transaction_< + bigint, + number, + TPending +> & { + feeCurrency: Address | null + gatewayFee: bigint | null + gatewayFeeRecipient: Address | null +} + +export type TransactionCIP42 = + TransactionBase & + FeeValuesEIP1559 & { + feeCurrency: Address | null + gatewayFee: bigint | null + gatewayFeeRecipient: Address | null + type: 'cip42' + } + +type TransactionRequest = TransactionRequest_ & { + feeCurrency?: Address + gatewayFee?: bigint + gatewayFeeRecipient?: Address +} + +export type TransactionRequestCIP42 = TransactionRequestBase & + Partial & { + accessList?: AccessList + feeCurrency?: Address + gatewayFee?: bigint + gatewayFeeRecipient?: Address + type?: 'cip42' + } + export type TransactionSerializableCIP42< TQuantity = bigint, TIndex = number, diff --git a/src/chains/utils.ts b/src/chains/utils.ts index d1764ebf82..5c94f54117 100644 --- a/src/chains/utils.ts +++ b/src/chains/utils.ts @@ -10,20 +10,20 @@ export type { CeloRpcBlock, CeloRpcBlockOverrides, CeloRpcTransaction, - CeloRpcTransactionOverrides, CeloRpcTransactionReceipt, CeloRpcTransactionReceiptOverrides, CeloRpcTransactionRequest, - CeloRpcTransactionRequestOverrides, CeloTransaction, - CeloTransactionOverrides, CeloTransactionReceipt, CeloTransactionReceiptOverrides, CeloTransactionRequest, - CeloTransactionRequestOverrides, CeloTransactionSerializable, CeloTransactionSerialized, CeloTransactionType, + RpcTransactionCIP42, + RpcTransactionRequestCIP42, + TransactionCIP42, + TransactionRequestCIP42, TransactionSerializableCIP42, TransactionSerializedCIP42, } from './celo/types.js' diff --git a/src/clients/createClient.test.ts b/src/clients/createClient.test.ts index d30f0d9ddd..c642575b78 100644 --- a/src/clients/createClient.test.ts +++ b/src/clients/createClient.test.ts @@ -482,8 +482,10 @@ describe('extends', () => { "multicall": [Function], "name": "Base Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "request": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "transport": { "key": "http", diff --git a/src/clients/createPublicClient.test.ts b/src/clients/createPublicClient.test.ts index 027cf0f535..11385a1e38 100644 --- a/src/clients/createPublicClient.test.ts +++ b/src/clients/createPublicClient.test.ts @@ -68,8 +68,10 @@ test('creates', () => { "multicall": [Function], "name": "Public Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "request": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "transport": { "key": "mock", @@ -203,8 +205,10 @@ describe('transports', () => { "multicall": [Function], "name": "Public Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "request": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "transport": { "key": "http", @@ -302,8 +306,10 @@ describe('transports', () => { "multicall": [Function], "name": "Public Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "request": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "transport": { "getSocket": [Function], @@ -377,8 +383,10 @@ describe('transports', () => { "multicall": [Function], "name": "Public Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "request": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "transport": { "key": "custom", @@ -489,6 +497,7 @@ test('extend', () => { "multicall": [Function], "name": "Public Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "removeBlockTimestampInterval": [Function], "request": [Function], @@ -496,6 +505,7 @@ test('extend', () => { "requestPermissions": [Function], "reset": [Function], "revert": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "sendUnsignedTransaction": [Function], "setAutomine": [Function], @@ -513,6 +523,7 @@ test('extend', () => { "setRpcUrl": [Function], "setStorageAt": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "simulateContract": [Function], "snapshot": [Function], diff --git a/src/clients/createTestClient.test.ts b/src/clients/createTestClient.test.ts index 70e2b45451..8e91d8bde1 100644 --- a/src/clients/createTestClient.test.ts +++ b/src/clients/createTestClient.test.ts @@ -373,6 +373,7 @@ test('extend', () => { "multicall": [Function], "name": "Test Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "removeBlockTimestampInterval": [Function], "request": [Function], @@ -380,6 +381,7 @@ test('extend', () => { "requestPermissions": [Function], "reset": [Function], "revert": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "sendUnsignedTransaction": [Function], "setAutomine": [Function], @@ -397,6 +399,7 @@ test('extend', () => { "setRpcUrl": [Function], "setStorageAt": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "simulateContract": [Function], "snapshot": [Function], diff --git a/src/clients/createWalletClient.test.ts b/src/clients/createWalletClient.test.ts index c08774645f..ab56e15675 100644 --- a/src/clients/createWalletClient.test.ts +++ b/src/clients/createWalletClient.test.ts @@ -46,11 +46,14 @@ test('creates', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -97,11 +100,14 @@ describe('args: account', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -152,11 +158,14 @@ describe('args: account', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -198,11 +207,14 @@ describe('args: transport', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -242,11 +254,14 @@ describe('args: transport', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -312,11 +327,14 @@ describe('args: transport', () => { "key": "wallet", "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "request": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "transport": { @@ -427,6 +445,7 @@ test('extend', () => { "multicall": [Function], "name": "Wallet Client", "pollingInterval": 4000, + "prepareTransactionRequest": [Function], "readContract": [Function], "removeBlockTimestampInterval": [Function], "request": [Function], @@ -434,6 +453,7 @@ test('extend', () => { "requestPermissions": [Function], "reset": [Function], "revert": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "sendUnsignedTransaction": [Function], "setAutomine": [Function], @@ -451,6 +471,7 @@ test('extend', () => { "setRpcUrl": [Function], "setStorageAt": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "simulateContract": [Function], "snapshot": [Function], diff --git a/src/clients/decorators/public.test.ts b/src/clients/decorators/public.test.ts index 9086a668e8..5331ca21a7 100644 --- a/src/clients/decorators/public.test.ts +++ b/src/clients/decorators/public.test.ts @@ -20,6 +20,7 @@ import { import { getBlockNumber } from '../../actions/public/getBlockNumber.js' import { parseEther } from '../../utils/unit/parseEther.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' import { publicActions } from './public.js' test('default', async () => { @@ -56,7 +57,9 @@ test('default', async () => { "getTransactionCount": [Function], "getTransactionReceipt": [Function], "multicall": [Function], + "prepareTransactionRequest": [Function], "readContract": [Function], + "sendRawTransaction": [Function], "simulateContract": [Function], "uninstallFilter": [Function], "verifyMessage": [Function], @@ -334,6 +337,30 @@ describe('smoke test', () => { ).toBeDefined() }) + test('prepareTransactionRequest', async () => { + expect( + await publicClient.prepareTransactionRequest({ + account: accounts[6].address, + to: accounts[7].address, + value: parseEther('1'), + }), + ).toBeDefined() + }) + + test('sendRawTransaction', async () => { + const request = await publicClient.prepareTransactionRequest({ + account: privateKeyToAccount(accounts[0].privateKey), + to: accounts[1].address, + value: parseEther('1'), + }) + const serializedTransaction = await walletClient.signTransaction(request) + expect( + await publicClient.sendRawTransaction({ + serializedTransaction, + }), + ).toBeDefined() + }) + test('readContract', async () => { expect( await publicClient.readContract({ diff --git a/src/clients/decorators/public.ts b/src/clients/decorators/public.ts index 638f8ecf4b..285ae60897 100644 --- a/src/clients/decorators/public.ts +++ b/src/clients/decorators/public.ts @@ -206,6 +206,16 @@ import { type WatchPendingTransactionsReturnType, watchPendingTransactions, } from '../../actions/public/watchPendingTransactions.js' +import { + type PrepareTransactionRequestParameters, + type PrepareTransactionRequestReturnType, + prepareTransactionRequest, +} from '../../actions/wallet/prepareTransactionRequest.js' +import { + type SendRawTransactionParameters, + type SendRawTransactionReturnType, + sendRawTransaction, +} from '../../actions/wallet/sendRawTransaction.js' import type { Account } from '../../types/account.js' import type { BlockNumber, BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' @@ -1199,6 +1209,47 @@ export type PublicActions< >( args: MulticallParameters, ) => Promise> + /** + * Prepares a transaction request for signing. + * + * - Docs: https://viem.sh/docs/actions/wallet/prepareTransactionRequest.html + * + * @param args - {@link PrepareTransactionRequestParameters} + * @returns The transaction request. {@link PrepareTransactionRequestReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ + prepareTransactionRequest: ( + args: PrepareTransactionRequestParameters, + ) => Promise /** * Calls a read-only function on a contract, and returns the response. * @@ -1236,6 +1287,33 @@ export type PublicActions< >( args: ReadContractParameters, ) => Promise> + /** + * Sends a **signed** transaction to the network + * + * - Docs: https://viem.sh/docs/actions/wallet/sendRawTransaction.html + * - JSON-RPC Method: [`eth_sendRawTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * + * @param client - Client to use + * @param parameters - {@link SendRawTransactionParameters} + * @returns The transaction hash. {@link SendRawTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { sendRawTransaction } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * + * const hash = await client.sendRawTransaction({ + * serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' + * }) + */ + sendRawTransaction: ( + args: SendRawTransactionParameters, + ) => Promise /** * Simulates/validates a contract interaction. This is useful for retrieving **return data** and **revert reasons** of contract write functions. * @@ -1567,7 +1645,10 @@ export function publicActions< getTransactionCount: (args) => getTransactionCount(client, args), getTransactionReceipt: (args) => getTransactionReceipt(client, args), multicall: (args) => multicall(client, args as any) as any, + prepareTransactionRequest: (args) => + prepareTransactionRequest(client as any, args as any), readContract: (args) => readContract(client, args), + sendRawTransaction: (args) => sendRawTransaction(client, args), simulateContract: (args) => simulateContract(client, args), verifyMessage: (args) => verifyMessage(client, args), verifyTypedData: (args) => verifyTypedData(client, args), diff --git a/src/clients/decorators/wallet.test.ts b/src/clients/decorators/wallet.test.ts index 585e4321c7..3877f4c654 100644 --- a/src/clients/decorators/wallet.test.ts +++ b/src/clients/decorators/wallet.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest' import { baycContractConfig, wagmiContractConfig } from '../../_test/abis.js' import { accounts } from '../../_test/constants.js' import { walletClient, walletClientWithAccount } from '../../_test/utils.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' import { avalanche } from '../../chains/index.js' import { parseEther } from '../../utils/unit/parseEther.js' @@ -16,10 +17,13 @@ test('default', async () => { "getAddresses": [Function], "getChainId": [Function], "getPermissions": [Function], + "prepareTransactionRequest": [Function], "requestAddresses": [Function], "requestPermissions": [Function], + "sendRawTransaction": [Function], "sendTransaction": [Function], "signMessage": [Function], + "signTransaction": [Function], "signTypedData": [Function], "switchChain": [Function], "watchAsset": [Function], @@ -70,6 +74,39 @@ describe('smoke test', () => { ).toBeDefined() }) + test('prepareTransactionRequest', async () => { + expect( + await walletClient.prepareTransactionRequest({ + account: accounts[6].address, + to: accounts[7].address, + value: parseEther('1'), + }), + ).toBeDefined() + }) + + test('prepareTransactionRequest (inferred account)', async () => { + expect( + await walletClientWithAccount.prepareTransactionRequest({ + to: accounts[7].address, + value: parseEther('1'), + }), + ).toBeDefined() + }) + + test('sendRawTransaction', async () => { + const request = await walletClient.prepareTransactionRequest({ + account: privateKeyToAccount(accounts[0].privateKey), + to: accounts[1].address, + value: parseEther('1'), + }) + const serializedTransaction = await walletClient.signTransaction(request) + expect( + await walletClient.sendRawTransaction({ + serializedTransaction, + }), + ).toBeDefined() + }) + test('sendTransaction', async () => { expect( await walletClient.sendTransaction({ @@ -106,6 +143,15 @@ describe('smoke test', () => { ).toBeDefined() }) + test('signTransaction', async () => { + const request = await walletClient.prepareTransactionRequest({ + account: privateKeyToAccount(accounts[0].privateKey), + to: accounts[1].address, + value: parseEther('1'), + }) + expect(await walletClient.signTransaction(request)).toBeDefined() + }) + test('signTypedData', async () => { expect( await walletClient.signTypedData({ diff --git a/src/clients/decorators/wallet.ts b/src/clients/decorators/wallet.ts index d48a9c9a42..0cddc9def3 100644 --- a/src/clients/decorators/wallet.ts +++ b/src/clients/decorators/wallet.ts @@ -22,6 +22,11 @@ import { type GetPermissionsReturnType, getPermissions, } from '../../actions/wallet/getPermissions.js' +import { + type PrepareTransactionRequestParameters, + type PrepareTransactionRequestReturnType, + prepareTransactionRequest, +} from '../../actions/wallet/prepareTransactionRequest.js' import { type RequestAddressesReturnType, requestAddresses, @@ -31,6 +36,11 @@ import { type RequestPermissionsReturnType, requestPermissions, } from '../../actions/wallet/requestPermissions.js' +import { + type SendRawTransactionParameters, + type SendRawTransactionReturnType, + sendRawTransaction, +} from '../../actions/wallet/sendRawTransaction.js' import { type SendTransactionParameters, type SendTransactionReturnType, @@ -41,6 +51,11 @@ import { type SignMessageReturnType, signMessage, } from '../../actions/wallet/signMessage.js' +import { + type SignTransactionParameters, + type SignTransactionReturnType, + signTransaction, +} from '../../actions/wallet/signTransaction.js' import { type SignTypedDataParameters, type SignTypedDataReturnType, @@ -175,6 +190,47 @@ export type WalletActions< * const permissions = await client.getPermissions() */ getPermissions: () => Promise + /** + * Prepares a transaction request for signing. + * + * - Docs: https://viem.sh/docs/actions/wallet/prepareTransactionRequest.html + * + * @param args - {@link PrepareTransactionRequestParameters} + * @returns The transaction request. {@link PrepareTransactionRequestReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + */ + prepareTransactionRequest: ( + args: PrepareTransactionRequestParameters, + ) => Promise /** * Requests a list of accounts managed by a wallet. * @@ -222,6 +278,33 @@ export type WalletActions< requestPermissions: ( args: RequestPermissionsParameters, ) => Promise + /** + * Sends a **signed** transaction to the network + * + * - Docs: https://viem.sh/docs/actions/wallet/sendRawTransaction.html + * - JSON-RPC Method: [`eth_sendRawTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * + * @param client - Client to use + * @param parameters - {@link SendRawTransactionParameters} + * @returns The transaction hash. {@link SendRawTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { sendRawTransaction } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * + * const hash = await client.sendRawTransaction({ + * serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' + * }) + */ + sendRawTransaction: ( + args: SendRawTransactionParameters, + ) => Promise /** * Creates, signs, and sends a new transaction to the network. * @@ -313,6 +396,52 @@ export type WalletActions< signMessage: ( args: SignMessageParameters, ) => Promise + /** + * Signs a transaction. + * + * - Docs: https://viem.sh/docs/actions/wallet/signTransaction.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTransaction`](https://ethereum.github.io/execution-apis/api-documentation/) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param args - {@link SignTransactionParameters} + * @returns The signed message. {@link SignTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * const signature = await client.signTransaction(request) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const request = await client.prepareTransactionRequest({ + * to: '0x0000000000000000000000000000000000000000', + * value: 1n, + * }) + * const signature = await client.signTransaction(request) + */ + signTransaction: ( + args: SignTransactionParameters, + ) => Promise /** * Signs typed data and calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. * @@ -536,10 +665,14 @@ export function walletActions< getAddresses: () => getAddresses(client), getChainId: () => getChainId(client), getPermissions: () => getPermissions(client), + prepareTransactionRequest: (args) => + prepareTransactionRequest(client as any, args as any), requestAddresses: () => requestAddresses(client), requestPermissions: (args) => requestPermissions(client, args), + sendRawTransaction: (args) => sendRawTransaction(client, args), sendTransaction: (args) => sendTransaction(client, args), signMessage: (args) => signMessage(client, args), + signTransaction: (args) => signTransaction(client, args), signTypedData: (args) => signTypedData(client, args), switchChain: (args) => switchChain(client, args), watchAsset: (args) => watchAsset(client, args), diff --git a/src/index.test.ts b/src/index.test.ts index 3a5e55ff16..68d9216b65 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -247,6 +247,7 @@ test('exports', () => { "defineTransactionReceipt", "defineTransactionRequest", "formatTransactionRequest", + "rpcTransactionType", "getAbiItem", "getContractAddress", "getCreate2Address", @@ -319,7 +320,6 @@ test('exports', () => { "parseGwei", "parseTransaction", "parseUnits", - "prepareRequest", "serializeAccessList", "serializeTransaction", "size", diff --git a/src/index.ts b/src/index.ts index 4179862b9e..e8504c0079 100644 --- a/src/index.ts +++ b/src/index.ts @@ -203,6 +203,12 @@ export type { SendTransactionParameters, SendTransactionReturnType, } from './actions/wallet/sendTransaction.js' +export type { + PrepareTransactionRequestParameters, + PrepareTransactionRequestReturnType, + /** @deprecated import `prepareTransactionRequest` from `viem/actions` instead. */ + prepareTransactionRequest as prepareRequest, +} from './actions/wallet/prepareTransactionRequest.js' export type { SendUnsignedTransactionParameters, SendUnsignedTransactionReturnType, @@ -740,6 +746,7 @@ export { type FormattedTransactionRequest, defineTransactionRequest, formatTransactionRequest, + rpcTransactionType, } from './utils/formatters/transactionRequest.js' export { type GetAbiItemParameters, @@ -862,7 +869,6 @@ export { parseEther } from './utils/unit/parseEther.js' export { parseGwei } from './utils/unit/parseGwei.js' export { parseTransaction } from './utils/transaction/parseTransaction.js' export { parseUnits } from './utils/unit/parseUnits.js' -export { prepareRequest } from './utils/transaction/prepareRequest.js' export { serializeAccessList } from './utils/transaction/serializeAccessList.js' export { serializeTransaction, diff --git a/src/types/chain.ts b/src/types/chain.ts index 35882cf7e1..33016b5f8a 100644 --- a/src/types/chain.ts +++ b/src/types/chain.ts @@ -1,6 +1,7 @@ import type { Address } from 'abitype' import type { EstimateFeesPerGasReturnType } from '../actions/public/estimateFeesPerGas.js' +import type { PrepareTransactionRequestParameters } from '../actions/wallet/prepareTransactionRequest.js' import type { Client } from '../clients/createClient.js' import type { Transport } from '../clients/transports/createTransport.js' import type { Account } from '../types/account.js' @@ -11,7 +12,6 @@ import type { } from '../types/transaction.js' import type { IsUndefined, Prettify } from '../types/utils.js' import type { FormattedBlock } from '../utils/formatters/block.js' -import type { PrepareRequestParameters } from '../utils/transaction/prepareRequest.js' import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js' export type Chain< @@ -112,7 +112,7 @@ export type ChainFeesFnParameters< * is outside of a transaction request context (e.g. a direct call to * the `estimateFeesPerGas` Action). */ - request?: PrepareRequestParameters< + request?: PrepareTransactionRequestParameters< Omit & { formatters: formatters }, Account | undefined, undefined diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index d994560462..4125e15c87 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -639,7 +639,7 @@ export type PublicRpcSchema = [ ReturnType: string }, /** - * @description Sends and already-signed transaction to the network + * @description Sends a **signed** transaction to the network * @link https://eips.ethereum.org/EIPS/eip-1474 * @example * provider.request({ method: 'eth_sendRawTransaction', params: ['0x...'] }) diff --git a/src/utils/data/isBytesEqual.ts b/src/utils/data/isBytesEqual.ts index 4767463fcb..6201aaf142 100644 --- a/src/utils/data/isBytesEqual.ts +++ b/src/utils/data/isBytesEqual.ts @@ -1,6 +1,9 @@ -import { type ByteArray, type Hex, isHex, toBytes } from '../../index.js' import { equalBytes } from '@noble/curves/abstract/utils' +import type { ByteArray, Hex } from '../../types/misc.js' +import { toBytes } from '../encoding/toBytes.js' +import { isHex } from './isHex.js' + export function isBytesEqual(a_: ByteArray | Hex, b_: ByteArray | Hex) { const a = isHex(a_) ? toBytes(a_) : a_ const b = isHex(b_) ? toBytes(b_) : b_ diff --git a/src/utils/ens/encodeLabelhash.ts b/src/utils/ens/encodeLabelhash.ts index e35c7dd533..4c707f9328 100644 --- a/src/utils/ens/encodeLabelhash.ts +++ b/src/utils/ens/encodeLabelhash.ts @@ -1,4 +1,4 @@ -import type { Hex } from '../../index.js' +import type { Hex } from '../../types/misc.js' export function encodeLabelhash(hash: Hex): `[${string}]` { return `[${hash.slice(2)}]` diff --git a/src/utils/ens/encodedLabelToLabelhash.ts b/src/utils/ens/encodedLabelToLabelhash.ts index c8568fbc30..5b9dc5dbfa 100644 --- a/src/utils/ens/encodedLabelToLabelhash.ts +++ b/src/utils/ens/encodedLabelToLabelhash.ts @@ -1,5 +1,5 @@ -import type { Hex } from '../../index.js' -import { isHex } from '../index.js' +import type { Hex } from '../../types/misc.js' +import { isHex } from '../data/isHex.js' export function encodedLabelToLabelhash(label: string): Hex | null { if (label.length !== 66) return null diff --git a/src/utils/formatters/transactionRequest.test.ts b/src/utils/formatters/transactionRequest.test.ts index 0bb5d3e463..76b6f4791b 100644 --- a/src/utils/formatters/transactionRequest.test.ts +++ b/src/utils/formatters/transactionRequest.test.ts @@ -7,7 +7,10 @@ import type { TransactionRequestLegacy, } from '../../types/transaction.js' -import { formatTransactionRequest } from './transactionRequest.js' +import { + formatTransactionRequest, + rpcTransactionType, +} from './transactionRequest.js' const base: TransactionRequest = { data: '0x1', @@ -23,6 +26,7 @@ test('legacy transaction', () => { formatTransactionRequest({ ...base, gasPrice: 69n, + type: 'legacy', } as TransactionRequestLegacy), ).toMatchInlineSnapshot(` { @@ -34,6 +38,7 @@ test('legacy transaction', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": "0x0", "value": "0x1", } `) @@ -50,6 +55,7 @@ test('eip2930 transaction', () => { }, ], gasPrice: 69n, + type: 'eip2930', } as TransactionRequestEIP2930), ).toMatchInlineSnapshot(` { @@ -69,6 +75,7 @@ test('eip2930 transaction', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": "0x1", "value": "0x1", } `) @@ -86,6 +93,7 @@ test('eip1559 transaction', () => { ], maxFeePerGas: 69n, maxPriorityFeePerGas: 69n, + type: 'eip1559', } as TransactionRequestEIP1559), ).toMatchInlineSnapshot(` { @@ -105,6 +113,7 @@ test('eip1559 transaction', () => { "maxPriorityFeePerGas": "0x45", "nonce": "0x1", "to": "0x1", + "type": "0x2", "value": "0x1", } `) @@ -126,6 +135,7 @@ test('nullish gas', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": undefined, "value": "0x1", } `) @@ -147,6 +157,7 @@ test('nullish gasPrice', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": undefined, "value": "0x1", } `) @@ -168,6 +179,7 @@ test('nullish maxFeePerGas', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": undefined, "value": "0x1", } `) @@ -189,6 +201,7 @@ test('nullish maxPriorityFeePerGas', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": undefined, "value": "0x1", } `) @@ -210,6 +223,7 @@ test('nullish nonce', () => { "maxPriorityFeePerGas": undefined, "nonce": undefined, "to": "0x1", + "type": undefined, "value": "0x1", } `) @@ -231,7 +245,18 @@ test('nullish value', () => { "maxPriorityFeePerGas": undefined, "nonce": "0x1", "to": "0x1", + "type": undefined, "value": undefined, } `) }) + +test('rpcTransactionType', () => { + expect(rpcTransactionType).toMatchInlineSnapshot(` + { + "eip1559": "0x2", + "eip2930": "0x1", + "legacy": "0x0", + } + `) +}) diff --git a/src/utils/formatters/transactionRequest.ts b/src/utils/formatters/transactionRequest.ts index c376220c8a..822518c3e1 100644 --- a/src/utils/formatters/transactionRequest.ts +++ b/src/utils/formatters/transactionRequest.ts @@ -15,6 +15,12 @@ export type FormattedTransactionRequest< TransactionRequest > +export const rpcTransactionType = { + legacy: '0x0', + eip2930: '0x1', + eip1559: '0x2', +} as const + export function formatTransactionRequest( transactionRequest: Partial, ) { @@ -40,6 +46,10 @@ export function formatTransactionRequest( typeof transactionRequest.nonce !== 'undefined' ? numberToHex(transactionRequest.nonce) : undefined, + type: + typeof transactionRequest.type !== 'undefined' + ? rpcTransactionType[transactionRequest.type] + : undefined, value: typeof transactionRequest.value !== 'undefined' ? numberToHex(transactionRequest.value) diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index f81ae8f96e..c178e8cc27 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -133,7 +133,7 @@ test('exports utils', () => { "parseGwei": [Function], "parseTransaction": [Function], "parseUnits": [Function], - "prepareRequest": [Function], + "prepareTransactionRequest": [Function], "publicKeyToAddress": [Function], "recoverAddress": [Function], "recoverMessageAddress": [Function], diff --git a/src/utils/index.ts b/src/utils/index.ts index 168624473f..a6c6d407d5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -230,7 +230,10 @@ export { assertTransactionLegacy, } from './transaction/assertTransaction.js' export { parseTransaction } from './transaction/parseTransaction.js' -export { prepareRequest } from './transaction/prepareRequest.js' +export { + /** @deprecated import `prepareTransactionRequest` from `viem/actions` instead. */ + prepareTransactionRequest, +} from '../actions/wallet/prepareTransactionRequest.js' export { serializeTransaction, type SerializeTransactionFn,