Skip to content

Commit

Permalink
feat: support parsing erc-4337 transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
hzhu committed Aug 18, 2024
1 parent 16e4b79 commit d1292d4
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 47 deletions.
14 changes: 7 additions & 7 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SupportedChainId } from "./types";

export const SETTLER_ABI = [
export const SETTLER_META_TXN_ABI = [
{
inputs: [
{
Expand All @@ -27,13 +27,11 @@ export const SETTLER_ABI = [
stateMutability: "nonpayable",
type: "function",
},
];
{ stateMutability: "payable", type: "receive" },
] as const;

export const FUNCTION_SELECTORS = { EXECUTE_META_TXN: "0xfd3ad6d4" };

export const NATIVE_TOKEN_ADDRESS =
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

export const NATIVE_SYMBOL_BY_CHAIN_ID: { [key in SupportedChainId]: string } =
{
1: "ETH", // Ethereum
Expand All @@ -45,6 +43,8 @@ export const NATIVE_SYMBOL_BY_CHAIN_ID: { [key in SupportedChainId]: string } =
43114: "AVAX", // Avalanche
};

export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const NATIVE_TOKEN_ADDRESS = `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`;

export const MULTICALL3_ADDRESS = `0xcA11bde05977b3631167028862bE2a173976CA11`;

export const NATIVE_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export const ERC_4337_ENTRY_POINT = `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`;
60 changes: 42 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@ import {
decodeFunctionData,
} from "viem";
import {
SETTLER_ABI,
MULTICALL3_ADDRESS,
FUNCTION_SELECTORS,
ERC_4337_ENTRY_POINT,
NATIVE_TOKEN_ADDRESS,
SETTLER_META_TXN_ABI,
NATIVE_SYMBOL_BY_CHAIN_ID,
} from "./constants";
import {
transferLogs,
isChainIdSupported,
extractNativeTransfer,
calculateNativeTransfer,
parseSmartContractWalletTx,
} from "./utils";
import type { Hash, Chain, Address, Transport, PublicClient } from "viem";
import type { TraceTransactionSchema } from "./types";

export async function parseSwap({
publicClient,
transactionHash: hash,
smartContractWallet,
}: {
publicClient: PublicClient<Transport, Chain>;
transactionHash: Address;
smartContractWallet?: Address;
}) {
const chainId = await publicClient.getChainId();

Expand All @@ -48,7 +52,11 @@ export async function parseSwap({

const { from: taker, value, to } = transaction;

const nativeTransferAmount = extractNativeTransfer(trace, taker);
const isToERC4337 = to === ERC_4337_ENTRY_POINT.toLowerCase();

const nativeAmountToTaker = calculateNativeTransfer(trace, {
recipient: taker,
});

const transactionReceipt = await publicClient.getTransactionReceipt({ hash });

Expand All @@ -59,20 +67,35 @@ export async function parseSwap({
transactionReceipt,
});

if (isToERC4337) {
if (!smartContractWallet) {
throw new Error(
"This is an ERC-4337 transaction. You must provide a smart contract wallet address to 0x-parser."
);
}

return parseSmartContractWalletTx({
logs,
trace,
chainId,
smartContractWallet,
});
}

const fromTaker = logs.filter(
(log) => log.from.toLowerCase() === taker.toLowerCase()
);

let input = fromTaker.length ? fromTaker[0] : logs[0];

let output =
nativeTransferAmount === "0"
nativeAmountToTaker === "0"
? logs.find((log) => {
return log.to.toLowerCase() === taker.toLowerCase();
})
: {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: nativeTransferAmount,
amount: nativeAmountToTaker,
address: NATIVE_TOKEN_ADDRESS,
};

Expand All @@ -82,41 +105,42 @@ export async function parseSwap({
data: transaction.input,
});

const { args: settlerArgs } = decodeFunctionData<any[]>({
abi: SETTLER_ABI,
const { args: settlerArgs } = decodeFunctionData({
abi: SETTLER_META_TXN_ABI,
data: multicallArgs[0][1].callData,
});

const takerForGaslessApprovalSwap =
settlerArgs[0].recipient.toLowerCase() as Address;

const nativeTransferAmount = extractNativeTransfer(
trace,
takerForGaslessApprovalSwap
);
const nativeAmountToTaker = calculateNativeTransfer(trace, {
recipient: takerForGaslessApprovalSwap,
});

if (nativeTransferAmount === "0") {
if (nativeAmountToTaker === "0") {
output = output = logs[logs.length - 1];
} else {
output = {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: nativeTransferAmount,
amount: nativeAmountToTaker,
address: NATIVE_TOKEN_ADDRESS,
};
}
}

if (transaction.input.startsWith(FUNCTION_SELECTORS.EXECUTE_META_TXN)) {
const { args } = decodeFunctionData<any[]>({
abi: SETTLER_ABI,
const { args } = decodeFunctionData({
abi: SETTLER_META_TXN_ABI,
data: transaction.input,
});

const { 3: msgSender } = args;

const nativeTransferAmount = extractNativeTransfer(trace, msgSender);
const nativeAmountToTaker = calculateNativeTransfer(trace, {
recipient: msgSender,
});

if (nativeTransferAmount === "0") {
if (nativeAmountToTaker === "0") {
output = logs[logs.length - 1];
const takerReceived = logs.filter(
(log) => log.to.toLowerCase() === msgSender.toLowerCase()
Expand All @@ -138,7 +162,7 @@ export async function parseSwap({
} else {
output = {
symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId],
amount: nativeTransferAmount,
amount: nativeAmountToTaker,
address: NATIVE_TOKEN_ADDRESS,
};
}
Expand Down
131 changes: 124 additions & 7 deletions src/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { arbitrum, base, mainnet, optimism, polygon } from "viem/chains";
import { test, expect } from "vitest";
import { parseSwap } from "../index";
import { NATIVE_ETH_ADDRESS } from "../constants";
import { NATIVE_TOKEN_ADDRESS } from "../constants";

require("dotenv").config();

Expand Down Expand Up @@ -347,7 +347,7 @@ test("parse a swap on Base (DEGEN for ETH)", async () => {
tokenOut: {
symbol: "ETH",
amount: "0.006410046715601835",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
});
});
Expand All @@ -373,7 +373,7 @@ test("parse a swap on Base (ETH for BRETT)", async () => {
tokenIn: {
symbol: "ETH",
amount: "0.027500863104380774",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
tokenOut: {
symbol: "BRETT",
Expand Down Expand Up @@ -409,7 +409,7 @@ test("parse a gasless approval + gasless swap on Polygon (USDC for MATIC)", asyn
tokenOut: {
symbol: "MATIC",
amount: "15.513683571865599415",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
});
});
Expand Down Expand Up @@ -471,7 +471,7 @@ test("parse a gasless swap on Base (USDC for ETH) for SettlerMetaTxn", async ()
tokenOut: {
symbol: "ETH",
amount: "0.006847116541535933",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
});
});
Expand Down Expand Up @@ -502,7 +502,7 @@ test("parse a gasless swap on Base (weirdo for ETH) for SettlerMetaTxn", async (
tokenOut: {
symbol: "ETH",
amount: "0.039633073597929391",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
});
});
Expand Down Expand Up @@ -564,7 +564,7 @@ test("parse a gasless swap on on Arbitrum (ARB for ETH)", async () => {
tokenOut: {
symbol: "ETH",
amount: "0.000304461782666722",
address: NATIVE_ETH_ADDRESS,
address: NATIVE_TOKEN_ADDRESS,
},
});
});
Expand Down Expand Up @@ -661,3 +661,120 @@ test("parse a swap on BNB Chain (ETH for USDC) for execute", async () => {
},
});
});

test("throws when smart contract wallet is not passed", async () => {
const publicClient = createPublicClient({
chain: base,
transport: http(
`https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
),
}) as PublicClient<Transport, Chain>;

const transactionHash =
"0x756289cdedd4c007268ef208fe2758a9fb6efd49fb241397b67089512b497662";

expect(async () => {
await parseSwap({
publicClient,
transactionHash,
});
}).rejects.toThrowError(
"This is an ERC-4337 transaction. You must provide a smart contract wallet address to 0x-parser."
);
});

// https://basescan.org/tx/0x756289cdedd4c007268ef208fe2758a9fb6efd49fb241397b67089512b497662
test("parse a swap on Base (DEGEN for BRETT) with smart contract wallet", async () => {
const publicClient = createPublicClient({
chain: base,
transport: http(
`https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
),
}) as PublicClient<Transport, Chain>;

const transactionHash =
"0x756289cdedd4c007268ef208fe2758a9fb6efd49fb241397b67089512b497662";

const result = await parseSwap({
publicClient,
transactionHash,
smartContractWallet: "0x3F6dAB60Cc16377Df9684959e20962f44De20988",
});

expect(result).toEqual({
tokenIn: {
symbol: "DEGEN",
amount: "882.414233540058884907",
address: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
},
tokenOut: {
symbol: "BRETT",
amount: "48.014669721245576995",
address: "0x532f27101965dd16442E59d40670FaF5eBB142E4",
},
});
});

// https://basescan.org/tx/0xaa09479aafdb1a33815fb3842c350ccedf5e3f9eaec31b8cba1f41eea674a8f3
test("parse a swap on Base (BRETT for ETH) with smart contract wallet", async () => {
const publicClient = createPublicClient({
chain: base,
transport: http(
`https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
),
}) as PublicClient<Transport, Chain>;

const transactionHash =
"0xaa09479aafdb1a33815fb3842c350ccedf5e3f9eaec31b8cba1f41eea674a8f3";

const result = await parseSwap({
publicClient,
transactionHash,
smartContractWallet: "0x3F6dAB60Cc16377Df9684959e20962f44De20988",
});

expect(result).toEqual({
tokenIn: {
symbol: "BRETT",
amount: "48.014669721245576995",
address: "0x532f27101965dd16442E59d40670FaF5eBB142E4",
},
tokenOut: {
symbol: "ETH",
amount: "0.001482901054900327",
address: NATIVE_TOKEN_ADDRESS,
},
});
});

// https://basescan.org/tx/0xe289a22987dcedfacb13584211c1d723ef5c42ea6e0dfd5c4d3271d20dec9ddc
test("parse a swap on Base (ETH for USDC) with smart contract wallet", async () => {
const publicClient = createPublicClient({
chain: base,
transport: http(
`https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
),
}) as PublicClient<Transport, Chain>;

const transactionHash =
"0xe289a22987dcedfacb13584211c1d723ef5c42ea6e0dfd5c4d3271d20dec9ddc";

const result = await parseSwap({
publicClient,
transactionHash,
smartContractWallet: "0x3F6dAB60Cc16377Df9684959e20962f44De20988",
});

expect(result).toEqual({
tokenIn: {
symbol: "ETH",
amount: "0.001",
address: NATIVE_TOKEN_ADDRESS,
},
tokenOut: {
symbol: "USDC",
amount: "2.600807",
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
},
});
});
Loading

0 comments on commit d1292d4

Please sign in to comment.