-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce parseSwapV2 for Settler
- Loading branch information
Showing
3 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { erc20Abi, getAddress, formatUnits } from "viem"; | ||
import type { | ||
Chain, | ||
Address, | ||
Transport, | ||
PublicClient, | ||
TransactionReceipt, | ||
} from "viem"; | ||
|
||
const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; | ||
|
||
type SupportedChainId = 1 | 8453; | ||
|
||
export const NATIVE_SYMBOL_BY_CHAIN_ID: { [key in SupportedChainId]: string } = | ||
{ | ||
1: "ETH", // Ethereum | ||
8453: "ETH", // Base | ||
}; | ||
|
||
export function isChainIdSupported( | ||
chainId: number | ||
): chainId is SupportedChainId { | ||
return [1, 10, 56, 137, 8453, 42161].includes(chainId); | ||
} | ||
|
||
export interface EnrichLogsArgs { | ||
transactionReceipt: TransactionReceipt; | ||
publicClient: PublicClient<Transport, Chain>; | ||
} | ||
export interface EnrichedLog { | ||
to: Address; | ||
from: Address; | ||
symbol: string; | ||
amount: string; | ||
address: Address; | ||
decimals: number; | ||
} | ||
|
||
export async function transferLogs({ | ||
publicClient, | ||
transactionReceipt, | ||
}: EnrichLogsArgs): Promise<any> { | ||
const EVENT_SIGNATURES = { | ||
Transfer: | ||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", | ||
} as const; | ||
const { logs } = transactionReceipt; | ||
const transferLogsAddresses = logs | ||
.filter((log) => log.topics[0] === EVENT_SIGNATURES.Transfer) | ||
.map((log) => ({ ...log, address: getAddress(log.address) })); | ||
const contracts = [ | ||
...transferLogsAddresses.map((log) => ({ | ||
abi: erc20Abi, | ||
address: log.address, | ||
functionName: "symbol", | ||
})), | ||
...transferLogsAddresses.map((log) => ({ | ||
abi: erc20Abi, | ||
address: log.address, | ||
functionName: "decimals", | ||
})), | ||
]; | ||
const results = await publicClient.multicall({ contracts }); | ||
const midpoint = Math.floor(results.length / 2); | ||
const enrichedLogs = transferLogsAddresses | ||
.map((log, index) => { | ||
const symbol = results[index].result as string; | ||
const decimals = results[midpoint + index].result as number; | ||
const amount = | ||
log.data === "0x" ? "0" : formatUnits(BigInt(log.data), decimals); | ||
const { address, topics } = log; | ||
const { 1: fromHex, 2: toHex } = topics; | ||
const from = getAddress(convertHexToAddress(fromHex)); | ||
const to = getAddress(convertHexToAddress(toHex)); | ||
|
||
return { to, from, symbol, amount, address, decimals }; | ||
}) | ||
.filter((log) => log.amount !== "0"); | ||
|
||
return enrichedLogs; | ||
} | ||
|
||
function convertHexToAddress(hexString: string): string { | ||
return `0x${hexString.slice(-40)}`; | ||
} | ||
|
||
export async function parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}: { | ||
publicClient: PublicClient<Transport, Chain>; | ||
transactionHash: Address; | ||
}) { | ||
const chainId = await publicClient.getChainId(); | ||
if (!isChainIdSupported(chainId)) { | ||
throw new Error(`chainId ${chainId} is unsupported…`); | ||
} | ||
|
||
const transactionReceipt = await publicClient.getTransactionReceipt({ | ||
hash: transactionHash, | ||
}); | ||
const { value } = await publicClient.getTransaction({ | ||
hash: transactionHash, | ||
}); | ||
const isNativeSell = value > 0n; | ||
const logs = await transferLogs({ | ||
publicClient, | ||
transactionReceipt, | ||
}); | ||
|
||
const input = logs[0]; | ||
const output = logs[logs.length - 1]; | ||
if (isNativeSell) { | ||
const sellAmount = formatUnits(value, 18); | ||
return { | ||
tokenIn: { | ||
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId], | ||
amount: sellAmount, | ||
address: NATIVE_TOKEN_ADDRESS, | ||
}, | ||
tokenOut: { | ||
symbol: output.symbol, | ||
amount: output.amount, | ||
address: output.address, | ||
}, | ||
}; | ||
} | ||
return { | ||
tokenIn: { | ||
symbol: input.symbol, | ||
amount: input.amount, | ||
address: input.address, | ||
}, | ||
tokenOut: { | ||
symbol: output.symbol, | ||
amount: output.amount, | ||
address: output.address, | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { http, createPublicClient } from "viem"; | ||
import { mainnet } from "viem/chains"; | ||
import { test, expect } from "vitest"; | ||
import { parseSwapV2 } from "../index"; | ||
|
||
require("dotenv").config(); | ||
|
||
if (!process.env.ETH_MAINNET_RPC) { | ||
throw new Error("An RPC URL required."); | ||
} | ||
|
||
const publicClient = createPublicClient({ | ||
chain: mainnet, | ||
transport: http(process.env.ETH_MAINNET_RPC), | ||
}); | ||
|
||
// https://etherscan.io/tx/0x2b9a12398613887e9813594e8583f488f0e8392d8e6e0ba8d9e140065826dd00 | ||
test("parses swapped amounts case 1 (default)", async () => { | ||
const transactionHash = | ||
"0x2b9a12398613887e9813594e8583f488f0e8392d8e6e0ba8d9e140065826dd00"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "30.084159", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "USDT", | ||
amount: "30.069172", | ||
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0x76b744ab42b05b30624bd5027b4f7da841cdc357bb1d6ee74e3d9e049dd8a126 | ||
test("parses swapped amounts case 2 (default)", async () => { | ||
const transactionHash = | ||
"0x76b744ab42b05b30624bd5027b4f7da841cdc357bb1d6ee74e3d9e049dd8a126"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "1", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "WETH", | ||
amount: "0.000280757770903965", | ||
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0x565e8e0582b620ee06618ee0b7705dc0e7f56dfd88b5eb3e008c0858f6f806d8 | ||
test("parses swapped amounts case 3 (default)", async () => { | ||
const transactionHash = | ||
"0x565e8e0582b620ee06618ee0b7705dc0e7f56dfd88b5eb3e008c0858f6f806d8"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "8.15942", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "WBTC", | ||
amount: "0.00013188", | ||
address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0xd024750c7dcb99ace02c6b083c68d73dcfebdee252ccbeb1b83981b609693271 | ||
test("parses swapped amounts case 4 (default)", async () => { | ||
const transactionHash = | ||
"0xd024750c7dcb99ace02c6b083c68d73dcfebdee252ccbeb1b83981b609693271"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "335.142587", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "WETH", | ||
amount: "0.105662100963455883", | ||
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0x4cbcf2e2512adb7e28f19f8cf28ddc29a9f9fea93c842cf3b735eeb526fe34b3 | ||
test("parses swapped amounts case 5 (native sell token)", async () => { | ||
const transactionHash = | ||
"0x4cbcf2e2512adb7e28f19f8cf28ddc29a9f9fea93c842cf3b735eeb526fe34b3"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "ETH", | ||
amount: "0.04", | ||
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", | ||
}, | ||
tokenOut: { | ||
symbol: "USDC", | ||
amount: "126.580558", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0x28c5bb3768bb64e81e1f3753ed1a8c30f0484a434d6c2b4af825d258ecb3bcf0 | ||
test("parses swapped amounts case 6 (buy DNT 404 token)", async () => { | ||
const transactionHash = | ||
"0x28c5bb3768bb64e81e1f3753ed1a8c30f0484a434d6c2b4af825d258ecb3bcf0"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "95", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "CHIB", | ||
amount: "10.527633901274097318", | ||
address: "0x7068263EDa099fB93BB3215c05e728c0b54b3137", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://etherscan.io/tx/0xb8beef6bf857f2fc22905b2872120abc634900b45941478aa9cf0ad1ceffcd67 | ||
// https://gopluslabs.io/token-security/1/0xcf0c122c6b73ff809c693db761e7baebe62b6a2e | ||
test("parses swapped amounts case 6 (buy FoT token, FLOKI)", async () => { | ||
const transactionHash = | ||
"0xb8beef6bf857f2fc22905b2872120abc634900b45941478aa9cf0ad1ceffcd67"; | ||
|
||
const result = await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
tokenIn: { | ||
symbol: "USDC", | ||
amount: "31.580558", | ||
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", | ||
}, | ||
tokenOut: { | ||
symbol: "FLOKI", | ||
amount: "172036.330384861", | ||
address: "0xcf0C122c6b73ff809C693DB761e7BaeBe62b6a2E", | ||
}, | ||
}); | ||
}); | ||
|
||
// https://explorer.celo.org/tx/0x615c5089f772a8f2074750e8c6070013d288af7681435aba1771f6bfc63d1286 | ||
test("throws an error for unsupported chains)", async () => { | ||
const transactionHash = | ||
"0x615c5089f772a8f2074750e8c6070013d288af7681435aba1771f6bfc63d1286"; | ||
|
||
const publicClient = createPublicClient({ | ||
chain: mainnet, | ||
transport: http("https://rpc.ankr.com/celo"), | ||
}); | ||
|
||
expect(async () => { | ||
await parseSwapV2({ | ||
publicClient, | ||
transactionHash, | ||
}); | ||
}).rejects.toThrowError("chainId 42220 is unsupported…"); | ||
}); |