├── .gitignore ├── jest.config.js ├── lib ├── decoders │ ├── art-gobblers.d.ts │ ├── art-gobblers.js │ ├── comet.d.ts │ ├── comet.js │ ├── curve.d.ts │ ├── curve.js │ ├── ens.d.ts │ ├── ens.js │ ├── fallback.d.ts │ ├── fallback.js │ ├── index.d.ts │ ├── index.js │ ├── uniswapv2.d.ts │ ├── uniswapv2.js │ ├── uniswapv3.d.ts │ ├── uniswapv3.js │ ├── wrapped.d.ts │ └── wrapped.js └── sdk │ ├── actions.d.ts │ ├── actions.js │ ├── decoder.d.ts │ ├── decoder.js │ ├── types.d.ts │ ├── types.js │ ├── utils.d.ts │ └── utils.js ├── package.json ├── pnpm-lock.yaml ├── src ├── decoders │ ├── art-gobblers.ts │ ├── comet.ts │ ├── curve.ts │ ├── ens.ts │ ├── fallback.ts │ ├── index.ts │ ├── uniswapv2.ts │ ├── uniswapv3.ts │ └── wrapped.ts └── sdk │ ├── actions.ts │ ├── decoder.ts │ ├── types.ts │ └── utils.ts ├── tests ├── test_art-gobblers_mint_decoder.test.ts ├── test_comet_supply_decoder.test.ts ├── testdata │ ├── art-gobblers_mint_decoder_input.json │ └── comet_supply_decoder_input.json └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /lib/decoders/art-gobblers.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi/lib'; 2 | import { MintNFTAction } from "../sdk/actions"; 3 | import { CallDecoder, DecoderInput, DecoderState } from "../sdk/types"; 4 | export declare class ArtGobblersMintDecoder extends CallDecoder { 5 | constructor(); 6 | isTargetContract(state: DecoderState, address: string): Promise; 7 | decodeMintFromGoo(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /lib/decoders/art-gobblers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ArtGobblersMintDecoder = void 0; 4 | const ethers_1 = require("ethers"); 5 | const types_1 = require("../sdk/types"); 6 | const utils_1 = require("../sdk/utils"); 7 | const gobblerPurchasedEventSignature = 'event GobblerPurchased(address indexed user, uint256 indexed gobblerId, uint256 price)'; 8 | class ArtGobblersMintDecoder extends types_1.CallDecoder { 9 | constructor() { 10 | super(); 11 | this.functions['mintFromGoo(uint256 maxPrice, bool useVirtualBalance) external returns (uint256 gobblerId)'] = this.decodeMintFromGoo; 12 | } 13 | async isTargetContract(state, address) { 14 | return (0, utils_1.isEqualAddress)(address, '0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769'); 15 | } 16 | async decodeMintFromGoo(state, node, input, output) { 17 | const result = { 18 | type: 'nft-mint', 19 | operator: node.from, 20 | recipient: node.from, 21 | collection: node.to, 22 | buyToken: ethers_1.ethers.utils.getAddress('0x600000000a36F3cD48407e35eB7C5c910dc1f7a8'), 23 | buyAmount: input['maxPrice'].toBigInt(), 24 | }; 25 | // Can only get tokenId if transaction was successful... 26 | if ((0, utils_1.hasReceiptExt)(node)) { 27 | const logs = (0, utils_1.flattenLogs)(node); 28 | // Second to last log is GobblerPurchased event 29 | const gobblerPurchasedLog = this.decodeEventWithFragment(logs[logs.length - 2], gobblerPurchasedEventSignature); 30 | result.tokenId = gobblerPurchasedLog.args['gobblerId'].toBigInt(); 31 | result.buyAmount = gobblerPurchasedLog.args['price'].toBigInt(); 32 | } 33 | return result; 34 | } 35 | } 36 | exports.ArtGobblersMintDecoder = ArtGobblersMintDecoder; 37 | -------------------------------------------------------------------------------- /lib/decoders/comet.d.ts: -------------------------------------------------------------------------------- 1 | import { Decoder, DecoderState, DecoderInput } from "../sdk/types"; 2 | import { SupplyAction } from "../sdk/actions"; 3 | export declare class CometSupplyDecoder extends Decoder { 4 | functions: string[]; 5 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /lib/decoders/comet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CometSupplyDecoder = void 0; 4 | const lib_1 = require("@ethersproject/abi/lib"); 5 | const types_1 = require("../sdk/types"); 6 | const utils_1 = require("../sdk/utils"); 7 | const cTokenAddresses = new Set([ 8 | '0xc3d688B66703497DAA19211EEdff47f25384cdc3', 9 | ]); 10 | class CometSupplyDecoder extends types_1.Decoder { 11 | constructor() { 12 | super(...arguments); 13 | // Have to make sure that we're only picking up supply calls for cTokens 14 | this.functions = [ 15 | 'supply(address asset,uint amount)', 16 | 'supplyTo(address dst,address asset,uint amount)', 17 | 'supplyFrom(address from,address dst,address asset,uint amount)', 18 | ]; 19 | } 20 | async decodeCall(state, node) { 21 | if (state.isConsumed(node)) 22 | return null; 23 | if (node.type !== 'call') 24 | return null; 25 | if (!cTokenAddresses.has(node.to)) 26 | return null; 27 | const functionName = this.functions.find((name) => { 28 | return (0, utils_1.hasSelector)(node.calldata, name); 29 | }); 30 | if (functionName === undefined) 31 | return null; 32 | const [inputs] = this.decodeFunctionWithFragment(node, lib_1.FunctionFragment.from(functionName)); 33 | state.consume(node); 34 | // Supply implies downstream transfer call, need to consume 35 | if ((0, utils_1.hasTraceExt)(node)) { 36 | // We know that the first external call from cToken supply is a delegatecall to Comet supply 37 | const cometSupplyDelegateCall = node.children[0]; 38 | const transferFromCall = cometSupplyDelegateCall.children.filter((v) => v.type === 'call')[0]; 39 | // First external call made from supply function is a transferFrom 40 | state.consumeTransferFrom(transferFromCall); 41 | // Consume last log from delegate call (also a transfer event) 42 | if (cometSupplyDelegateCall.logs) { 43 | state.consume(cometSupplyDelegateCall.logs[cometSupplyDelegateCall.logs.length - 1]); 44 | } 45 | } 46 | const supplyResult = { 47 | type: 'supply', 48 | protocol: 'Compound', 49 | operator: node.from, 50 | supplier: functionName === 'supplyFrom(address from,address dst,address asset,uint amount)' 51 | ? inputs['from'] 52 | : node.from, 53 | supplyToken: inputs['asset'], 54 | amount: inputs['amount'].toBigInt(), 55 | }; 56 | // Metadata for cToken 57 | state.requestTokenMetadata(node.to); 58 | // Metadata for underlying token 59 | state.requestTokenMetadata(supplyResult.supplyToken); 60 | return supplyResult; 61 | } 62 | } 63 | exports.CometSupplyDecoder = CometSupplyDecoder; 64 | -------------------------------------------------------------------------------- /lib/decoders/curve.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi'; 2 | import { SwapAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | export declare class CurveSwapDecoder extends CallDecoder { 5 | constructor(); 6 | isTargetContract(state: DecoderState, address: string): Promise; 7 | decodeExchange(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 8 | decodeExchangeWithEth(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /lib/decoders/curve.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CurveSwapDecoder = void 0; 4 | const abi_1 = require("@ethersproject/abi"); 5 | const types_1 = require("../sdk/types"); 6 | const utils_1 = require("../sdk/utils"); 7 | const curveContracts = { 8 | ethereum: [ 9 | '0xB576491F1E6e5E62f1d8F26062Ee822B40B0E0d4', 10 | '0x8301AE4fc9c624d1D396cbDAa1ed877821D7C511', 11 | ] 12 | }; 13 | const coinsSignature = 'function coins(uint256) returns (address coin)'; 14 | const tokenExchangeSignature = 'event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought)'; 15 | class CurveSwapDecoder extends types_1.CallDecoder { 16 | constructor() { 17 | super(); 18 | this.functions['exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth)'] = this.decodeExchangeWithEth; 19 | this.functions['exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy)'] = this.decodeExchange; 20 | } 21 | async isTargetContract(state, address) { 22 | return !!curveContracts['ethereum'].find(addr => (0, utils_1.isEqualAddress)(addr, address)); 23 | } 24 | async decodeExchange(state, node, input, output) { 25 | const i = input['i']; 26 | const j = input['j']; 27 | const [tokenIn] = await state.call(coinsSignature, node.to, [i]); 28 | const [tokenOut] = await state.call(coinsSignature, node.to, [i]); 29 | const result = { 30 | type: 'swap', 31 | exchange: 'curve', 32 | operator: node.from, 33 | recipient: node.from, 34 | tokenIn: tokenIn, 35 | tokenOut: tokenOut, 36 | amountIn: input['dx'].toBigInt(), 37 | amountOutMin: input['min_dy'].toBigInt(), 38 | }; 39 | if ((0, utils_1.hasReceiptExt)(node)) { 40 | const exchangeLog = this.decodeEventWithFragment(node.logs[node.logs.length - 1], tokenExchangeSignature); 41 | result.amountOut = exchangeLog.args['tokens_bought'].toBigInt(); 42 | } 43 | return result; 44 | } 45 | async decodeExchangeWithEth(state, node, input, output) { 46 | const i = input['i']; 47 | const j = input['j']; 48 | const useEth = input['use_eth']; 49 | const intf = new abi_1.Interface([ 50 | 'function coins(uint256) returns (address coin)', 51 | ]); 52 | const tokenIn = intf.decodeFunctionResult(intf.getFunction('coins'), await state.access.call({ 53 | to: node.to, 54 | data: intf.encodeFunctionData(intf.getFunction('coins'), [i]), 55 | }))['coin']; 56 | const tokenOut = intf.decodeFunctionResult(intf.getFunction('coins'), await state.access.call({ 57 | to: node.to, 58 | data: intf.encodeFunctionData(intf.getFunction('coins'), [j]), 59 | }))['coin']; 60 | const result = { 61 | type: 'swap', 62 | exchange: 'curve', 63 | operator: node.from, 64 | recipient: node.from, 65 | tokenIn: tokenIn, 66 | tokenOut: tokenOut, 67 | amountIn: input['dx'].toBigInt(), 68 | amountOutMin: input['min_dy'].toBigInt(), 69 | }; 70 | if ((0, utils_1.hasReceiptExt)(node)) { 71 | const exchangeLog = this.decodeEventWithFragment(node.logs[node.logs.length - 1], 'event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought)'); 72 | result.amountOut = exchangeLog.args['tokens_bought'].toBigInt(); 73 | } 74 | return result; 75 | } 76 | } 77 | exports.CurveSwapDecoder = CurveSwapDecoder; 78 | -------------------------------------------------------------------------------- /lib/decoders/ens.d.ts: -------------------------------------------------------------------------------- 1 | import { ENSRegisterAction } from '../sdk/actions'; 2 | import { Decoder, DecoderInput, DecoderState } from '../sdk/types'; 3 | export declare class ENSDecoder extends Decoder { 4 | functions: { 5 | 'register(string name, address owner, uint256 duration, bytes32 secret)': { 6 | hasResolver: boolean; 7 | }; 8 | 'registerWithConfig(string name, address owner, uint256 duration, bytes32 secret, address resolver, address addr)': { 9 | hasResolver: boolean; 10 | }; 11 | }; 12 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /lib/decoders/ens.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ENSDecoder = void 0; 4 | const lib_1 = require("@ethersproject/abi/lib"); 5 | const ethers_1 = require("ethers"); 6 | const types_1 = require("../sdk/types"); 7 | const utils_1 = require("../sdk/utils"); 8 | class ENSDecoder extends types_1.Decoder { 9 | constructor() { 10 | super(...arguments); 11 | this.functions = { 12 | 'register(string name, address owner, uint256 duration, bytes32 secret)': { 13 | hasResolver: false, 14 | }, 15 | 'registerWithConfig(string name, address owner, uint256 duration, bytes32 secret, address resolver, address addr)': { 16 | hasResolver: true, 17 | }, 18 | }; 19 | } 20 | async decodeCall(state, node) { 21 | if (state.isConsumed(node)) 22 | return null; 23 | if (node.to.toLowerCase() !== '0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5'.toLowerCase()) 24 | return null; 25 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 26 | return (0, utils_1.hasSelector)(node.calldata, name); 27 | }); 28 | if (!functionInfo) 29 | return null; 30 | // todo: don't consume if we have a resolver set because that makes an external call 31 | state.consumeAllRecursively(node); 32 | const [inputs] = this.decodeFunctionWithFragment(node, lib_1.FunctionFragment.from(functionInfo[0])); 33 | const functionMetadata = functionInfo[1]; 34 | let cost = node.value.toBigInt(); 35 | if ((0, utils_1.hasReceiptExt)(node)) { 36 | const registeredFragment = lib_1.EventFragment.from(`NameRegistered(string name, bytes32 indexed label, address indexed owner, uint cost, uint expires)`); 37 | const lastLog = node.logs.reverse().find((log) => (0, utils_1.hasTopic)(log, registeredFragment)); 38 | if (lastLog) { 39 | const abi = new ethers_1.ethers.utils.Interface([registeredFragment]); 40 | const parsedEvent = abi.parseLog(lastLog); 41 | cost = parsedEvent.args['cost'].toBigInt(); 42 | } 43 | } 44 | const result = { 45 | type: 'ens-register', 46 | operator: node.from, 47 | owner: inputs['owner'], 48 | name: inputs['name'] + '.eth', 49 | duration: inputs['duration'].toNumber(), 50 | cost: cost, 51 | }; 52 | if (functionMetadata.hasResolver) { 53 | result.resolver = inputs['resolver']; 54 | result.addr = inputs['addr']; 55 | } 56 | return result; 57 | } 58 | } 59 | exports.ENSDecoder = ENSDecoder; 60 | -------------------------------------------------------------------------------- /lib/decoders/fallback.d.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '@ethersproject/abstract-provider'; 2 | import { BurnERC20Action, MintERC20Action, TransferAction } from '../sdk/actions'; 3 | import { Decoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | export declare class TransferDecoder extends Decoder { 5 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 6 | decodeLog(state: DecoderState, node: DecoderInput, log: Log): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /lib/decoders/fallback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.TransferDecoder = void 0; 4 | const actions_1 = require("../sdk/actions"); 5 | const types_1 = require("../sdk/types"); 6 | const utils_1 = require("../sdk/utils"); 7 | class TransferDecoder extends types_1.Decoder { 8 | async decodeCall(state, node) { 9 | if (state.isConsumed(node)) 10 | return null; 11 | if (node.value.isZero()) 12 | return null; 13 | return { 14 | type: 'transfer', 15 | operator: node.from, 16 | from: node.from, 17 | to: node.to, 18 | token: actions_1.NATIVE_TOKEN, 19 | amount: node.value.toBigInt(), 20 | }; 21 | } 22 | async decodeLog(state, node, log) { 23 | if (state.isConsumed(log)) 24 | return null; 25 | if (!(0, utils_1.hasTopic)(log, `Transfer(address,address,uint256)`)) 26 | return null; 27 | if (node.abi) { 28 | const decodedEvent = node.abi.parseLog(log); 29 | state.requestTokenMetadata(log.address); 30 | if (decodedEvent.args[0] === '0x0000000000000000000000000000000000000000') { 31 | return { 32 | type: 'mint-erc20', 33 | operator: node.from, 34 | token: log.address, 35 | to: decodedEvent.args[1], 36 | amount: decodedEvent.args[2].toBigInt(), 37 | }; 38 | } 39 | else if (decodedEvent.args[1] === "0x0000000000000000000000000000000000000000") { 40 | return { 41 | type: 'burn-erc20', 42 | operator: node.from, 43 | token: log.address, 44 | from: decodedEvent.args[0], 45 | amount: decodedEvent.args[2].toBigInt(), 46 | }; 47 | } 48 | return { 49 | type: 'transfer', 50 | operator: node.from, 51 | token: log.address, 52 | from: decodedEvent.args[0], 53 | to: decodedEvent.args[1], 54 | amount: decodedEvent.args[2].toBigInt(), 55 | }; 56 | } 57 | return null; 58 | } 59 | } 60 | exports.TransferDecoder = TransferDecoder; 61 | -------------------------------------------------------------------------------- /lib/decoders/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ArtGobblersMintDecoder } from "./art-gobblers"; 2 | import { CometSupplyDecoder } from "./comet"; 3 | import { CurveSwapDecoder } from "./curve"; 4 | import { ENSDecoder } from "./ens"; 5 | import { UniswapV2PairSwapDecoder, UniswapV2RouterSwapDecoder } from "./uniswapv2"; 6 | import { UniswapV3RouterSwapDecoder } from "./uniswapv3"; 7 | import { WrappedNativeTokenDecoder } from "./wrapped"; 8 | export declare const defaultDecoders: (ArtGobblersMintDecoder | CometSupplyDecoder | CurveSwapDecoder | ENSDecoder | UniswapV2RouterSwapDecoder | UniswapV2PairSwapDecoder | UniswapV3RouterSwapDecoder | WrappedNativeTokenDecoder)[]; 9 | -------------------------------------------------------------------------------- /lib/decoders/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.defaultDecoders = void 0; 4 | const art_gobblers_1 = require("./art-gobblers"); 5 | const comet_1 = require("./comet"); 6 | const curve_1 = require("./curve"); 7 | const ens_1 = require("./ens"); 8 | const uniswapv2_1 = require("./uniswapv2"); 9 | const uniswapv3_1 = require("./uniswapv3"); 10 | const wrapped_1 = require("./wrapped"); 11 | exports.defaultDecoders = [ 12 | new art_gobblers_1.ArtGobblersMintDecoder(), 13 | new comet_1.CometSupplyDecoder(), 14 | new curve_1.CurveSwapDecoder(), 15 | new ens_1.ENSDecoder(), 16 | new uniswapv2_1.UniswapV2RouterSwapDecoder(), 17 | new uniswapv2_1.UniswapV2PairSwapDecoder(), 18 | new uniswapv3_1.UniswapV3RouterSwapDecoder(), 19 | new wrapped_1.WrappedNativeTokenDecoder(), 20 | ]; 21 | -------------------------------------------------------------------------------- /lib/decoders/uniswapv2.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi'; 2 | import { SwapAction } from '../sdk/actions'; 3 | import { CallDecoder, Decoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | type UniswapDeployment = { 5 | name: string; 6 | factory: string; 7 | initcodeHash: string; 8 | routers: string[]; 9 | }; 10 | export declare class UniswapV2RouterSwapDecoder extends Decoder { 11 | functions: { 12 | 'swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 13 | exactIn: boolean; 14 | input: string; 15 | output: string; 16 | fee: boolean; 17 | }; 18 | 'swapTokensForExactTokens(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 19 | exactIn: boolean; 20 | input: string; 21 | output: string; 22 | fee: boolean; 23 | }; 24 | 'swapExactETHForTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 25 | exactIn: boolean; 26 | input: string; 27 | output: string; 28 | fee: boolean; 29 | }; 30 | 'swapTokensForExactETH(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 31 | exactIn: boolean; 32 | input: string; 33 | output: string; 34 | fee: boolean; 35 | }; 36 | 'swapExactTokensForETH(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 37 | exactIn: boolean; 38 | input: string; 39 | output: string; 40 | fee: boolean; 41 | }; 42 | 'swapETHForExactTokens(uint256 amountOut,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 43 | exactIn: boolean; 44 | input: string; 45 | output: string; 46 | fee: boolean; 47 | }; 48 | 'swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 49 | exactIn: boolean; 50 | input: string; 51 | output: string; 52 | fee: boolean; 53 | }; 54 | 'swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 55 | exactIn: boolean; 56 | input: string; 57 | output: string; 58 | fee: boolean; 59 | }; 60 | 'swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 61 | exactIn: boolean; 62 | input: string; 63 | output: string; 64 | fee: boolean; 65 | }; 66 | }; 67 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 68 | consumeSwaps(state: DecoderState, node: DecoderInput): void; 69 | consumeTokenInputSwap(state: DecoderState, node: DecoderInput): void; 70 | consumeETHInputSwap(state: DecoderState, node: DecoderInput): void; 71 | consumeETHOutputSwap(state: DecoderState, node: DecoderInput): void; 72 | } 73 | export declare class UniswapV2PairSwapDecoder extends CallDecoder { 74 | constructor(); 75 | getDeploymentForPair(state: DecoderState, address: string): Promise<[string, string, UniswapDeployment] | null>; 76 | isTargetContract(state: DecoderState, address: string): Promise; 77 | decodeSwap(state: DecoderState, node: DecoderInput, inputs: Result, outputs: Result | null): Promise; 78 | } 79 | export {}; 80 | -------------------------------------------------------------------------------- /lib/decoders/uniswapv2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.UniswapV2PairSwapDecoder = exports.UniswapV2RouterSwapDecoder = void 0; 4 | const abi_1 = require("@ethersproject/abi"); 5 | const lib_1 = require("@ethersproject/abi/lib"); 6 | const ethers_1 = require("ethers"); 7 | const types_1 = require("../sdk/types"); 8 | const utils_1 = require("../sdk/utils"); 9 | const uniswaps = [ 10 | { 11 | name: 'uniswap-v2', 12 | factory: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', 13 | initcodeHash: '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f', 14 | routers: [ 15 | '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a', 16 | '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 17 | ], 18 | }, 19 | { 20 | name: 'sushiswap', 21 | factory: '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac', 22 | initcodeHash: '0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303', 23 | routers: [ 24 | '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F', 25 | ], 26 | }, 27 | ]; 28 | const getTokens = (tokenA, tokenB) => { 29 | return tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA]; 30 | }; 31 | const computePairAddress = (factory, initcodeHash, tokenA, tokenB) => { 32 | const [token0, token1] = getTokens(tokenA, tokenB); 33 | const salt = ethers_1.ethers.utils.solidityKeccak256(['address', 'address'], [token0, token1]); 34 | return ethers_1.ethers.utils.getCreate2Address(factory, salt, initcodeHash); 35 | }; 36 | class UniswapV2RouterSwapDecoder extends types_1.Decoder { 37 | constructor() { 38 | super(...arguments); 39 | this.functions = { 40 | 'swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 41 | exactIn: true, 42 | input: 'tokens', 43 | output: 'tokens', 44 | fee: false, 45 | }, 46 | 'swapTokensForExactTokens(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 47 | exactIn: false, 48 | input: 'tokens', 49 | output: 'tokens', 50 | fee: false, 51 | }, 52 | 'swapExactETHForTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 53 | exactIn: true, 54 | input: 'eth', 55 | output: 'tokens', 56 | fee: false, 57 | }, 58 | 'swapTokensForExactETH(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 59 | exactIn: false, 60 | input: 'tokens', 61 | output: 'eth', 62 | fee: false, 63 | }, 64 | 'swapExactTokensForETH(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 65 | exactIn: true, 66 | input: 'tokens', 67 | output: 'eth', 68 | fee: false, 69 | }, 70 | 'swapETHForExactTokens(uint256 amountOut,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': { 71 | exactIn: false, 72 | input: 'eth', 73 | output: 'tokens', 74 | fee: false, 75 | }, 76 | 'swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 77 | exactIn: true, 78 | input: 'tokens', 79 | output: 'tokens', 80 | fee: true, 81 | }, 82 | 'swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 83 | exactIn: true, 84 | input: 'eth', 85 | output: 'tokens', 86 | fee: true, 87 | }, 88 | 'swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': { 89 | exactIn: true, 90 | input: 'tokens', 91 | output: 'eth', 92 | fee: true, 93 | }, 94 | }; 95 | } 96 | async decodeCall(state, node) { 97 | if (state.isConsumed(node)) 98 | return null; 99 | if (node.type !== 'call') 100 | return null; 101 | const routerInfo = uniswaps.find(v => v.routers.includes(node.to)); 102 | if (!routerInfo) 103 | return null; 104 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 105 | return (0, utils_1.hasSelector)(node.calldata, name); 106 | }); 107 | if (!functionInfo) 108 | return null; 109 | const [inputs, outputs] = this.decodeFunctionWithFragment(node, lib_1.FunctionFragment.from(functionInfo[0])); 110 | const swapMetadata = functionInfo[1]; 111 | // consume events and calls if we have them 112 | state.consume(node); 113 | if (swapMetadata.input === 'tokens') { 114 | this.consumeTokenInputSwap(state, node); 115 | } 116 | else { 117 | this.consumeETHInputSwap(state, node); 118 | } 119 | this.consumeSwaps(state, node); 120 | if (swapMetadata.output === 'eth') { 121 | this.consumeETHOutputSwap(state, node); 122 | } 123 | const path = inputs['path']; 124 | const swapResult = { 125 | type: 'swap', 126 | exchange: routerInfo.name, 127 | operator: node.from, 128 | recipient: inputs['to'], 129 | tokenIn: path[0], 130 | tokenOut: path[path.length - 1], 131 | }; 132 | // flag that we want token metadata to render the result 133 | state.requestTokenMetadata(swapResult.tokenIn); 134 | state.requestTokenMetadata(swapResult.tokenOut); 135 | const inputAmountIn = inputs['amountIn'] ? inputs['amountIn'].toBigInt() : undefined; 136 | const inputAmountOutMin = inputs['amountOutMin'] ? inputs['amountOutMin'].toBigInt() : undefined; 137 | const inputAmountOut = inputs['amountOut'] ? inputs['amountOut'].toBigInt() : undefined; 138 | const inputAmountInMax = inputs['amountInMax'] ? inputs['amountInMax'].toBigInt() : undefined; 139 | // pull info from from calldata 140 | if (swapMetadata.exactIn) { 141 | swapResult.amountIn = swapMetadata.input === 'eth' ? node.value.toBigInt() : inputAmountIn; 142 | swapResult.amountOutMin = inputAmountOutMin; 143 | } 144 | else { 145 | swapResult.amountOut = inputAmountOut; 146 | swapResult.amountInMax = inputAmountInMax; 147 | } 148 | // pull info from events 149 | if ((0, utils_1.hasReceiptExt)(node)) { 150 | const swapEventSelector = 'Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to)'; 151 | const abi = new ethers_1.ethers.utils.Interface([abi_1.EventFragment.from(swapEventSelector)]); 152 | const swapEvents = node.logs.filter((log) => (0, utils_1.hasTopic)(log, swapEventSelector)); 153 | const [firstToken0, firstToken1] = getTokens(path[0], path[1]); 154 | const firstPairAddress = computePairAddress(routerInfo.factory, routerInfo.initcodeHash, firstToken0, firstToken1); 155 | const [lastToken0, lastToken1] = getTokens(path[path.length - 2], path[path.length - 1]); 156 | const lastPairAddress = computePairAddress(routerInfo.factory, routerInfo.initcodeHash, lastToken0, lastToken1); 157 | const firstSwapEvent = swapEvents.find((event) => event.address === firstPairAddress); 158 | const lastSwapEvent = swapEvents.reverse().find((event) => event.address === lastPairAddress); 159 | if (firstSwapEvent) { 160 | const parsedEvent = abi.parseLog(firstSwapEvent); 161 | const eventAmount0In = parsedEvent.args['amount0In'] ? parsedEvent.args['amount0In'].toBigInt() : undefined; 162 | const eventAmount1In = parsedEvent.args['amount1In'] ? parsedEvent.args['amount1In'].toBigInt() : undefined; 163 | swapResult.amountIn = 164 | firstToken0 === path[0] 165 | ? eventAmount0In 166 | : eventAmount1In; 167 | } 168 | if (lastSwapEvent) { 169 | const parsedEvent = abi.parseLog(lastSwapEvent); 170 | const eventAmount0Out = parsedEvent.args['amount0Out'] ? parsedEvent.args['amount0Out'].toBigInt() : undefined; 171 | const eventAmount1Out = parsedEvent.args['amount1Out'] ? parsedEvent.args['amount1Out'].toBigInt() : undefined; 172 | swapResult.amountOut = 173 | lastToken0 === path[path.length - 1] 174 | ? eventAmount0Out 175 | : eventAmount1Out; 176 | } 177 | } 178 | // pull info from returndata 179 | if (outputs) { 180 | if (!swapMetadata.fee) { 181 | // if the swap is fee-less, we just check get the last amount 182 | const amounts = outputs['amounts']; 183 | const lastAmount = amounts[amounts.length - 1] ? amounts[amounts.length - 1].toBigInt() : undefined; 184 | swapResult.amountOut = lastAmount; 185 | } 186 | else { 187 | // otherwise, we need to check the call tree to pull out balance information 188 | if ((0, utils_1.hasTraceExt)(node)) { 189 | switch (swapMetadata.output) { 190 | case 'tokens': 191 | const balanceOfCalls = node.children 192 | .filter((v) => v.type === 'staticcall') 193 | .filter((v) => (0, utils_1.hasSelector)(v.calldata, 'balanceOf(address)')); 194 | // pull out the balanceOf calls 195 | const initialBalance = ethers_1.BigNumber.from(balanceOfCalls[0].returndata); 196 | const finalBalance = ethers_1.BigNumber.from(balanceOfCalls[balanceOfCalls.length - 1].returndata); 197 | swapResult.amountOut = finalBalance.sub(initialBalance).toBigInt(); 198 | break; 199 | case 'eth': 200 | const calls = node.children.filter((v) => v.type === 'call'); 201 | const lastAmount = calls[calls.length - 1].value ? calls[calls.length - 1].value.toBigInt() : undefined; 202 | swapResult.amountOut = calls[calls.length - 1].value.toBigInt(); 203 | break; 204 | } 205 | } 206 | } 207 | } 208 | return swapResult; 209 | } 210 | consumeSwaps(state, node) { 211 | if (!(0, utils_1.hasTraceExt)(node)) 212 | return; 213 | node.children 214 | .filter((call) => call.type === 'call') 215 | .filter((call) => (0, utils_1.hasSelector)(call.calldata, 'swap(uint256,uint256,address,bytes)')) 216 | .forEach((call) => { 217 | state.consume(call); 218 | call.children 219 | .filter((v) => v.type === 'call' && (0, utils_1.hasSelector)(v.calldata, 'transfer(address,uint256)')) 220 | .forEach((v) => state.consumeTransfer(v)); 221 | }); 222 | } 223 | consumeTokenInputSwap(state, node) { 224 | if (!(0, utils_1.hasTraceExt)(node)) 225 | return; 226 | const calls = node.children.filter((v) => v.type === 'call'); 227 | state.consumeTransferFrom(calls[0]); 228 | } 229 | consumeETHInputSwap(state, node) { 230 | if (!(0, utils_1.hasTraceExt)(node)) 231 | return; 232 | const calls = node.children.filter((v) => v.type === 'call'); 233 | // weth deposit 234 | state.consumeAll(calls[0]); 235 | // weth transfer 236 | state.consumeAll(calls[1]); 237 | // weth refund 238 | if (!calls[calls.length - 1].value.isZero()) { 239 | state.consumeAll(calls[calls.length - 1]); 240 | } 241 | } 242 | consumeETHOutputSwap(state, node) { 243 | if (!(0, utils_1.hasTraceExt)(node)) 244 | return; 245 | const calls = node.children.filter((v) => v.type === 'call'); 246 | // weth withdraw 247 | state.consumeAll(calls[calls.length - 2]); 248 | // eth transfer 249 | state.consumeAll(calls[calls.length - 1]); 250 | } 251 | } 252 | exports.UniswapV2RouterSwapDecoder = UniswapV2RouterSwapDecoder; 253 | const swapEventSignature = 'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)'; 254 | class UniswapV2PairSwapDecoder extends types_1.CallDecoder { 255 | constructor() { 256 | super(); 257 | this.functions['swap(uint256 amount0Out, uint256 amount1Out, address to, bytes data)'] = this.decodeSwap; 258 | } 259 | async getDeploymentForPair(state, address) { 260 | const [token0] = await state.call('function token0() returns (address)', address, []); 261 | const [token1] = await state.call('function token1() returns (address)', address, []); 262 | const deployment = uniswaps.find(deployment => { 263 | const pairAddress = computePairAddress(deployment.factory, deployment.initcodeHash, token0, token1); 264 | return pairAddress.toLocaleLowerCase() === address.toLocaleLowerCase(); 265 | }); 266 | if (!deployment) { 267 | return null; 268 | } 269 | return [token0, token1, deployment]; 270 | } 271 | async isTargetContract(state, address) { 272 | return !!(await this.getDeploymentForPair(state, address)); 273 | } 274 | async decodeSwap(state, node, inputs, outputs) { 275 | const [token0, token1, deployment] = (await this.getDeploymentForPair(state, node.to)); 276 | if ((0, utils_1.hasReceiptExt)(node)) { 277 | // the last log must be a swap 278 | state.consume(node.logs[node.logs.length - 1]); 279 | } 280 | if ((0, utils_1.hasTraceExt)(node)) { 281 | // there must be at least one transfer out 282 | state.consumeTransfer(node.children[0]); 283 | } 284 | const reversedDecode = Array.from(state.decodeOrder).reverse(); 285 | for (let result of reversedDecode) { 286 | const newResults = result.results.filter(action => { 287 | return action.type !== 'transfer' || (action.to.toLocaleLowerCase() !== node.to.toLocaleLowerCase()); 288 | }); 289 | result.results = newResults; 290 | } 291 | state.decoded.get(state.root); 292 | let tokenIn = token0; 293 | let tokenOut = token1; 294 | let amountIn; 295 | let amountOut; 296 | if ((0, utils_1.hasReceiptExt)(node)) { 297 | const swapEvent = this.decodeEventWithFragment(node.logs[node.logs.length - 1], swapEventSignature); 298 | if (swapEvent.args['amount0In'].toBigInt() && !swapEvent.args['amount1In'].toBigInt()) { 299 | console.log("used branch a"); 300 | tokenIn = token0; 301 | tokenOut = token1; 302 | amountIn = swapEvent.args['amount0In']; 303 | amountOut = swapEvent.args['amount1Out']; 304 | } 305 | else { 306 | console.log("used branch b", swapEvent.args); 307 | tokenIn = token1; 308 | tokenOut = token0; 309 | amountIn = swapEvent.args['amount1In']; 310 | amountOut = swapEvent.args['amount0Out']; 311 | } 312 | } 313 | else { 314 | console.log("node has no logs?"); 315 | } 316 | console.log('decoded swap???', tokenIn, tokenOut, amountIn, amountOut); 317 | const action = { 318 | type: 'swap', 319 | exchange: deployment.name, 320 | operator: node.from, 321 | recipient: inputs['to'], 322 | tokenIn: tokenIn, 323 | tokenOut: tokenOut, 324 | amountIn: amountIn, 325 | amountOut: amountOut, 326 | }; 327 | return action; 328 | } 329 | } 330 | exports.UniswapV2PairSwapDecoder = UniswapV2PairSwapDecoder; 331 | // export type UniswapV2RouterAddLiquidityResult = { 332 | // type: string; 333 | // actor: string; 334 | // recipient: string; 335 | // pool: string; 336 | // tokenA: string; 337 | // tokenB: string; 338 | // amountADesired: BigNumber; 339 | // amountBDesired: BigNumber; 340 | // amountAMin: BigNumber; 341 | // amountBMin: BigNumber; 342 | // amountA?: BigNumber; 343 | // amountB?: BigNumber; 344 | // liquidity?: BigNumber; 345 | // }; 346 | // export class UniswapV2RouterAddLiquidityDecoder extends Decoder { 347 | // functions = { 348 | // 'addLiquidity(address tokenA,address tokenB,uint256 amountADesired,uint256 amountBDesired,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline) returns (uint amountA, uint amountB, uint liquidity)': 349 | // { 350 | // eth: false, 351 | // }, 352 | // 'addLiquidityETH(address token,uint256 amountTokenDesired,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256) returns (uint amountToken, uint amountETH, uint liquidity)': 353 | // { 354 | // eth: true, 355 | // }, 356 | // }; 357 | // constructor() { 358 | // super('uniswap-v2-router-add-liquidity'); 359 | // } 360 | // decodeCall(state: DecoderState, node: DecoderInput): UniswapV2RouterAddLiquidityResult | null { 361 | // if (state.isConsumed(node)) return null; 362 | // if (node.type !== 'call') return null; 363 | // const functionInfo = Object.entries(this.functions).find(([name, func]) => { 364 | // return hasSelector(node.calldata, name); 365 | // }); 366 | // if (!functionInfo) return null; 367 | // const [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionInfo[0])); 368 | // const functionMetadata = functionInfo[1]; 369 | // const result = { 370 | // type: this.name, 371 | // actor: node.from, 372 | // recipient: inputs['to'], 373 | // tokenA: functionMetadata.eth ? inputs['token'] : inputs['tokenA'], 374 | // tokenB: functionMetadata.eth ? '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' : inputs['tokenB'], 375 | // amountAMin: functionMetadata.eth ? inputs['amountTokenMin'] : inputs['amountAMin'], 376 | // amountBMin: functionMetadata.eth ? inputs['amountETHMin'] : inputs['amountBMin'], 377 | // amountADesired: functionMetadata.eth ? inputs['amountTokenMin'] : inputs['amountAMin'], 378 | // amountBDesired: functionMetadata.eth ? undefined : inputs['amountBMin'], 379 | // }; 380 | // state.requestTokenMetadata(result.tokenA); 381 | // state.requestTokenMetadata(result.tokenB); 382 | // state.requestTokenMetadata(result.pool); 383 | // return result; 384 | // } 385 | // format(result: UniswapV2RouterAddLiquidityResult, opts: DecodeFormatOpts): JSX.Element { 386 | // return this.renderResult( 387 | // 'add liquidity', 388 | // '#6c969d', 389 | // ['tokenA', 'tokenB', 'liquidity', 'recipient', 'actor'], 390 | // [ 391 | // this.formatTokenAmount(opts, result.tokenA, result.amountA), 392 | // this.formatTokenAmount(opts, result.tokenB, result.amountB), 393 | // this.formatTokenAmount(opts, result.pool, result.liquidity), 394 | // , 395 | // , 396 | // ], 397 | // ); 398 | // } 399 | // // handles the _addLiquidity function 400 | // handleAddLiquidity(node: TraceEntryCall, subcalls: TraceEntryCall[], state: DecodeState): boolean { 401 | // return subcalls[0].input.substring(0, 10) === ethers.utils.id('createPair(address,address)').substring(0, 10); 402 | // } 403 | // decodeAddLiquidity( 404 | // node: TraceEntryCall, 405 | // state: DecodeState, 406 | // inputs: Result, 407 | // outputs: Result, 408 | // subcalls: TraceEntryCall[], 409 | // ) { 410 | // let idx = this.handleAddLiquidity(node, subcalls, state) ? 1 : 0; 411 | // // handle the transfer from tokenA -> pair 412 | // this.handleTransferFrom(state, subcalls[idx]); 413 | // // handle the transfer from tokenB -> pair 414 | // this.handleTransfer(state, subcalls[idx + 1]); 415 | // // handle the mint call 416 | // this.handleRecursively(state, subcalls[idx + 2]); 417 | // return [ 418 | // subcalls[idx + 2].to, 419 | // inputs['tokenA'], 420 | // inputs['tokenB'], 421 | // outputs['amountA'], 422 | // outputs['amountB'], 423 | // outputs['liquidity'], 424 | // inputs['to'], 425 | // ]; 426 | // } 427 | // decodeAddLiquidityETH( 428 | // node: TraceEntryCall, 429 | // state: DecodeState, 430 | // inputs: Result, 431 | // outputs: Result, 432 | // subcalls: TraceEntryCall[], 433 | // ) { 434 | // let idx = this.handleAddLiquidity(node, subcalls, state) ? 1 : 0; 435 | // // handle the transfer from tokenA -> pair 436 | // this.handleTransferFrom(state, subcalls[idx]); 437 | // // handle the weth deposit 438 | // this.handleRecursively(state, subcalls[idx + 1]); 439 | // // handle the weth transfer 440 | // this.handleRecursively(state, subcalls[idx + 2]); 441 | // // handle the mint call 442 | // this.handleRecursively(state, subcalls[idx + 3]); 443 | // // handle the optional eth refund 444 | // if (idx + 4 < subcalls.length) { 445 | // this.handleRecursively(state, subcalls[idx + 4]); 446 | // } 447 | // return [ 448 | // subcalls[idx + 3].to, 449 | // inputs['token'], 450 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 451 | // outputs['amountToken'], 452 | // outputs['amountETH'], 453 | // outputs['liquidity'], 454 | // inputs['to'], 455 | // ]; 456 | // } 457 | // } 458 | // export type UniswapV2RouterRemoveLiquidityResult = { 459 | // type: string; 460 | // actor: string; 461 | // recipient: string; 462 | // pool: string; 463 | // tokenA: string; 464 | // tokenB: string; 465 | // amountA: BigNumber; 466 | // amountB: BigNumber; 467 | // liquidity: BigNumber; 468 | // }; 469 | // export class UniswapV2RouterRemoveLiquidityDecoder extends Decoder { 470 | // addLiquidityFunctions = { 471 | // 'removeLiquidity(address tokenA,address tokenB,uint256 liquidity,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline) returns (uint amountA, uint amountB)': 472 | // this.decodeRemoveLiquidity.bind(this), 473 | // 'removeLiquidityETH(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline) returns (uint amountToken, uint amountETH)': 474 | // this.decodeRemoveLiquidityETH.bind(this), 475 | // 'removeLiquidityWithPermit(address tokenA,address tokenB,uint256 liquidity,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountA, uint amountB)': 476 | // this.decodeRemoveLiquidityWithPermit.bind(this), 477 | // 'removeLiquidityETHWithPermit(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountToken, uint amountETH)': 478 | // this.decodeRemoveLiquidityETHWithPermit.bind(this), 479 | // 'removeLiquidityETHSupportingFeeOnTransferTokens(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline) returns (uint amountETH)': 480 | // this.decodeRemoveLiquidityETHSupportingFeeOnTransferTokens.bind(this), 481 | // 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountETH)': 482 | // this.decodeRemoveLiquidityETHWithPermitSupportingFeeOnTransferTokens.bind(this), 483 | // }; 484 | // constructor() { 485 | // super('uniswap-v2-router-remove-liquidity'); 486 | // } 487 | // decode(node: TraceEntry, state: DecodeState): UniswapV2RouterRemoveLiquidityResult | null { 488 | // if (state.handled[node.path]) return null; 489 | // if (node.type !== 'call') return null; 490 | // let selector = node.input.substring(0, 10); 491 | // let decoder = Object.entries(this.addLiquidityFunctions).find(([name, func]) => { 492 | // return ethers.utils.id(FunctionFragment.from(name).format()).substring(0, 10) === selector; 493 | // }); 494 | // if (!decoder) return null; 495 | // state.handled[node.path] = true; 496 | // let [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(decoder[0])); 497 | // let subcalls: TraceEntryCall[] = node.children.filter( 498 | // (v) => v.type === 'call' && v.variant === 'call', 499 | // ) as TraceEntryCall[]; 500 | // let [pool, tokenA, tokenB, amountA, amountB, liquidity, to] = decoder[1]( 501 | // node, 502 | // state, 503 | // inputs, 504 | // outputs, 505 | // subcalls, 506 | // ); 507 | // this.requestTokenMetadata(state, tokenA); 508 | // this.requestTokenMetadata(state, tokenB); 509 | // this.requestTokenMetadata(state, pool); 510 | // return { 511 | // type: this.name, 512 | // actor: node.from, 513 | // recipient: to, 514 | // pool: pool, 515 | // tokenA: tokenA, 516 | // tokenB: tokenB, 517 | // amountA: amountA, 518 | // amountB: amountB, 519 | // liquidity: liquidity, 520 | // }; 521 | // } 522 | // format(result: UniswapV2RouterRemoveLiquidityResult, opts: DecodeFormatOpts): JSX.Element { 523 | // return this.renderResult( 524 | // 'remove liquidity', 525 | // '#392b58', 526 | // ['tokenA', 'tokenB', 'liquidity', 'recipient', 'actor'], 527 | // [ 528 | // this.formatTokenAmount(opts, result.tokenA, result.amountA), 529 | // this.formatTokenAmount(opts, result.tokenB, result.amountB), 530 | // this.formatTokenAmount(opts, result.pool, result.liquidity), 531 | // , 532 | // , 533 | // ], 534 | // ); 535 | // } 536 | // // handles the removeLiquidity function 537 | // handleRemoveLiquidity( 538 | // node: TraceEntryCall, 539 | // subcalls: TraceEntryCall[], 540 | // state: DecodeState, 541 | // offset: number, 542 | // ): number { 543 | // // handle the transfer from tokenA -> pair 544 | // this.handleTransferFrom(state, subcalls[offset]); 545 | // // handle the burn call 546 | // this.handleRecursively(state, subcalls[offset + 1]); 547 | // return offset + 2; 548 | // } 549 | // decodeRemoveLiquidity( 550 | // node: TraceEntryCall, 551 | // state: DecodeState, 552 | // inputs: Result, 553 | // outputs: Result, 554 | // subcalls: TraceEntryCall[], 555 | // ) { 556 | // this.handleRemoveLiquidity(node, subcalls, state, 0); 557 | // return [ 558 | // subcalls[0].to, 559 | // inputs['tokenA'], 560 | // inputs['tokenB'], 561 | // outputs['amountA'], 562 | // outputs['amountB'], 563 | // inputs['liquidity'], 564 | // inputs['to'], 565 | // ]; 566 | // } 567 | // decodeRemoveLiquidityETH( 568 | // node: TraceEntryCall, 569 | // state: DecodeState, 570 | // inputs: Result, 571 | // outputs: Result, 572 | // subcalls: TraceEntryCall[], 573 | // ) { 574 | // this.handleRemoveLiquidity(node, subcalls, state, 0); 575 | // // handle the transfer 576 | // this.handleTransfer(state, subcalls[2]); 577 | // // handle the weth withdraw 578 | // this.handleRecursively(state, subcalls[3]); 579 | // // handle the eth return 580 | // state.handled[subcalls[4].path] = true; 581 | // return [ 582 | // subcalls[0].to, 583 | // inputs['token'], 584 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 585 | // outputs['amountToken'], 586 | // outputs['amountETH'], 587 | // inputs['liquidity'], 588 | // inputs['to'], 589 | // ]; 590 | // } 591 | // decodeRemoveLiquidityWithPermit( 592 | // node: TraceEntryCall, 593 | // state: DecodeState, 594 | // inputs: Result, 595 | // outputs: Result, 596 | // subcalls: TraceEntryCall[], 597 | // ) { 598 | // this.handleRemoveLiquidity(node, subcalls, state, 1); 599 | // return [ 600 | // subcalls[0].to, 601 | // inputs['tokenA'], 602 | // inputs['tokenB'], 603 | // outputs['amountA'], 604 | // outputs['amountB'], 605 | // inputs['liquidity'], 606 | // inputs['to'], 607 | // ]; 608 | // } 609 | // decodeRemoveLiquidityETHWithPermit( 610 | // node: TraceEntryCall, 611 | // state: DecodeState, 612 | // inputs: Result, 613 | // outputs: Result, 614 | // subcalls: TraceEntryCall[], 615 | // ) { 616 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 1); 617 | // // handle the transfer 618 | // this.handleTransfer(state, subcalls[offset]); 619 | // // handle the weth withdraw 620 | // this.handleRecursively(state, subcalls[offset + 1]); 621 | // // handle the eth return 622 | // state.handled[subcalls[offset + 2].path] = true; 623 | // return [ 624 | // subcalls[0].to, 625 | // inputs['token'], 626 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 627 | // outputs['amountToken'], 628 | // outputs['amountETH'], 629 | // inputs['liquidity'], 630 | // inputs['to'], 631 | // ]; 632 | // } 633 | // decodeRemoveLiquidityETHSupportingFeeOnTransferTokens( 634 | // node: TraceEntryCall, 635 | // state: DecodeState, 636 | // inputs: Result, 637 | // outputs: Result, 638 | // subcalls: TraceEntryCall[], 639 | // ) { 640 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 0); 641 | // // handle the transfer 642 | // this.handleTransfer(state, subcalls[offset]); 643 | // // handle the weth withdraw 644 | // this.handleRecursively(state, subcalls[offset + 1]); 645 | // // handle the eth return 646 | // state.handled[subcalls[offset + 2].path] = true; 647 | // let staticcalls = node.children.filter( 648 | // (v): v is TraceEntryCall => v.type === 'call' && v.variant === 'staticcall', 649 | // ); 650 | // let output = BigNumber.from(staticcalls[staticcalls.length - 1].output); 651 | // return [ 652 | // subcalls[0].to, 653 | // inputs['token'], 654 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 655 | // output, 656 | // outputs['amountETH'], 657 | // inputs['liquidity'], 658 | // inputs['to'], 659 | // ]; 660 | // } 661 | // decodeRemoveLiquidityETHWithPermitSupportingFeeOnTransferTokens( 662 | // node: TraceEntryCall, 663 | // state: DecodeState, 664 | // inputs: Result, 665 | // outputs: Result, 666 | // subcalls: TraceEntryCall[], 667 | // ) { 668 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 1); 669 | // // handle the transfer 670 | // this.handleTransfer(state, subcalls[offset]); 671 | // // handle the weth withdraw 672 | // this.handleRecursively(state, subcalls[offset + 1]); 673 | // // handle the eth return 674 | // state.handled[subcalls[offset + 2].path] = true; 675 | // let staticcalls = node.children.filter( 676 | // (v): v is TraceEntryCall => v.type === 'call' && v.variant === 'staticcall', 677 | // ); 678 | // let output = BigNumber.from(staticcalls[staticcalls.length - 1].output); 679 | // return [ 680 | // subcalls[0].to, 681 | // inputs['token'], 682 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 683 | // output, 684 | // outputs['amountETH'], 685 | // inputs['liquidity'], 686 | // inputs['to'], 687 | // ]; 688 | // } 689 | // } 690 | -------------------------------------------------------------------------------- /lib/decoders/uniswapv3.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi/lib'; 2 | import { SwapAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | export declare class UniswapV3RouterSwapDecoder extends CallDecoder { 5 | constructor(); 6 | isTargetContract(state: DecoderState, address: string): Promise; 7 | decodeExactInput(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /lib/decoders/uniswapv3.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.UniswapV3RouterSwapDecoder = void 0; 4 | const types_1 = require("../sdk/types"); 5 | const utils_1 = require("../sdk/utils"); 6 | const swapEventSignature = `event Swap( 7 | address indexed sender, 8 | address indexed recipient, 9 | int256 amount0, 10 | int256 amount1, 11 | uint160 sqrtPriceX96, 12 | uint128 liquidity, 13 | int24 tick 14 | );`; 15 | class UniswapV3RouterSwapDecoder extends types_1.CallDecoder { 16 | constructor() { 17 | super(); 18 | this.functions['exactInput(tuple(bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)'] = this.decodeExactInput; 19 | } 20 | async isTargetContract(state, address) { 21 | return (0, utils_1.isEqualAddress)(address, '0xE592427A0AEce92De3Edee1F18E0157C05861564'); 22 | } 23 | async decodeExactInput(state, node, input, output) { 24 | const path = input['params']['path']; 25 | const amountIn = input['params']['amountIn']; 26 | const amountOutMin = input['params']['amountOutMinimum']; 27 | const recipient = input['params']['recipient']; 28 | const tokenIn = "0x" + path.substring(2, 42); 29 | const tokenOut = "0x" + path.substring(path.length - 40); 30 | const result = { 31 | type: 'swap', 32 | exchange: 'uniswap-v3', 33 | operator: node.from, 34 | recipient: recipient, 35 | tokenIn: tokenIn, 36 | tokenOut: tokenOut, 37 | amountIn: amountIn, 38 | amountOutMin: amountOutMin, 39 | }; 40 | if ((0, utils_1.hasReceiptExt)(node)) { 41 | const logs = (0, utils_1.flattenLogs)(node); 42 | const swapLog = this.decodeEventWithFragment(logs[logs.length - 1], swapEventSignature); 43 | const amount0 = swapLog.args['amount0'].toBigInt(); 44 | const amount1 = swapLog.args['amount1'].toBigInt(); 45 | result.amountOut = amount0 < 0n ? amount0 : amount1; 46 | } 47 | return result; 48 | } 49 | } 50 | exports.UniswapV3RouterSwapDecoder = UniswapV3RouterSwapDecoder; 51 | -------------------------------------------------------------------------------- /lib/decoders/wrapped.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi'; 2 | import { UnwrapNativeTokenAction, WrapNativeTokenAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | export declare class WrappedNativeTokenDecoder extends CallDecoder { 5 | constructor(); 6 | isTargetContract(state: DecoderState, address: string): Promise; 7 | decodeWrap(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 8 | decodeUnwrap(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /lib/decoders/wrapped.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.WrappedNativeTokenDecoder = void 0; 4 | const actions_1 = require("../sdk/actions"); 5 | const types_1 = require("../sdk/types"); 6 | const utils_1 = require("../sdk/utils"); 7 | const wrappedNativeTokens = { 8 | ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 9 | }; 10 | class WrappedNativeTokenDecoder extends types_1.CallDecoder { 11 | constructor() { 12 | super(); 13 | this.functions[''] = this.decodeWrap; 14 | this.functions['deposit()'] = this.decodeWrap; 15 | this.functions['withdraw(uint256 amount)'] = this.decodeUnwrap; 16 | } 17 | async isTargetContract(state, address) { 18 | return (0, utils_1.isEqualAddress)(wrappedNativeTokens['ethereum'], address); 19 | } 20 | async decodeWrap(state, node, input, output) { 21 | if ((0, utils_1.hasTraceExt)(node)) { 22 | state.consumeAll(node); 23 | } 24 | state.requestTokenMetadata(node.to); 25 | return { 26 | type: 'wrap-native-token', 27 | token: actions_1.NATIVE_TOKEN, 28 | operator: node.from, 29 | amount: node.value.toBigInt(), 30 | }; 31 | } 32 | async decodeUnwrap(state, node, input, output) { 33 | if ((0, utils_1.hasTraceExt)(node)) { 34 | state.consumeAllRecursively(node); 35 | } 36 | state.requestTokenMetadata(node.to); 37 | return { 38 | type: 'unwrap-native-token', 39 | token: actions_1.NATIVE_TOKEN, 40 | operator: node.from, 41 | amount: input['amount'].toBigInt(), 42 | }; 43 | } 44 | } 45 | exports.WrappedNativeTokenDecoder = WrappedNativeTokenDecoder; 46 | -------------------------------------------------------------------------------- /lib/sdk/actions.d.ts: -------------------------------------------------------------------------------- 1 | export declare const NATIVE_TOKEN = "native_token"; 2 | export type BaseAction = { 3 | type: string; 4 | }; 5 | export interface TransferAction { 6 | type: 'transfer'; 7 | operator: string; 8 | from: string; 9 | to: string; 10 | token: string; 11 | amount: bigint; 12 | } 13 | export interface MintERC20Action { 14 | type: 'mint-erc20'; 15 | operator: string; 16 | to: string; 17 | token: string; 18 | amount: bigint; 19 | } 20 | export interface BurnERC20Action { 21 | type: 'burn-erc20'; 22 | operator: string; 23 | from: string; 24 | token: string; 25 | amount: bigint; 26 | } 27 | export type SwapAction = { 28 | type: 'swap'; 29 | exchange: string; 30 | operator: string; 31 | recipient: string; 32 | tokenIn: string; 33 | tokenOut: string; 34 | amountIn?: bigint; 35 | amountInMax?: bigint; 36 | amountOut?: bigint; 37 | amountOutMin?: bigint; 38 | }; 39 | export type ENSRegisterAction = { 40 | type: 'ens-register'; 41 | operator: string; 42 | owner: string; 43 | name: string; 44 | duration: number; 45 | cost: bigint; 46 | resolver?: string; 47 | addr?: string; 48 | }; 49 | export type SupplyAction = { 50 | type: 'supply'; 51 | protocol: string; 52 | operator: string; 53 | supplier: string; 54 | supplyToken: string; 55 | amount: bigint; 56 | }; 57 | export type WrapNativeTokenAction = { 58 | type: 'wrap-native-token'; 59 | token: string; 60 | operator: string; 61 | amount: bigint; 62 | }; 63 | export type UnwrapNativeTokenAction = { 64 | type: 'unwrap-native-token'; 65 | token: string; 66 | operator: string; 67 | amount: bigint; 68 | }; 69 | export type MintNFTAction = { 70 | type: 'nft-mint'; 71 | operator: string; 72 | recipient: string; 73 | collection: string; 74 | tokenId?: bigint; 75 | buyToken?: string; 76 | buyAmount?: bigint; 77 | }; 78 | export type Action = MintERC20Action | BurnERC20Action | TransferAction | SwapAction | ENSRegisterAction | WrapNativeTokenAction | UnwrapNativeTokenAction | SupplyAction | MintNFTAction; 79 | -------------------------------------------------------------------------------- /lib/sdk/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.NATIVE_TOKEN = void 0; 4 | exports.NATIVE_TOKEN = 'native_token'; 5 | -------------------------------------------------------------------------------- /lib/sdk/decoder.d.ts: -------------------------------------------------------------------------------- 1 | import { Decoder, DecoderChainAccess, DecoderInput, DecoderOutput, MetadataRequest } from './types'; 2 | export declare class DecoderManager { 3 | decoders: Decoder[]; 4 | fallbackDecoder: Decoder; 5 | constructor(decoders: Decoder[], fallbackDecoder: Decoder); 6 | addDecoder: (decoder: Decoder) => void; 7 | decode: (input: DecoderInput, access: DecoderChainAccess) => Promise<[DecoderOutput, MetadataRequest]>; 8 | } 9 | -------------------------------------------------------------------------------- /lib/sdk/decoder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.DecoderManager = void 0; 4 | const types_1 = require("./types"); 5 | const utils_1 = require("./utils"); 6 | class DecoderManager { 7 | constructor(decoders, fallbackDecoder) { 8 | this.decoders = []; 9 | this.addDecoder = (decoder) => { 10 | this.decoders.push(decoder); 11 | }; 12 | this.decode = async (input, access) => { 13 | const allDecodersArray = [...this.decoders, this.fallbackDecoder]; 14 | const state = new types_1.DecoderState(input, access); 15 | const visit = async (node) => { 16 | if ((0, utils_1.hasReceiptExt)(node) && node.failed) { 17 | // we don't decode anything that failed, because there should be no reason 18 | // to care about something that had no effect 19 | return state.getOutputFor(node); 20 | } 21 | const decodeLog = async (child, log) => { 22 | const output = state.getOutputFor(log); 23 | await Promise.all(allDecodersArray.map(async (v) => { 24 | try { 25 | const results = await v.decodeLog(state, node, log); 26 | if (!results) 27 | return; 28 | if (Array.isArray(results)) { 29 | output.results.push(...results); 30 | } 31 | else { 32 | output.results.push(results); 33 | } 34 | } 35 | catch (e) { 36 | console.log('decoder failed to decode log', v, node, log, e); 37 | } 38 | })); 39 | return output; 40 | }; 41 | const output = state.getOutputFor(node); 42 | for (const decoder of allDecodersArray) { 43 | try { 44 | const result = await decoder.decodeCall(state, node); 45 | if (result) { 46 | output.results.push(result); 47 | } 48 | } 49 | catch (e) { 50 | console.log('decoder failed to decode call', decoder, node, e); 51 | } 52 | } 53 | if ((0, utils_1.hasTraceExt)(node)) { 54 | for (let child of node.childOrder) { 55 | let result; 56 | if (child[0] === 'log') { 57 | result = await decodeLog(node, node.logs[child[1]]); 58 | } 59 | else { 60 | result = await visit(node.children[child[1]]); 61 | } 62 | output.children.push(result); 63 | } 64 | } 65 | else if ((0, utils_1.hasReceiptExt)(node)) { 66 | if (node.logs) { 67 | for (let log of node.logs) { 68 | output.children.push(await decodeLog(node, log)); 69 | } 70 | } 71 | } 72 | return output; 73 | }; 74 | return [await visit(input), state.requestedMetadata]; 75 | }; 76 | this.decoders = decoders; 77 | this.fallbackDecoder = fallbackDecoder; 78 | } 79 | } 80 | exports.DecoderManager = DecoderManager; 81 | -------------------------------------------------------------------------------- /lib/sdk/types.d.ts: -------------------------------------------------------------------------------- 1 | import { EventFragment, FunctionFragment, Result } from '@ethersproject/abi/lib'; 2 | import { Log, Provider, TransactionRequest } from '@ethersproject/abstract-provider'; 3 | import { BigNumber, BytesLike, ethers } from 'ethers'; 4 | import { LogDescription, ParamType } from 'ethers/lib/utils'; 5 | import { Action, BaseAction } from './actions'; 6 | export interface DecoderChainAccess { 7 | getStorageAt(address: string, slot: string): Promise; 8 | call(tx: TransactionRequest): Promise; 9 | } 10 | export interface DecoderInput { 11 | id: string; 12 | abi?: ethers.utils.Interface; 13 | type: 'call' | 'staticcall' | 'callcode' | 'delegatecall' | 'create' | 'create2' | 'selfdestruct'; 14 | from: string; 15 | to: string; 16 | value: BigNumber; 17 | calldata: BytesLike; 18 | } 19 | export interface DecoderInputReceiptExt extends DecoderInput { 20 | failed: boolean; 21 | logs: Array; 22 | } 23 | export interface DecoderInputTraceExt extends DecoderInputReceiptExt { 24 | returndata: BytesLike; 25 | children: Array; 26 | childOrder: Array<['log' | 'call', number]>; 27 | } 28 | export type DecoderOutput = { 29 | node: DecoderInput | Log; 30 | results: Action[]; 31 | children: DecoderOutput[]; 32 | }; 33 | export type MetadataRequest = { 34 | tokens: Set; 35 | }; 36 | export declare class ProviderDecoderChainAccess implements DecoderChainAccess { 37 | private provider; 38 | private cache; 39 | constructor(provider: Provider); 40 | call(transaction: TransactionRequest): Promise; 41 | getStorageAt(address: string, slot: string): Promise; 42 | } 43 | export declare class DecoderState { 44 | access: DecoderChainAccess; 45 | consumed: Set; 46 | root: DecoderInput; 47 | decoded: Map; 48 | decodeOrder: DecoderOutput[]; 49 | requestedMetadata: MetadataRequest; 50 | constructor(root: DecoderInput, access: DecoderChainAccess); 51 | getOutputFor(input: DecoderInput | Log): DecoderOutput; 52 | call(signature: string, address: string, args: any[]): Promise; 53 | requestTokenMetadata(token: string): void; 54 | isConsumed(node: DecoderInput | Log): boolean; 55 | consume(node: DecoderInput | Log): void; 56 | consumeAll(node: DecoderInput): void; 57 | consumeAllRecursively(node: DecoderInput): void; 58 | consumeTransfer(node: DecoderInput, params?: Array): void; 59 | consumeTransferFrom(node: DecoderInput, params?: Array): void; 60 | consumeTransferCommon(node: DecoderInput, from: string, to: string): void; 61 | } 62 | export declare abstract class Decoder { 63 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 64 | decodeLog(state: DecoderState, node: DecoderInput, log: Log): Promise; 65 | decodeFunctionWithFragment(node: DecoderInput, functionFragment: FunctionFragment): [Result, Result | null]; 66 | decodeEventWithFragment(log: Log, eventFragment: string | EventFragment): LogDescription; 67 | } 68 | export declare abstract class CallDecoder extends Decoder { 69 | functions: Record Promise>; 70 | constructor(); 71 | decodeCall(state: DecoderState, node: DecoderInput): Promise; 72 | abstract isTargetContract(state: DecoderState, address: string): Promise; 73 | } 74 | -------------------------------------------------------------------------------- /lib/sdk/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CallDecoder = exports.Decoder = exports.DecoderState = exports.ProviderDecoderChainAccess = void 0; 4 | const lib_1 = require("@ethersproject/abi/lib"); 5 | const ethers_1 = require("ethers"); 6 | const utils_1 = require("ethers/lib/utils"); 7 | const utils_2 = require("./utils"); 8 | class ProviderDecoderChainAccess { 9 | constructor(provider) { 10 | this.provider = provider; 11 | this.cache = {}; 12 | } 13 | async call(transaction) { 14 | return await this.provider.call(transaction); 15 | } 16 | async getStorageAt(address, slot) { 17 | if (!this.cache[address]) { 18 | this.cache[address] = {}; 19 | } 20 | if (!this.cache[address][slot]) { 21 | this.cache[address][slot] = await this.provider.getStorageAt(address, slot); 22 | } 23 | return this.cache[address][slot]; 24 | } 25 | } 26 | exports.ProviderDecoderChainAccess = ProviderDecoderChainAccess; 27 | class DecoderState { 28 | constructor(root, access) { 29 | this.root = root; 30 | this.access = access; 31 | this.consumed = new Set(); 32 | this.decoded = new Map(); 33 | this.decodeOrder = []; 34 | this.requestedMetadata = { 35 | tokens: new Set(), 36 | }; 37 | } 38 | getOutputFor(input) { 39 | if (!this.decoded.has(input)) { 40 | this.decoded.set(input, { 41 | node: input, 42 | results: [], 43 | children: [], 44 | }); 45 | this.decodeOrder.push(this.decoded.get(input)); 46 | } 47 | return this.decoded.get(input); 48 | } 49 | async call(signature, address, args) { 50 | const fragment = lib_1.Fragment.from(signature); 51 | const intf = new lib_1.Interface([ 52 | fragment, 53 | ]); 54 | return intf.decodeFunctionResult(fragment.name, await this.access.call({ 55 | to: address, 56 | data: intf.encodeFunctionData(fragment.name, args), 57 | })); 58 | } 59 | requestTokenMetadata(token) { 60 | this.requestedMetadata.tokens.add(token.toLowerCase()); 61 | } 62 | // check if a node is consumed - most decoders should ignore consumed nodes 63 | isConsumed(node) { 64 | return this.consumed.has((0, utils_2.getNodeId)(node)); 65 | } 66 | // mark the node as consumed 67 | consume(node) { 68 | this.consumed.add((0, utils_2.getNodeId)(node)); 69 | } 70 | // consume the node and all logs in it 71 | consumeAll(node) { 72 | this.consume(node); 73 | if ((0, utils_2.hasReceiptExt)(node)) { 74 | node.logs.forEach(this.consume.bind(this)); 75 | } 76 | } 77 | // consume the node and all logs in it, including all child calls 78 | consumeAllRecursively(node) { 79 | this.consumeAll(node); 80 | if ((0, utils_2.hasTraceExt)(node)) { 81 | node.children?.forEach(this.consumeAllRecursively.bind(this)); 82 | } 83 | } 84 | // assuming the input node is a call with `transfer`-like semantics (i.e. it causes a transfer from the caller 85 | // to an address specified in the calldata), consume the node and any Transfer events which correspond to the 86 | // transfer 87 | consumeTransfer(node, params) { 88 | if (!params) { 89 | params = [utils_1.ParamType.from('address to'), utils_1.ParamType.from('uint256 amount')]; 90 | } 91 | let inputs = lib_1.defaultAbiCoder.decode(params, ethers_1.ethers.utils.arrayify(node.calldata).slice(4)); 92 | this.consumeTransferCommon(node, ethers_1.ethers.utils.getAddress(node.from), inputs['to']); 93 | } 94 | // assuming the input node is a call with `transferFrom`-like semantics (i.e. it causes a transfer from one address 95 | // to another address specified in the calldata), consume the node and any Transfer events which correspond to the 96 | // transfer 97 | consumeTransferFrom(node, params) { 98 | if (!params) { 99 | params = [utils_1.ParamType.from('address from'), utils_1.ParamType.from('address to'), utils_1.ParamType.from('uint256 amount')]; 100 | } 101 | let inputs = lib_1.defaultAbiCoder.decode(params, ethers_1.ethers.utils.arrayify(node.calldata).slice(4)); 102 | this.consumeTransferCommon(node, inputs['from'], inputs['to']); 103 | } 104 | consumeTransferCommon(node, from, to) { 105 | // consume the current node 106 | this.consume(node); 107 | if (!(0, utils_2.hasTraceExt)(node)) 108 | return; 109 | const visit = (node) => { 110 | // handle any transfer events we might find, must be a match on from and to, because it might take fees 111 | node.logs 112 | .filter((v) => v.topics.length > 0 && 113 | v.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') 114 | .forEach((v) => { 115 | let abi = node.abi; 116 | if (!abi) { 117 | abi = new ethers_1.ethers.utils.Interface([ 118 | lib_1.EventFragment.from('Transfer(address indexed from, address indexed to, uint amount)'), 119 | ]); 120 | } 121 | try { 122 | let values = abi.parseLog(v); 123 | if (values.args[0] === from && values.args[1] === to) { 124 | this.consume(v); 125 | } 126 | } 127 | catch { } 128 | }); 129 | // if we have a delegatecall, we need to recurse because it will emit the log in the context of the 130 | // current contract 131 | node.children.filter((v) => v.type === 'delegatecall').forEach(visit); 132 | }; 133 | visit(node); 134 | } 135 | } 136 | exports.DecoderState = DecoderState; 137 | class Decoder { 138 | async decodeCall(state, node) { 139 | return null; 140 | } 141 | async decodeLog(state, node, log) { 142 | return null; 143 | } 144 | decodeFunctionWithFragment(node, functionFragment) { 145 | return [ 146 | lib_1.defaultAbiCoder.decode(functionFragment.inputs, ethers_1.ethers.utils.arrayify(node.calldata).slice(4)), 147 | (0, utils_2.hasTraceExt)(node) && functionFragment.outputs 148 | ? lib_1.defaultAbiCoder.decode(functionFragment.outputs, ethers_1.ethers.utils.arrayify(node.returndata)) 149 | : null, 150 | ]; 151 | } 152 | decodeEventWithFragment(log, eventFragment) { 153 | const abi = new ethers_1.ethers.utils.Interface([eventFragment]); 154 | return abi.parseLog(log); 155 | } 156 | } 157 | exports.Decoder = Decoder; 158 | class CallDecoder extends Decoder { 159 | constructor() { 160 | super(); 161 | this.functions = {}; 162 | } 163 | async decodeCall(state, node) { 164 | if (state.isConsumed(node)) 165 | return null; 166 | if (node.type !== 'call') 167 | return null; 168 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 169 | return (name === '' && node.calldata.length === 0) || (name !== '' && (0, utils_2.hasSelector)(node.calldata, name)); 170 | }); 171 | if (!functionInfo) 172 | return null; 173 | if (!await this.isTargetContract(state, node.to)) 174 | return null; 175 | state.consume(node); 176 | const [inputs, outputs] = this.decodeFunctionWithFragment(node, lib_1.FunctionFragment.from(functionInfo[0])); 177 | const functionMetadata = functionInfo[1]; 178 | return functionMetadata.bind(this)(state, node, inputs, outputs); 179 | } 180 | } 181 | exports.CallDecoder = CallDecoder; 182 | -------------------------------------------------------------------------------- /lib/sdk/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { EventFragment, FunctionFragment } from '@ethersproject/abi/lib'; 2 | import { Log } from '@ethersproject/abstract-provider'; 3 | import { BytesLike } from "ethers"; 4 | import { DecoderInput, DecoderInputReceiptExt, DecoderInputTraceExt } from './types'; 5 | export declare const hasSelector: (calldata: BytesLike, selector: string | FunctionFragment) => boolean; 6 | export declare const hasTopic: (log: Log, selector: string | EventFragment) => boolean; 7 | export declare const isEqualAddress: (a: string, b: string) => boolean; 8 | export declare const hasReceiptExt: (node: DecoderInput) => node is DecoderInputReceiptExt; 9 | export declare const hasTraceExt: (node: DecoderInput) => node is DecoderInputTraceExt; 10 | export declare const getCalls: (node: DecoderInputTraceExt) => DecoderInputTraceExt[]; 11 | export declare const flattenLogs: (node: DecoderInputReceiptExt) => Log[]; 12 | export declare const isDecoderInput: (node: DecoderInput | Log) => node is DecoderInput; 13 | export declare const getNodeId: (node: DecoderInput | Log) => string; 14 | -------------------------------------------------------------------------------- /lib/sdk/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.getNodeId = exports.isDecoderInput = exports.flattenLogs = exports.getCalls = exports.hasTraceExt = exports.hasReceiptExt = exports.isEqualAddress = exports.hasTopic = exports.hasSelector = void 0; 4 | const lib_1 = require("@ethersproject/abi/lib"); 5 | const ethers_1 = require("ethers"); 6 | const hasSelector = (calldata, selector) => { 7 | return (ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.arrayify(calldata).slice(0, 4)) === 8 | ethers_1.ethers.utils.id(lib_1.FunctionFragment.from(selector).format()).substring(0, 10)); 9 | }; 10 | exports.hasSelector = hasSelector; 11 | const hasTopic = (log, selector) => { 12 | return log.topics.length > 0 && log.topics[0] == ethers_1.ethers.utils.id(lib_1.EventFragment.from(selector).format()); 13 | }; 14 | exports.hasTopic = hasTopic; 15 | const isEqualAddress = (a, b) => { 16 | return a.toLocaleLowerCase() === b.toLocaleLowerCase(); 17 | }; 18 | exports.isEqualAddress = isEqualAddress; 19 | const hasReceiptExt = (node) => { 20 | return node.logs !== undefined; 21 | }; 22 | exports.hasReceiptExt = hasReceiptExt; 23 | const hasTraceExt = (node) => { 24 | return node.returndata !== undefined; 25 | }; 26 | exports.hasTraceExt = hasTraceExt; 27 | const getCalls = (node) => { 28 | return node.children.filter(node => node.type === 'call'); 29 | }; 30 | exports.getCalls = getCalls; 31 | const flattenLogs = (node) => { 32 | if (!(0, exports.hasTraceExt)(node)) { 33 | return node.logs; 34 | } 35 | const result = []; 36 | const visit = (node) => { 37 | node.childOrder.forEach(([type, val]) => { 38 | if (type === 'log') { 39 | result.push(node.logs[val]); 40 | } 41 | else { 42 | visit(node.children[val]); 43 | } 44 | }); 45 | }; 46 | visit(node); 47 | return result; 48 | }; 49 | exports.flattenLogs = flattenLogs; 50 | const isDecoderInput = (node) => { 51 | return node.id !== undefined; 52 | }; 53 | exports.isDecoderInput = isDecoderInput; 54 | const getNodeId = (node) => { 55 | if ((0, exports.isDecoderInput)(node)) { 56 | return 'node:' + node.id; 57 | } 58 | else { 59 | return 'log:' + node.transactionHash + '.' + node.logIndex; 60 | } 61 | }; 62 | exports.getNodeId = getNodeId; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@samczsun/transaction-decoder", 3 | "version": "0.0.1", 4 | "description": "Decode EVM-based transaction into structured data", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/samczsun/transaction-decoder.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/samczsun/transaction-decoder/issues" 18 | }, 19 | "homepage": "https://github.com/samczsun/transaction-decoder#readme", 20 | "dependencies": { 21 | "ethers": "^5.7.2" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^29.2.3", 25 | "jest": "^29.3.1", 26 | "ts-jest": "^29.0.3", 27 | "typescript": "^4.9.3" 28 | } 29 | } -------------------------------------------------------------------------------- /src/decoders/art-gobblers.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi/lib'; 2 | import { BigNumber, ethers } from 'ethers'; 3 | 4 | import { MintNFTAction } from "../sdk/actions"; 5 | import { CallDecoder, DecoderInput, DecoderState } from "../sdk/types"; 6 | import { flattenLogs, hasReceiptExt, isEqualAddress } from '../sdk/utils'; 7 | 8 | const gobblerPurchasedEventSignature = 'event GobblerPurchased(address indexed user, uint256 indexed gobblerId, uint256 price)'; 9 | 10 | export class ArtGobblersMintDecoder extends CallDecoder { 11 | constructor() { 12 | super(); 13 | 14 | this.functions['mintFromGoo(uint256 maxPrice, bool useVirtualBalance) external returns (uint256 gobblerId)'] = this.decodeMintFromGoo; 15 | } 16 | 17 | async isTargetContract(state: DecoderState, address: string): Promise { 18 | return isEqualAddress(address, '0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769'); 19 | } 20 | 21 | async decodeMintFromGoo(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 22 | const result: MintNFTAction = { 23 | type: 'nft-mint', 24 | operator: node.from, 25 | recipient: node.from, 26 | collection: node.to, 27 | buyToken: ethers.utils.getAddress('0x600000000a36F3cD48407e35eB7C5c910dc1f7a8'), 28 | buyAmount: (input['maxPrice'] as BigNumber).toBigInt(), 29 | }; 30 | 31 | // Can only get tokenId if transaction was successful... 32 | if (hasReceiptExt(node)) { 33 | const logs = flattenLogs(node); 34 | // Second to last log is GobblerPurchased event 35 | const gobblerPurchasedLog = this.decodeEventWithFragment(logs[logs.length - 2], gobblerPurchasedEventSignature); 36 | result.tokenId = gobblerPurchasedLog.args['gobblerId'].toBigInt(); 37 | result.buyAmount = (gobblerPurchasedLog.args['price'] as BigNumber).toBigInt(); 38 | } 39 | 40 | return result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/decoders/comet.ts: -------------------------------------------------------------------------------- 1 | import { FunctionFragment } from '@ethersproject/abi/lib'; 2 | 3 | import { 4 | Decoder, 5 | DecoderState, 6 | DecoderInput, 7 | } from "../sdk/types"; 8 | import { hasSelector, hasTraceExt } from "../sdk/utils"; 9 | import { SupplyAction } from "../sdk/actions"; 10 | import { BigNumber } from 'ethers'; 11 | 12 | const cTokenAddresses = new Set([ 13 | '0xc3d688B66703497DAA19211EEdff47f25384cdc3', 14 | ]); 15 | 16 | export class CometSupplyDecoder extends Decoder { 17 | // Have to make sure that we're only picking up supply calls for cTokens 18 | functions = [ 19 | 'supply(address asset,uint amount)', 20 | 'supplyTo(address dst,address asset,uint amount)', 21 | 'supplyFrom(address from,address dst,address asset,uint amount)', 22 | ]; 23 | 24 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 25 | if (state.isConsumed(node)) return null; 26 | if (node.type !== 'call') return null; 27 | 28 | if (!cTokenAddresses.has(node.to)) return null; 29 | 30 | const functionName = this.functions.find((name) => { 31 | return hasSelector(node.calldata, name); 32 | }); 33 | 34 | if (functionName === undefined) return null; 35 | 36 | const [inputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionName)); 37 | 38 | state.consume(node); 39 | 40 | // Supply implies downstream transfer call, need to consume 41 | if (hasTraceExt(node)) { 42 | // We know that the first external call from cToken supply is a delegatecall to Comet supply 43 | const cometSupplyDelegateCall = node.children[0]!; 44 | const transferFromCall = cometSupplyDelegateCall.children!.filter((v) => v.type === 'call')[0]; 45 | 46 | // First external call made from supply function is a transferFrom 47 | state.consumeTransferFrom(transferFromCall); 48 | 49 | // Consume last log from delegate call (also a transfer event) 50 | if (cometSupplyDelegateCall.logs) { 51 | state.consume(cometSupplyDelegateCall.logs[cometSupplyDelegateCall.logs!.length - 1]); 52 | } 53 | } 54 | 55 | const supplyResult: SupplyAction = { 56 | type: 'supply', 57 | protocol: 'Compound', 58 | operator: node.from, 59 | supplier: functionName === 'supplyFrom(address from,address dst,address asset,uint amount)' 60 | ? inputs['from'] 61 | : node.from, 62 | supplyToken: inputs['asset'], 63 | amount: (inputs['amount'] as BigNumber).toBigInt(), 64 | }; 65 | 66 | // Metadata for cToken 67 | state.requestTokenMetadata(node.to); 68 | // Metadata for underlying token 69 | state.requestTokenMetadata(supplyResult.supplyToken); 70 | 71 | return supplyResult; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/decoders/curve.ts: -------------------------------------------------------------------------------- 1 | import { Interface, Result } from '@ethersproject/abi'; 2 | import { SwapAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | import { hasReceiptExt, isEqualAddress } from '../sdk/utils'; 5 | 6 | const curveContracts = { 7 | ethereum: [ 8 | '0xB576491F1E6e5E62f1d8F26062Ee822B40B0E0d4', 9 | '0x8301AE4fc9c624d1D396cbDAa1ed877821D7C511', 10 | ] 11 | } 12 | 13 | const coinsSignature = 'function coins(uint256) returns (address coin)'; 14 | const tokenExchangeSignature = 'event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought)'; 15 | 16 | export class CurveSwapDecoder extends CallDecoder { 17 | constructor() { 18 | super(); 19 | 20 | this.functions['exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth)'] = this.decodeExchangeWithEth; 21 | this.functions['exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy)'] = this.decodeExchange; 22 | } 23 | 24 | async isTargetContract(state: DecoderState, address: string): Promise { 25 | return !!curveContracts['ethereum'].find(addr => isEqualAddress(addr, address)) 26 | } 27 | 28 | async decodeExchange(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 29 | const i = input['i']; 30 | const j = input['j']; 31 | 32 | const [tokenIn] = await state.call(coinsSignature, node.to, [i]); 33 | const [tokenOut] = await state.call(coinsSignature, node.to, [i]); 34 | 35 | const result: SwapAction = { 36 | type: 'swap', 37 | 38 | exchange: 'curve', 39 | operator: node.from, 40 | recipient: node.from, 41 | 42 | tokenIn: tokenIn, 43 | tokenOut: tokenOut, 44 | 45 | amountIn: input['dx'].toBigInt(), 46 | amountOutMin: input['min_dy'].toBigInt(), 47 | }; 48 | 49 | if (hasReceiptExt(node)) { 50 | const exchangeLog = this.decodeEventWithFragment(node.logs[node.logs.length - 1], tokenExchangeSignature); 51 | 52 | result.amountOut = exchangeLog.args['tokens_bought'].toBigInt(); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | async decodeExchangeWithEth(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 59 | const i = input['i']; 60 | const j = input['j']; 61 | const useEth = input['use_eth']; 62 | 63 | const intf = new Interface([ 64 | 'function coins(uint256) returns (address coin)', 65 | ]); 66 | 67 | const tokenIn = intf.decodeFunctionResult(intf.getFunction('coins'), await state.access.call({ 68 | to: node.to, 69 | data: intf.encodeFunctionData(intf.getFunction('coins'), [i]), 70 | }))['coin']; 71 | 72 | const tokenOut = intf.decodeFunctionResult(intf.getFunction('coins'), await state.access.call({ 73 | to: node.to, 74 | data: intf.encodeFunctionData(intf.getFunction('coins'), [j]), 75 | }))['coin']; 76 | 77 | const result: SwapAction = { 78 | type: 'swap', 79 | 80 | exchange: 'curve', 81 | operator: node.from, 82 | recipient: node.from, 83 | 84 | tokenIn: tokenIn, 85 | tokenOut: tokenOut, 86 | 87 | amountIn: input['dx'].toBigInt(), 88 | amountOutMin: input['min_dy'].toBigInt(), 89 | }; 90 | 91 | if (hasReceiptExt(node)) { 92 | const exchangeLog = this.decodeEventWithFragment(node.logs[node.logs.length - 1], 'event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought)'); 93 | 94 | result.amountOut = exchangeLog.args['tokens_bought'].toBigInt(); 95 | } 96 | 97 | return result; 98 | } 99 | } -------------------------------------------------------------------------------- /src/decoders/ens.ts: -------------------------------------------------------------------------------- 1 | import { EventFragment, FunctionFragment } from '@ethersproject/abi/lib'; 2 | import { ethers } from 'ethers'; 3 | import { ENSRegisterAction } from '../sdk/actions'; 4 | import { Decoder, DecoderInput, DecoderState } from '../sdk/types'; 5 | import { hasReceiptExt, hasSelector, hasTopic } from '../sdk/utils'; 6 | 7 | export class ENSDecoder extends Decoder { 8 | functions = { 9 | 'register(string name, address owner, uint256 duration, bytes32 secret)': { 10 | hasResolver: false, 11 | }, 12 | 'registerWithConfig(string name, address owner, uint256 duration, bytes32 secret, address resolver, address addr)': 13 | { 14 | hasResolver: true, 15 | }, 16 | }; 17 | 18 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 19 | if (state.isConsumed(node)) return null; 20 | 21 | if (node.to.toLowerCase() !== '0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5'.toLowerCase()) return null; 22 | 23 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 24 | return hasSelector(node.calldata, name); 25 | }); 26 | 27 | if (!functionInfo) return null; 28 | 29 | // todo: don't consume if we have a resolver set because that makes an external call 30 | state.consumeAllRecursively(node); 31 | 32 | const [inputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionInfo[0])); 33 | 34 | const functionMetadata = functionInfo[1]; 35 | 36 | let cost = node.value.toBigInt(); 37 | 38 | if (hasReceiptExt(node)) { 39 | const registeredFragment = EventFragment.from( 40 | `NameRegistered(string name, bytes32 indexed label, address indexed owner, uint cost, uint expires)`, 41 | ); 42 | 43 | const lastLog = node.logs.reverse().find((log) => hasTopic(log, registeredFragment)); 44 | if (lastLog) { 45 | const abi = new ethers.utils.Interface([registeredFragment]); 46 | const parsedEvent = abi.parseLog(lastLog); 47 | 48 | cost = parsedEvent.args['cost'].toBigInt(); 49 | } 50 | } 51 | 52 | const result: ENSRegisterAction = { 53 | type: 'ens-register', 54 | operator: node.from, 55 | owner: inputs['owner'], 56 | name: inputs['name'] + '.eth', 57 | duration: inputs['duration'].toNumber(), 58 | cost: cost, 59 | }; 60 | 61 | if (functionMetadata.hasResolver) { 62 | result.resolver = inputs['resolver']; 63 | result.addr = inputs['addr']; 64 | } 65 | 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/decoders/fallback.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '@ethersproject/abstract-provider'; 2 | import { BurnERC20Action, MintERC20Action, NATIVE_TOKEN, TransferAction } from '../sdk/actions'; 3 | import { Decoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | import { hasTopic } from '../sdk/utils'; 5 | 6 | export class TransferDecoder extends Decoder { 7 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 8 | if (state.isConsumed(node)) return null; 9 | 10 | if (node.value.isZero()) return null; 11 | 12 | return { 13 | type: 'transfer', 14 | operator: node.from, 15 | from: node.from, 16 | to: node.to, 17 | token: NATIVE_TOKEN, 18 | amount: node.value.toBigInt(), 19 | }; 20 | } 21 | 22 | async decodeLog(state: DecoderState, node: DecoderInput, log: Log): Promise { 23 | if (state.isConsumed(log)) return null; 24 | 25 | if (!hasTopic(log, `Transfer(address,address,uint256)`)) return null; 26 | 27 | if (node.abi) { 28 | const decodedEvent = node.abi.parseLog(log); 29 | 30 | state.requestTokenMetadata(log.address); 31 | 32 | if (decodedEvent.args[0] === '0x0000000000000000000000000000000000000000') { 33 | return { 34 | type: 'mint-erc20', 35 | operator: node.from, 36 | token: log.address, 37 | to: decodedEvent.args[1], 38 | amount: decodedEvent.args[2].toBigInt(), 39 | } 40 | } else if (decodedEvent.args[1] === "0x0000000000000000000000000000000000000000") { 41 | return { 42 | type: 'burn-erc20', 43 | operator: node.from, 44 | token: log.address, 45 | from: decodedEvent.args[0], 46 | amount: decodedEvent.args[2].toBigInt(), 47 | } 48 | } 49 | 50 | return { 51 | type: 'transfer', 52 | operator: node.from, 53 | token: log.address, 54 | from: decodedEvent.args[0], 55 | to: decodedEvent.args[1], 56 | amount: decodedEvent.args[2].toBigInt(), 57 | }; 58 | } 59 | 60 | return null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/decoders/index.ts: -------------------------------------------------------------------------------- 1 | import { ArtGobblersMintDecoder } from "./art-gobblers"; 2 | import { CometSupplyDecoder } from "./comet"; 3 | import { CurveSwapDecoder } from "./curve"; 4 | import { ENSDecoder } from "./ens"; 5 | import { UniswapV2PairSwapDecoder, UniswapV2RouterSwapDecoder } from "./uniswapv2"; 6 | import { UniswapV3RouterSwapDecoder } from "./uniswapv3"; 7 | import { WrappedNativeTokenDecoder } from "./wrapped"; 8 | 9 | export const defaultDecoders = [ 10 | new ArtGobblersMintDecoder(), 11 | new CometSupplyDecoder(), 12 | new CurveSwapDecoder(), 13 | new ENSDecoder(), 14 | new UniswapV2RouterSwapDecoder(), 15 | new UniswapV2PairSwapDecoder(), 16 | new UniswapV3RouterSwapDecoder(), 17 | new WrappedNativeTokenDecoder(), 18 | ]; -------------------------------------------------------------------------------- /src/decoders/uniswapv2.ts: -------------------------------------------------------------------------------- 1 | import { EventFragment, Result } from '@ethersproject/abi'; 2 | import { FunctionFragment } from '@ethersproject/abi/lib'; 3 | import { BigNumber, BytesLike, ethers } from 'ethers'; 4 | import { SwapAction } from '../sdk/actions'; 5 | import { CallDecoder, Decoder, DecoderInput, DecoderState } from '../sdk/types'; 6 | import { hasReceiptExt, hasSelector, hasTopic, hasTraceExt } from '../sdk/utils'; 7 | 8 | type UniswapDeployment = { 9 | name: string; 10 | factory: string; 11 | initcodeHash: string; 12 | routers: string[]; 13 | } 14 | 15 | const uniswaps: UniswapDeployment[] = [ 16 | { 17 | name: 'uniswap-v2', 18 | factory: '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', 19 | initcodeHash: '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f', 20 | routers: [ 21 | '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a', 22 | '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', 23 | ], 24 | }, 25 | { 26 | name: 'sushiswap', 27 | factory: '0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac', 28 | initcodeHash: '0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303', 29 | routers: [ 30 | '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F', 31 | ], 32 | }, 33 | ] 34 | 35 | 36 | const getTokens = (tokenA: string, tokenB: string): [string, string] => { 37 | return tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA]; 38 | } 39 | 40 | const computePairAddress = (factory: string, initcodeHash: BytesLike, tokenA: string, tokenB: string) => { 41 | const [token0, token1] = getTokens(tokenA, tokenB); 42 | 43 | const salt = ethers.utils.solidityKeccak256(['address', 'address'], [token0, token1]); 44 | 45 | return ethers.utils.getCreate2Address(factory, salt, initcodeHash); 46 | } 47 | 48 | export class UniswapV2RouterSwapDecoder extends Decoder { 49 | functions = { 50 | 'swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 51 | { 52 | exactIn: true, 53 | input: 'tokens', 54 | output: 'tokens', 55 | fee: false, 56 | }, 57 | 'swapTokensForExactTokens(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 58 | { 59 | exactIn: false, 60 | input: 'tokens', 61 | output: 'tokens', 62 | fee: false, 63 | }, 64 | 'swapExactETHForTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 65 | { 66 | exactIn: true, 67 | input: 'eth', 68 | output: 'tokens', 69 | fee: false, 70 | }, 71 | 'swapTokensForExactETH(uint256 amountOut,uint256 amountInMax,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 72 | { 73 | exactIn: false, 74 | input: 'tokens', 75 | output: 'eth', 76 | fee: false, 77 | }, 78 | 'swapExactTokensForETH(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 79 | { 80 | exactIn: true, 81 | input: 'tokens', 82 | output: 'eth', 83 | fee: false, 84 | }, 85 | 'swapETHForExactTokens(uint256 amountOut,address[] memory path,address to,uint256 deadline) returns (uint[] memory amounts)': 86 | { 87 | exactIn: false, 88 | input: 'eth', 89 | output: 'tokens', 90 | fee: false, 91 | }, 92 | 'swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': 93 | { 94 | exactIn: true, 95 | input: 'tokens', 96 | output: 'tokens', 97 | fee: true, 98 | }, 99 | 'swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': 100 | { 101 | exactIn: true, 102 | input: 'eth', 103 | output: 'tokens', 104 | fee: true, 105 | }, 106 | 'swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn,uint256 amountOutMin,address[] memory path,address to,uint256 deadline)': 107 | { 108 | exactIn: true, 109 | input: 'tokens', 110 | output: 'eth', 111 | fee: true, 112 | }, 113 | }; 114 | 115 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 116 | if (state.isConsumed(node)) return null; 117 | if (node.type !== 'call') return null; 118 | 119 | const routerInfo = uniswaps.find(v => v.routers.includes(node.to)); 120 | if (!routerInfo) return null; 121 | 122 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 123 | return hasSelector(node.calldata, name); 124 | }); 125 | 126 | if (!functionInfo) return null; 127 | 128 | const [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionInfo[0])); 129 | 130 | const swapMetadata = functionInfo[1]; 131 | 132 | // consume events and calls if we have them 133 | state.consume(node); 134 | if (swapMetadata.input === 'tokens') { 135 | this.consumeTokenInputSwap(state, node); 136 | } else { 137 | this.consumeETHInputSwap(state, node); 138 | } 139 | this.consumeSwaps(state, node); 140 | if (swapMetadata.output === 'eth') { 141 | this.consumeETHOutputSwap(state, node); 142 | } 143 | 144 | const path = inputs['path']; 145 | 146 | const swapResult: SwapAction = { 147 | type: 'swap', 148 | exchange: routerInfo.name, 149 | operator: node.from, 150 | recipient: inputs['to'], 151 | tokenIn: path[0], 152 | tokenOut: path[path.length - 1], 153 | }; 154 | 155 | // flag that we want token metadata to render the result 156 | state.requestTokenMetadata(swapResult.tokenIn); 157 | state.requestTokenMetadata(swapResult.tokenOut); 158 | 159 | const inputAmountIn: bigint | undefined = inputs['amountIn'] ? (inputs['amountIn'] as BigNumber).toBigInt() : undefined; 160 | const inputAmountOutMin: bigint | undefined = inputs['amountOutMin'] ? (inputs['amountOutMin'] as BigNumber).toBigInt() : undefined; 161 | const inputAmountOut: bigint | undefined = inputs['amountOut'] ? (inputs['amountOut'] as BigNumber).toBigInt() : undefined; 162 | const inputAmountInMax: bigint | undefined = inputs['amountInMax'] ? (inputs['amountInMax'] as BigNumber).toBigInt() : undefined; 163 | 164 | // pull info from from calldata 165 | if (swapMetadata.exactIn) { 166 | swapResult.amountIn = swapMetadata.input === 'eth' ? node.value.toBigInt() : inputAmountIn; 167 | swapResult.amountOutMin = inputAmountOutMin; 168 | } else { 169 | swapResult.amountOut = inputAmountOut; 170 | swapResult.amountInMax = inputAmountInMax; 171 | } 172 | 173 | // pull info from events 174 | if (hasReceiptExt(node)) { 175 | const swapEventSelector = 176 | 'Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to)'; 177 | 178 | const abi = new ethers.utils.Interface([EventFragment.from(swapEventSelector)]); 179 | 180 | const swapEvents = node.logs.filter((log) => hasTopic(log, swapEventSelector)); 181 | const [firstToken0, firstToken1] = getTokens(path[0], path[1]); 182 | const firstPairAddress = computePairAddress( 183 | routerInfo.factory, 184 | routerInfo.initcodeHash, 185 | firstToken0, 186 | firstToken1, 187 | ); 188 | 189 | const [lastToken0, lastToken1] = getTokens(path[path.length - 2], path[path.length - 1]); 190 | const lastPairAddress = computePairAddress( 191 | routerInfo.factory, 192 | routerInfo.initcodeHash, 193 | lastToken0, 194 | lastToken1, 195 | ); 196 | 197 | const firstSwapEvent = swapEvents.find((event) => event.address === firstPairAddress); 198 | const lastSwapEvent = swapEvents.reverse().find((event) => event.address === lastPairAddress); 199 | 200 | if (firstSwapEvent) { 201 | const parsedEvent = abi.parseLog(firstSwapEvent); 202 | 203 | const eventAmount0In: bigint | undefined = parsedEvent.args['amount0In'] ? (parsedEvent.args['amount0In'] as BigNumber).toBigInt() : undefined; 204 | const eventAmount1In: bigint | undefined = parsedEvent.args['amount1In'] ? (parsedEvent.args['amount1In'] as BigNumber).toBigInt() : undefined; 205 | 206 | swapResult.amountIn = 207 | firstToken0 === path[0] 208 | ? eventAmount0In 209 | : eventAmount1In; 210 | } 211 | 212 | if (lastSwapEvent) { 213 | const parsedEvent = abi.parseLog(lastSwapEvent); 214 | 215 | const eventAmount0Out: bigint | undefined = parsedEvent.args['amount0Out'] ? (parsedEvent.args['amount0Out'] as BigNumber).toBigInt() : undefined; 216 | const eventAmount1Out: bigint | undefined = parsedEvent.args['amount1Out'] ? (parsedEvent.args['amount1Out'] as BigNumber).toBigInt() : undefined; 217 | 218 | swapResult.amountOut = 219 | lastToken0 === path[path.length - 1] 220 | ? eventAmount0Out 221 | : eventAmount1Out; 222 | } 223 | } 224 | 225 | // pull info from returndata 226 | if (outputs) { 227 | if (!swapMetadata.fee) { 228 | // if the swap is fee-less, we just check get the last amount 229 | const amounts = outputs['amounts']; 230 | 231 | const lastAmount = amounts[amounts.length - 1] ? (amounts[amounts.length - 1] as BigNumber).toBigInt() : undefined; 232 | 233 | swapResult.amountOut = lastAmount; 234 | } else { 235 | // otherwise, we need to check the call tree to pull out balance information 236 | if (hasTraceExt(node)) { 237 | switch (swapMetadata.output) { 238 | case 'tokens': 239 | const balanceOfCalls = node.children 240 | .filter((v) => v.type === 'staticcall') 241 | .filter((v) => hasSelector(v.calldata, 'balanceOf(address)')); 242 | 243 | // pull out the balanceOf calls 244 | const initialBalance = BigNumber.from(balanceOfCalls[0].returndata); 245 | const finalBalance = BigNumber.from(balanceOfCalls[balanceOfCalls.length - 1].returndata); 246 | swapResult.amountOut = finalBalance.sub(initialBalance).toBigInt(); 247 | break; 248 | case 'eth': 249 | const calls = node.children.filter((v) => v.type === 'call'); 250 | 251 | const lastAmount = calls[calls.length - 1].value ? (calls[calls.length - 1].value as BigNumber).toBigInt() : undefined; 252 | 253 | swapResult.amountOut = calls[calls.length - 1].value.toBigInt(); 254 | break; 255 | } 256 | } 257 | } 258 | } 259 | 260 | return swapResult; 261 | } 262 | 263 | consumeSwaps(state: DecoderState, node: DecoderInput) { 264 | if (!hasTraceExt(node)) return; 265 | 266 | node.children 267 | .filter((call) => call.type === 'call') 268 | .filter((call) => hasSelector(call.calldata, 'swap(uint256,uint256,address,bytes)')) 269 | .forEach((call) => { 270 | state.consume(call); 271 | 272 | call.children 273 | .filter((v) => v.type === 'call' && hasSelector(v.calldata, 'transfer(address,uint256)')) 274 | .forEach((v) => state.consumeTransfer(v)); 275 | }); 276 | } 277 | 278 | consumeTokenInputSwap(state: DecoderState, node: DecoderInput) { 279 | if (!hasTraceExt(node)) return; 280 | 281 | const calls = node.children.filter((v) => v.type === 'call'); 282 | 283 | state.consumeTransferFrom(calls[0]); 284 | } 285 | 286 | consumeETHInputSwap(state: DecoderState, node: DecoderInput) { 287 | if (!hasTraceExt(node)) return; 288 | 289 | const calls = node.children.filter((v) => v.type === 'call'); 290 | 291 | // weth deposit 292 | state.consumeAll(calls[0]); 293 | 294 | // weth transfer 295 | state.consumeAll(calls[1]); 296 | 297 | // weth refund 298 | if (!calls[calls.length - 1].value.isZero()) { 299 | state.consumeAll(calls[calls.length - 1]); 300 | } 301 | } 302 | 303 | consumeETHOutputSwap(state: DecoderState, node: DecoderInput) { 304 | if (!hasTraceExt(node)) return; 305 | 306 | const calls = node.children.filter((v) => v.type === 'call'); 307 | 308 | // weth withdraw 309 | state.consumeAll(calls[calls.length - 2]); 310 | 311 | // eth transfer 312 | state.consumeAll(calls[calls.length - 1]); 313 | } 314 | } 315 | 316 | const swapEventSignature = 'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)'; 317 | 318 | export class UniswapV2PairSwapDecoder extends CallDecoder { 319 | constructor() { 320 | super(); 321 | this.functions['swap(uint256 amount0Out, uint256 amount1Out, address to, bytes data)'] = this.decodeSwap; 322 | } 323 | 324 | async getDeploymentForPair(state: DecoderState, address: string): Promise<[string, string, UniswapDeployment] | null> { 325 | const [token0] = await state.call('function token0() returns (address)', address, []); 326 | const [token1] = await state.call('function token1() returns (address)', address, []); 327 | 328 | const deployment = uniswaps.find(deployment => { 329 | const pairAddress = computePairAddress(deployment.factory, deployment.initcodeHash, token0, token1); 330 | return pairAddress.toLocaleLowerCase() === address.toLocaleLowerCase(); 331 | }); 332 | 333 | if (!deployment) { 334 | return null; 335 | } 336 | 337 | return [token0, token1, deployment]; 338 | } 339 | 340 | async isTargetContract(state: DecoderState, address: string): Promise { 341 | return !!(await this.getDeploymentForPair(state, address)) 342 | } 343 | 344 | async decodeSwap(state: DecoderState, node: DecoderInput, inputs: Result, outputs: Result | null): Promise { 345 | const [token0, token1, deployment] = (await this.getDeploymentForPair(state, node.to))!; 346 | 347 | if (hasReceiptExt(node)) { 348 | // the last log must be a swap 349 | state.consume(node.logs[node.logs.length - 1]); 350 | } 351 | 352 | if (hasTraceExt(node)) { 353 | // there must be at least one transfer out 354 | state.consumeTransfer(node.children[0]); 355 | } 356 | 357 | const reversedDecode = Array.from(state.decodeOrder).reverse(); 358 | for (let result of reversedDecode) { 359 | const newResults = result.results.filter(action => { 360 | return action.type !== 'transfer' || (action.to.toLocaleLowerCase() !== node.to.toLocaleLowerCase()); 361 | }); 362 | result.results = newResults; 363 | } 364 | 365 | state.decoded.get(state.root) 366 | 367 | let tokenIn = token0; 368 | let tokenOut = token1; 369 | let amountIn; 370 | let amountOut; 371 | 372 | if (hasReceiptExt(node)) { 373 | const swapEvent = this.decodeEventWithFragment(node.logs[node.logs.length - 1], swapEventSignature); 374 | 375 | if (swapEvent.args['amount0In'].toBigInt() && !swapEvent.args['amount1In'].toBigInt()) { 376 | console.log("used branch a") 377 | tokenIn = token0; 378 | tokenOut = token1; 379 | amountIn = swapEvent.args['amount0In']; 380 | amountOut = swapEvent.args['amount1Out']; 381 | } else { 382 | console.log("used branch b", swapEvent.args) 383 | tokenIn = token1; 384 | tokenOut = token0; 385 | amountIn = swapEvent.args['amount1In']; 386 | amountOut = swapEvent.args['amount0Out']; 387 | } 388 | } else { 389 | console.log("node has no logs?") 390 | } 391 | 392 | console.log('decoded swap???', tokenIn, tokenOut, amountIn, amountOut); 393 | 394 | const action: SwapAction = { 395 | type: 'swap', 396 | exchange: deployment.name, 397 | operator: node.from, 398 | recipient: inputs['to'], 399 | tokenIn: tokenIn, 400 | tokenOut: tokenOut, 401 | amountIn: amountIn, 402 | amountOut: amountOut, 403 | }; 404 | 405 | return action; 406 | } 407 | } 408 | 409 | // export type UniswapV2RouterAddLiquidityResult = { 410 | // type: string; 411 | // actor: string; 412 | // recipient: string; 413 | // pool: string; 414 | // tokenA: string; 415 | // tokenB: string; 416 | 417 | // amountADesired: BigNumber; 418 | // amountBDesired: BigNumber; 419 | // amountAMin: BigNumber; 420 | // amountBMin: BigNumber; 421 | 422 | // amountA?: BigNumber; 423 | // amountB?: BigNumber; 424 | // liquidity?: BigNumber; 425 | // }; 426 | 427 | // export class UniswapV2RouterAddLiquidityDecoder extends Decoder { 428 | // functions = { 429 | // 'addLiquidity(address tokenA,address tokenB,uint256 amountADesired,uint256 amountBDesired,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline) returns (uint amountA, uint amountB, uint liquidity)': 430 | // { 431 | // eth: false, 432 | // }, 433 | // 'addLiquidityETH(address token,uint256 amountTokenDesired,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256) returns (uint amountToken, uint amountETH, uint liquidity)': 434 | // { 435 | // eth: true, 436 | // }, 437 | // }; 438 | 439 | // constructor() { 440 | // super('uniswap-v2-router-add-liquidity'); 441 | // } 442 | 443 | // decodeCall(state: DecoderState, node: DecoderInput): UniswapV2RouterAddLiquidityResult | null { 444 | // if (state.isConsumed(node)) return null; 445 | // if (node.type !== 'call') return null; 446 | 447 | // const functionInfo = Object.entries(this.functions).find(([name, func]) => { 448 | // return hasSelector(node.calldata, name); 449 | // }); 450 | 451 | // if (!functionInfo) return null; 452 | 453 | // const [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionInfo[0])); 454 | 455 | // const functionMetadata = functionInfo[1]; 456 | 457 | // const result = { 458 | // type: this.name, 459 | // actor: node.from, 460 | // recipient: inputs['to'], 461 | // tokenA: functionMetadata.eth ? inputs['token'] : inputs['tokenA'], 462 | // tokenB: functionMetadata.eth ? '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' : inputs['tokenB'], 463 | // amountAMin: functionMetadata.eth ? inputs['amountTokenMin'] : inputs['amountAMin'], 464 | // amountBMin: functionMetadata.eth ? inputs['amountETHMin'] : inputs['amountBMin'], 465 | // amountADesired: functionMetadata.eth ? inputs['amountTokenMin'] : inputs['amountAMin'], 466 | // amountBDesired: functionMetadata.eth ? undefined : inputs['amountBMin'], 467 | // }; 468 | 469 | // state.requestTokenMetadata(result.tokenA); 470 | // state.requestTokenMetadata(result.tokenB); 471 | // state.requestTokenMetadata(result.pool); 472 | 473 | // return result; 474 | // } 475 | 476 | // format(result: UniswapV2RouterAddLiquidityResult, opts: DecodeFormatOpts): JSX.Element { 477 | // return this.renderResult( 478 | // 'add liquidity', 479 | // '#6c969d', 480 | // ['tokenA', 'tokenB', 'liquidity', 'recipient', 'actor'], 481 | // [ 482 | // this.formatTokenAmount(opts, result.tokenA, result.amountA), 483 | // this.formatTokenAmount(opts, result.tokenB, result.amountB), 484 | // this.formatTokenAmount(opts, result.pool, result.liquidity), 485 | // , 486 | // , 487 | // ], 488 | // ); 489 | // } 490 | 491 | // // handles the _addLiquidity function 492 | // handleAddLiquidity(node: TraceEntryCall, subcalls: TraceEntryCall[], state: DecodeState): boolean { 493 | // return subcalls[0].input.substring(0, 10) === ethers.utils.id('createPair(address,address)').substring(0, 10); 494 | // } 495 | 496 | // decodeAddLiquidity( 497 | // node: TraceEntryCall, 498 | // state: DecodeState, 499 | // inputs: Result, 500 | // outputs: Result, 501 | // subcalls: TraceEntryCall[], 502 | // ) { 503 | // let idx = this.handleAddLiquidity(node, subcalls, state) ? 1 : 0; 504 | 505 | // // handle the transfer from tokenA -> pair 506 | // this.handleTransferFrom(state, subcalls[idx]); 507 | 508 | // // handle the transfer from tokenB -> pair 509 | // this.handleTransfer(state, subcalls[idx + 1]); 510 | 511 | // // handle the mint call 512 | // this.handleRecursively(state, subcalls[idx + 2]); 513 | 514 | // return [ 515 | // subcalls[idx + 2].to, 516 | // inputs['tokenA'], 517 | // inputs['tokenB'], 518 | // outputs['amountA'], 519 | // outputs['amountB'], 520 | // outputs['liquidity'], 521 | // inputs['to'], 522 | // ]; 523 | // } 524 | 525 | // decodeAddLiquidityETH( 526 | // node: TraceEntryCall, 527 | // state: DecodeState, 528 | // inputs: Result, 529 | // outputs: Result, 530 | // subcalls: TraceEntryCall[], 531 | // ) { 532 | // let idx = this.handleAddLiquidity(node, subcalls, state) ? 1 : 0; 533 | 534 | // // handle the transfer from tokenA -> pair 535 | // this.handleTransferFrom(state, subcalls[idx]); 536 | 537 | // // handle the weth deposit 538 | // this.handleRecursively(state, subcalls[idx + 1]); 539 | 540 | // // handle the weth transfer 541 | // this.handleRecursively(state, subcalls[idx + 2]); 542 | 543 | // // handle the mint call 544 | // this.handleRecursively(state, subcalls[idx + 3]); 545 | 546 | // // handle the optional eth refund 547 | // if (idx + 4 < subcalls.length) { 548 | // this.handleRecursively(state, subcalls[idx + 4]); 549 | // } 550 | 551 | // return [ 552 | // subcalls[idx + 3].to, 553 | // inputs['token'], 554 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 555 | // outputs['amountToken'], 556 | // outputs['amountETH'], 557 | // outputs['liquidity'], 558 | // inputs['to'], 559 | // ]; 560 | // } 561 | // } 562 | 563 | // export type UniswapV2RouterRemoveLiquidityResult = { 564 | // type: string; 565 | // actor: string; 566 | // recipient: string; 567 | // pool: string; 568 | // tokenA: string; 569 | // tokenB: string; 570 | // amountA: BigNumber; 571 | // amountB: BigNumber; 572 | // liquidity: BigNumber; 573 | // }; 574 | 575 | // export class UniswapV2RouterRemoveLiquidityDecoder extends Decoder { 576 | // addLiquidityFunctions = { 577 | // 'removeLiquidity(address tokenA,address tokenB,uint256 liquidity,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline) returns (uint amountA, uint amountB)': 578 | // this.decodeRemoveLiquidity.bind(this), 579 | // 'removeLiquidityETH(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline) returns (uint amountToken, uint amountETH)': 580 | // this.decodeRemoveLiquidityETH.bind(this), 581 | // 'removeLiquidityWithPermit(address tokenA,address tokenB,uint256 liquidity,uint256 amountAMin,uint256 amountBMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountA, uint amountB)': 582 | // this.decodeRemoveLiquidityWithPermit.bind(this), 583 | // 'removeLiquidityETHWithPermit(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountToken, uint amountETH)': 584 | // this.decodeRemoveLiquidityETHWithPermit.bind(this), 585 | // 'removeLiquidityETHSupportingFeeOnTransferTokens(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline) returns (uint amountETH)': 586 | // this.decodeRemoveLiquidityETHSupportingFeeOnTransferTokens.bind(this), 587 | // 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(address token,uint256 liquidity,uint256 amountTokenMin,uint256 amountETHMin,address to,uint256 deadline,bool approveMax,uint8 v,bytes32 r,bytes32 s) returns (uint amountETH)': 588 | // this.decodeRemoveLiquidityETHWithPermitSupportingFeeOnTransferTokens.bind(this), 589 | // }; 590 | 591 | // constructor() { 592 | // super('uniswap-v2-router-remove-liquidity'); 593 | // } 594 | 595 | // decode(node: TraceEntry, state: DecodeState): UniswapV2RouterRemoveLiquidityResult | null { 596 | // if (state.handled[node.path]) return null; 597 | 598 | // if (node.type !== 'call') return null; 599 | 600 | // let selector = node.input.substring(0, 10); 601 | // let decoder = Object.entries(this.addLiquidityFunctions).find(([name, func]) => { 602 | // return ethers.utils.id(FunctionFragment.from(name).format()).substring(0, 10) === selector; 603 | // }); 604 | 605 | // if (!decoder) return null; 606 | 607 | // state.handled[node.path] = true; 608 | 609 | // let [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(decoder[0])); 610 | 611 | // let subcalls: TraceEntryCall[] = node.children.filter( 612 | // (v) => v.type === 'call' && v.variant === 'call', 613 | // ) as TraceEntryCall[]; 614 | 615 | // let [pool, tokenA, tokenB, amountA, amountB, liquidity, to] = decoder[1]( 616 | // node, 617 | // state, 618 | // inputs, 619 | // outputs, 620 | // subcalls, 621 | // ); 622 | 623 | // this.requestTokenMetadata(state, tokenA); 624 | // this.requestTokenMetadata(state, tokenB); 625 | // this.requestTokenMetadata(state, pool); 626 | 627 | // return { 628 | // type: this.name, 629 | // actor: node.from, 630 | // recipient: to, 631 | // pool: pool, 632 | // tokenA: tokenA, 633 | // tokenB: tokenB, 634 | // amountA: amountA, 635 | // amountB: amountB, 636 | // liquidity: liquidity, 637 | // }; 638 | // } 639 | 640 | // format(result: UniswapV2RouterRemoveLiquidityResult, opts: DecodeFormatOpts): JSX.Element { 641 | // return this.renderResult( 642 | // 'remove liquidity', 643 | // '#392b58', 644 | // ['tokenA', 'tokenB', 'liquidity', 'recipient', 'actor'], 645 | // [ 646 | // this.formatTokenAmount(opts, result.tokenA, result.amountA), 647 | // this.formatTokenAmount(opts, result.tokenB, result.amountB), 648 | // this.formatTokenAmount(opts, result.pool, result.liquidity), 649 | // , 650 | // , 651 | // ], 652 | // ); 653 | // } 654 | 655 | // // handles the removeLiquidity function 656 | // handleRemoveLiquidity( 657 | // node: TraceEntryCall, 658 | // subcalls: TraceEntryCall[], 659 | // state: DecodeState, 660 | // offset: number, 661 | // ): number { 662 | // // handle the transfer from tokenA -> pair 663 | // this.handleTransferFrom(state, subcalls[offset]); 664 | 665 | // // handle the burn call 666 | // this.handleRecursively(state, subcalls[offset + 1]); 667 | 668 | // return offset + 2; 669 | // } 670 | 671 | // decodeRemoveLiquidity( 672 | // node: TraceEntryCall, 673 | // state: DecodeState, 674 | // inputs: Result, 675 | // outputs: Result, 676 | // subcalls: TraceEntryCall[], 677 | // ) { 678 | // this.handleRemoveLiquidity(node, subcalls, state, 0); 679 | 680 | // return [ 681 | // subcalls[0].to, 682 | // inputs['tokenA'], 683 | // inputs['tokenB'], 684 | // outputs['amountA'], 685 | // outputs['amountB'], 686 | // inputs['liquidity'], 687 | // inputs['to'], 688 | // ]; 689 | // } 690 | 691 | // decodeRemoveLiquidityETH( 692 | // node: TraceEntryCall, 693 | // state: DecodeState, 694 | // inputs: Result, 695 | // outputs: Result, 696 | // subcalls: TraceEntryCall[], 697 | // ) { 698 | // this.handleRemoveLiquidity(node, subcalls, state, 0); 699 | 700 | // // handle the transfer 701 | // this.handleTransfer(state, subcalls[2]); 702 | 703 | // // handle the weth withdraw 704 | // this.handleRecursively(state, subcalls[3]); 705 | 706 | // // handle the eth return 707 | // state.handled[subcalls[4].path] = true; 708 | 709 | // return [ 710 | // subcalls[0].to, 711 | // inputs['token'], 712 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 713 | // outputs['amountToken'], 714 | // outputs['amountETH'], 715 | // inputs['liquidity'], 716 | // inputs['to'], 717 | // ]; 718 | // } 719 | 720 | // decodeRemoveLiquidityWithPermit( 721 | // node: TraceEntryCall, 722 | // state: DecodeState, 723 | // inputs: Result, 724 | // outputs: Result, 725 | // subcalls: TraceEntryCall[], 726 | // ) { 727 | // this.handleRemoveLiquidity(node, subcalls, state, 1); 728 | 729 | // return [ 730 | // subcalls[0].to, 731 | // inputs['tokenA'], 732 | // inputs['tokenB'], 733 | // outputs['amountA'], 734 | // outputs['amountB'], 735 | // inputs['liquidity'], 736 | // inputs['to'], 737 | // ]; 738 | // } 739 | 740 | // decodeRemoveLiquidityETHWithPermit( 741 | // node: TraceEntryCall, 742 | // state: DecodeState, 743 | // inputs: Result, 744 | // outputs: Result, 745 | // subcalls: TraceEntryCall[], 746 | // ) { 747 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 1); 748 | 749 | // // handle the transfer 750 | // this.handleTransfer(state, subcalls[offset]); 751 | 752 | // // handle the weth withdraw 753 | // this.handleRecursively(state, subcalls[offset + 1]); 754 | 755 | // // handle the eth return 756 | // state.handled[subcalls[offset + 2].path] = true; 757 | 758 | // return [ 759 | // subcalls[0].to, 760 | // inputs['token'], 761 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 762 | // outputs['amountToken'], 763 | // outputs['amountETH'], 764 | // inputs['liquidity'], 765 | // inputs['to'], 766 | // ]; 767 | // } 768 | 769 | // decodeRemoveLiquidityETHSupportingFeeOnTransferTokens( 770 | // node: TraceEntryCall, 771 | // state: DecodeState, 772 | // inputs: Result, 773 | // outputs: Result, 774 | // subcalls: TraceEntryCall[], 775 | // ) { 776 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 0); 777 | 778 | // // handle the transfer 779 | // this.handleTransfer(state, subcalls[offset]); 780 | 781 | // // handle the weth withdraw 782 | // this.handleRecursively(state, subcalls[offset + 1]); 783 | 784 | // // handle the eth return 785 | // state.handled[subcalls[offset + 2].path] = true; 786 | 787 | // let staticcalls = node.children.filter( 788 | // (v): v is TraceEntryCall => v.type === 'call' && v.variant === 'staticcall', 789 | // ); 790 | // let output = BigNumber.from(staticcalls[staticcalls.length - 1].output); 791 | 792 | // return [ 793 | // subcalls[0].to, 794 | // inputs['token'], 795 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 796 | // output, 797 | // outputs['amountETH'], 798 | // inputs['liquidity'], 799 | // inputs['to'], 800 | // ]; 801 | // } 802 | 803 | // decodeRemoveLiquidityETHWithPermitSupportingFeeOnTransferTokens( 804 | // node: TraceEntryCall, 805 | // state: DecodeState, 806 | // inputs: Result, 807 | // outputs: Result, 808 | // subcalls: TraceEntryCall[], 809 | // ) { 810 | // let offset = this.handleRemoveLiquidity(node, subcalls, state, 1); 811 | 812 | // // handle the transfer 813 | // this.handleTransfer(state, subcalls[offset]); 814 | 815 | // // handle the weth withdraw 816 | // this.handleRecursively(state, subcalls[offset + 1]); 817 | 818 | // // handle the eth return 819 | // state.handled[subcalls[offset + 2].path] = true; 820 | 821 | // let staticcalls = node.children.filter( 822 | // (v): v is TraceEntryCall => v.type === 'call' && v.variant === 'staticcall', 823 | // ); 824 | // let output = BigNumber.from(staticcalls[staticcalls.length - 1].output); 825 | 826 | // return [ 827 | // subcalls[0].to, 828 | // inputs['token'], 829 | // '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 830 | // output, 831 | // outputs['amountETH'], 832 | // inputs['liquidity'], 833 | // inputs['to'], 834 | // ]; 835 | // } 836 | // } 837 | -------------------------------------------------------------------------------- /src/decoders/uniswapv3.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi/lib'; 2 | import { SwapAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | import { flattenLogs, hasReceiptExt, isEqualAddress } from '../sdk/utils'; 5 | 6 | const swapEventSignature = `event Swap( 7 | address indexed sender, 8 | address indexed recipient, 9 | int256 amount0, 10 | int256 amount1, 11 | uint160 sqrtPriceX96, 12 | uint128 liquidity, 13 | int24 tick 14 | );` 15 | 16 | export class UniswapV3RouterSwapDecoder extends CallDecoder { 17 | constructor() { 18 | super(); 19 | 20 | this.functions['exactInput(tuple(bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)'] = this.decodeExactInput; 21 | } 22 | 23 | async isTargetContract(state: DecoderState, address: string): Promise { 24 | return isEqualAddress(address, '0xE592427A0AEce92De3Edee1F18E0157C05861564'); 25 | } 26 | 27 | async decodeExactInput(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 28 | const path = input['params']['path']; 29 | const amountIn = input['params']['amountIn']; 30 | const amountOutMin = input['params']['amountOutMinimum']; 31 | const recipient = input['params']['recipient']; 32 | 33 | const tokenIn = "0x" + path.substring(2, 42); 34 | const tokenOut = "0x" + path.substring(path.length - 40); 35 | 36 | const result: SwapAction = { 37 | type: 'swap', 38 | exchange: 'uniswap-v3', 39 | operator: node.from, 40 | recipient: recipient, 41 | tokenIn: tokenIn, 42 | tokenOut: tokenOut, 43 | amountIn: amountIn, 44 | amountOutMin: amountOutMin, 45 | }; 46 | 47 | if (hasReceiptExt(node)) { 48 | const logs = flattenLogs(node); 49 | 50 | const swapLog = this.decodeEventWithFragment(logs[logs.length - 1], swapEventSignature); 51 | 52 | const amount0 = swapLog.args['amount0'].toBigInt(); 53 | const amount1 = swapLog.args['amount1'].toBigInt(); 54 | 55 | result.amountOut = amount0 < 0n ? amount0 : amount1; 56 | } 57 | 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/decoders/wrapped.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@ethersproject/abi'; 2 | import { NATIVE_TOKEN, UnwrapNativeTokenAction, WrapNativeTokenAction } from '../sdk/actions'; 3 | import { CallDecoder, DecoderInput, DecoderState } from '../sdk/types'; 4 | import { hasTraceExt, isEqualAddress } from '../sdk/utils'; 5 | 6 | const wrappedNativeTokens = { 7 | ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 8 | }; 9 | 10 | export class WrappedNativeTokenDecoder extends CallDecoder { 11 | constructor() { 12 | super(); 13 | 14 | this.functions[''] = this.decodeWrap; 15 | this.functions['deposit()'] = this.decodeWrap; 16 | this.functions['withdraw(uint256 amount)'] = this.decodeUnwrap; 17 | } 18 | 19 | async isTargetContract(state: DecoderState, address: string): Promise { 20 | return isEqualAddress(wrappedNativeTokens['ethereum'], address); 21 | } 22 | 23 | async decodeWrap(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 24 | if (hasTraceExt(node)) { 25 | state.consumeAll(node); 26 | } 27 | 28 | state.requestTokenMetadata(node.to); 29 | 30 | return { 31 | type: 'wrap-native-token', 32 | token: NATIVE_TOKEN, 33 | operator: node.from, 34 | amount: node.value.toBigInt(), 35 | }; 36 | } 37 | 38 | async decodeUnwrap(state: DecoderState, node: DecoderInput, input: Result, output: Result | null): Promise { 39 | if (hasTraceExt(node)) { 40 | state.consumeAllRecursively(node); 41 | } 42 | 43 | state.requestTokenMetadata(node.to); 44 | 45 | return { 46 | type: 'unwrap-native-token', 47 | token: NATIVE_TOKEN, 48 | operator: node.from, 49 | amount: input['amount'].toBigInt(), 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/sdk/actions.ts: -------------------------------------------------------------------------------- 1 | export const NATIVE_TOKEN = 'native_token'; 2 | 3 | export type BaseAction = { 4 | type: string; 5 | }; 6 | 7 | export interface TransferAction { 8 | type: 'transfer'; 9 | 10 | operator: string; 11 | 12 | from: string; 13 | to: string; 14 | 15 | token: string; 16 | amount: bigint; 17 | } 18 | 19 | export interface MintERC20Action { 20 | type: 'mint-erc20'; 21 | 22 | operator: string; 23 | 24 | to: string; 25 | 26 | token: string; 27 | amount: bigint; 28 | } 29 | 30 | export interface BurnERC20Action { 31 | type: 'burn-erc20'; 32 | 33 | operator: string; 34 | 35 | from: string; 36 | 37 | token: string; 38 | amount: bigint; 39 | } 40 | 41 | export type SwapAction = { 42 | type: 'swap'; 43 | 44 | exchange: string; 45 | 46 | operator: string; 47 | 48 | recipient: string; 49 | 50 | tokenIn: string; 51 | tokenOut: string; 52 | 53 | amountIn?: bigint; 54 | amountInMax?: bigint; 55 | amountOut?: bigint; 56 | amountOutMin?: bigint; 57 | }; 58 | 59 | export type ENSRegisterAction = { 60 | type: 'ens-register'; 61 | 62 | operator: string; 63 | 64 | owner: string; 65 | name: string; 66 | duration: number; 67 | cost: bigint; 68 | 69 | resolver?: string; 70 | addr?: string; 71 | }; 72 | 73 | export type SupplyAction = { 74 | type: 'supply'; 75 | 76 | protocol: string; 77 | 78 | operator: string; 79 | 80 | supplier: string; 81 | 82 | supplyToken: string; 83 | 84 | amount: bigint; 85 | } 86 | 87 | export type WrapNativeTokenAction = { 88 | type: 'wrap-native-token'; 89 | 90 | token: string; 91 | 92 | operator: string; 93 | amount: bigint; 94 | }; 95 | 96 | export type UnwrapNativeTokenAction = { 97 | type: 'unwrap-native-token'; 98 | 99 | token: string; 100 | 101 | operator: string; 102 | amount: bigint; 103 | }; 104 | 105 | // TODO: Add support for batch minting a la ERC1155 106 | export type MintNFTAction = { 107 | type: 'nft-mint'; 108 | 109 | operator: string; 110 | recipient: string; 111 | 112 | collection: string; 113 | tokenId?: bigint; 114 | 115 | buyToken?: string; 116 | buyAmount?: bigint; 117 | } 118 | 119 | 120 | export type Action = 121 | MintERC20Action 122 | | BurnERC20Action 123 | | TransferAction 124 | | SwapAction 125 | | ENSRegisterAction 126 | | WrapNativeTokenAction 127 | | UnwrapNativeTokenAction 128 | | SupplyAction 129 | | MintNFTAction 130 | ; 131 | -------------------------------------------------------------------------------- /src/sdk/decoder.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '@ethersproject/abstract-provider'; 2 | import { 3 | Decoder, 4 | DecoderChainAccess, 5 | DecoderInput, 6 | DecoderOutput, 7 | DecoderState, 8 | MetadataRequest 9 | } from './types'; 10 | import { hasReceiptExt, hasTraceExt } from './utils'; 11 | 12 | export class DecoderManager { 13 | 14 | decoders: Decoder[] = []; 15 | fallbackDecoder: Decoder; 16 | 17 | constructor(decoders: Decoder[], fallbackDecoder: Decoder) { 18 | this.decoders = decoders; 19 | this.fallbackDecoder = fallbackDecoder; 20 | } 21 | 22 | public addDecoder = (decoder: Decoder) => { 23 | this.decoders.push(decoder); 24 | }; 25 | 26 | public decode = async (input: DecoderInput, access: DecoderChainAccess): Promise<[DecoderOutput, MetadataRequest]> => { 27 | const allDecodersArray = [...this.decoders, this.fallbackDecoder]; 28 | const state = new DecoderState(input, access); 29 | 30 | const visit = async (node: DecoderInput): Promise => { 31 | if (hasReceiptExt(node) && node.failed) { 32 | // we don't decode anything that failed, because there should be no reason 33 | // to care about something that had no effect 34 | return state.getOutputFor(node); 35 | } 36 | 37 | const decodeLog = async (child: DecoderInput, log: Log): Promise => { 38 | const output: DecoderOutput = state.getOutputFor(log); 39 | 40 | await Promise.all(allDecodersArray.map(async (v) => { 41 | try { 42 | const results = await v.decodeLog(state, node, log); 43 | if (!results) return; 44 | 45 | if (Array.isArray(results)) { 46 | output.results.push(...results); 47 | } else { 48 | output.results.push(results); 49 | } 50 | } catch (e) { 51 | console.log('decoder failed to decode log', v, node, log, e); 52 | } 53 | })); 54 | 55 | return output; 56 | }; 57 | 58 | const output = state.getOutputFor(node); 59 | 60 | for (const decoder of allDecodersArray) { 61 | try { 62 | const result = await decoder.decodeCall(state, node); 63 | if (result) { 64 | output.results.push(result); 65 | } 66 | } catch (e) { 67 | console.log('decoder failed to decode call', decoder, node, e); 68 | } 69 | } 70 | 71 | if (hasTraceExt(node)) { 72 | for (let child of node.childOrder) { 73 | let result; 74 | if (child[0] === 'log') { 75 | result = await decodeLog(node, node.logs[child[1]]); 76 | } else { 77 | result = await visit(node.children[child[1]]); 78 | } 79 | 80 | output.children.push(result); 81 | 82 | } 83 | } else if (hasReceiptExt(node)) { 84 | if (node.logs) { 85 | for (let log of node.logs) { 86 | output.children.push(await decodeLog(node, log)); 87 | } 88 | } 89 | } 90 | 91 | return output; 92 | }; 93 | 94 | return [await visit(input), state.requestedMetadata]; 95 | }; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/sdk/types.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder, EventFragment, Fragment, FunctionFragment, Interface, Result } from '@ethersproject/abi/lib'; 2 | import { Log, Provider, TransactionRequest } from '@ethersproject/abstract-provider'; 3 | import { BigNumber, BytesLike, ethers } from 'ethers'; 4 | import { LogDescription, ParamType } from 'ethers/lib/utils'; 5 | import { Action, BaseAction } from './actions'; 6 | 7 | import { getNodeId, hasReceiptExt, hasSelector, hasTraceExt } from './utils'; 8 | 9 | export interface DecoderChainAccess { 10 | getStorageAt(address: string, slot: string): Promise; 11 | 12 | call(tx: TransactionRequest): Promise; 13 | } 14 | 15 | export interface DecoderInput { 16 | // a unique id per input node 17 | id: string; 18 | 19 | // optional: attach an abi to this node if you like 20 | abi?: ethers.utils.Interface; 21 | 22 | type: 'call' | 'staticcall' | 'callcode' | 'delegatecall' | 'create' | 'create2' | 'selfdestruct'; 23 | from: string; 24 | to: string; 25 | value: BigNumber; 26 | calldata: BytesLike; 27 | } 28 | 29 | export interface DecoderInputReceiptExt extends DecoderInput { 30 | failed: boolean; 31 | logs: Array; 32 | } 33 | 34 | export interface DecoderInputTraceExt extends DecoderInputReceiptExt { 35 | returndata: BytesLike; 36 | children: Array; 37 | childOrder: Array<['log' | 'call', number]>; 38 | } 39 | 40 | export type DecoderOutput = { 41 | node: DecoderInput | Log; 42 | results: Action[]; 43 | children: DecoderOutput[]; 44 | }; 45 | 46 | export type MetadataRequest = { 47 | tokens: Set; 48 | }; 49 | 50 | export class ProviderDecoderChainAccess implements DecoderChainAccess { 51 | private provider: Provider; 52 | private cache: Record> 53 | 54 | constructor(provider: Provider) { 55 | this.provider = provider; 56 | this.cache = {}; 57 | } 58 | 59 | async call(transaction: TransactionRequest): Promise { 60 | return await this.provider.call(transaction); 61 | } 62 | 63 | 64 | async getStorageAt(address: string, slot: string): Promise { 65 | if (!this.cache[address]) { 66 | this.cache[address] = {}; 67 | } 68 | if (!this.cache[address][slot]) { 69 | this.cache[address][slot] = await this.provider.getStorageAt(address, slot); 70 | } 71 | return this.cache[address][slot]; 72 | } 73 | } 74 | 75 | export class DecoderState { 76 | access: DecoderChainAccess; 77 | 78 | consumed: Set; 79 | 80 | root: DecoderInput; 81 | decoded: Map; 82 | decodeOrder: DecoderOutput[]; 83 | 84 | requestedMetadata: MetadataRequest; 85 | 86 | constructor(root: DecoderInput, access: DecoderChainAccess) { 87 | this.root = root; 88 | this.access = access; 89 | this.consumed = new Set(); 90 | this.decoded = new Map(); 91 | this.decodeOrder = []; 92 | this.requestedMetadata = { 93 | tokens: new Set(), 94 | }; 95 | } 96 | 97 | public getOutputFor(input: DecoderInput | Log): DecoderOutput { 98 | if (!this.decoded.has(input)) { 99 | this.decoded.set(input, { 100 | node: input, 101 | results: [], 102 | children: [], 103 | }); 104 | this.decodeOrder.push(this.decoded.get(input)!); 105 | } 106 | 107 | return this.decoded.get(input)!; 108 | } 109 | 110 | public async call(signature: string, address: string, args: any[]): Promise { 111 | const fragment = Fragment.from(signature); 112 | const intf = new Interface([ 113 | fragment, 114 | ]); 115 | 116 | return intf.decodeFunctionResult(fragment.name, await this.access.call({ 117 | to: address, 118 | data: intf.encodeFunctionData(fragment.name, args), 119 | })); 120 | } 121 | 122 | requestTokenMetadata(token: string) { 123 | this.requestedMetadata.tokens.add(token.toLowerCase()); 124 | } 125 | 126 | // check if a node is consumed - most decoders should ignore consumed nodes 127 | isConsumed(node: DecoderInput | Log) { 128 | return this.consumed.has(getNodeId(node)); 129 | } 130 | 131 | // mark the node as consumed 132 | consume(node: DecoderInput | Log) { 133 | this.consumed.add(getNodeId(node)); 134 | } 135 | 136 | // consume the node and all logs in it 137 | consumeAll(node: DecoderInput) { 138 | this.consume(node); 139 | 140 | if (hasReceiptExt(node)) { 141 | node.logs.forEach(this.consume.bind(this)); 142 | } 143 | } 144 | 145 | // consume the node and all logs in it, including all child calls 146 | consumeAllRecursively(node: DecoderInput) { 147 | this.consumeAll(node); 148 | 149 | if (hasTraceExt(node)) { 150 | node.children?.forEach(this.consumeAllRecursively.bind(this)); 151 | } 152 | } 153 | 154 | // assuming the input node is a call with `transfer`-like semantics (i.e. it causes a transfer from the caller 155 | // to an address specified in the calldata), consume the node and any Transfer events which correspond to the 156 | // transfer 157 | consumeTransfer(node: DecoderInput, params?: Array) { 158 | if (!params) { 159 | params = [ParamType.from('address to'), ParamType.from('uint256 amount')]; 160 | } 161 | 162 | let inputs = defaultAbiCoder.decode(params, ethers.utils.arrayify(node.calldata).slice(4)); 163 | 164 | this.consumeTransferCommon(node, ethers.utils.getAddress(node.from), inputs['to']); 165 | } 166 | 167 | // assuming the input node is a call with `transferFrom`-like semantics (i.e. it causes a transfer from one address 168 | // to another address specified in the calldata), consume the node and any Transfer events which correspond to the 169 | // transfer 170 | consumeTransferFrom(node: DecoderInput, params?: Array) { 171 | if (!params) { 172 | params = [ParamType.from('address from'), ParamType.from('address to'), ParamType.from('uint256 amount')]; 173 | } 174 | 175 | let inputs = defaultAbiCoder.decode(params, ethers.utils.arrayify(node.calldata).slice(4)); 176 | 177 | this.consumeTransferCommon(node, inputs['from'], inputs['to']); 178 | } 179 | 180 | consumeTransferCommon(node: DecoderInput, from: string, to: string) { 181 | // consume the current node 182 | this.consume(node); 183 | 184 | if (!hasTraceExt(node)) return; 185 | 186 | const visit = (node: DecoderInputTraceExt) => { 187 | // handle any transfer events we might find, must be a match on from and to, because it might take fees 188 | node.logs 189 | .filter( 190 | (v) => 191 | v.topics.length > 0 && 192 | v.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', 193 | ) 194 | .forEach((v) => { 195 | let abi = node.abi; 196 | if (!abi) { 197 | abi = new ethers.utils.Interface([ 198 | EventFragment.from('Transfer(address indexed from, address indexed to, uint amount)'), 199 | ]); 200 | } 201 | 202 | try { 203 | let values = abi.parseLog(v); 204 | if (values.args[0] === from && values.args[1] === to) { 205 | this.consume(v); 206 | } 207 | } catch { } 208 | }); 209 | 210 | // if we have a delegatecall, we need to recurse because it will emit the log in the context of the 211 | // current contract 212 | node.children.filter((v) => v.type === 'delegatecall').forEach(visit); 213 | }; 214 | visit(node); 215 | } 216 | } 217 | 218 | export abstract class Decoder { 219 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 220 | return null; 221 | } 222 | 223 | async decodeLog(state: DecoderState, node: DecoderInput, log: Log): Promise { 224 | return null; 225 | } 226 | 227 | decodeFunctionWithFragment(node: DecoderInput, functionFragment: FunctionFragment): [Result, Result | null] { 228 | return [ 229 | defaultAbiCoder.decode(functionFragment.inputs, ethers.utils.arrayify(node.calldata).slice(4)), 230 | hasTraceExt(node) && functionFragment.outputs 231 | ? defaultAbiCoder.decode(functionFragment.outputs, ethers.utils.arrayify(node.returndata)) 232 | : null, 233 | ]; 234 | } 235 | 236 | decodeEventWithFragment(log: Log, eventFragment: string | EventFragment): LogDescription { 237 | const abi = new ethers.utils.Interface([eventFragment]); 238 | return abi.parseLog(log); 239 | } 240 | } 241 | 242 | export abstract class CallDecoder extends Decoder { 243 | functions: Record Promise>; 244 | 245 | constructor() { 246 | super(); 247 | this.functions = {}; 248 | } 249 | 250 | async decodeCall(state: DecoderState, node: DecoderInput): Promise { 251 | if (state.isConsumed(node)) return null; 252 | 253 | if (node.type !== 'call') return null; 254 | 255 | const functionInfo = Object.entries(this.functions).find(([name, func]) => { 256 | return (name === '' && node.calldata.length === 0) || (name !== '' && hasSelector(node.calldata, name)); 257 | }); 258 | 259 | if (!functionInfo) return null; 260 | 261 | if (!await this.isTargetContract(state, node.to)) return null; 262 | 263 | state.consume(node); 264 | 265 | const [inputs, outputs] = this.decodeFunctionWithFragment(node, FunctionFragment.from(functionInfo[0])); 266 | 267 | const functionMetadata = functionInfo[1]; 268 | 269 | return functionMetadata.bind(this)(state, node, inputs, outputs); 270 | } 271 | 272 | abstract isTargetContract(state: DecoderState, address: string): Promise; 273 | } 274 | -------------------------------------------------------------------------------- /src/sdk/utils.ts: -------------------------------------------------------------------------------- 1 | import { EventFragment, FunctionFragment } from '@ethersproject/abi/lib'; 2 | import { Log } from '@ethersproject/abstract-provider'; 3 | import { BytesLike, ethers } from "ethers"; 4 | 5 | import { 6 | DecoderInput, 7 | DecoderInputReceiptExt, 8 | DecoderInputTraceExt 9 | } from './types'; 10 | 11 | export const hasSelector = (calldata: BytesLike, selector: string | FunctionFragment) => { 12 | return ( 13 | ethers.utils.hexlify(ethers.utils.arrayify(calldata).slice(0, 4)) === 14 | ethers.utils.id(FunctionFragment.from(selector).format()).substring(0, 10) 15 | ); 16 | }; 17 | 18 | export const hasTopic = (log: Log, selector: string | EventFragment) => { 19 | return log.topics.length > 0 && log.topics[0] == ethers.utils.id(EventFragment.from(selector).format()); 20 | }; 21 | 22 | export const isEqualAddress = (a: string, b: string): boolean => { 23 | return a.toLocaleLowerCase() === b.toLocaleLowerCase(); 24 | } 25 | 26 | export const hasReceiptExt = (node: DecoderInput): node is DecoderInputReceiptExt => { 27 | return (node as DecoderInputReceiptExt).logs !== undefined; 28 | } 29 | 30 | export const hasTraceExt = (node: DecoderInput): node is DecoderInputTraceExt => { 31 | return (node as DecoderInputTraceExt).returndata !== undefined; 32 | } 33 | 34 | export const getCalls = (node: DecoderInputTraceExt): DecoderInputTraceExt[] => { 35 | return node.children.filter(node => node.type === 'call'); 36 | } 37 | 38 | export const flattenLogs = (node: DecoderInputReceiptExt): Log[] => { 39 | if (!hasTraceExt(node)) { 40 | return node.logs; 41 | } 42 | const result: Log[] = []; 43 | 44 | const visit = (node: DecoderInputTraceExt) => { 45 | node.childOrder.forEach(([type, val]) => { 46 | if (type === 'log') { 47 | result.push(node.logs[val]); 48 | } else { 49 | visit(node.children[val]); 50 | } 51 | }); 52 | }; 53 | 54 | visit(node); 55 | 56 | return result; 57 | } 58 | 59 | export const isDecoderInput = (node: DecoderInput | Log): node is DecoderInput => { 60 | return (node as DecoderInput).id !== undefined; 61 | }; 62 | 63 | export const getNodeId = (node: DecoderInput | Log) => { 64 | if (isDecoderInput(node)) { 65 | return 'node:' + node.id; 66 | } else { 67 | return 'log:' + node.transactionHash + '.' + node.logIndex; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /tests/test_art-gobblers_mint_decoder.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { getInput, getDummyDecoderState } from "./utils"; 4 | import decoderInputJson from "./testdata/art-gobblers_mint_decoder_input.json"; 5 | import { ArtGobblersMintDecoder } from "../src/decoders/art-gobblers"; 6 | import { isEqualAddress } from "../src/sdk/utils"; 7 | import { BigNumber } from "ethers"; 8 | 9 | 10 | describe("CometSupplyDecoder", () => { 11 | describe("decodeCall", () => { 12 | it("should decode to valid MintNFTAction", async () => { 13 | const input = getInput(decoderInputJson); 14 | const state = getDummyDecoderState(input); 15 | const decoder = new ArtGobblersMintDecoder(); 16 | const mintAction = await decoder.decodeCall(state, input); 17 | assert.strictEqual(mintAction!.type, "nft-mint"); 18 | assert(isEqualAddress(mintAction!.operator, '0x3d11e2d2a0e44061236b4F54980AC763E0Abd6f7')); 19 | assert(isEqualAddress(mintAction!.recipient, '0x3d11e2d2a0e44061236b4F54980AC763E0Abd6f7')); 20 | assert(isEqualAddress(mintAction!.collection, '0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769')); 21 | assert.strictEqual(mintAction!.tokenId, BigNumber.from(2260n).toBigInt()); 22 | assert(isEqualAddress(mintAction!.buyToken!, '0x600000000a36F3cD48407e35eB7C5c910dc1f7a8')); 23 | assert.strictEqual(mintAction!.buyAmount, BigNumber.from(3580931783591734677959n).toBigInt()); 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/test_comet_supply_decoder.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { JsonRpcProvider } from '@ethersproject/providers'; 3 | import { BigNumber, ethers } from 'ethers'; 4 | 5 | import { CometSupplyDecoder } from "../src/decoders/comet"; 6 | import { isEqualAddress } from "../src/sdk/utils"; 7 | import { getInput, getDummyDecoderState } from "./utils"; 8 | import decoderInputJson from "./testdata/comet_supply_decoder_input.json"; 9 | 10 | 11 | describe("CometSupplyDecoder", () => { 12 | describe("decodeCall", () => { 13 | it("should decode to valid SupplyAction", async () => { 14 | const input = getInput(decoderInputJson); 15 | const state = getDummyDecoderState(input); 16 | const decoder = new CometSupplyDecoder(); 17 | const supplyAction = await decoder.decodeCall(state, input); 18 | assert.strictEqual(supplyAction!.type, "supply"); 19 | assert(isEqualAddress(supplyAction!.operator, '0x89E9e55d4ddC6492cdB13afeF3Eaf44863EEDf44')); 20 | assert(isEqualAddress(supplyAction!.supplier, '0x89E9e55d4ddC6492cdB13afeF3Eaf44863EEDf44')); 21 | assert(isEqualAddress(supplyAction!.supplyToken, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')); 22 | assert(BigNumber.from(10000000).eq(supplyAction!.amount)); 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/testdata/art-gobblers_mint_decoder_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0", 3 | "type": "call", 4 | "from": "0x3d11e2d2a0e44061236b4F54980AC763E0Abd6f7", 5 | "to": "0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769", 6 | "value": { 7 | "type": "BigNumber", 8 | "hex": "0x00" 9 | }, 10 | "calldata": { 11 | "0": 201, 12 | "1": 189, 13 | "2": 218, 14 | "3": 198, 15 | "4": 0, 16 | "5": 0, 17 | "6": 0, 18 | "7": 0, 19 | "8": 0, 20 | "9": 0, 21 | "10": 0, 22 | "11": 0, 23 | "12": 0, 24 | "13": 0, 25 | "14": 0, 26 | "15": 0, 27 | "16": 0, 28 | "17": 0, 29 | "18": 0, 30 | "19": 0, 31 | "20": 0, 32 | "21": 0, 33 | "22": 0, 34 | "23": 0, 35 | "24": 0, 36 | "25": 0, 37 | "26": 0, 38 | "27": 194, 39 | "28": 36, 40 | "29": 136, 41 | "30": 178, 42 | "31": 204, 43 | "32": 108, 44 | "33": 186, 45 | "34": 213, 46 | "35": 217, 47 | "36": 0, 48 | "37": 0, 49 | "38": 0, 50 | "39": 0, 51 | "40": 0, 52 | "41": 0, 53 | "42": 0, 54 | "43": 0, 55 | "44": 0, 56 | "45": 0, 57 | "46": 0, 58 | "47": 0, 59 | "48": 0, 60 | "49": 0, 61 | "50": 0, 62 | "51": 0, 63 | "52": 0, 64 | "53": 0, 65 | "54": 0, 66 | "55": 0, 67 | "56": 0, 68 | "57": 0, 69 | "58": 0, 70 | "59": 0, 71 | "60": 0, 72 | "61": 0, 73 | "62": 0, 74 | "63": 0, 75 | "64": 0, 76 | "65": 0, 77 | "66": 0, 78 | "67": 1 79 | }, 80 | "status": true, 81 | "logs": [ 82 | { 83 | "address": "0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769", 84 | "blockHash": "", 85 | "blockNumber": 0, 86 | "data": "0x0000000000000000000000000000000000000000000002b671c1d0f1fb9290d3", 87 | "logIndex": "0.4", 88 | "removed": false, 89 | "topics": [ 90 | "0x2171f1d8b6b7927c42287fd11040aa8c3569c5c2040b8453104ced365ed84cd4", 91 | "0x0000000000000000000000003d11e2d2a0e44061236b4f54980ac763e0abd6f7" 92 | ], 93 | "transactionHash": "0x52109ceef4ac70b65990e66a31a820d13cabbe9090351510396b51e335a3090d", 94 | "transactionIndex": 0 95 | }, 96 | { 97 | "address": "0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769", 98 | "blockHash": "", 99 | "blockNumber": 0, 100 | "data": "0x0000000000000000000000000000000000000000000000c21f695496435211c7", 101 | "logIndex": "0.7", 102 | "removed": false, 103 | "topics": [ 104 | "0xb5f2881b4bfa5b331603accccda550cb5a421c7696237b28e2ec4d5669093d1e", 105 | "0x0000000000000000000000003d11e2d2a0e44061236b4f54980ac763e0abd6f7", 106 | "0x00000000000000000000000000000000000000000000000000000000000008d4" 107 | ], 108 | "transactionHash": "0x52109ceef4ac70b65990e66a31a820d13cabbe9090351510396b51e335a3090d", 109 | "transactionIndex": 0 110 | }, 111 | { 112 | "address": "0x60bb1e2AA1c9ACAfB4d34F71585D7e959f387769", 113 | "blockHash": "", 114 | "blockNumber": 0, 115 | "data": "0x", 116 | "logIndex": "0.12", 117 | "removed": false, 118 | "topics": [ 119 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 120 | "0x0000000000000000000000000000000000000000000000000000000000000000", 121 | "0x0000000000000000000000003d11e2d2a0e44061236b4f54980ac763e0abd6f7", 122 | "0x00000000000000000000000000000000000000000000000000000000000008d4" 123 | ], 124 | "transactionHash": "0x52109ceef4ac70b65990e66a31a820d13cabbe9090351510396b51e335a3090d", 125 | "transactionIndex": 0 126 | } 127 | ], 128 | "returndata": { 129 | "0": 0, 130 | "1": 0, 131 | "2": 0, 132 | "3": 0, 133 | "4": 0, 134 | "5": 0, 135 | "6": 0, 136 | "7": 0, 137 | "8": 0, 138 | "9": 0, 139 | "10": 0, 140 | "11": 0, 141 | "12": 0, 142 | "13": 0, 143 | "14": 0, 144 | "15": 0, 145 | "16": 0, 146 | "17": 0, 147 | "18": 0, 148 | "19": 0, 149 | "20": 0, 150 | "21": 0, 151 | "22": 0, 152 | "23": 0, 153 | "24": 0, 154 | "25": 0, 155 | "26": 0, 156 | "27": 0, 157 | "28": 0, 158 | "29": 0, 159 | "30": 8, 160 | "31": 212 161 | }, 162 | "children": [], 163 | "childOrder": [ 164 | [ 165 | "log", 166 | 0 167 | ], 168 | [ 169 | "log", 170 | 1 171 | ], 172 | [ 173 | "log", 174 | 2 175 | ] 176 | ], 177 | "abi": { 178 | "fragments": [ 179 | { 180 | "type": "function", 181 | "name": "mintFromGoo", 182 | "constant": false, 183 | "inputs": [ 184 | { 185 | "name": "maxPrice", 186 | "type": "uint256", 187 | "indexed": null, 188 | "components": null, 189 | "arrayLength": null, 190 | "arrayChildren": null, 191 | "baseType": "uint256", 192 | "_isParamType": true 193 | }, 194 | { 195 | "name": "useVirtualBalance", 196 | "type": "bool", 197 | "indexed": null, 198 | "components": null, 199 | "arrayLength": null, 200 | "arrayChildren": null, 201 | "baseType": "bool", 202 | "_isParamType": true 203 | } 204 | ], 205 | "outputs": [ 206 | { 207 | "name": "gobblerId", 208 | "type": "uint256", 209 | "indexed": null, 210 | "components": null, 211 | "arrayLength": null, 212 | "arrayChildren": null, 213 | "baseType": "uint256", 214 | "_isParamType": true 215 | } 216 | ], 217 | "payable": true, 218 | "stateMutability": "payable", 219 | "gas": null, 220 | "_isFragment": true 221 | }, 222 | { 223 | "name": "GooBalanceUpdated", 224 | "anonymous": false, 225 | "inputs": [ 226 | { 227 | "name": "user", 228 | "type": "address", 229 | "indexed": true, 230 | "components": null, 231 | "arrayLength": null, 232 | "arrayChildren": null, 233 | "baseType": "address", 234 | "_isParamType": true 235 | }, 236 | { 237 | "name": "newGooBalance", 238 | "type": "uint256", 239 | "indexed": false, 240 | "components": null, 241 | "arrayLength": null, 242 | "arrayChildren": null, 243 | "baseType": "uint256", 244 | "_isParamType": true 245 | } 246 | ], 247 | "type": "event", 248 | "_isFragment": true 249 | }, 250 | { 251 | "name": "GobblerPurchased", 252 | "anonymous": false, 253 | "inputs": [ 254 | { 255 | "name": "user", 256 | "type": "address", 257 | "indexed": true, 258 | "components": null, 259 | "arrayLength": null, 260 | "arrayChildren": null, 261 | "baseType": "address", 262 | "_isParamType": true 263 | }, 264 | { 265 | "name": "gobblerId", 266 | "type": "uint256", 267 | "indexed": true, 268 | "components": null, 269 | "arrayLength": null, 270 | "arrayChildren": null, 271 | "baseType": "uint256", 272 | "_isParamType": true 273 | }, 274 | { 275 | "name": "price", 276 | "type": "uint256", 277 | "indexed": false, 278 | "components": null, 279 | "arrayLength": null, 280 | "arrayChildren": null, 281 | "baseType": "uint256", 282 | "_isParamType": true 283 | } 284 | ], 285 | "type": "event", 286 | "_isFragment": true 287 | }, 288 | { 289 | "name": "Transfer", 290 | "anonymous": false, 291 | "inputs": [ 292 | { 293 | "name": "from", 294 | "type": "address", 295 | "indexed": true, 296 | "components": null, 297 | "arrayLength": null, 298 | "arrayChildren": null, 299 | "baseType": "address", 300 | "_isParamType": true 301 | }, 302 | { 303 | "name": "to", 304 | "type": "address", 305 | "indexed": true, 306 | "components": null, 307 | "arrayLength": null, 308 | "arrayChildren": null, 309 | "baseType": "address", 310 | "_isParamType": true 311 | }, 312 | { 313 | "name": "id", 314 | "type": "uint256", 315 | "indexed": true, 316 | "components": null, 317 | "arrayLength": null, 318 | "arrayChildren": null, 319 | "baseType": "uint256", 320 | "_isParamType": true 321 | } 322 | ], 323 | "type": "event", 324 | "_isFragment": true 325 | } 326 | ], 327 | "_abiCoder": { 328 | "coerceFunc": null 329 | }, 330 | "functions": { 331 | "mintFromGoo(uint256,bool)": { 332 | "type": "function", 333 | "name": "mintFromGoo", 334 | "constant": false, 335 | "inputs": [ 336 | { 337 | "name": "maxPrice", 338 | "type": "uint256", 339 | "indexed": null, 340 | "components": null, 341 | "arrayLength": null, 342 | "arrayChildren": null, 343 | "baseType": "uint256", 344 | "_isParamType": true 345 | }, 346 | { 347 | "name": "useVirtualBalance", 348 | "type": "bool", 349 | "indexed": null, 350 | "components": null, 351 | "arrayLength": null, 352 | "arrayChildren": null, 353 | "baseType": "bool", 354 | "_isParamType": true 355 | } 356 | ], 357 | "outputs": [ 358 | { 359 | "name": "gobblerId", 360 | "type": "uint256", 361 | "indexed": null, 362 | "components": null, 363 | "arrayLength": null, 364 | "arrayChildren": null, 365 | "baseType": "uint256", 366 | "_isParamType": true 367 | } 368 | ], 369 | "payable": true, 370 | "stateMutability": "payable", 371 | "gas": null, 372 | "_isFragment": true 373 | } 374 | }, 375 | "errors": {}, 376 | "events": { 377 | "GooBalanceUpdated(address,uint256)": { 378 | "name": "GooBalanceUpdated", 379 | "anonymous": false, 380 | "inputs": [ 381 | { 382 | "name": "user", 383 | "type": "address", 384 | "indexed": true, 385 | "components": null, 386 | "arrayLength": null, 387 | "arrayChildren": null, 388 | "baseType": "address", 389 | "_isParamType": true 390 | }, 391 | { 392 | "name": "newGooBalance", 393 | "type": "uint256", 394 | "indexed": false, 395 | "components": null, 396 | "arrayLength": null, 397 | "arrayChildren": null, 398 | "baseType": "uint256", 399 | "_isParamType": true 400 | } 401 | ], 402 | "type": "event", 403 | "_isFragment": true 404 | }, 405 | "GobblerPurchased(address,uint256,uint256)": { 406 | "name": "GobblerPurchased", 407 | "anonymous": false, 408 | "inputs": [ 409 | { 410 | "name": "user", 411 | "type": "address", 412 | "indexed": true, 413 | "components": null, 414 | "arrayLength": null, 415 | "arrayChildren": null, 416 | "baseType": "address", 417 | "_isParamType": true 418 | }, 419 | { 420 | "name": "gobblerId", 421 | "type": "uint256", 422 | "indexed": true, 423 | "components": null, 424 | "arrayLength": null, 425 | "arrayChildren": null, 426 | "baseType": "uint256", 427 | "_isParamType": true 428 | }, 429 | { 430 | "name": "price", 431 | "type": "uint256", 432 | "indexed": false, 433 | "components": null, 434 | "arrayLength": null, 435 | "arrayChildren": null, 436 | "baseType": "uint256", 437 | "_isParamType": true 438 | } 439 | ], 440 | "type": "event", 441 | "_isFragment": true 442 | }, 443 | "Transfer(address,address,uint256)": { 444 | "name": "Transfer", 445 | "anonymous": false, 446 | "inputs": [ 447 | { 448 | "name": "from", 449 | "type": "address", 450 | "indexed": true, 451 | "components": null, 452 | "arrayLength": null, 453 | "arrayChildren": null, 454 | "baseType": "address", 455 | "_isParamType": true 456 | }, 457 | { 458 | "name": "to", 459 | "type": "address", 460 | "indexed": true, 461 | "components": null, 462 | "arrayLength": null, 463 | "arrayChildren": null, 464 | "baseType": "address", 465 | "_isParamType": true 466 | }, 467 | { 468 | "name": "id", 469 | "type": "uint256", 470 | "indexed": true, 471 | "components": null, 472 | "arrayLength": null, 473 | "arrayChildren": null, 474 | "baseType": "uint256", 475 | "_isParamType": true 476 | } 477 | ], 478 | "type": "event", 479 | "_isFragment": true 480 | } 481 | }, 482 | "structs": {}, 483 | "deploy": { 484 | "name": null, 485 | "type": "constructor", 486 | "inputs": [], 487 | "payable": false, 488 | "stateMutability": "nonpayable", 489 | "gas": null, 490 | "_isFragment": true 491 | }, 492 | "_isInterface": true 493 | } 494 | } -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from '@ethersproject/providers'; 2 | 3 | import { DecoderInput, DecoderState, ProviderDecoderChainAccess } from "../src/sdk/types" 4 | 5 | export const transformDecoderInput = (jsonInput: any) => { 6 | const keys = Object.keys(jsonInput); 7 | 8 | keys.forEach(key => { 9 | if (key === 'children') { 10 | jsonInput[key].forEach((child: any) => { 11 | transformDecoderInput(child); 12 | }); 13 | } else if (key === 'calldata' || key === 'returndata') { 14 | jsonInput[key] = Object.values(jsonInput[key]); 15 | } 16 | }) 17 | } 18 | 19 | export const getInput = (rawInputJson: any): DecoderInput => { 20 | const input = rawInputJson as any as DecoderInput; 21 | transformDecoderInput(input); 22 | return input; 23 | } 24 | 25 | export const getDummyDecoderState = (input: DecoderInput): DecoderState => { 26 | return new DecoderState(input, new ProviderDecoderChainAccess(new JsonRpcProvider(""))); 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "commonjs", /* Specify what module code is generated. */ 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | "resolveJsonModule": true, /* Enable importing .json files. */ 36 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 37 | /* JavaScript Support */ 38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 41 | /* Emit */ 42 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 47 | "outDir": "lib", /* Specify an output folder for all emitted files. */ 48 | // "removeComments": true, /* Disable emitting comments. */ 49 | // "noEmit": true, /* Disable emitting files from a compilation. */ 50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 58 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 59 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 62 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 64 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 65 | /* Interop Constraints */ 66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 68 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 70 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 71 | /* Type Checking */ 72 | "strict": true, /* Enable all strict type-checking options. */ 73 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 74 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 75 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 76 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 77 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 78 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 79 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 80 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 81 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 82 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 83 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 84 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 85 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 86 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 87 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 88 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 89 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 90 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 91 | /* Completeness */ 92 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 93 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 94 | }, 95 | "include": [ 96 | "src" 97 | ] 98 | } --------------------------------------------------------------------------------