viem-tx-sim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/contracts/TxSimulator.sol +305 -0
  4. package/dist/constants.d.ts +15 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +15 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/errors.d.ts +35 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +39 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/generated/txSimulatorBytecode.d.ts +2 -0
  13. package/dist/generated/txSimulatorBytecode.d.ts.map +1 -0
  14. package/dist/generated/txSimulatorBytecode.js +3 -0
  15. package/dist/generated/txSimulatorBytecode.js.map +1 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +4 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/internal/data.d.ts +8 -0
  21. package/dist/internal/data.d.ts.map +1 -0
  22. package/dist/internal/data.js +31 -0
  23. package/dist/internal/data.js.map +1 -0
  24. package/dist/internal/probes.d.ts +29 -0
  25. package/dist/internal/probes.d.ts.map +1 -0
  26. package/dist/internal/probes.js +159 -0
  27. package/dist/internal/probes.js.map +1 -0
  28. package/dist/internal/requirements.d.ts +5 -0
  29. package/dist/internal/requirements.d.ts.map +1 -0
  30. package/dist/internal/requirements.js +173 -0
  31. package/dist/internal/requirements.js.map +1 -0
  32. package/dist/internal/rpc.d.ts +36 -0
  33. package/dist/internal/rpc.d.ts.map +1 -0
  34. package/dist/internal/rpc.js +124 -0
  35. package/dist/internal/rpc.js.map +1 -0
  36. package/dist/internal/simulator.d.ts +33 -0
  37. package/dist/internal/simulator.d.ts.map +1 -0
  38. package/dist/internal/simulator.js +199 -0
  39. package/dist/internal/simulator.js.map +1 -0
  40. package/dist/internal/slots.d.ts +7 -0
  41. package/dist/internal/slots.d.ts.map +1 -0
  42. package/dist/internal/slots.js +129 -0
  43. package/dist/internal/slots.js.map +1 -0
  44. package/dist/txSimulator.d.ts +83 -0
  45. package/dist/txSimulator.d.ts.map +1 -0
  46. package/dist/txSimulator.js +75 -0
  47. package/dist/txSimulator.js.map +1 -0
  48. package/dist/types.d.ts +214 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +82 -0
  53. package/src/constants.ts +15 -0
  54. package/src/errors.ts +45 -0
  55. package/src/generated/txSimulatorBytecode.ts +5 -0
  56. package/src/index.ts +34 -0
  57. package/src/internal/data.ts +36 -0
  58. package/src/internal/probes.ts +221 -0
  59. package/src/internal/requirements.ts +240 -0
  60. package/src/internal/rpc.ts +207 -0
  61. package/src/internal/simulator.ts +306 -0
  62. package/src/internal/slots.ts +211 -0
  63. package/src/txSimulator.ts +176 -0
  64. package/src/types.ts +240 -0
@@ -0,0 +1,221 @@
1
+ import type { Address, Hex, StateOverride } from "viem";
2
+ import { encodeFunctionData, erc20Abi } from "viem";
3
+
4
+ import { addressKey } from "./data.js";
5
+ import { withRpcDebug } from "./rpc.js";
6
+ import { getCallData, uint256Hex } from "./data.js";
7
+ import type { RpcCallArgs } from "./rpc.js";
8
+ import { blockOptionsSpread, buildCallParameters, createAccessList } from "./rpc.js";
9
+
10
+ type ProbedSlot = {
11
+ token: Address;
12
+ slot: Hex;
13
+ };
14
+
15
+ type ProbedAllowanceSlot = ProbedSlot & {
16
+ spender: Address;
17
+ };
18
+
19
+ async function readBalanceOf(
20
+ args: RpcCallArgs & {
21
+ token: Address;
22
+ owner: Address;
23
+ stateOverride?: StateOverride;
24
+ debugStep?: string;
25
+ },
26
+ ): Promise<bigint | undefined> {
27
+ const data = encodeFunctionData({
28
+ abi: erc20Abi,
29
+ functionName: "balanceOf",
30
+ args: [args.owner],
31
+ });
32
+
33
+ return readUint256Call({
34
+ client: args.client,
35
+ account: args.owner,
36
+ to: args.token,
37
+ data,
38
+ stateOverride: args.stateOverride,
39
+ gas: args.gas,
40
+ debug: args.debug,
41
+ debugStep: args.debugStep ?? "erc20.balanceOf",
42
+ ...blockOptionsSpread(args),
43
+ });
44
+ }
45
+
46
+ export async function readAllowance(
47
+ args: RpcCallArgs & {
48
+ token: Address;
49
+ owner: Address;
50
+ spender: Address;
51
+ stateOverride?: StateOverride;
52
+ debugStep?: string;
53
+ },
54
+ ): Promise<bigint | undefined> {
55
+ const data = encodeFunctionData({
56
+ abi: erc20Abi,
57
+ functionName: "allowance",
58
+ args: [args.owner, args.spender],
59
+ });
60
+
61
+ return readUint256Call({
62
+ client: args.client,
63
+ account: args.owner,
64
+ to: args.token,
65
+ data,
66
+ stateOverride: args.stateOverride,
67
+ gas: args.gas,
68
+ debug: args.debug,
69
+ debugStep: args.debugStep ?? "erc20.allowance",
70
+ ...blockOptionsSpread(args),
71
+ });
72
+ }
73
+
74
+ export async function discoverBalanceSlot(
75
+ args: RpcCallArgs & {
76
+ token: Address;
77
+ owner: Address;
78
+ sentinel: bigint;
79
+ },
80
+ ): Promise<ProbedSlot | undefined> {
81
+ const data = encodeFunctionData({
82
+ abi: erc20Abi,
83
+ functionName: "balanceOf",
84
+ args: [args.owner],
85
+ });
86
+
87
+ let storageKeys: Hex[];
88
+ try {
89
+ const accessList = await createAccessList({
90
+ client: args.client,
91
+ from: args.owner,
92
+ to: args.token,
93
+ data,
94
+ gas: args.gas,
95
+ debug: args.debug,
96
+ debugStep: "balanceSlot.accessList",
97
+ ...blockOptionsSpread(args),
98
+ });
99
+ storageKeys = accessList
100
+ .filter((entry) => addressKey(entry.address) === addressKey(args.token))
101
+ .flatMap((entry) => entry.storageKeys);
102
+ } catch {
103
+ return undefined;
104
+ }
105
+
106
+ const sentinelHex = uint256Hex(args.sentinel);
107
+ for (const slot of storageKeys) {
108
+ const balance = await readBalanceOf({
109
+ client: args.client,
110
+ token: args.token,
111
+ owner: args.owner,
112
+ stateOverride: [{ address: args.token, stateDiff: [{ slot, value: sentinelHex }] }],
113
+ gas: args.gas,
114
+ debug: args.debug,
115
+ debugStep: "balanceSlot.verify",
116
+ ...blockOptionsSpread(args),
117
+ });
118
+ if (balance === args.sentinel) return { token: args.token, slot };
119
+ }
120
+
121
+ return undefined;
122
+ }
123
+
124
+ export async function discoverAllowanceSlot(
125
+ args: RpcCallArgs & {
126
+ token: Address;
127
+ owner: Address;
128
+ spender: Address;
129
+ sentinel: bigint;
130
+ },
131
+ ): Promise<ProbedAllowanceSlot | undefined> {
132
+ const data = encodeFunctionData({
133
+ abi: erc20Abi,
134
+ functionName: "allowance",
135
+ args: [args.owner, args.spender],
136
+ });
137
+
138
+ let storageKeys: Hex[];
139
+ try {
140
+ const accessList = await createAccessList({
141
+ client: args.client,
142
+ from: args.owner,
143
+ to: args.token,
144
+ data,
145
+ gas: args.gas,
146
+ debug: args.debug,
147
+ debugStep: "allowanceSlot.accessList",
148
+ ...blockOptionsSpread(args),
149
+ });
150
+ storageKeys = accessList
151
+ .filter((entry) => addressKey(entry.address) === addressKey(args.token))
152
+ .flatMap((entry) => entry.storageKeys);
153
+ } catch {
154
+ return undefined;
155
+ }
156
+
157
+ const sentinelHex = uint256Hex(args.sentinel);
158
+ for (const slot of storageKeys) {
159
+ const allowance = await readAllowance({
160
+ client: args.client,
161
+ token: args.token,
162
+ owner: args.owner,
163
+ spender: args.spender,
164
+ stateOverride: [{ address: args.token, stateDiff: [{ slot, value: sentinelHex }] }],
165
+ gas: args.gas,
166
+ debug: args.debug,
167
+ debugStep: "allowanceSlot.verify",
168
+ ...blockOptionsSpread(args),
169
+ });
170
+ if (allowance === args.sentinel) {
171
+ return {
172
+ token: args.token,
173
+ spender: args.spender,
174
+ slot,
175
+ };
176
+ }
177
+ }
178
+
179
+ return undefined;
180
+ }
181
+
182
+ async function readUint256Call(
183
+ args: RpcCallArgs & {
184
+ account: Address;
185
+ to: Address;
186
+ data: Hex;
187
+ stateOverride?: StateOverride;
188
+ debugStep: string;
189
+ },
190
+ ): Promise<bigint | undefined> {
191
+ try {
192
+ const result = await withRpcDebug(
193
+ args.debug,
194
+ {
195
+ method: "eth_call",
196
+ step: args.debugStep,
197
+ details: {
198
+ account: args.account,
199
+ to: args.to,
200
+ stateOverrides: args.stateOverride?.length ?? 0,
201
+ },
202
+ },
203
+ () =>
204
+ args.client.call(
205
+ buildCallParameters({
206
+ account: args.account,
207
+ to: args.to,
208
+ data: args.data,
209
+ stateOverride: args.stateOverride,
210
+ gas: args.gas,
211
+ ...blockOptionsSpread(args),
212
+ }),
213
+ ),
214
+ );
215
+ const data = getCallData(result);
216
+ if (data.length < 66) return undefined;
217
+ return BigInt(data);
218
+ } catch {
219
+ return undefined;
220
+ }
221
+ }
@@ -0,0 +1,240 @@
1
+ import type { Address, Hex } from "viem";
2
+ import { decodeFunctionData, parseAbi } from "viem";
3
+
4
+ import { InvalidSimulationInputError } from "../errors.js";
5
+ import type {
6
+ AllowanceSlotPair,
7
+ EstimatedAssetRequirements,
8
+ EstimateAssetRequirementsArgs,
9
+ SimulatedCall,
10
+ } from "../types.js";
11
+ import { prepareAllowanceOverrides, prepareBalanceOverrides } from "./slots.js";
12
+ import { addressKey, uniqueAddresses } from "./data.js";
13
+ import type { ClientArgs } from "./rpc.js";
14
+ import { blockOptionsSpread } from "./rpc.js";
15
+ import { discoverCandidateAddresses, runSimulator } from "./simulator.js";
16
+
17
+ const allowanceSettingAbi = parseAbi([
18
+ "function approve(address spender, uint256 amount) returns (bool)",
19
+ "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
20
+ ]);
21
+
22
+ type AllowanceProbe = {
23
+ token: Address;
24
+ spender: Address;
25
+ };
26
+
27
+ /** @internal Implements {@link TxSimulator.estimateAssetRequirements}. Prefer the instance API from the package root. */
28
+ export async function estimateAssetRequirements(
29
+ args: EstimateAssetRequirementsArgs & ClientArgs,
30
+ ): Promise<EstimatedAssetRequirements> {
31
+ if (args.calls.length === 0) {
32
+ throw new InvalidSimulationInputError("estimateAssetRequirements requires at least one call.");
33
+ }
34
+
35
+ const calls = args.calls.map((call) => ({
36
+ to: call.to,
37
+ data: call.data,
38
+ value: call.value ?? 0n,
39
+ })) satisfies SimulatedCall[];
40
+ const candidateAddresses = await discoverCandidateAddresses({
41
+ client: args.client,
42
+ from: args.from,
43
+ calls,
44
+ gas: args.gas,
45
+ debug: args.debug,
46
+ ...blockOptionsSpread(args),
47
+ });
48
+ const recon = await runSimulator({
49
+ client: args.client,
50
+ from: args.from,
51
+ calls,
52
+ candidates: candidateAddresses,
53
+ allowanceProbes: [],
54
+ gas: args.gas,
55
+ debug: args.debug,
56
+ ...blockOptionsSpread(args),
57
+ ...(args.errorAbi !== undefined ? { errorAbi: args.errorAbi } : {}),
58
+ });
59
+ const tokens = recon.probeData.observedTokens;
60
+ const spenders = uniqueAddresses([...calls.map((call) => call.to), ...candidateAddresses]).filter(
61
+ (address) => addressKey(address) !== addressKey(args.from),
62
+ );
63
+
64
+ const balanceOverrides = await prepareBalanceOverrides({
65
+ client: args.client,
66
+ from: args.from,
67
+ tokens,
68
+ gas: args.gas,
69
+ debug: args.debug,
70
+ ...blockOptionsSpread(args),
71
+ });
72
+ const allowanceOverrides = await prepareAllowanceOverrides({
73
+ client: args.client,
74
+ from: args.from,
75
+ pairs: allowancePairs(tokens, spenders),
76
+ gas: args.gas,
77
+ debug: args.debug,
78
+ ...blockOptionsSpread(args),
79
+ });
80
+ const balanceSlots = balanceOverrides.slots;
81
+ const allowanceSlots = allowanceOverrides.slots;
82
+ const allowanceProbes = allowanceSlots.map((slot) => ({
83
+ token: slot.token,
84
+ spender: slot.spender,
85
+ }));
86
+ const tokenSlotOverrides = [...balanceSlots, ...allowanceSlots];
87
+ const measurement = await runSimulator({
88
+ client: args.client,
89
+ from: args.from,
90
+ calls,
91
+ candidates: candidateAddresses,
92
+ tokenSlotOverrides,
93
+ allowanceProbes,
94
+ gas: args.gas,
95
+ debug: args.debug,
96
+ ...blockOptionsSpread(args),
97
+ ...(args.errorAbi !== undefined ? { errorAbi: args.errorAbi } : {}),
98
+ });
99
+ const measuredAllowances = requiredAllowances(
100
+ args.from,
101
+ calls,
102
+ allowanceProbes,
103
+ measurement.probeData.allowanceCheckpoints,
104
+ measurement.probeData.candidates,
105
+ measurement.probeData.maxTokenOutflows,
106
+ );
107
+
108
+ const shared = {
109
+ native: measurement.probeData.maxNativeOutflow,
110
+ balances: requiredBalances(
111
+ measurement.probeData.candidates,
112
+ tokens,
113
+ measurement.probeData.maxTokenOutflows,
114
+ ),
115
+ allowances: measuredAllowances.allowances,
116
+ slots: tokenSlotOverrides,
117
+ unresolved: {
118
+ balanceSlots: balanceOverrides.unresolved,
119
+ allowanceSlots: allowanceOverrides.unresolved,
120
+ allowances: measuredAllowances.discarded,
121
+ },
122
+ };
123
+
124
+ if (measurement.status === "reverted") {
125
+ return {
126
+ status: "reverted",
127
+ ...shared,
128
+ revertData: measurement.revertData,
129
+ ...(measurement.revertReason !== undefined ? { revertReason: measurement.revertReason } : {}),
130
+ ...(measurement.revertError !== undefined ? { revertError: measurement.revertError } : {}),
131
+ ...(measurement.revertSelector !== undefined
132
+ ? { revertSelector: measurement.revertSelector }
133
+ : {}),
134
+ failingCallIndex: measurement.failingCallIndex,
135
+ };
136
+ }
137
+
138
+ return { status: "success", ...shared };
139
+ }
140
+
141
+ function allowancePairs(
142
+ tokens: readonly Address[],
143
+ spenders: readonly Address[],
144
+ ): AllowanceSlotPair[] {
145
+ return tokens.flatMap((token) =>
146
+ spenders
147
+ .filter((spender) => addressKey(token) !== addressKey(spender))
148
+ .map((spender) => ({ token, spender })),
149
+ );
150
+ }
151
+
152
+ function requiredBalances(
153
+ candidates: readonly Address[],
154
+ tokens: readonly Address[],
155
+ maxTokenOutflows: readonly bigint[],
156
+ ): EstimatedAssetRequirements["balances"] {
157
+ const tokenKeys = new Set(tokens.map(addressKey));
158
+ const balances: EstimatedAssetRequirements["balances"] = [];
159
+ for (let i = 0; i < candidates.length; ++i) {
160
+ const amount = maxTokenOutflows[i] ?? 0n;
161
+ const token = candidates[i];
162
+ if (token !== undefined && amount > 0n && tokenKeys.has(addressKey(token))) {
163
+ balances.push({ token, amount });
164
+ }
165
+ }
166
+ return balances;
167
+ }
168
+
169
+ function requiredAllowances(
170
+ owner: Address,
171
+ calls: readonly SimulatedCall[],
172
+ probes: readonly AllowanceProbe[],
173
+ checkpoints: readonly bigint[],
174
+ candidates: readonly Address[],
175
+ maxTokenOutflows: readonly bigint[],
176
+ ): {
177
+ allowances: EstimatedAssetRequirements["allowances"];
178
+ discarded: AllowanceSlotPair[];
179
+ } {
180
+ const allowances: EstimatedAssetRequirements["allowances"] = [];
181
+ const discarded: AllowanceSlotPair[] = [];
182
+ const stride = calls.length + 1;
183
+
184
+ for (let probeIndex = 0; probeIndex < probes.length; ++probeIndex) {
185
+ const probe = probes[probeIndex];
186
+ if (probe === undefined) continue;
187
+
188
+ const firstAllowanceSetIndex = firstInBatchAllowanceSetIndex(calls, owner, probe);
189
+ const limit = firstAllowanceSetIndex ?? calls.length;
190
+ let amount = 0n;
191
+ for (let callIndex = 0; callIndex < limit; ++callIndex) {
192
+ const before = checkpoints[probeIndex * stride + callIndex] ?? 0n;
193
+ const after = checkpoints[probeIndex * stride + callIndex + 1] ?? 0n;
194
+ if (before > after) amount += before - after;
195
+ }
196
+ if (amount > tokenOutflow(probe.token, candidates, maxTokenOutflows)) {
197
+ discarded.push({ token: probe.token, spender: probe.spender });
198
+ continue;
199
+ }
200
+ if (amount > 0n) allowances.push({ token: probe.token, spender: probe.spender, amount });
201
+ }
202
+
203
+ return { allowances, discarded };
204
+ }
205
+
206
+ function firstInBatchAllowanceSetIndex(
207
+ calls: readonly SimulatedCall[],
208
+ owner: Address,
209
+ probe: AllowanceProbe,
210
+ ): number | undefined {
211
+ for (let i = 0; i < calls.length; ++i) {
212
+ const call = calls[i];
213
+ if (call === undefined || addressKey(call.to) !== addressKey(probe.token)) continue;
214
+ if (isAllowanceSetForSpender(call.data, owner, probe.spender)) return i;
215
+ }
216
+ return undefined;
217
+ }
218
+
219
+ function isAllowanceSetForSpender(data: Hex, owner: Address, spender: Address): boolean {
220
+ try {
221
+ const decoded = decodeFunctionData({ abi: allowanceSettingAbi, data });
222
+ if (decoded.functionName === "approve")
223
+ return addressKey(decoded.args[0]) === addressKey(spender);
224
+ return (
225
+ addressKey(decoded.args[0]) === addressKey(owner) &&
226
+ addressKey(decoded.args[1]) === addressKey(spender)
227
+ );
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ function tokenOutflow(
234
+ token: Address,
235
+ candidates: readonly Address[],
236
+ maxTokenOutflows: readonly bigint[],
237
+ ): bigint {
238
+ const index = candidates.findIndex((candidate) => addressKey(candidate) === addressKey(token));
239
+ return index === -1 ? 0n : (maxTokenOutflows[index] ?? 0n);
240
+ }
@@ -0,0 +1,207 @@
1
+ import type {
2
+ AccessList,
3
+ Address,
4
+ BlockTag,
5
+ CallParameters,
6
+ Hex,
7
+ PublicClient,
8
+ StateOverride,
9
+ } from "viem";
10
+ import { numberToHex } from "viem";
11
+
12
+ import { AccessListUnsupportedError } from "../errors.js";
13
+ import type { SimulationDebug, SimulationDebugEvent } from "../types.js";
14
+
15
+ // Internal RPC layer: shared argument types, call-shaping helpers, then RPC wrappers.
16
+ // Add new RPC methods here so debug and infrastructure-error behavior stays consistent.
17
+ export type BlockOptions = {
18
+ blockNumber?: bigint;
19
+ blockTag?: BlockTag;
20
+ };
21
+
22
+ /** Attaches the bound viem client to public per-call args for internal implementations. */
23
+ export type ClientArgs = { client: PublicClient };
24
+
25
+ export type RpcCallArgs = {
26
+ client: PublicClient;
27
+ gas?: bigint;
28
+ debug?: SimulationDebug;
29
+ } & BlockOptions;
30
+
31
+ /** Returns the block selector for RPC calls; `blockNumber` takes precedence over `blockTag`. */
32
+ export function blockOptionsSpread(args: BlockOptions): BlockOptions {
33
+ return args.blockNumber !== undefined
34
+ ? { blockNumber: args.blockNumber }
35
+ : args.blockTag !== undefined
36
+ ? { blockTag: args.blockTag }
37
+ : {};
38
+ }
39
+
40
+ export function buildCallParameters(
41
+ args: {
42
+ account: Address;
43
+ to: Address;
44
+ data: Hex;
45
+ gas?: bigint;
46
+ stateOverride?: StateOverride;
47
+ } & BlockOptions,
48
+ ): CallParameters {
49
+ const base = {
50
+ account: args.account,
51
+ to: args.to,
52
+ data: args.data,
53
+ ...(args.stateOverride !== undefined ? { stateOverride: args.stateOverride } : {}),
54
+ ...(args.gas !== undefined ? { gas: args.gas } : {}),
55
+ };
56
+ return (
57
+ args.blockNumber !== undefined
58
+ ? { ...base, blockNumber: args.blockNumber }
59
+ : { ...base, ...(args.blockTag !== undefined ? { blockTag: args.blockTag } : {}) }
60
+ ) satisfies CallParameters;
61
+ }
62
+
63
+ export type AccessListEntry = AccessList[number];
64
+
65
+ type AccessListRpcRequest = {
66
+ from: Address;
67
+ to: Address;
68
+ data: Hex;
69
+ value?: Hex;
70
+ gas?: Hex;
71
+ };
72
+
73
+ type AccessListRpcResult = {
74
+ accessList?: AccessList;
75
+ gasUsed?: Hex;
76
+ error?: string | { message?: string };
77
+ };
78
+
79
+ export async function createAccessList(
80
+ args: RpcCallArgs & {
81
+ from: Address;
82
+ to: Address;
83
+ data: Hex;
84
+ value?: bigint;
85
+ debugStep?: string;
86
+ },
87
+ ): Promise<AccessList> {
88
+ const request = {
89
+ from: args.from,
90
+ to: args.to,
91
+ data: args.data,
92
+ ...(args.value !== undefined ? { value: numberToHex(args.value) } : {}),
93
+ ...(args.gas !== undefined ? { gas: numberToHex(args.gas) } : {}),
94
+ } satisfies AccessListRpcRequest;
95
+ const block =
96
+ args.blockNumber !== undefined ? numberToHex(args.blockNumber) : (args.blockTag ?? "latest");
97
+
98
+ try {
99
+ const result = await withRpcDebug(
100
+ args.debug,
101
+ {
102
+ method: "eth_createAccessList",
103
+ step: args.debugStep ?? "createAccessList",
104
+ details: {
105
+ from: args.from,
106
+ to: args.to,
107
+ hasValue: (args.value ?? 0n) > 0n,
108
+ hasGas: args.gas !== undefined,
109
+ },
110
+ },
111
+ () => requestAccessList(args.client, request, block),
112
+ );
113
+ if (result.accessList !== undefined) return result.accessList;
114
+ if (isRpcExecutionRevert(result.error)) return [];
115
+ throw new Error(formatRpcError("eth_createAccessList returned no access list", result.error));
116
+ } catch (cause) {
117
+ if (isExecutionRevert(cause)) return [];
118
+ throw new AccessListUnsupportedError(formatRpcError("eth_createAccessList failed", cause));
119
+ }
120
+ }
121
+
122
+ export function formatRpcError(prefix: string, cause: unknown): string {
123
+ if (cause instanceof Error && cause.message) return `${prefix}: ${cause.message}`;
124
+ return prefix;
125
+ }
126
+
127
+ function isExecutionRevert(cause: unknown): boolean {
128
+ if (!(cause instanceof Error)) return false;
129
+ return /execution reverted|Execution reverted/i.test(cause.message);
130
+ }
131
+
132
+ function isRpcExecutionRevert(error: AccessListRpcResult["error"]): boolean {
133
+ const message = typeof error === "string" ? error : error?.message;
134
+ return message !== undefined && /execution reverted|Execution reverted/i.test(message);
135
+ }
136
+
137
+ async function requestAccessList(
138
+ client: PublicClient,
139
+ request: AccessListRpcRequest,
140
+ block: Hex | BlockTag,
141
+ ): Promise<AccessListRpcResult> {
142
+ return client.request<{
143
+ Method: "eth_createAccessList";
144
+ Parameters: [transaction: AccessListRpcRequest, block: Hex | BlockTag];
145
+ ReturnType: AccessListRpcResult;
146
+ }>({
147
+ method: "eth_createAccessList",
148
+ params: [request, block],
149
+ });
150
+ }
151
+
152
+ export function emitDebug(debug: SimulationDebug | undefined, event: SimulationDebugEvent): void {
153
+ if (typeof debug === "function") {
154
+ debug(event);
155
+ return;
156
+ }
157
+
158
+ if (debug === true || envDebugEnabled()) {
159
+ console.debug(formatDebugEvent(event));
160
+ }
161
+ }
162
+
163
+ export async function withRpcDebug<T>(
164
+ debug: SimulationDebug | undefined,
165
+ event: Omit<SimulationDebugEvent, "phase">,
166
+ run: () => Promise<T>,
167
+ ): Promise<T> {
168
+ const startedAt = Date.now();
169
+ emitDebug(debug, { ...event, phase: "start" });
170
+
171
+ try {
172
+ const result = await run();
173
+ emitDebug(debug, { ...event, phase: "success", durationMs: Date.now() - startedAt });
174
+ return result;
175
+ } catch (error) {
176
+ emitDebug(debug, {
177
+ ...event,
178
+ phase: "error",
179
+ durationMs: Date.now() - startedAt,
180
+ error: error instanceof Error ? error.message : String(error),
181
+ });
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ function envDebugEnabled(): boolean {
187
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
188
+ ?.env;
189
+ return env?.VIEM_TX_SIM_DEBUG_RPC === "1" || env?.DEBUG_RPC === "1";
190
+ }
191
+
192
+ function formatDebugEvent(event: SimulationDebugEvent): string {
193
+ const parts = [
194
+ `[viem-tx-sim] ${event.phase} ${event.method}`,
195
+ `step=${event.step}`,
196
+ ...(event.durationMs === undefined ? [] : [`durationMs=${event.durationMs}`]),
197
+ ...Object.entries(event.details ?? {}).map(([key, value]) => `${key}=${formatValue(value)}`),
198
+ ...(event.error ? [`error=${event.error}`] : []),
199
+ ];
200
+ return parts.join(" ");
201
+ }
202
+
203
+ function formatValue(value: unknown): string {
204
+ if (typeof value === "bigint") return value.toString();
205
+ if (Array.isArray(value)) return `[${value.map(formatValue).join(",")}]`;
206
+ return String(value);
207
+ }