-
Notifications
You must be signed in to change notification settings - Fork 200
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(common): add createContract, createNonceManager utils #1261
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
"@latticexyz/common": major | ||
--- | ||
|
||
Add utils for using viem with MUD | ||
|
||
- `createContract` is a wrapper around [viem's `getContract`](https://viem.sh/docs/contract/getContract.html) but with better nonce handling for faster executing of transactions. It has the same arguments and return type as `getContract`. | ||
- `createNonceManager` helps track local nonces, used by `createContract`. | ||
|
||
Also renames `mudTransportObserver` to `transportObserver`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { | ||
Abi, | ||
Account, | ||
Address, | ||
Chain, | ||
GetContractParameters, | ||
GetContractReturnType, | ||
Hex, | ||
PublicClient, | ||
SimulateContractParameters, | ||
Transport, | ||
WalletClient, | ||
WriteContractParameters, | ||
getContract, | ||
} from "viem"; | ||
import pQueue from "p-queue"; | ||
import pRetry from "p-retry"; | ||
import { createNonceManager } from "./createNonceManager"; | ||
import { debug as parentDebug } from "./debug"; | ||
|
||
const debug = parentDebug.extend("createContract"); | ||
|
||
// copied from viem because it isn't exported | ||
// TODO: import from viem? | ||
function getFunctionParameters(values: [args?: readonly unknown[], options?: object]): { | ||
args: readonly unknown[]; | ||
options: object; | ||
} { | ||
const hasArgs = values.length && Array.isArray(values[0]); | ||
const args = hasArgs ? values[0]! : []; | ||
const options = (hasArgs ? values[1] : values[0]) ?? {}; | ||
return { args, options }; | ||
} | ||
|
||
export function createContract< | ||
TTransport extends Transport, | ||
TAddress extends Address, | ||
TAbi extends Abi, | ||
TChain extends Chain, | ||
TAccount extends Account, | ||
TPublicClient extends PublicClient<TTransport, TChain>, | ||
TWalletClient extends WalletClient<TTransport, TChain, TAccount> | ||
>({ | ||
abi, | ||
address, | ||
publicClient, | ||
walletClient, | ||
}: Required< | ||
GetContractParameters<TTransport, TChain, TAccount, TAbi, TPublicClient, TWalletClient, TAddress> | ||
>): GetContractReturnType<TAbi, TPublicClient, TWalletClient, TAddress> { | ||
const contract = getContract<TTransport, TAddress, TAbi, TChain, TAccount, TPublicClient, TWalletClient>({ | ||
abi, | ||
address, | ||
publicClient, | ||
walletClient, | ||
}) as unknown as GetContractReturnType<Abi, PublicClient, WalletClient>; | ||
|
||
if (contract.write) { | ||
const nonceManager = createNonceManager({ | ||
publicClient: publicClient as PublicClient, | ||
address: walletClient.account.address, | ||
}); | ||
|
||
// Concurrency of one means transactions will be queued and inserted into the mem pool synchronously and in order. | ||
// Although increasing this will allow for more parallel requests/transactions and nonce errors will get automatically retried, | ||
// we can't guarantee local nonce accurancy due to needing async operations (simulate) before incrementing the nonce. | ||
const queue = new pQueue({ concurrency: 1 }); | ||
|
||
// Replace write calls with our own proxy. Implemented ~the same as viem, but adds better handling of nonces (via queue + retries). | ||
contract.write = new Proxy( | ||
{}, | ||
{ | ||
get(_, functionName: string): GetContractReturnType<Abi, PublicClient, WalletClient>["write"][string] { | ||
return async (...parameters) => { | ||
const { args, options } = getFunctionParameters(parameters as any); | ||
|
||
async function write(): Promise<Hex> { | ||
if (!nonceManager.hasNonce()) { | ||
await nonceManager.resetNonce(); | ||
} | ||
|
||
debug("simulating write", functionName, args, options); | ||
const { request } = await publicClient.simulateContract({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep! |
||
account: walletClient.account, | ||
address, | ||
abi, | ||
functionName, | ||
args, | ||
...options, | ||
} as unknown as SimulateContractParameters<TAbi, typeof functionName, TChain>); | ||
|
||
const nonce = nonceManager.nextNonce(); | ||
debug("calling write function with nonce", nonce, request); | ||
const result = await walletClient.writeContract({ | ||
nonce, | ||
...request, | ||
} as unknown as WriteContractParameters<TAbi, typeof functionName, TChain, TAccount>); | ||
|
||
return result; | ||
} | ||
|
||
return await queue.add( | ||
() => | ||
pRetry(write, { | ||
retries: 3, | ||
onFailedAttempt: async (error) => { | ||
// On nonce errors, reset the nonce and retry | ||
if (nonceManager.shouldResetNonce(error)) { | ||
debug("got nonce error, retrying", error); | ||
await nonceManager.resetNonce(); | ||
return; | ||
} | ||
throw error; | ||
}, | ||
}), | ||
{ throwOnTimeout: true } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't have a timeout configured but |
||
); | ||
}; | ||
}, | ||
} | ||
); | ||
} | ||
|
||
return contract as unknown as GetContractReturnType<TAbi, TPublicClient, TWalletClient, TAddress>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { BlockTag, Hex, PublicClient } from "viem"; | ||
import { debug as parentDebug } from "./debug"; | ||
|
||
const debug = parentDebug.extend("createNonceManager"); | ||
|
||
type CreateNonceManagerOptions = { | ||
publicClient: PublicClient; | ||
address: Hex; | ||
blockTag?: BlockTag; | ||
}; | ||
|
||
type CreateNonceManagerResult = { | ||
hasNonce: () => boolean; | ||
nextNonce: () => number; | ||
resetNonce: () => Promise<void>; | ||
shouldResetNonce: (error: unknown) => boolean; | ||
}; | ||
|
||
export function createNonceManager({ | ||
publicClient, | ||
address, | ||
blockTag, | ||
}: CreateNonceManagerOptions): CreateNonceManagerResult { | ||
const nonceRef = { current: -1 }; | ||
const channel = | ||
typeof BroadcastChannel !== "undefined" | ||
? // TODO: fetch chain ID or require it via types? | ||
new BroadcastChannel(`mud:createNonceManager:${publicClient.chain?.id}:${address}`) | ||
: null; | ||
|
||
if (channel) { | ||
channel.addEventListener("message", (event) => { | ||
const nonce = JSON.parse(event.data); | ||
debug("got nonce from broadcast channel", nonce); | ||
nonceRef.current = nonce; | ||
}); | ||
} | ||
|
||
function hasNonce(): boolean { | ||
return nonceRef.current >= 0; | ||
} | ||
|
||
function nextNonce(): number { | ||
if (!hasNonce()) throw new Error("call resetNonce before using nextNonce"); | ||
const nonce = nonceRef.current++; | ||
channel?.postMessage(JSON.stringify(nonceRef.current)); | ||
return nonce; | ||
} | ||
|
||
async function resetNonce(): Promise<void> { | ||
const nonce = await publicClient.getTransactionCount({ address, blockTag }); | ||
nonceRef.current = nonce; | ||
channel?.postMessage(JSON.stringify(nonceRef.current)); | ||
debug("reset nonce to", nonceRef.current); | ||
} | ||
|
||
function shouldResetNonce(error: unknown): boolean { | ||
return /already known|nonce too low/.test(String(error)); | ||
} | ||
|
||
return { | ||
hasNonce, | ||
nextNonce, | ||
resetNonce, | ||
shouldResetNonce, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
export * from "./createBurnerAccount"; | ||
export * from "./createContract"; | ||
export * from "./createNonceManager"; | ||
export * from "./hexToTableId"; | ||
export * from "./mudTransportObserver"; | ||
export * from "./tableIdToHex"; | ||
export * from "./transportObserver"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we open a viem issue for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤷♂️
hopefully they'll have some better hooks for us for nonce handling