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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/contracts/TxSimulator.sol +305 -0
- package/dist/constants.d.ts +15 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/txSimulatorBytecode.d.ts +2 -0
- package/dist/generated/txSimulatorBytecode.d.ts.map +1 -0
- package/dist/generated/txSimulatorBytecode.js +3 -0
- package/dist/generated/txSimulatorBytecode.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/data.d.ts +8 -0
- package/dist/internal/data.d.ts.map +1 -0
- package/dist/internal/data.js +31 -0
- package/dist/internal/data.js.map +1 -0
- package/dist/internal/probes.d.ts +29 -0
- package/dist/internal/probes.d.ts.map +1 -0
- package/dist/internal/probes.js +159 -0
- package/dist/internal/probes.js.map +1 -0
- package/dist/internal/requirements.d.ts +5 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +173 -0
- package/dist/internal/requirements.js.map +1 -0
- package/dist/internal/rpc.d.ts +36 -0
- package/dist/internal/rpc.d.ts.map +1 -0
- package/dist/internal/rpc.js +124 -0
- package/dist/internal/rpc.js.map +1 -0
- package/dist/internal/simulator.d.ts +33 -0
- package/dist/internal/simulator.d.ts.map +1 -0
- package/dist/internal/simulator.js +199 -0
- package/dist/internal/simulator.js.map +1 -0
- package/dist/internal/slots.d.ts +7 -0
- package/dist/internal/slots.d.ts.map +1 -0
- package/dist/internal/slots.js +129 -0
- package/dist/internal/slots.js.map +1 -0
- package/dist/txSimulator.d.ts +83 -0
- package/dist/txSimulator.d.ts.map +1 -0
- package/dist/txSimulator.js +75 -0
- package/dist/txSimulator.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
- package/src/constants.ts +15 -0
- package/src/errors.ts +45 -0
- package/src/generated/txSimulatorBytecode.ts +5 -0
- package/src/index.ts +34 -0
- package/src/internal/data.ts +36 -0
- package/src/internal/probes.ts +221 -0
- package/src/internal/requirements.ts +240 -0
- package/src/internal/rpc.ts +207 -0
- package/src/internal/simulator.ts +306 -0
- package/src/internal/slots.ts +211 -0
- package/src/txSimulator.ts +176 -0
- package/src/types.ts +240 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { Abi, Address, Hex, StateOverride } from "viem";
|
|
2
|
+
import {
|
|
3
|
+
decodeErrorResult,
|
|
4
|
+
decodeFunctionResult,
|
|
5
|
+
encodeFunctionData,
|
|
6
|
+
parseAbi,
|
|
7
|
+
slice,
|
|
8
|
+
size,
|
|
9
|
+
} from "viem";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AssetBalanceDelta,
|
|
13
|
+
SimulatedCall,
|
|
14
|
+
SimulationResult,
|
|
15
|
+
TokenSlotOverride,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { InvalidSimulationInputError, StateOverrideUnsupportedError } from "../errors.js";
|
|
18
|
+
import { txSimulatorRuntimeBytecode } from "../generated/txSimulatorBytecode.js";
|
|
19
|
+
import {
|
|
20
|
+
MAX_UINT256,
|
|
21
|
+
addressKey,
|
|
22
|
+
getCallData,
|
|
23
|
+
normalizeAddress,
|
|
24
|
+
uint256Hex,
|
|
25
|
+
uniqueAddresses,
|
|
26
|
+
} from "./data.js";
|
|
27
|
+
import type { RpcCallArgs } from "./rpc.js";
|
|
28
|
+
import {
|
|
29
|
+
blockOptionsSpread,
|
|
30
|
+
buildCallParameters,
|
|
31
|
+
createAccessList,
|
|
32
|
+
formatRpcError,
|
|
33
|
+
withRpcDebug,
|
|
34
|
+
} from "./rpc.js";
|
|
35
|
+
|
|
36
|
+
type ProbeData = {
|
|
37
|
+
observedTokens: Address[];
|
|
38
|
+
candidates: Address[];
|
|
39
|
+
maxTokenOutflows: readonly bigint[];
|
|
40
|
+
maxNativeOutflow: bigint;
|
|
41
|
+
allowanceCheckpoints: readonly bigint[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SimulatorResult = SimulationResult & {
|
|
45
|
+
probeData: ProbeData;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const txSimulatorAbi = parseAbi([
|
|
49
|
+
"struct SimulatedCall { address to; uint256 value; bytes data; }",
|
|
50
|
+
"struct AllowanceProbe { address token; address spender; }",
|
|
51
|
+
"struct SimulationResult { bool success; uint256 failingCallIndex; bytes revertData; int256 nativeDelta; address[] observedTokens; address[] deltaTokens; int256[] tokenDeltas; uint256[] maxTokenOutflows; uint256 maxNativeOutflow; uint256[] allowanceCheckpoints; }",
|
|
52
|
+
"function simulate(SimulatedCall[] calls, address[] candidates, AllowanceProbe[] probes) returns (SimulationResult)",
|
|
53
|
+
"function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export async function runSimulator(
|
|
57
|
+
args: RpcCallArgs & {
|
|
58
|
+
from: Address;
|
|
59
|
+
calls: readonly SimulatedCall[];
|
|
60
|
+
candidates: readonly Address[];
|
|
61
|
+
tokenSlotOverrides?: readonly TokenSlotOverride[];
|
|
62
|
+
extraStateOverrides?: readonly StateOverrideEntry[];
|
|
63
|
+
allowanceProbes?: readonly { token: Address; spender: Address }[];
|
|
64
|
+
debugStep?: string;
|
|
65
|
+
errorAbi?: Abi;
|
|
66
|
+
},
|
|
67
|
+
): Promise<SimulatorResult> {
|
|
68
|
+
const candidates = uniqueAddresses(args.candidates);
|
|
69
|
+
const data = encodeFunctionData({
|
|
70
|
+
abi: txSimulatorAbi,
|
|
71
|
+
functionName: "simulate",
|
|
72
|
+
args: [
|
|
73
|
+
args.calls.map((call) => ({
|
|
74
|
+
to: call.to,
|
|
75
|
+
value: call.value ?? 0n,
|
|
76
|
+
data: call.data,
|
|
77
|
+
})),
|
|
78
|
+
candidates,
|
|
79
|
+
args.allowanceProbes ?? [],
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const stateOverride = buildStateOverride([
|
|
84
|
+
{ address: args.from, code: txSimulatorRuntimeBytecode },
|
|
85
|
+
...tokenSlotOverridesToStateDiff(args.tokenSlotOverrides ?? []),
|
|
86
|
+
...(args.extraStateOverrides ?? []),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
let callData: Hex;
|
|
90
|
+
try {
|
|
91
|
+
const result = await withRpcDebug(
|
|
92
|
+
args.debug,
|
|
93
|
+
{
|
|
94
|
+
method: "eth_call",
|
|
95
|
+
step: args.debugStep ?? "txSimulator.simulate",
|
|
96
|
+
details: {
|
|
97
|
+
from: args.from,
|
|
98
|
+
calls: args.calls.length,
|
|
99
|
+
candidates: candidates.length,
|
|
100
|
+
storageOverrides: args.tokenSlotOverrides?.length ?? 0,
|
|
101
|
+
stateOverrideAccounts: stateOverride.length,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
() =>
|
|
105
|
+
args.client.call(
|
|
106
|
+
buildCallParameters({
|
|
107
|
+
account: args.from,
|
|
108
|
+
to: args.from,
|
|
109
|
+
data,
|
|
110
|
+
gas: args.gas,
|
|
111
|
+
stateOverride,
|
|
112
|
+
...blockOptionsSpread(args),
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
callData = getCallData(result);
|
|
117
|
+
} catch (cause) {
|
|
118
|
+
throw new StateOverrideUnsupportedError(
|
|
119
|
+
formatRpcError("eth_call with state override failed", cause),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = decodeFunctionResult({
|
|
126
|
+
abi: txSimulatorAbi,
|
|
127
|
+
functionName: "simulate",
|
|
128
|
+
data: callData,
|
|
129
|
+
});
|
|
130
|
+
} catch (cause) {
|
|
131
|
+
throw new StateOverrideUnsupportedError(
|
|
132
|
+
formatRpcError("eth_call returned undecodable simulator output", cause),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const assetBalanceDeltas: AssetBalanceDelta[] = [];
|
|
137
|
+
if (result.nativeDelta !== 0n) {
|
|
138
|
+
assetBalanceDeltas.push({ asset: "native", delta: result.nativeDelta });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < result.deltaTokens.length; ++i) {
|
|
142
|
+
const token = result.deltaTokens[i];
|
|
143
|
+
const delta = result.tokenDeltas[i];
|
|
144
|
+
if (token && delta !== undefined && delta !== 0n) {
|
|
145
|
+
assetBalanceDeltas.push({ asset: token, delta });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const probeData = {
|
|
150
|
+
observedTokens: uniqueAddresses(result.observedTokens),
|
|
151
|
+
candidates,
|
|
152
|
+
maxTokenOutflows: result.maxTokenOutflows,
|
|
153
|
+
maxNativeOutflow: result.maxNativeOutflow,
|
|
154
|
+
allowanceCheckpoints: result.allowanceCheckpoints,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
const decodedRevert = decodeRevert(result.revertData, args.errorAbi);
|
|
159
|
+
return {
|
|
160
|
+
status: "reverted",
|
|
161
|
+
assetBalanceDeltas,
|
|
162
|
+
revertData: result.revertData,
|
|
163
|
+
...(decodedRevert.revertReason !== undefined
|
|
164
|
+
? { revertReason: decodedRevert.revertReason }
|
|
165
|
+
: {}),
|
|
166
|
+
...(decodedRevert.revertError !== undefined
|
|
167
|
+
? { revertError: decodedRevert.revertError }
|
|
168
|
+
: {}),
|
|
169
|
+
...(decodedRevert.revertSelector !== undefined
|
|
170
|
+
? { revertSelector: decodedRevert.revertSelector }
|
|
171
|
+
: {}),
|
|
172
|
+
failingCallIndex: Number(result.failingCallIndex),
|
|
173
|
+
probeData,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
status: "success",
|
|
179
|
+
assetBalanceDeltas,
|
|
180
|
+
probeData,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function discoverCandidateAddresses(
|
|
185
|
+
args: RpcCallArgs & {
|
|
186
|
+
from: Address;
|
|
187
|
+
calls: readonly SimulatedCall[];
|
|
188
|
+
},
|
|
189
|
+
): Promise<Address[]> {
|
|
190
|
+
const accessLists = await Promise.all(
|
|
191
|
+
args.calls.map((call) =>
|
|
192
|
+
createAccessList({
|
|
193
|
+
client: args.client,
|
|
194
|
+
from: args.from,
|
|
195
|
+
to: call.to,
|
|
196
|
+
data: call.data,
|
|
197
|
+
value: call.value ?? 0n,
|
|
198
|
+
gas: args.gas,
|
|
199
|
+
debug: args.debug,
|
|
200
|
+
debugStep: "candidateDiscovery.accessList",
|
|
201
|
+
...blockOptionsSpread(args),
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
const candidates = args.calls.flatMap((call, index) => [
|
|
206
|
+
call.to,
|
|
207
|
+
...(accessLists[index] ?? []).map((entry) => entry.address),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
return uniqueAddresses(candidates);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type DecodedRevert = {
|
|
214
|
+
revertReason?: string;
|
|
215
|
+
revertError?: { name: string; args: readonly unknown[] };
|
|
216
|
+
revertSelector?: Hex;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function decodeRevert(data: Hex | undefined, errorAbi?: Abi): DecodedRevert {
|
|
220
|
+
if (!data || data === "0x") return {};
|
|
221
|
+
|
|
222
|
+
const revertSelector = size(data) >= 4 ? slice(data, 0, 4) : undefined;
|
|
223
|
+
try {
|
|
224
|
+
const decoded = decodeErrorResult({ abi: errorAbi ?? [], data });
|
|
225
|
+
return {
|
|
226
|
+
revertReason: formatReason(decoded.errorName, decoded.args ?? []),
|
|
227
|
+
revertError: { name: decoded.errorName, args: decoded.args ?? [] },
|
|
228
|
+
...(revertSelector !== undefined ? { revertSelector } : {}),
|
|
229
|
+
};
|
|
230
|
+
} catch {
|
|
231
|
+
return revertSelector !== undefined ? { revertSelector } : {};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatReason(name: string, args: readonly unknown[]): string {
|
|
236
|
+
if (name === "Error") return String(args[0]);
|
|
237
|
+
if (name === "Panic") return `Panic(${String(args[0])})`;
|
|
238
|
+
return `${name}(${args.map((arg) => String(arg)).join(", ")})`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
type StateOverrideEntry = StateOverride[number];
|
|
242
|
+
|
|
243
|
+
type MutableStateOverrideEntry = {
|
|
244
|
+
address: Address;
|
|
245
|
+
code?: Hex;
|
|
246
|
+
balance?: bigint;
|
|
247
|
+
stateDiff?: {
|
|
248
|
+
slot: Hex;
|
|
249
|
+
value: Hex;
|
|
250
|
+
}[];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function buildStateOverride(entries: readonly StateOverrideEntry[]): StateOverride {
|
|
254
|
+
const merged = new Map<string, MutableStateOverrideEntry>();
|
|
255
|
+
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const normalized = normalizeAddress(entry.address);
|
|
258
|
+
const key = addressKey(normalized);
|
|
259
|
+
const existing = merged.get(key) ?? { address: normalized, stateDiff: [] };
|
|
260
|
+
|
|
261
|
+
if (entry.code) existing.code = entry.code;
|
|
262
|
+
if (entry.balance !== undefined) existing.balance = entry.balance;
|
|
263
|
+
if (entry.stateDiff) {
|
|
264
|
+
const bySlot = new Map(
|
|
265
|
+
(existing.stateDiff ?? []).map((item) => [item.slot.toLowerCase(), item]),
|
|
266
|
+
);
|
|
267
|
+
for (const diff of entry.stateDiff) bySlot.set(diff.slot.toLowerCase(), diff);
|
|
268
|
+
existing.stateDiff = [...bySlot.values()];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
merged.set(key, existing);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [...merged.values()].map((entry): StateOverrideEntry => {
|
|
275
|
+
if (entry.stateDiff?.length === 0) {
|
|
276
|
+
return {
|
|
277
|
+
address: entry.address,
|
|
278
|
+
code: entry.code,
|
|
279
|
+
balance: entry.balance,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return entry;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function tokenSlotOverridesToStateDiff(overrides: readonly TokenSlotOverride[]): StateOverride {
|
|
287
|
+
const byAddress = new Map<string, MutableStateOverrideEntry>();
|
|
288
|
+
|
|
289
|
+
for (const override of overrides) {
|
|
290
|
+
if (override.amount === MAX_UINT256) {
|
|
291
|
+
throw new InvalidSimulationInputError(
|
|
292
|
+
"tokenSlotOverrides amount must be below uint256 max: max-allowance skips ERC-20 decrements and max-balance overflows incoming transfers. Use OVERRIDE_TOKEN_AMOUNT.",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const normalized = normalizeAddress(override.token);
|
|
296
|
+
const key = addressKey(normalized);
|
|
297
|
+
const entry = byAddress.get(key) ?? { address: normalized, stateDiff: [] };
|
|
298
|
+
entry.stateDiff?.push({
|
|
299
|
+
slot: override.slot,
|
|
300
|
+
value: uint256Hex(override.amount),
|
|
301
|
+
});
|
|
302
|
+
byAddress.set(key, entry);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return [...byAddress.values()];
|
|
306
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Address, Hex } from "viem";
|
|
2
|
+
import { encodeAbiParameters, keccak256 } from "viem";
|
|
3
|
+
|
|
4
|
+
import { OVERRIDE_TOKEN_AMOUNT } from "../constants.js";
|
|
5
|
+
import type {
|
|
6
|
+
PreparedAllowanceOverrides,
|
|
7
|
+
PreparedBalanceOverrides,
|
|
8
|
+
PrepareAllowanceOverridesArgs,
|
|
9
|
+
PrepareBalanceOverridesArgs,
|
|
10
|
+
TokenSlotOverride,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { addressKey, uint256Hex } from "./data.js";
|
|
13
|
+
import { discoverAllowanceSlot, discoverBalanceSlot, readAllowance } from "./probes.js";
|
|
14
|
+
import type { ClientArgs, RpcCallArgs } from "./rpc.js";
|
|
15
|
+
import { blockOptionsSpread } from "./rpc.js";
|
|
16
|
+
|
|
17
|
+
type SlotFact = {
|
|
18
|
+
token: Address;
|
|
19
|
+
slot: Hex;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AllowanceSlotFact = SlotFact & {
|
|
23
|
+
spender: Address;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Orchestration
|
|
27
|
+
/** @internal Implements `TxSimulator.prepareBalanceOverrides`. Prefer the instance API from the package root. */
|
|
28
|
+
export async function prepareBalanceOverrides(
|
|
29
|
+
args: PrepareBalanceOverridesArgs & ClientArgs,
|
|
30
|
+
): Promise<PreparedBalanceOverrides> {
|
|
31
|
+
const slots = await Promise.all(
|
|
32
|
+
args.tokens.map((token) =>
|
|
33
|
+
discoverBalanceSlot({
|
|
34
|
+
client: args.client,
|
|
35
|
+
token,
|
|
36
|
+
owner: args.from,
|
|
37
|
+
sentinel: OVERRIDE_TOKEN_AMOUNT,
|
|
38
|
+
gas: args.gas,
|
|
39
|
+
debug: args.debug,
|
|
40
|
+
...blockOptionsSpread(args),
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
return {
|
|
45
|
+
slots: slots.filter(isDefined).map(withOverrideAmount),
|
|
46
|
+
unresolved: args.tokens.filter((_, index) => slots[index] === undefined),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @internal Implements `TxSimulator.prepareAllowanceOverrides`. Prefer the instance API from the package root. */
|
|
51
|
+
export async function prepareAllowanceOverrides(
|
|
52
|
+
args: PrepareAllowanceOverridesArgs & ClientArgs,
|
|
53
|
+
): Promise<PreparedAllowanceOverrides> {
|
|
54
|
+
const slots = await prepareAllowanceOverridesWithInference({
|
|
55
|
+
client: args.client,
|
|
56
|
+
from: args.from,
|
|
57
|
+
pairs: args.pairs,
|
|
58
|
+
sentinel: OVERRIDE_TOKEN_AMOUNT,
|
|
59
|
+
gas: args.gas,
|
|
60
|
+
debug: args.debug,
|
|
61
|
+
...blockOptionsSpread(args),
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
slots: slots.filter(isDefined).map(withOverrideAmount),
|
|
65
|
+
unresolved: args.pairs.filter((_, index) => slots[index] === undefined),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isDefined<T>(value: T | undefined): value is T {
|
|
70
|
+
return value !== undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function withOverrideAmount<T extends SlotFact>(slot: T): T & TokenSlotOverride {
|
|
74
|
+
return { ...slot, amount: OVERRIDE_TOKEN_AMOUNT };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Inference internals
|
|
78
|
+
type AllowancePair = {
|
|
79
|
+
token: Address;
|
|
80
|
+
spender: Address;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type IndexedAllowancePair = AllowancePair & {
|
|
84
|
+
index: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function prepareAllowanceOverridesWithInference(
|
|
88
|
+
args: RpcCallArgs & {
|
|
89
|
+
from: Address;
|
|
90
|
+
pairs: readonly AllowancePair[];
|
|
91
|
+
sentinel: bigint;
|
|
92
|
+
},
|
|
93
|
+
): Promise<(AllowanceSlotFact | undefined)[]> {
|
|
94
|
+
const slots: (AllowanceSlotFact | undefined)[] = Array.from({ length: args.pairs.length });
|
|
95
|
+
const groups = groupPairsByToken(args.pairs);
|
|
96
|
+
|
|
97
|
+
await Promise.all(
|
|
98
|
+
groups.map(async (pairs) => {
|
|
99
|
+
const firstPair = pairs[0];
|
|
100
|
+
if (firstPair === undefined) return;
|
|
101
|
+
|
|
102
|
+
const firstSlot = await probeAllowanceSlot({ ...args, ...firstPair });
|
|
103
|
+
slots[firstPair.index] = firstSlot;
|
|
104
|
+
const baseSlot =
|
|
105
|
+
firstSlot === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: inferAllowanceBaseSlot({
|
|
108
|
+
probedSlot: firstSlot.slot,
|
|
109
|
+
owner: args.from,
|
|
110
|
+
spender: firstPair.spender,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await Promise.all(
|
|
114
|
+
pairs.slice(1).map(async (pair) => {
|
|
115
|
+
slots[pair.index] =
|
|
116
|
+
baseSlot === undefined
|
|
117
|
+
? await probeAllowanceSlot({ ...args, ...pair })
|
|
118
|
+
: await computeAllowanceSlot({ ...args, ...pair, baseSlot });
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return slots;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function groupPairsByToken(pairs: readonly AllowancePair[]): IndexedAllowancePair[][] {
|
|
128
|
+
const groupsByToken = new Map<string, IndexedAllowancePair[]>();
|
|
129
|
+
for (let index = 0; index < pairs.length; ++index) {
|
|
130
|
+
const pair = pairs[index];
|
|
131
|
+
if (pair === undefined) continue;
|
|
132
|
+
const key = addressKey(pair.token);
|
|
133
|
+
const group = groupsByToken.get(key) ?? [];
|
|
134
|
+
group.push({ ...pair, index });
|
|
135
|
+
groupsByToken.set(key, group);
|
|
136
|
+
}
|
|
137
|
+
return [...groupsByToken.values()];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function probeAllowanceSlot(
|
|
141
|
+
args: RpcCallArgs & {
|
|
142
|
+
from: Address;
|
|
143
|
+
token: Address;
|
|
144
|
+
spender: Address;
|
|
145
|
+
sentinel: bigint;
|
|
146
|
+
},
|
|
147
|
+
): Promise<AllowanceSlotFact | undefined> {
|
|
148
|
+
return discoverAllowanceSlot({
|
|
149
|
+
client: args.client,
|
|
150
|
+
token: args.token,
|
|
151
|
+
owner: args.from,
|
|
152
|
+
spender: args.spender,
|
|
153
|
+
sentinel: args.sentinel,
|
|
154
|
+
gas: args.gas,
|
|
155
|
+
debug: args.debug,
|
|
156
|
+
...blockOptionsSpread(args),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function computeAllowanceSlot(
|
|
161
|
+
args: RpcCallArgs & {
|
|
162
|
+
from: Address;
|
|
163
|
+
token: Address;
|
|
164
|
+
spender: Address;
|
|
165
|
+
baseSlot: bigint;
|
|
166
|
+
sentinel: bigint;
|
|
167
|
+
},
|
|
168
|
+
): Promise<AllowanceSlotFact | undefined> {
|
|
169
|
+
const slot = allowanceSlotFor(args.from, args.spender, args.baseSlot);
|
|
170
|
+
const allowance = await readAllowance({
|
|
171
|
+
client: args.client,
|
|
172
|
+
token: args.token,
|
|
173
|
+
owner: args.from,
|
|
174
|
+
spender: args.spender,
|
|
175
|
+
stateOverride: [
|
|
176
|
+
{ address: args.token, stateDiff: [{ slot, value: uint256Hex(args.sentinel) }] },
|
|
177
|
+
],
|
|
178
|
+
gas: args.gas,
|
|
179
|
+
debug: args.debug,
|
|
180
|
+
debugStep: "allowanceSlot.computedVerify",
|
|
181
|
+
...blockOptionsSpread(args),
|
|
182
|
+
});
|
|
183
|
+
if (allowance === args.sentinel) return { token: args.token, spender: args.spender, slot };
|
|
184
|
+
return probeAllowanceSlot(args);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Layout math
|
|
188
|
+
function mappingSlot(key: Address, baseSlot: Hex | bigint): Hex {
|
|
189
|
+
return keccak256(
|
|
190
|
+
encodeAbiParameters(
|
|
191
|
+
[{ type: "address" }, { type: "uint256" }],
|
|
192
|
+
[key, typeof baseSlot === "bigint" ? baseSlot : BigInt(baseSlot)],
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function allowanceSlotFor(owner: Address, spender: Address, base: bigint): Hex {
|
|
198
|
+
return mappingSlot(spender, mappingSlot(owner, base));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function inferAllowanceBaseSlot(args: {
|
|
202
|
+
probedSlot: Hex;
|
|
203
|
+
owner: Address;
|
|
204
|
+
spender: Address;
|
|
205
|
+
}): bigint | undefined {
|
|
206
|
+
const target = args.probedSlot.toLowerCase();
|
|
207
|
+
for (let base = 0n; base <= 64n; ++base) {
|
|
208
|
+
if (allowanceSlotFor(args.owner, args.spender, base).toLowerCase() === target) return base;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { DEFAULT_SIMULATION_GAS_LIMIT } from "./constants.js";
|
|
2
|
+
import { InvalidSimulationInputError } from "./errors.js";
|
|
3
|
+
import { estimateAssetRequirements } from "./internal/requirements.js";
|
|
4
|
+
import { blockOptionsSpread, type ClientArgs } from "./internal/rpc.js";
|
|
5
|
+
import { discoverCandidateAddresses, runSimulator } from "./internal/simulator.js";
|
|
6
|
+
import { prepareAllowanceOverrides, prepareBalanceOverrides } from "./internal/slots.js";
|
|
7
|
+
import type {
|
|
8
|
+
PreparedAllowanceOverrides,
|
|
9
|
+
PreparedBalanceOverrides,
|
|
10
|
+
PrepareAllowanceOverridesArgs,
|
|
11
|
+
PrepareBalanceOverridesArgs,
|
|
12
|
+
EstimateAssetRequirementsArgs,
|
|
13
|
+
EstimatedAssetRequirements,
|
|
14
|
+
SimulateArgs,
|
|
15
|
+
SimulatedCall,
|
|
16
|
+
SimulationResult,
|
|
17
|
+
TxSimulatorConfig,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
type BoundCallDefaults = {
|
|
21
|
+
gas?: bigint;
|
|
22
|
+
debug?: TxSimulatorConfig["debug"];
|
|
23
|
+
errorAbi?: TxSimulatorConfig["errorAbi"];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bound transaction simulator for one viem public client.
|
|
28
|
+
*
|
|
29
|
+
* Bind the RPC client once, then pass `from` per call so applications can switch accounts without
|
|
30
|
+
* rebuilding the simulator. Per-call `gas` and `debug` override defaults supplied to
|
|
31
|
+
* {@link TxSimulator.create}.
|
|
32
|
+
*/
|
|
33
|
+
export interface TxSimulator {
|
|
34
|
+
/**
|
|
35
|
+
* Simulates one call or sequential batch and returns raw native/token balance deltas.
|
|
36
|
+
*
|
|
37
|
+
* This uses `eth_createAccessList` for candidate discovery and one `eth_call` with state
|
|
38
|
+
* overrides that injects the simulator at `from`. It does not automatically forge balances or
|
|
39
|
+
* allowances; preparation methods return ready-to-use `tokenSlotOverrides` for view-only or
|
|
40
|
+
* unfunded accounts. Transaction reverts return `status: "reverted"` instead of throwing.
|
|
41
|
+
*
|
|
42
|
+
* @throws InvalidSimulationInputError when `calls` is empty.
|
|
43
|
+
* @throws AccessListUnsupportedError when the RPC endpoint cannot provide access lists.
|
|
44
|
+
* @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides or
|
|
45
|
+
* returns undecodable simulator output.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const sim = TxSimulator.create({ client });
|
|
50
|
+
* const result = await sim.simulate({
|
|
51
|
+
* from,
|
|
52
|
+
* calls: [{ to, data, value: 0n }],
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
simulate: (args: SimulateArgs) => Promise<SimulationResult>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prepares ERC-20 balance overrides for `from`.
|
|
60
|
+
*
|
|
61
|
+
* Each token is probed with RPC-only access lists and sentinel state overrides. Tokens the
|
|
62
|
+
* simulator cannot `deal` by verified storage write are returned in `unresolved` rather than
|
|
63
|
+
* thrown.
|
|
64
|
+
*
|
|
65
|
+
* @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides.
|
|
66
|
+
*/
|
|
67
|
+
prepareBalanceOverrides: (args: PrepareBalanceOverridesArgs) => Promise<PreparedBalanceOverrides>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prepares ERC-20 allowance overrides for `from` and the requested token/spender pairs.
|
|
71
|
+
*
|
|
72
|
+
* Standard Solidity allowance layouts are inferred after one verified probe per token where
|
|
73
|
+
* possible; non-standard layouts fall back to per-pair probing. Pairs the simulator cannot `deal`
|
|
74
|
+
* via verified storage write are returned in `unresolved` rather than thrown.
|
|
75
|
+
*
|
|
76
|
+
* @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides.
|
|
77
|
+
*/
|
|
78
|
+
prepareAllowanceOverrides: (
|
|
79
|
+
args: PrepareAllowanceOverridesArgs,
|
|
80
|
+
) => Promise<PreparedAllowanceOverrides>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Estimates the balances and approvals needed to execute the observed path.
|
|
84
|
+
*
|
|
85
|
+
* Use this when the tokens or spenders are not known ahead of time. Returned amounts are estimated
|
|
86
|
+
* under forged balances/allowances and should be padded before display or transaction assembly;
|
|
87
|
+
* unreliable measurements are reported under `unresolved`.
|
|
88
|
+
*
|
|
89
|
+
* @throws InvalidSimulationInputError when `calls` is empty.
|
|
90
|
+
* @throws AccessListUnsupportedError when the RPC endpoint cannot provide access lists.
|
|
91
|
+
* @throws StateOverrideUnsupportedError when the RPC endpoint cannot execute state overrides or
|
|
92
|
+
* returns undecodable simulator output.
|
|
93
|
+
*/
|
|
94
|
+
estimateAssetRequirements: (
|
|
95
|
+
args: EstimateAssetRequirementsArgs,
|
|
96
|
+
) => Promise<EstimatedAssetRequirements>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Factory for {@link TxSimulator} instances bound to one viem public client. */
|
|
100
|
+
export const TxSimulator = {
|
|
101
|
+
/**
|
|
102
|
+
* Creates a simulator with optional default gas and debug settings.
|
|
103
|
+
*
|
|
104
|
+
* `gas` defaults to `DEFAULT_SIMULATION_GAS_LIMIT`; `debug` may be `true` for console logging or a
|
|
105
|
+
* callback for structured events. Per-call `gas` and `debug` take precedence over these defaults.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const sim = TxSimulator.create({ client, debug: true });
|
|
110
|
+
* const result = await sim.simulate({ from, calls });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
create(bound: TxSimulatorConfig): TxSimulator {
|
|
114
|
+
const defaults = (args: BoundCallDefaults) => {
|
|
115
|
+
const gas = args.gas ?? bound.gas ?? DEFAULT_SIMULATION_GAS_LIMIT;
|
|
116
|
+
const debug = args.debug ?? bound.debug;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
gas,
|
|
120
|
+
...(debug !== undefined ? { debug } : {}),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
const revertDefaults = (args: BoundCallDefaults) => {
|
|
124
|
+
const errorAbi = [...(bound.errorAbi ?? []), ...(args.errorAbi ?? [])];
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...defaults(args),
|
|
128
|
+
...(errorAbi.length > 0 ? { errorAbi } : {}),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
simulate: (args) => runSimulate({ ...args, ...revertDefaults(args), client: bound.client }),
|
|
134
|
+
prepareBalanceOverrides: (args) =>
|
|
135
|
+
prepareBalanceOverrides({ ...args, ...defaults(args), client: bound.client }),
|
|
136
|
+
prepareAllowanceOverrides: (args) =>
|
|
137
|
+
prepareAllowanceOverrides({ ...args, ...defaults(args), client: bound.client }),
|
|
138
|
+
estimateAssetRequirements: (args) =>
|
|
139
|
+
estimateAssetRequirements({ ...args, ...revertDefaults(args), client: bound.client }),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
async function runSimulate(args: SimulateArgs & ClientArgs): Promise<SimulationResult> {
|
|
145
|
+
if (args.calls.length === 0) {
|
|
146
|
+
throw new InvalidSimulationInputError("simulate requires at least one call.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const calls = args.calls.map((call) => ({
|
|
150
|
+
to: call.to,
|
|
151
|
+
data: call.data,
|
|
152
|
+
value: call.value ?? 0n,
|
|
153
|
+
})) satisfies SimulatedCall[];
|
|
154
|
+
const candidateAddresses = await discoverCandidateAddresses({
|
|
155
|
+
client: args.client,
|
|
156
|
+
from: args.from,
|
|
157
|
+
calls,
|
|
158
|
+
...blockOptionsSpread(args),
|
|
159
|
+
gas: args.gas,
|
|
160
|
+
...(args.debug !== undefined ? { debug: args.debug } : {}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const tokenSlotOverrides = args.tokenSlotOverrides ?? [];
|
|
164
|
+
|
|
165
|
+
return runSimulator({
|
|
166
|
+
client: args.client,
|
|
167
|
+
from: args.from,
|
|
168
|
+
calls,
|
|
169
|
+
candidates: [...candidateAddresses, ...tokenSlotOverrides.map((slot) => slot.token)],
|
|
170
|
+
tokenSlotOverrides,
|
|
171
|
+
debug: args.debug,
|
|
172
|
+
...blockOptionsSpread(args),
|
|
173
|
+
gas: args.gas,
|
|
174
|
+
...(args.errorAbi !== undefined ? { errorAbi: args.errorAbi } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|