Skip to content

Commit f68007e

Browse files
feat: add last transfer heuristic for transactions which fail to parse (#93)
1 parent 10b9607 commit f68007e

File tree

5 files changed

+198
-10
lines changed

5 files changed

+198
-10
lines changed

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@
33
# Create an Alchemy API key: https://docs.alchemy.com/docs/alchemy-quickstart-guide
44
# Or even better, run a local node: https://ethereum.org/en/run-a-node and use it's RPC URL.
55
ETH_MAINNET_RPC="https://eth-mainnet.g.alchemy.com/v2/<your-api-key>"
6+
ALCHEMY_API_KEY="your-alchemy-api-key-here"
7+
8+
9+
# Required: Quicknode API keys for specific chain tests
10+
# Sign up at https://quicknode.com for free trial with debug method support
11+
QUICKNODE_BNB_CHAIN_API_KEY="your-quicknode-bnb-key"
12+
QUICKNODE_MANTLE_API_KEY="your-quicknode-mantle-key"
13+
QUICKNODE_MODE_API_KEY="your-quicknode-mode-key"

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
<br>Mantle
6161
</div>
6262
</td>
63-
<td style="width:100px; text-align:center;">
63+
<td style="width:100px; text-align:center;">
6464
<div align="center">
6565
<img alt="mantle" src="https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/monad/info/logo.png" width="22"/>
6666
<br>Monad
@@ -84,14 +84,14 @@
8484
<br>Scroll
8585
</div>
8686
</td>
87+
</tr>
88+
<tr>
8789
<td style="width:100px; text-align:center;">
8890
<div align="center">
8991
<img alt="coming soon" src="https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/mode/info/logo.png" width="22"/>
9092
<br>Mode
9193
</div>
9294
</td>
93-
</tr>
94-
<tr>
9595
<td style="width:100px; text-align:center;">
9696
<div align="center">
9797
<img alt="Worldchain" src="https://cdn.prod.website-files.com/6503306c491d20f69e484470/6718ce22ee5879d832765fd6_66ced64f18a10922ffcff77d_65d8bce782514cfb6c149b7a_1VQdZPHJ_400x400.webp" width="22"/>
@@ -121,15 +121,15 @@
121121
<img alt="coming soon" src="https://i.imgur.com/CexTjqF.png" width="22"/>
122122
<br>🔜
123123
</div>
124-
</td>
124+
</td>
125+
</tr>
126+
<tr>
125127
<td style="width:100px; text-align:center;">
126128
<div align="center">
127129
<img alt="coming soon" src="https://cdn.prod.website-files.com/63692bf32544bee8b1836ea6/637b01428c7bd8e16af26756_favicon-32.png" width="22"/>
128130
<br>🔜
129131
</div>
130-
</td>
131-
</tr>
132-
<tr>
132+
</td>
133133
<td style="width:100px; text-align:center;">
134134
<div align="center">
135135
<img alt="coming soon" src="https://i.imgur.com/OPA8A9u.png" width="22"/>

src/constants.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,71 @@ import {
1919
} from "viem/chains";
2020
import type { SupportedChainId } from "./types";
2121

22+
23+
export const FORWARDING_MULTICALL_ABI = [
24+
{
25+
"type": "receive",
26+
"stateMutability": "payable"
27+
},
28+
{
29+
"type": "function",
30+
"name": "multicall",
31+
"inputs": [
32+
{
33+
"name": "calls",
34+
"type": "tuple[]",
35+
"internalType": "struct IMultiCall.Call[]",
36+
"components": [
37+
{
38+
"name": "target",
39+
"type": "address",
40+
"internalType": "address"
41+
},
42+
{
43+
"name": "revertPolicy",
44+
"type": "uint8",
45+
"internalType": "enum IMultiCall.RevertPolicy"
46+
},
47+
{
48+
"name": "value",
49+
"type": "uint256",
50+
"internalType": "uint256"
51+
},
52+
{
53+
"name": "data",
54+
"type": "bytes",
55+
"internalType": "bytes"
56+
}
57+
]
58+
},
59+
{
60+
"name": "contextdepth",
61+
"type": "uint256",
62+
"internalType": "uint256"
63+
}
64+
],
65+
"outputs": [
66+
{
67+
"name": "",
68+
"type": "tuple[]",
69+
"internalType": "struct IMultiCall.Result[]",
70+
"components": [
71+
{
72+
"name": "success",
73+
"type": "bool",
74+
"internalType": "bool"
75+
},
76+
{
77+
"name": "data",
78+
"type": "bytes",
79+
"internalType": "bytes"
80+
}
81+
]
82+
}
83+
],
84+
"stateMutability": "payable"
85+
}
86+
]
2287
export const SETTLER_META_TXN_ABI = [
2388
{
2489
inputs: [
@@ -92,6 +157,8 @@ export const NATIVE_SYMBOL_BY_CHAIN_ID: { [key in SupportedChainId]: string } =
92157

93158
export const NATIVE_TOKEN_ADDRESS = `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`;
94159

160+
export const FORWARDING_MULTICALL_ADDRESS = `0x00000000000000cf9e3c5a26621af382fa17f24f`;
95161
export const MULTICALL3_ADDRESS = `0xcA11bde05977b3631167028862bE2a173976CA11`;
96162

163+
97164
export const ERC_4337_ENTRY_POINT = `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`;

src/index.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
} from "viem";
88
import {
99
SUPPORTED_CHAINS,
10-
MULTICALL3_ADDRESS,
10+
FORWARDING_MULTICALL_ABI,
1111
FUNCTION_SELECTORS,
1212
ERC_4337_ENTRY_POINT,
1313
NATIVE_TOKEN_ADDRESS,
1414
SETTLER_META_TXN_ABI,
1515
NATIVE_SYMBOL_BY_CHAIN_ID,
16+
FORWARDING_MULTICALL_ADDRESS,
17+
MULTICALL3_ADDRESS,
1618
} from "./constants";
1719
import {
1820
transferLogs,
@@ -112,7 +114,40 @@ export async function parseSwap({
112114
amount: nativeAmountToTaker,
113115
address: NATIVE_TOKEN_ADDRESS,
114116
};
117+
if (to?.toLowerCase() === FORWARDING_MULTICALL_ADDRESS.toLowerCase()) {
118+
const { args: multicallArgs } = decodeFunctionData({
119+
abi: FORWARDING_MULTICALL_ABI,
120+
data: transaction.input,
121+
});
115122

123+
if (multicallArgs && Array.isArray(multicallArgs) && multicallArgs[0] && Array.isArray(multicallArgs[0])) {
124+
const { args: settlerArgs } = decodeFunctionData({
125+
abi: SETTLER_META_TXN_ABI,
126+
data: multicallArgs[0][1]?.data,
127+
});
128+
129+
const recipient =
130+
settlerArgs[0].recipient.toLowerCase() as Address;
131+
132+
const msgSender = settlerArgs[3];
133+
134+
const nativeAmountToTaker = calculateNativeTransfer(trace, {
135+
recipient,
136+
});
137+
138+
if (nativeAmountToTaker === "0") {
139+
[output] = logs.filter(
140+
(log) => log.to.toLowerCase() === msgSender.toLowerCase()
141+
);
142+
} else {
143+
output = {
144+
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
145+
amount: nativeAmountToTaker,
146+
address: NATIVE_TOKEN_ADDRESS,
147+
};
148+
}
149+
}
150+
}
116151
if (to?.toLowerCase() === MULTICALL3_ADDRESS.toLowerCase()) {
117152
const { args: multicallArgs } = decodeFunctionData({
118153
abi: multicall3Abi,
@@ -122,7 +157,7 @@ export async function parseSwap({
122157
if (multicallArgs[0]) {
123158
const { args: settlerArgs } = decodeFunctionData({
124159
abi: SETTLER_META_TXN_ABI,
125-
data: multicallArgs[0][1].callData,
160+
data: multicallArgs[0][1]?.callData,
126161
});
127162

128163
const takerForGaslessApprovalSwap =
@@ -245,4 +280,4 @@ export async function parseSwap({
245280
address: output.address,
246281
},
247282
};
248-
}
283+
}

src/tests/index.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,3 +1455,81 @@ test("logs a warning for reverted transactions)", async () => {
14551455

14561456
warnSpy.mockRestore();
14571457
});
1458+
1459+
//https://etherscan.io/tx/0x7eea91c5c715ef4bb1e39ddf4c7832113693e87c18392740353d5ae669406a46
1460+
test("parse a swap on Ethereum (USDC for WMON) with SettlerIntent", async () => {
1461+
const transactionHash = "0x7eea91c5c715ef4bb1e39ddf4c7832113693e87c18392740353d5ae669406a46";
1462+
1463+
const result = await parseSwap({
1464+
publicClient: publicClient,
1465+
transactionHash,
1466+
});
1467+
1468+
expect(result).toEqual({
1469+
tokenIn: {
1470+
symbol: "USDC",
1471+
amount: "1000.080833",
1472+
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1473+
},
1474+
tokenOut: {
1475+
symbol: "WMON",
1476+
amount: "22204.573291811240325155",
1477+
address: "0x6917037F8944201b2648198a89906Edf863B9517",
1478+
},
1479+
});
1480+
});
1481+
1482+
// https://optimistic.etherscan.io/tx/0xdee6f4fea0250f297ed9663c4ca4479e8a253c62e16faa60759e25832cd1f34f
1483+
test("parse a swap on Optimism (wstETH for ETH) via Balancer pool", async () => {
1484+
const publicClient = createPublicClient({
1485+
chain: optimism,
1486+
transport: http(
1487+
`https://opt-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
1488+
),
1489+
}) as PublicClient<Transport, Chain>;
1490+
1491+
const transactionHash =
1492+
"0xdee6f4fea0250f297ed9663c4ca4479e8a253c62e16faa60759e25832cd1f34f";
1493+
1494+
const result = await parseSwap({
1495+
publicClient,
1496+
transactionHash,
1497+
});
1498+
1499+
expect(result).toEqual({
1500+
tokenIn: {
1501+
symbol: "wstETH",
1502+
amount: "0.008868",
1503+
address: "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb",
1504+
},
1505+
tokenOut: {
1506+
symbol: "ETH",
1507+
amount: "0.010671015314389981",
1508+
address: NATIVE_TOKEN_ADDRESS,
1509+
},
1510+
});
1511+
});
1512+
1513+
// https://etherscan.io/tx/0x967cb342227a2541a845058862e70833d638bf5bb7ce229c6506466dbb43a004
1514+
test("parse a swap on Mainnet (TRG for SHITCOIN)", async () => {
1515+
const transactionHash =
1516+
"0x967cb342227a2541a845058862e70833d638bf5bb7ce229c6506466dbb43a004" as `0x${string}`;
1517+
1518+
const result = await parseSwap({
1519+
publicClient,
1520+
transactionHash,
1521+
});
1522+
1523+
expect(result).toEqual({
1524+
tokenIn: {
1525+
symbol: "TRG",
1526+
amount: "5053634.405791388940070628",
1527+
address: "0x93eEB426782BD88fCD4B48D7b0368CF061044928",
1528+
},
1529+
tokenOut: {
1530+
symbol: "SHITCOIN",
1531+
amount: "881342.949331124",
1532+
address: "0x4fD1b29d1aAFeA37A2d19E7d912b6eda44dBd82C",
1533+
},
1534+
});
1535+
});

0 commit comments

Comments
 (0)