Skip to content

Commit

Permalink
feat: introduce parseSwapV2 for Settler
Browse files Browse the repository at this point in the history
  • Loading branch information
hzhu committed Jun 19, 2024
1 parent d9483ca commit b408e3a
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ export async function parseSwap({
callData: transaction.input,
});
}

/**
* This function is specific to 0x API transactions on Settler.
*/
export { parseSwapV2 } from "./parseSwapV2";
140 changes: 140 additions & 0 deletions src/parseSwapV2.ts
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,
},
};
}
202 changes: 202 additions & 0 deletions src/tests/parseSwapV2.test.ts
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…");
});

0 comments on commit b408e3a

Please sign in to comment.