@txfence/evm 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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @txfence/evm
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial public release — policy engine, intent execution, formal verification, adversarial stress testing, cryptographic provenance chains, temporal rules, and MEV protection across EVM, Solana, and Cosmos
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @txfence/core@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aditya Chauhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @txfence/evm
2
+
3
+ EVM chain adapter for [txfence](https://github.com/AdityaChauhanX07/txfence). Simulation (`eth_call` + Tenderly), fork simulation, signing, broadcast, and MEV-protected RPC routing for Ethereum, Arbitrum, Optimism, Base, and any EVM chain supported by viem.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @txfence/evm @txfence/core viem
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```typescript
14
+ import { createAgent, type Policy } from '@txfence/core'
15
+ import { simulateEvmAction, executeEvmAction, privateKeySigner } from '@txfence/evm'
16
+
17
+ const signer = privateKeySigner(process.env.PRIVATE_KEY as `0x${string}`)
18
+
19
+ const agent = createAgent(
20
+ { chains: ['ethereum'], policies: policy, signer },
21
+ { ethereum: { simulate: simulateEvmAction } },
22
+ { ethereum: 'https://ethereum.publicnode.com' },
23
+ (action, chainId, rpcUrl, evaluation, simulation) =>
24
+ executeEvmAction(action, chainId, rpcUrl, signer, evaluation, simulation),
25
+ )
26
+ ```
27
+
28
+ ## Tenderly simulation
29
+
30
+ Pass a `TenderlyConfig` and `simulateEvmAction` upgrades from `eth_call` to a full Tenderly simulation — state diffs, traces, balance changes:
31
+
32
+ ```typescript
33
+ import { simulateEvmAction } from '@txfence/evm'
34
+
35
+ const result = await simulateEvmAction(action, 'ethereum', rpcUrl, {
36
+ tenderly: {
37
+ accessKey: process.env.TENDERLY_ACCESS_KEY!,
38
+ accountSlug: process.env.TENDERLY_ACCOUNT_SLUG!,
39
+ projectSlug: process.env.TENDERLY_PROJECT_SLUG!,
40
+ },
41
+ })
42
+ // result.coverageLevel: 'full' when Tenderly returns; 'partial' on eth_call fallback
43
+ ```
44
+
45
+ ## Fork simulation
46
+
47
+ Simulate a multi-step intent against a forked chain state:
48
+
49
+ ```typescript
50
+ import { simulateIntentOnFork } from '@txfence/evm'
51
+
52
+ const result = await simulateIntentOnFork(
53
+ rebalanceIntent,
54
+ {
55
+ provider: 'tenderly',
56
+ tenderlyConfig: { accessKey, accountSlug, projectSlug },
57
+ fromAddress: agentAddress,
58
+ },
59
+ 'ethereum',
60
+ rpcUrl,
61
+ )
62
+ // result.wouldAllSucceed, result.finalPosition, result.failingStepId
63
+ // The fork is automatically deleted after simulation.
64
+ ```
65
+
66
+ ## MEV protection
67
+
68
+ Route through Flashbots Protect or MEV Blocker:
69
+
70
+ ```typescript
71
+ const policy: Policy = {
72
+ // ...
73
+ mevProtection: 'flashbots', // 'flashbots' | 'mev-blocker' | 'none'
74
+ }
75
+ ```
76
+
77
+ `getMevProtectedRpcUrl()` and `broadcastWithMevProtection()` are exported for advanced cases.
78
+
79
+ ## Metadata verification
80
+
81
+ Validate that a contract's on-chain bytecode matches an expected implementation hash:
82
+
83
+ ```typescript
84
+ import { createEvmMetadataVerifier } from '@txfence/evm'
85
+
86
+ const verifier = createEvmMetadataVerifier(rpcUrl)
87
+ const ok = await verifier.verify('0xCONTRACT', 'ethereum', { implementationHash: '0x...' })
88
+ ```
89
+
90
+ Catches proxy upgrades and unexpected implementations before signing.
91
+
92
+ ## Exports
93
+
94
+ - `simulateEvmAction`, `simulateWithTenderly` — simulation
95
+ - `executeEvmAction`, `buildEvmTransaction` — execution
96
+ - `createFork`, `simulateOnFork`, `deleteFork`, `simulateIntentOnFork` — fork simulation
97
+ - `privateKeySigner` — viem-backed `Signer`
98
+ - `getMevProtectedRpcUrl`, `broadcastWithMevProtection` — MEV routing
99
+ - `createEvmMetadataVerifier` — on-chain contract verification
100
+ - `getViemChain` — chain id → viem `Chain` helper
101
+
102
+ Full project README: https://github.com/AdityaChauhanX07/txfence
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,55 @@
1
+ import { Action, TenderlyForkConfig, ChainId, StateChange, Intent, ForkSimulationConfig, ForkSimulationResult, SimulateOptions, SimulationResult, Signer, PolicyEvaluation, MevProtectionMode, MevProtectionConfig, SuccessReceipt, SerializedTransaction, MetadataVerifier } from '@txfence/core';
2
+ import { Chain } from 'viem';
3
+
4
+ declare function createFork(config: TenderlyForkConfig, chainId: ChainId, blockNumber?: number): Promise<{
5
+ forkId: string;
6
+ forkedAtBlock: number;
7
+ }>;
8
+ declare function simulateOnFork(config: TenderlyForkConfig, forkId: string, transaction: {
9
+ from: string;
10
+ to: string;
11
+ input: string;
12
+ value: string;
13
+ gas: number;
14
+ }, chainId: ChainId, blockNumber: number): Promise<{
15
+ success: boolean;
16
+ wouldRevert: boolean;
17
+ revertReason?: string;
18
+ gasUsed: number;
19
+ stateChanges: StateChange[];
20
+ rawTrace: unknown;
21
+ }>;
22
+ declare function deleteFork(config: TenderlyForkConfig, forkId: string): Promise<void>;
23
+ declare function buildForkTransactionParams(action: Action, fromAddress: string): {
24
+ from: string;
25
+ to: string;
26
+ input: string;
27
+ value: string;
28
+ gas: number;
29
+ };
30
+
31
+ declare function simulateIntentOnFork(intent: Intent, config: ForkSimulationConfig, chainId: ChainId, rpcUrl: string): Promise<ForkSimulationResult>;
32
+
33
+ type TenderlyConfig = {
34
+ accountSlug: string;
35
+ projectSlug: string;
36
+ accessKey: string;
37
+ };
38
+ declare function simulateEvmAction(action: Action, chainId: ChainId, rpcUrl: string, options?: SimulateOptions, tenderlyConfig?: TenderlyConfig): Promise<SimulationResult>;
39
+
40
+ declare function simulateWithTenderly(action: Action, chainId: ChainId, tenderlyConfig: TenderlyConfig, options?: SimulateOptions): Promise<SimulationResult>;
41
+
42
+ declare function getViemChain(chainId: ChainId): Chain;
43
+
44
+ declare function executeEvmAction(action: Action, chainId: ChainId, rpcUrl: string, signer: Signer, evaluation: PolicyEvaluation, simulation: SimulationResult, mevProtection?: MevProtectionMode, mevConfig?: MevProtectionConfig): Promise<SuccessReceipt>;
45
+
46
+ declare function privateKeySigner(privateKey: `0x${string}`): Signer;
47
+
48
+ declare function buildEvmTransaction(action: Action, chainId: ChainId, rpcUrl: string, gasEstimate: bigint, gasBufferMultiplier: number, _fromAddress: `0x${string}`): Promise<SerializedTransaction>;
49
+
50
+ declare function createEvmMetadataVerifier(rpcUrls: Partial<Record<string, string>>): MetadataVerifier;
51
+
52
+ declare function getMevProtectedRpcUrl(mode: MevProtectionMode | undefined, config?: MevProtectionConfig, fallbackRpcUrl?: string): string;
53
+ declare function broadcastWithMevProtection(signedTxHex: `0x${string}`, mode: MevProtectionMode | undefined, config?: MevProtectionConfig, fallbackRpcUrl?: string): Promise<string>;
54
+
55
+ export { type TenderlyConfig, broadcastWithMevProtection, buildEvmTransaction, buildForkTransactionParams, createEvmMetadataVerifier, createFork, deleteFork, executeEvmAction, getMevProtectedRpcUrl, getViemChain, privateKeySigner, simulateEvmAction, simulateIntentOnFork, simulateOnFork, simulateWithTenderly };
package/dist/index.js ADDED
@@ -0,0 +1,620 @@
1
+ // src/chains.ts
2
+ import { mainnet, arbitrum, optimism, base } from "viem/chains";
3
+ var viemChains = {
4
+ ethereum: mainnet,
5
+ arbitrum,
6
+ optimism,
7
+ base
8
+ };
9
+ function getViemChain(chainId) {
10
+ const chain = viemChains[chainId];
11
+ if (chain === void 0) {
12
+ throw new Error(`chain not supported by EVM adapter: ${chainId}`);
13
+ }
14
+ return chain;
15
+ }
16
+
17
+ // src/fork-client.ts
18
+ async function createFork(config, chainId, blockNumber) {
19
+ const viemChain = getViemChain(chainId);
20
+ const networkId = viemChain.id.toString();
21
+ const body = {
22
+ network_id: networkId
23
+ };
24
+ if (blockNumber !== void 0) {
25
+ body.block_number = blockNumber;
26
+ }
27
+ const response = await fetch(
28
+ `https://api.tenderly.co/api/v1/account/${config.accountSlug}/project/${config.projectSlug}/fork`,
29
+ {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ "X-Access-Key": config.accessKey
34
+ },
35
+ body: JSON.stringify(body)
36
+ }
37
+ );
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to create Tenderly fork: ${response.status} ${await response.text()}`);
40
+ }
41
+ const data = await response.json();
42
+ return {
43
+ forkId: data.simulation_fork.id,
44
+ forkedAtBlock: data.simulation_fork.block_number
45
+ };
46
+ }
47
+ async function simulateOnFork(config, forkId, transaction, chainId, blockNumber) {
48
+ const response = await fetch(
49
+ `https://api.tenderly.co/api/v1/account/${config.accountSlug}/project/${config.projectSlug}/fork/${forkId}/simulate`,
50
+ {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "X-Access-Key": config.accessKey
55
+ },
56
+ body: JSON.stringify({
57
+ network_id: getViemChain(chainId).id.toString(),
58
+ from: transaction.from,
59
+ to: transaction.to,
60
+ input: transaction.input,
61
+ value: transaction.value,
62
+ gas: transaction.gas,
63
+ save: false,
64
+ save_if_fails: false,
65
+ simulation_type: "full"
66
+ })
67
+ }
68
+ );
69
+ if (!response.ok) {
70
+ throw new Error(`Fork simulation failed: ${response.status} ${await response.text()}`);
71
+ }
72
+ const data = await response.json();
73
+ const stateChanges = (data.contracts ?? []).filter((c) => c.balanceDiff !== void 0).map((c) => {
74
+ const before = BigInt(c.balanceDiff.before);
75
+ const after = BigInt(c.balanceDiff.after);
76
+ return {
77
+ address: c.address,
78
+ balanceBefore: before,
79
+ balanceAfter: after,
80
+ delta: after - before
81
+ };
82
+ });
83
+ const revertReason = data.simulation.error_message;
84
+ return {
85
+ success: true,
86
+ wouldRevert: !data.simulation.status,
87
+ ...revertReason !== null ? { revertReason } : {},
88
+ gasUsed: data.simulation.gas_used,
89
+ stateChanges,
90
+ rawTrace: data.call_trace
91
+ };
92
+ }
93
+ async function deleteFork(config, forkId) {
94
+ const response = await fetch(
95
+ `https://api.tenderly.co/api/v1/account/${config.accountSlug}/project/${config.projectSlug}/fork/${forkId}`,
96
+ {
97
+ method: "DELETE",
98
+ headers: {
99
+ "X-Access-Key": config.accessKey
100
+ }
101
+ }
102
+ );
103
+ if (!response.ok && response.status !== 404) {
104
+ console.error(`[txfence] Failed to delete Tenderly fork ${forkId}: ${response.status}`);
105
+ }
106
+ }
107
+ function buildForkTransactionParams(action, fromAddress) {
108
+ switch (action.kind) {
109
+ case "transfer":
110
+ return {
111
+ from: fromAddress,
112
+ to: action.to,
113
+ input: "0x",
114
+ value: "0x" + action.token.amount.toString(16),
115
+ gas: 21e3
116
+ };
117
+ case "swap":
118
+ return {
119
+ from: fromAddress,
120
+ to: action.via,
121
+ input: action.calldata ?? "0x",
122
+ value: "0x0",
123
+ gas: 3e5
124
+ };
125
+ case "contract_call":
126
+ return {
127
+ from: fromAddress,
128
+ to: action.contract,
129
+ input: action.calldata ?? "0x",
130
+ value: action.value !== void 0 ? "0x" + action.value.amount.toString(16) : "0x0",
131
+ gas: 3e5
132
+ };
133
+ }
134
+ }
135
+
136
+ // src/fork-simulation.ts
137
+ import { getExecutionPlan, mergePositionChanges } from "@txfence/core";
138
+ async function simulateIntentOnFork(intent, config, chainId, rpcUrl) {
139
+ if (config.provider !== "tenderly" || config.tenderlyConfig === void 0) {
140
+ throw new Error(
141
+ 'simulateIntentOnFork requires provider: "tenderly" with tenderlyConfig. Solana fork simulation is planned for v2.'
142
+ );
143
+ }
144
+ const tenderlyConfig = config.tenderlyConfig;
145
+ const simulatedAt = Date.now();
146
+ let executionPlan;
147
+ try {
148
+ executionPlan = getExecutionPlan(intent);
149
+ } catch (err) {
150
+ throw new Error(`Intent graph is invalid: ${err.message}`);
151
+ }
152
+ const stepMap = new Map(intent.steps.map((s) => [s.id, s]));
153
+ let forkId;
154
+ let forkedAtBlock;
155
+ try {
156
+ const fork = await createFork(tenderlyConfig, chainId, config.blockNumber);
157
+ forkId = fork.forkId;
158
+ forkedAtBlock = fork.forkedAtBlock;
159
+ } catch (err) {
160
+ throw new Error(`Failed to create fork: ${err.message}`);
161
+ }
162
+ const stepResults = [];
163
+ let cumulativePosition = [];
164
+ let wouldAllSucceed = true;
165
+ let failingStepId;
166
+ try {
167
+ for (const stepId of executionPlan) {
168
+ const step = stepMap.get(stepId);
169
+ if (step === void 0) continue;
170
+ const txParams = buildForkTransactionParams(step.action, config.fromAddress);
171
+ let simResult;
172
+ try {
173
+ simResult = await simulateOnFork(
174
+ tenderlyConfig,
175
+ forkId,
176
+ txParams,
177
+ chainId,
178
+ forkedAtBlock
179
+ );
180
+ } catch (err) {
181
+ stepResults.push({
182
+ stepId,
183
+ simulation: {
184
+ success: false,
185
+ wouldRevert: false,
186
+ chain: chainId,
187
+ simulatedAtBlock: forkedAtBlock,
188
+ gasEstimate: 0n,
189
+ gasBufferApplied: 1,
190
+ coverageLevel: "none",
191
+ caveats: ["state_may_diverge"],
192
+ provider: "tenderly"
193
+ },
194
+ stateChanges: [],
195
+ cumulativePosition: [...cumulativePosition],
196
+ wouldRevert: false
197
+ });
198
+ if (step.optional !== true) {
199
+ wouldAllSucceed = false;
200
+ if (failingStepId === void 0) failingStepId = stepId;
201
+ }
202
+ continue;
203
+ }
204
+ const stepPositionChanges = simResult.stateChanges.filter((sc) => sc.delta !== 0n).map((sc) => ({
205
+ token: sc.token ?? "native",
206
+ amount: sc.delta,
207
+ chain: chainId
208
+ }));
209
+ cumulativePosition = mergePositionChanges(cumulativePosition, stepPositionChanges);
210
+ const revertReason = simResult.revertReason;
211
+ const simulation = {
212
+ success: !simResult.wouldRevert,
213
+ wouldRevert: simResult.wouldRevert,
214
+ ...revertReason !== void 0 ? { revertReason } : {},
215
+ chain: chainId,
216
+ simulatedAtBlock: forkedAtBlock,
217
+ gasEstimate: BigInt(simResult.gasUsed),
218
+ gasBufferApplied: 1,
219
+ coverageLevel: "deep",
220
+ caveats: ["state_may_diverge"],
221
+ provider: "tenderly",
222
+ trace: {
223
+ callTrace: simResult.rawTrace,
224
+ stateDiff: simResult.stateChanges,
225
+ logs: [],
226
+ gasUsed: simResult.gasUsed
227
+ }
228
+ };
229
+ stepResults.push({
230
+ stepId,
231
+ simulation,
232
+ stateChanges: simResult.stateChanges,
233
+ cumulativePosition: [...cumulativePosition],
234
+ wouldRevert: simResult.wouldRevert,
235
+ ...revertReason !== void 0 ? { revertReason } : {}
236
+ });
237
+ if (simResult.wouldRevert && step.optional !== true) {
238
+ wouldAllSucceed = false;
239
+ if (failingStepId === void 0) failingStepId = stepId;
240
+ }
241
+ }
242
+ } finally {
243
+ await deleteFork(tenderlyConfig, forkId);
244
+ }
245
+ return {
246
+ forkId,
247
+ chain: chainId,
248
+ forkedAtBlock,
249
+ steps: stepResults,
250
+ finalPosition: cumulativePosition.filter((p) => p.amount !== 0n),
251
+ wouldAllSucceed,
252
+ ...failingStepId !== void 0 ? { failingStepId } : {},
253
+ simulatedAt
254
+ };
255
+ }
256
+
257
+ // src/simulate.ts
258
+ import { createPublicClient, http } from "viem";
259
+
260
+ // src/tenderly.ts
261
+ var TENDERLY_NETWORK_IDS = {
262
+ ethereum: "1",
263
+ arbitrum: "42161",
264
+ optimism: "10",
265
+ base: "8453"
266
+ };
267
+ var DEFAULT_GAS_BUFFER = 1.2;
268
+ async function simulateWithTenderly(action, chainId, tenderlyConfig, options) {
269
+ const networkId = TENDERLY_NETWORK_IDS[chainId];
270
+ if (networkId === void 0) {
271
+ throw new Error(`Tenderly simulation not supported for chain: ${chainId}`);
272
+ }
273
+ let to;
274
+ let value = "0";
275
+ let input = "0x";
276
+ if (action.kind === "swap") {
277
+ to = action.via;
278
+ } else if (action.kind === "transfer") {
279
+ to = action.to;
280
+ value = action.token.amount.toString();
281
+ } else {
282
+ to = action.contract;
283
+ if (action.value !== void 0) value = action.value.amount.toString();
284
+ if (action.calldata !== void 0) input = action.calldata;
285
+ }
286
+ const stateObjects = {};
287
+ if (options?.stateOverrides !== void 0) {
288
+ for (const [addr, override] of Object.entries(options.stateOverrides)) {
289
+ stateObjects[addr] = {
290
+ ...override.balance !== void 0 ? { balance: "0x" + override.balance.toString(16) } : {},
291
+ ...override.nonce !== void 0 ? { nonce: override.nonce } : {}
292
+ };
293
+ }
294
+ }
295
+ const url = `https://api.tenderly.co/api/v1/account/${tenderlyConfig.accountSlug}/project/${tenderlyConfig.projectSlug}/simulate`;
296
+ const body = {
297
+ network_id: networkId,
298
+ from: "0x0000000000000000000000000000000000000001",
299
+ to,
300
+ input,
301
+ value,
302
+ save_if_fails: false,
303
+ simulation_type: "full"
304
+ };
305
+ if (Object.keys(stateObjects).length > 0) {
306
+ body["state_objects"] = stateObjects;
307
+ }
308
+ const failResult = (wouldRevert2 = false) => ({
309
+ success: false,
310
+ wouldRevert: wouldRevert2,
311
+ chain: chainId,
312
+ simulatedAtBlock: 0,
313
+ gasEstimate: 0n,
314
+ gasBufferApplied: 0,
315
+ coverageLevel: "none",
316
+ caveats: ["state_may_diverge"],
317
+ provider: "tenderly"
318
+ });
319
+ let response;
320
+ try {
321
+ response = await fetch(url, {
322
+ method: "POST",
323
+ headers: {
324
+ "Content-Type": "application/json",
325
+ "X-Access-Key": tenderlyConfig.accessKey
326
+ },
327
+ body: JSON.stringify(body)
328
+ });
329
+ } catch {
330
+ return failResult();
331
+ }
332
+ if (!response.ok) {
333
+ return failResult();
334
+ }
335
+ const data = await response.json();
336
+ const sim = data.simulation;
337
+ const wouldRevert = !sim.status;
338
+ const gasEstimate = BigInt(Math.ceil(sim.gas_used * DEFAULT_GAS_BUFFER));
339
+ return {
340
+ success: !wouldRevert,
341
+ wouldRevert,
342
+ ...wouldRevert && sim.error_message !== void 0 ? { revertReason: sim.error_message } : {},
343
+ chain: chainId,
344
+ simulatedAtBlock: sim.block_number,
345
+ gasEstimate,
346
+ gasBufferApplied: DEFAULT_GAS_BUFFER,
347
+ coverageLevel: "deep",
348
+ caveats: [],
349
+ provider: "tenderly",
350
+ trace: {
351
+ callTrace: data.transaction?.call_trace ?? null,
352
+ stateDiff: data.transaction?.state_diff ?? null,
353
+ logs: data.transaction?.logs ?? [],
354
+ gasUsed: sim.gas_used
355
+ }
356
+ };
357
+ }
358
+
359
+ // src/simulate.ts
360
+ var PLACEHOLDER_FROM = "0x0000000000000000000000000000000000000001";
361
+ var DEFAULT_GAS_BUFFER2 = 1.2;
362
+ async function simulateEvmAction(action, chainId, rpcUrl, options, tenderlyConfig) {
363
+ if (tenderlyConfig !== void 0) {
364
+ return simulateWithTenderly(action, chainId, tenderlyConfig, options);
365
+ }
366
+ const chain = getViemChain(chainId);
367
+ const client = createPublicClient({ chain, transport: http(rpcUrl) });
368
+ try {
369
+ let gasEstimate;
370
+ if (action.kind === "swap") {
371
+ gasEstimate = 200000n;
372
+ } else if (action.kind === "transfer") {
373
+ gasEstimate = await client.estimateGas({
374
+ account: PLACEHOLDER_FROM,
375
+ to: action.to,
376
+ value: action.token.amount
377
+ });
378
+ } else {
379
+ gasEstimate = await client.estimateGas({
380
+ account: PLACEHOLDER_FROM,
381
+ to: action.contract,
382
+ value: 0n
383
+ });
384
+ }
385
+ const blockNumber = await client.getBlockNumber();
386
+ const caveats = ["state_may_diverge"];
387
+ if (action.kind === "swap") {
388
+ caveats.push("proxy_implementation_unverified");
389
+ }
390
+ return {
391
+ success: true,
392
+ wouldRevert: false,
393
+ chain: chainId,
394
+ simulatedAtBlock: Number(blockNumber),
395
+ gasEstimate,
396
+ gasBufferApplied: DEFAULT_GAS_BUFFER2,
397
+ coverageLevel: "basic",
398
+ caveats,
399
+ provider: "eth_call"
400
+ };
401
+ } catch {
402
+ return {
403
+ success: false,
404
+ wouldRevert: false,
405
+ chain: chainId,
406
+ simulatedAtBlock: 0,
407
+ gasEstimate: 0n,
408
+ gasBufferApplied: 0,
409
+ coverageLevel: "none",
410
+ caveats: ["state_may_diverge"],
411
+ provider: "eth_call"
412
+ };
413
+ }
414
+ }
415
+
416
+ // src/build.ts
417
+ import { getAddress } from "viem";
418
+ async function buildEvmTransaction(action, chainId, rpcUrl, gasEstimate, gasBufferMultiplier, _fromAddress) {
419
+ const chain = getViemChain(chainId);
420
+ const gas = BigInt(Math.ceil(Number(gasEstimate) * gasBufferMultiplier));
421
+ let to;
422
+ let value;
423
+ let data;
424
+ if (action.kind === "swap") {
425
+ to = getAddress(action.via);
426
+ value = 0n;
427
+ data = action.calldata ?? "0x";
428
+ } else if (action.kind === "transfer") {
429
+ to = getAddress(action.to);
430
+ value = action.token.amount;
431
+ data = "0x";
432
+ } else {
433
+ to = getAddress(action.contract);
434
+ value = action.value?.amount ?? 0n;
435
+ data = action.calldata ?? "0x";
436
+ }
437
+ return { chain: chainId, to, value, data, gas, chainId: chain.id, rpcUrl };
438
+ }
439
+
440
+ // src/broadcast.ts
441
+ import { createPublicClient as createPublicClient2, http as http2 } from "viem";
442
+
443
+ // src/mev.ts
444
+ function getMevProtectedRpcUrl(mode, config, fallbackRpcUrl) {
445
+ switch (mode) {
446
+ case "flashbots":
447
+ return config?.flashbots?.rpcUrl ?? "https://rpc.flashbots.net";
448
+ case "mev-blocker":
449
+ return config?.mevBlocker?.rpcUrl ?? "https://rpc.mevblocker.io";
450
+ case "none":
451
+ case void 0:
452
+ default:
453
+ return fallbackRpcUrl ?? "https://ethereum.publicnode.com";
454
+ }
455
+ }
456
+ function buildFlashbotsAuthHeader(_body, _authSignerKey) {
457
+ return void 0;
458
+ }
459
+ async function broadcastWithMevProtection(signedTxHex, mode, config, fallbackRpcUrl) {
460
+ const rpcUrl = getMevProtectedRpcUrl(mode, config, fallbackRpcUrl);
461
+ const body = JSON.stringify({
462
+ jsonrpc: "2.0",
463
+ method: "eth_sendRawTransaction",
464
+ params: [signedTxHex],
465
+ id: 1
466
+ });
467
+ const headers = {
468
+ "Content-Type": "application/json"
469
+ };
470
+ if (mode === "flashbots" && config?.flashbots?.authSignerKey !== void 0) {
471
+ const authHeader = buildFlashbotsAuthHeader(body, config.flashbots.authSignerKey);
472
+ if (authHeader !== void 0) {
473
+ headers["X-Flashbots-Signature"] = authHeader;
474
+ }
475
+ }
476
+ const response = await fetch(rpcUrl, {
477
+ method: "POST",
478
+ headers,
479
+ body
480
+ });
481
+ if (!response.ok) {
482
+ throw new Error(
483
+ `MEV-protected broadcast failed: ${response.status} ${await response.text()}`
484
+ );
485
+ }
486
+ const data = await response.json();
487
+ if (data.error !== void 0) {
488
+ throw new Error(`MEV-protected broadcast error: ${data.error.message}`);
489
+ }
490
+ if (data.result === void 0) {
491
+ throw new Error("MEV-protected broadcast: no txHash in response");
492
+ }
493
+ return data.result;
494
+ }
495
+
496
+ // src/broadcast.ts
497
+ async function broadcastAndConfirm(signedTx, action, chainId, rpcUrl, evaluation, simulation, mevProtection, mevConfig) {
498
+ const chain = getViemChain(chainId);
499
+ const publicClient = createPublicClient2({ chain, transport: http2(rpcUrl) });
500
+ let txHash;
501
+ if (mevProtection !== void 0 && mevProtection !== "none") {
502
+ const hash = await broadcastWithMevProtection(signedTx, mevProtection, mevConfig, rpcUrl);
503
+ txHash = hash;
504
+ } else {
505
+ txHash = await publicClient.sendRawTransaction({ serializedTransaction: signedTx });
506
+ }
507
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
508
+ return {
509
+ status: "success",
510
+ action,
511
+ policyEvaluation: evaluation,
512
+ simulation,
513
+ txHash,
514
+ confirmedAtBlock: Number(receipt.blockNumber),
515
+ confirmedAtMs: Date.now(),
516
+ gasUsed: receipt.gasUsed
517
+ };
518
+ }
519
+
520
+ // src/execute.ts
521
+ async function executeEvmAction(action, chainId, rpcUrl, signer, evaluation, simulation, mevProtection, mevConfig) {
522
+ const serializedTx = await buildEvmTransaction(
523
+ action,
524
+ chainId,
525
+ rpcUrl,
526
+ simulation.gasEstimate,
527
+ 1.2,
528
+ signer.address
529
+ );
530
+ const signedTx = await signer.sign(serializedTx);
531
+ return broadcastAndConfirm(
532
+ signedTx,
533
+ action,
534
+ chainId,
535
+ rpcUrl,
536
+ evaluation,
537
+ simulation,
538
+ mevProtection,
539
+ mevConfig
540
+ );
541
+ }
542
+
543
+ // src/signers.ts
544
+ import { createWalletClient, http as http3 } from "viem";
545
+ import { privateKeyToAccount } from "viem/accounts";
546
+ function privateKeySigner(privateKey) {
547
+ const account = privateKeyToAccount(privateKey);
548
+ return {
549
+ address: account.address,
550
+ sign: async (tx) => {
551
+ const chain = getViemChain(tx.chain);
552
+ const client = createWalletClient({
553
+ account,
554
+ chain,
555
+ transport: http3(tx.rpcUrl)
556
+ });
557
+ const request = await client.prepareTransactionRequest({
558
+ to: tx.to,
559
+ value: tx.value,
560
+ data: tx.data,
561
+ gas: tx.gas
562
+ });
563
+ const serialized = await client.signTransaction(request);
564
+ return serialized;
565
+ }
566
+ };
567
+ }
568
+
569
+ // src/verify.ts
570
+ import { createPublicClient as createPublicClient3, http as http4, keccak256 } from "viem";
571
+ function createEvmMetadataVerifier(rpcUrls) {
572
+ return {
573
+ async verifyContract(entry) {
574
+ const rpcUrl = rpcUrls[entry.chain];
575
+ if (rpcUrl === void 0) throw new Error("no rpcUrl for chain: " + entry.chain);
576
+ const chain = getViemChain(entry.chain);
577
+ const client = createPublicClient3({ chain, transport: http4(rpcUrl) });
578
+ if (entry.bytecodeHash !== void 0) {
579
+ const bytecode = await client.getBytecode({ address: entry.address });
580
+ if (bytecode === void 0 || bytecode === "0x") {
581
+ return { verified: false, reason: "bytecode_hash_mismatch" };
582
+ }
583
+ const hash = keccak256(bytecode);
584
+ if (hash !== entry.bytecodeHash) {
585
+ return { verified: false, reason: "bytecode_hash_mismatch" };
586
+ }
587
+ }
588
+ if (entry.ownerAddress !== void 0) {
589
+ try {
590
+ const result = await client.readContract({
591
+ address: entry.address,
592
+ abi: [{ name: "owner", type: "function", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" }],
593
+ functionName: "owner"
594
+ });
595
+ if (result.toLowerCase() !== entry.ownerAddress.toLowerCase()) {
596
+ return { verified: false, reason: "owner_address_mismatch" };
597
+ }
598
+ } catch {
599
+ }
600
+ }
601
+ return { verified: true };
602
+ }
603
+ };
604
+ }
605
+ export {
606
+ broadcastWithMevProtection,
607
+ buildEvmTransaction,
608
+ buildForkTransactionParams,
609
+ createEvmMetadataVerifier,
610
+ createFork,
611
+ deleteFork,
612
+ executeEvmAction,
613
+ getMevProtectedRpcUrl,
614
+ getViemChain,
615
+ privateKeySigner,
616
+ simulateEvmAction,
617
+ simulateIntentOnFork,
618
+ simulateOnFork,
619
+ simulateWithTenderly
620
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@txfence/evm",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "dependencies": {
19
+ "viem": "^2.48.8",
20
+ "@txfence/core": "0.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "vitest": "^4.1.5"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "clean": "rm -rf dist",
29
+ "test": "vitest run --passWithNoTests",
30
+ "test:watch": "vitest"
31
+ }
32
+ }