@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 +12 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +620 -0
- package/package.json +32 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|