Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: nonce manager + dedupe in-flight requests #2418

Merged
merged 21 commits into from
Jun 17, 2024
5 changes: 5 additions & 0 deletions .changeset/early-emus-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": minor
---

Added support for a Nonce Manager on Local Accounts via `nonceManager`.
5 changes: 5 additions & 0 deletions .changeset/orange-lies-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Implemented in-flight request deduplication for Transport JSON-RPC requests.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
{
"name": "viem (esm)",
"path": "./src/_esm/index.js",
"limit": "59.8 kB",
"limit": "60.2 kB",
"import": "*"
},
{
Expand All @@ -119,7 +119,7 @@
{
"name": "viem (minimal surface - tree-shaking)",
"path": "./src/_esm/index.js",
"limit": "4.1 kB",
"limit": "6.3 kB",
"import": "{ createClient, http }"
},
{
Expand Down Expand Up @@ -173,7 +173,7 @@
{
"name": "viem/ens (tree-shaking)",
"path": "./src/_esm/ens/index.js",
"limit": "22.5 kB",
"limit": "22.6 kB",
"import": "{ getEnsAvatar }"
},
{
Expand Down
103 changes: 103 additions & 0 deletions site/pages/docs/accounts/createNonceManager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# createNonceManager [Creates a Nonce Manager for automatic nonce generation]

Creates a new Nonce Manager instance to be used with a [Local Account](/docs/accounts/local). The Nonce Manager is used to automatically manage & generate nonces for transactions.

:::warning
A Nonce Manager can only be used with [Local Accounts](/docs/accounts/local) (ie. Private Key, Mnemonic, etc).

For [JSON-RPC Accounts](/docs/accounts/jsonRpc) (ie. Browser Extension, WalletConnect, Backend, etc), the Wallet or Backend will manage the nonces.
:::

## Import

```ts twoslash
import { createNonceManager } from 'viem/nonce'
```

## Usage

A Nonce Manager can be instantiated with the `createNonceManager` function with a provided `source`.

The example below demonstrates how to create a Nonce Manager with a JSON-RPC source (ie. uses `eth_getTransactionCount` as the source of truth).

```ts twoslash
import { createNonceManager, jsonRpc } from 'viem/nonce'

const nonceManager = createNonceManager({
source: jsonRpc()
})
```

:::tip
Viem also exports a default `nonceManager` instance that you can use directly.

```ts twoslash
import { nonceManager } from 'viem'
```
:::

### Integration with Local Accounts

A `nonceManager` can be passed as an option to [Local Accounts](/docs/accounts/local) to automatically manage nonces for transactions.

:::code-group

```ts twoslash [example.ts]
import { privateKeyToAccount, nonceManager } from 'viem/accounts' // [!code focus]
import { client } from './config'

const account = privateKeyToAccount('0x...', { nonceManager }) // [!code focus]

const hashes = await Promise.all([ // [!code focus]
// @log: ↓ nonce = 0
client.sendTransaction({ // [!code focus]
account, // [!code focus]
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus]
value: parseEther('0.1'), // [!code focus]
}), // [!code focus]
// @log: ↓ nonce = 1
client.sendTransaction({ // [!code focus]
account, // [!code focus]
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus]
value: parseEther('0.2'), // [!code focus]
}), // [!code focus]
]) // [!code focus]
```

```ts twoslash [config.ts] filename="config.ts"
import { createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'

export const client = createWalletClient({
chain: mainnet,
transport: http(),
})
```

:::

## Return Type

`NonceManager`

The Nonce Manager.

## Parameters

### source

- **Type:** `NonceManagerSource`

The source of truth for the Nonce Manager.

Available sources:

- `jsonRpc`

```ts twoslash
import { createNonceManager, jsonRpc } from 'viem/nonce'

const nonceManager = createNonceManager({
source: jsonRpc() // [!code focus]
})
```
13 changes: 11 additions & 2 deletions site/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,9 @@ export const sidebar = {
text: 'Accounts',
collapsed: true,
items: [
{ text: 'JSON-RPC', link: '/docs/accounts/jsonRpc' },
{ text: 'JSON-RPC Account', link: '/docs/accounts/jsonRpc' },
{
text: 'Local',
text: 'Local Accounts',
link: '/docs/accounts/local',
items: [
{ text: 'Private Key', link: '/docs/accounts/privateKey' },
Expand All @@ -462,6 +462,15 @@ export const sidebar = {
link: '/docs/accounts/hd',
},
{ text: 'Custom', link: '/docs/accounts/custom' },
],
},
{
text: 'Utilities',
items: [
{
text: 'createNonceManager',
link: '/docs/accounts/createNonceManager',
},
{ text: 'signMessage', link: '/docs/accounts/signMessage' },
{ text: 'signTransaction', link: '/docs/accounts/signTransaction' },
{ text: 'signTypedData', link: '/docs/accounts/signTypedData' },
Expand Down
1 change: 1 addition & 0 deletions src/accounts/hdKeyToAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test('default', () => {
{
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"getHdKey": [Function],
"nonceManager": undefined,
"publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5",
"signMessage": [Function],
"signTransaction": [Function],
Expand Down
13 changes: 11 additions & 2 deletions src/accounts/hdKeyToAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import type { ErrorType } from '../errors/utils.js'
import type { HDKey } from '../types/account.js'
import {
type PrivateKeyToAccountErrorType,
type PrivateKeyToAccountOptions,
privateKeyToAccount,
} from './privateKeyToAccount.js'
import type { HDAccount, HDOptions } from './types.js'

export type HDKeyToAccountOptions = HDOptions & PrivateKeyToAccountOptions

export type HDKeyToAccountErrorType =
| PrivateKeyToAccountErrorType
| ToHexErrorType
Expand All @@ -20,12 +23,18 @@ export type HDKeyToAccountErrorType =
*/
export function hdKeyToAccount(
hdKey_: HDKey,
{ accountIndex = 0, addressIndex = 0, changeIndex = 0, path }: HDOptions = {},
{
accountIndex = 0,
addressIndex = 0,
changeIndex = 0,
path,
...options
}: HDKeyToAccountOptions = {},
): HDAccount {
const hdKey = hdKey_.derive(
path || `m/44'/60'/${accountIndex}'/${changeIndex}/${addressIndex}`,
)
const account = privateKeyToAccount(toHex(hdKey.privateKey!))
const account = privateKeyToAccount(toHex(hdKey.privateKey!), options)
return {
...account,
getHdKey: () => hdKey,
Expand Down
2 changes: 2 additions & 0 deletions src/accounts/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ test('exports utils', () => {
"parseAccount",
"publicKeyToAddress",
"privateKeyToAddress",
"createNonceManager",
"nonceManager",
]
`)
})
10 changes: 10 additions & 0 deletions src/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ export {
generatePrivateKey,
} from './generatePrivateKey.js'
export {
type HDKeyToAccountOptions,
type HDKeyToAccountErrorType,
hdKeyToAccount,
} from './hdKeyToAccount.js'
export {
type MnemonicToAccountOptions,
type MnemonicToAccountErrorType,
mnemonicToAccount,
} from './mnemonicToAccount.js'
export {
type PrivateKeyToAccountOptions,
type PrivateKeyToAccountErrorType,
privateKeyToAccount,
} from './privateKeyToAccount.js'
Expand Down Expand Up @@ -87,3 +90,10 @@ export {
type PrivateKeyToAddressErrorType,
privateKeyToAddress,
} from './utils/privateKeyToAddress.js'
export {
type CreateNonceManagerParameters,
type NonceManager,
type NonceManagerSource,
createNonceManager,
nonceManager,
} from '../utils/nonceManager.js'
1 change: 1 addition & 0 deletions src/accounts/mnemonicToAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('default', () => {
{
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"getHdKey": [Function],
"nonceManager": undefined,
"publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5",
"signMessage": [Function],
"signTransaction": [Function],
Expand Down
3 changes: 3 additions & 0 deletions src/accounts/mnemonicToAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { mnemonicToSeedSync } from '@scure/bip39'
import type { ErrorType } from '../errors/utils.js'
import {
type HDKeyToAccountErrorType,
type HDKeyToAccountOptions,
hdKeyToAccount,
} from './hdKeyToAccount.js'
import type { HDAccount, HDOptions } from './types.js'

export type MnemonicToAccountOptions = HDKeyToAccountOptions

export type MnemonicToAccountErrorType = HDKeyToAccountErrorType | ErrorType

/**
Expand Down
1 change: 1 addition & 0 deletions src/accounts/privateKeyToAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ test('default', () => {
expect(privateKeyToAccount(accounts[0].privateKey)).toMatchInlineSnapshot(`
{
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"nonceManager": undefined,
"publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5",
"signMessage": [Function],
"signTransaction": [Function],
Expand Down
12 changes: 11 additions & 1 deletion src/accounts/privateKeyToAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Hex } from '../types/misc.js'
import { type ToHexErrorType, toHex } from '../utils/encoding/toHex.js'

import type { ErrorType } from '../errors/utils.js'
import type { NonceManager } from '../utils/nonceManager.js'
import { type ToAccountErrorType, toAccount } from './toAccount.js'
import type { PrivateKeyAccount } from './types.js'
import {
Expand All @@ -20,6 +21,10 @@ import {
signTypedData,
} from './utils/signTypedData.js'

export type PrivateKeyToAccountOptions = {
nonceManager?: NonceManager | undefined
}

export type PrivateKeyToAccountErrorType =
| ToAccountErrorType
| ToHexErrorType
Expand All @@ -34,12 +39,17 @@ export type PrivateKeyToAccountErrorType =
*
* @returns A Private Key Account.
*/
export function privateKeyToAccount(privateKey: Hex): PrivateKeyAccount {
export function privateKeyToAccount(
privateKey: Hex,
options: PrivateKeyToAccountOptions = {},
): PrivateKeyAccount {
const { nonceManager } = options
const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false))
const address = publicKeyToAddress(publicKey)

const account = toAccount({
address,
nonceManager,
async signMessage({ message }) {
return signMessage({ message, privateKey })
},
Expand Down
1 change: 1 addition & 0 deletions src/accounts/toAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('toAccount', () => {
).toMatchInlineSnapshot(`
{
"address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"nonceManager": undefined,
"signMessage": [Function],
"signTransaction": [Function],
"signTypedData": [Function],
Expand Down
1 change: 1 addition & 0 deletions src/accounts/toAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function toAccount<TAccountSource extends AccountSource>(
throw new InvalidAddressError({ address: source.address })
return {
address: source.address,
nonceManager: source.nonceManager,
signMessage: source.signMessage,
signTransaction: source.signTransaction,
signTypedData: source.signTypedData,
Expand Down
2 changes: 2 additions & 0 deletions src/accounts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from '../types/transaction.js'
import type { TypedDataDefinition } from '../types/typedData.js'
import type { IsNarrowable, OneOf } from '../types/utils.js'
import type { NonceManager } from '../utils/nonceManager.js'
import type { GetTransactionType } from '../utils/transaction/getTransactionType.js'
import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js'

Expand All @@ -18,6 +19,7 @@ export type Account<TAddress extends Address = Address> = OneOf<
export type AccountSource = Address | CustomSource
export type CustomSource = {
address: Address
nonceManager?: NonceManager | undefined
signMessage: ({ message }: { message: SignableMessage }) => Promise<Hash>
signTransaction: <
serializer extends
Expand Down
1 change: 1 addition & 0 deletions src/accounts/utils/parseAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test('account', () => {
).toMatchInlineSnapshot(`
{
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"nonceManager": undefined,
"publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5",
"signMessage": [Function],
"signTransaction": [Function],
Expand Down
22 changes: 14 additions & 8 deletions src/actions/public/getBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,21 @@

let block: RpcBlock | null = null
if (blockHash) {
block = await client.request({
method: 'eth_getBlockByHash',
params: [blockHash, includeTransactions],
})
block = await client.request(
{
method: 'eth_getBlockByHash',
params: [blockHash, includeTransactions],
},
{ dedupe: true },
)

Check warning on line 118 in src/actions/public/getBlock.ts

View check run for this annotation

Codecov / codecov/patch

src/actions/public/getBlock.ts#L112-L118

Added lines #L112 - L118 were not covered by tests
} else {
block = await client.request({
method: 'eth_getBlockByNumber',
params: [blockNumberHex || blockTag, includeTransactions],
})
block = await client.request(
{
method: 'eth_getBlockByNumber',
params: [blockNumberHex || blockTag, includeTransactions],
},
{ dedupe: Boolean(blockNumberHex) },
)
}

if (!block) throw new BlockNotFoundError({ blockHash, blockNumber })
Expand Down
Loading
Loading