├── .eslintrc.json ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── STYLE_GUIDE.md ├── api └── index.ts ├── assets └── goldrush-decoder-banner.png ├── jest.config.js ├── microservices └── tx │ ├── tx.routes.ts │ ├── tx.schema.ts │ └── tx.service.ts ├── middlewares ├── index.ts └── validate-query.ts ├── package.json ├── scripts └── add-config.ts ├── services ├── decoder │ ├── decoder.constants.ts │ ├── decoder.ts │ ├── decoder.types.ts │ ├── fallbacks │ │ ├── approval-for-all │ │ │ ├── abis │ │ │ │ └── approval-for-all.abi.json │ │ │ ├── approval-for-all.fallback.ts │ │ │ └── approval-for-all.test.ts │ │ ├── approval │ │ │ ├── abis │ │ │ │ ├── approval-erc20.abi.json │ │ │ │ └── approval-erc721.abi.json │ │ │ ├── approval.fallback.ts │ │ │ └── approval.test.ts │ │ └── transfer │ │ │ ├── abis │ │ │ ├── transfer-erc20.abi.json │ │ │ └── transfer-erc721.abi.json │ │ │ ├── transfer.fallback.ts │ │ │ └── transfer.test.ts │ ├── native │ │ ├── native.decoder.ts │ │ └── native.test.ts │ └── protocols │ │ ├── 4337-entry-point │ │ ├── 4337-entry-point.configs.ts │ │ ├── 4337-entry-point.decoders.ts │ │ ├── 4337-entry-point.test.ts │ │ └── abis │ │ │ └── 4337-entry-point.abi.json │ │ ├── aave-v3 │ │ ├── aave-v3.configs.ts │ │ ├── aave-v3.decoders.ts │ │ ├── aave-v3.test.ts │ │ └── abis │ │ │ └── aave-v3.abi.json │ │ ├── blur │ │ ├── abis │ │ │ └── blur.BlurExchange.abi.json │ │ ├── blur.configs.ts │ │ ├── blur.decoders.ts │ │ └── blur.test.ts │ │ ├── connext │ │ ├── abis │ │ │ ├── connext-call.abi.json │ │ │ └── connext-router.abi.json │ │ ├── connext.configs.ts │ │ ├── connext.decoders.ts │ │ └── connext.test.ts │ │ ├── covalent-network │ │ ├── abis │ │ │ ├── new-block-specimen-proof.abi.json │ │ │ ├── new-operational-staking.abi.json │ │ │ ├── old-block-specimen-proof.abi.json │ │ │ └── old-operational-staking.abi.json │ │ ├── covalent-network.configs.ts │ │ ├── covalent-network.decoders.ts │ │ └── covalent-network.test.ts │ │ ├── defi-kingdoms │ │ ├── abis │ │ │ ├── defi-kingdoms.hero-auction.abi.json │ │ │ └── defi-kingdoms.pets.abi.json │ │ ├── defi-kingdoms.configs.ts │ │ ├── defi-kingdoms.decoders.ts │ │ └── defi-kingdoms.test.ts │ │ ├── lido │ │ ├── abis │ │ │ ├── lido.steth.abi.json │ │ │ └── lido.withdrawalQueue.abi.json │ │ ├── lido.configs.ts │ │ ├── lido.decoders.ts │ │ └── lido.test.ts │ │ ├── opensea │ │ ├── abis │ │ │ └── seaport-1.1.abi.json │ │ ├── opensea.configs.ts │ │ ├── opensea.decoders.ts │ │ └── opensea.test.ts │ │ ├── paraswap-v5 │ │ ├── abis │ │ │ └── paraswap-v5.simple-swap.abi.json │ │ ├── paraswap-v5.configs.ts │ │ ├── paraswap-v5.decoders.ts │ │ └── paraswap-v5.test.ts │ │ ├── pendle │ │ ├── abis │ │ │ ├── pendle-router-v3.abi.json │ │ │ └── vePendle.abi.json │ │ ├── pendle.configs.ts │ │ ├── pendle.decoders.ts │ │ └── pendle.test.ts │ │ ├── renzo │ │ ├── abis │ │ │ ├── renzo.deposit-queue-abi.json │ │ │ ├── renzo.eigen-layer-strategy-manager.json │ │ │ └── renzo.restake-manager-abi.json │ │ ├── renzo.configs.ts │ │ ├── renzo.decoders.ts │ │ └── renzo.test.ts │ │ ├── uniswap-v2 │ │ ├── abis │ │ │ ├── uniswap-v2.factory.abi.json │ │ │ └── uniswap-v2.pair.abi.json │ │ ├── uniswap-v2.configs.ts │ │ ├── uniswap-v2.decoders.ts │ │ └── uniswap-v2.test.ts │ │ ├── uniswap-v3 │ │ ├── abis │ │ │ ├── uniswap-v3.NonfungiblePositionManager.abi.json │ │ │ ├── uniswap-v3.factory.abi.json │ │ │ └── uniswap-v3.pair.abi.json │ │ ├── uniswap-v3.configs.ts │ │ ├── uniswap-v3.decoders.ts │ │ └── uniswap-v3.test.ts │ │ └── wormhole │ │ ├── abis │ │ ├── wormhole-eth-core.abi.json │ │ └── wormhole-portal-bridge.abi.json │ │ ├── wormhole.configs.ts │ │ ├── wormhole.decoders.ts │ │ └── wormhole.test.ts └── index.ts ├── tsconfig.json ├── utils └── functions │ ├── chunkify.ts │ ├── currency-to-number.ts │ ├── index.ts │ ├── is-null-address.ts │ ├── slugify.ts │ └── timestamp-parser.ts ├── vercel.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "semi": "error", 6 | "no-multiple-empty-lines": "error", 7 | "indent": "off", 8 | "react/jsx-no-target-blank": "off", 9 | "react/no-unescaped-entities": "off", 10 | "react/no-children-prop": "off", 11 | "no-unsafe-optional-chaining": "warn", 12 | "@typescript-eslint/no-var-requires": "off", 13 | "@typescript-eslint/no-unused-vars": "warn", 14 | "@typescript-eslint/ban-types": [ 15 | "error", 16 | { 17 | "types": { 18 | "Function": false 19 | } 20 | } 21 | ], 22 | "@typescript-eslint/no-inferrable-types": "off", 23 | "@typescript-eslint/no-non-null-assertion": "off", 24 | "@typescript-eslint/consistent-type-imports": "error", 25 | "@typescript-eslint/no-explicit-any": "warn", 26 | "prettier/prettier": "error" 27 | }, 28 | "ignorePatterns": ["dist/*", "jest.config.js"] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: [opened, synchronize] 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Install dependencies 17 | run: yarn install 18 | - name: Lint 19 | run: yarn lint 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: [opened, synchronize] 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: 'Create env file' 17 | run: echo "${{ secrets.ENV_FILE }}" > .env 18 | - name: Install dependencies 19 | run: yarn install 20 | - name: Run tests 21 | run: yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Custom added 133 | decoders.dump 134 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .build 2 | .next 3 | .github 4 | public 5 | coverage 6 | node_modules 7 | dist 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "always", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "preserve", 16 | "htmlWhitespaceSensitivity": "css", 17 | "vueIndentScriptAndStyle": false, 18 | "endOfLine": "lf", 19 | "embeddedLanguageFormatting": "auto", 20 | "singleAttributePerLine": false 21 | } 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Welcome to Goldrush Decoder! We appreciate your interest and contributions in making the web3 world more human-friendly. 4 | 5 | Follow these steps to contribute: 6 | 7 | 1. **Fork the Repository**: Click on the "Fork" button on the top right corner of the page. 8 | 9 | 2. **Clone Your Fork**: `git clone https://github.com/covalenthq/goldrush-decoder.git` 10 | 11 | 3. **Create a Branch**: `git checkout -b -/` 12 | 13 | 4. **Make Changes**: Implement your awesome features or fix bugs. 14 | 15 | 5. **Commit Changes**: `git commit -m "feat(scope/inner-scope): description"` 16 | 17 | 6. **Push to Your Fork**: `git push origin -/` 18 | 19 | 7. **Submit a Pull Request**: Open a pull request on the GitHub repository. 20 | 21 | 8. **Follow Code Review**: Be open to feedback and make necessary changes. 22 | 23 | Thank you for your contribution! 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Covalent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | GoldRush Kit Logo 3 |
4 | 5 |

6 | MIT 7 |

8 | 9 |

10 | Decode unstructured, raw event logs into structured data with a simple API. 11 |

12 | 13 |
14 | Open-source. Public Good. 200+ Chains. 15 |
16 | 17 |
18 | 19 | This repository contains the logic for decoding a `raw_log_event` of a transaction to meaningful, human-readable, structured data. 20 | 21 | ## Knowledge Primer 22 | 23 | 1. **Config**: A `config` is a mapping of a contract address, chain name, and protocol name to create a unique configuration for every protocol across all the chains for all the contracts. A protocol can have a collection of configs in an array. It looks like 24 | 25 | ```ts 26 | export type Configs = { 27 | protocol_name: string; 28 | chain_name: Chain; 29 | address: string; 30 | is_factory: boolean; 31 | }[]; 32 | ``` 33 | 34 | 2. **GoldRushDecoder**: The `GoldRushDecoder` class has different methods that enable the decoding logic to run. The various methods are 35 | 36 | 1. `initDecoder`: Scans the `./services/decoder/protocols` directory for all the protocols, extracts the `configs` from them and creates a mapping to the respective decoding function. It is run when the server starts. 37 | 2. `on`: Creates a decoding function for the specified protocol name on the specified chains. Its declaration is: 38 | 39 | ```ts 40 | GoldRushDecoder.on( 41 | ":", 42 | ["", ""], 43 | ABI as Abi, 44 | async (log_event, tx, chain_name, covalent_client): Promise => { 45 | 46 | } 47 | ); 48 | ``` 49 | 50 | The method has 4 arguments: 51 | 52 | 1. **Event Id**: A case-sensitive string concatenation of the `protocol name` with the `event name` by a `:`. 53 | 2. **Chain Names**: An array of all the chains the defined decoding function will run for. 54 | 3. **ABI**: The ABI of the contract on which the event exists. 55 | 4. **Decoding Function**: The actual decoding function, it has 3 arguments passed to it: 56 | 1. `log_event`: The raw log event that is being decoded. 57 | 2. `tx`: The transaction object that generated this log. 58 | 3. `chain_name`: Name of the chain to which the log belongs to. 59 | 4. `covalent_client`: The covalent client created with your covalent API key. 60 | 61 | 3. `fallback`: Creates a fallback function for the specified event name. This function is not linked to any chain or contract. Its declaration is: 62 | 63 | ```ts 64 | GoldRushDecoder.fallback( 65 | "EventName", 66 | ABI as Abi, 67 | async (log_event, tx, chain_name, covalent_client): Promise => { 68 | 69 | } 70 | ); 71 | ``` 72 | 73 | The method has 3 arguments: 74 | 75 | 1. **Event Name**: A case-sensitive name of the event to be decoded. 76 | 2. **ABI**: The ABI of the contract on which the event exists. 77 | 3. **Decoding Function**: The actual decoding function, it has 3 arguments passed to it: 78 | 1. `log_event`: The raw log event that is being decoded. 79 | 2. `tx`: The transaction object that generated this log. 80 | 3. `chain_name`: Name of the chain to which the log belongs to. 81 | 4. `covalent_client`: The covalent client created with your covalent API key. 82 | 83 | 4. `decode`: The function that chooses which decoding function needs to be called for which log event. It collects all the decoded events for a transaction and returns them in an array of structured data. It is run when the API server receives a request. 84 | 85 | ### 1. Running the Development Server 86 | 87 | Follow the following steps to start the development server of the **GoldRush Decoder**. 88 | 89 | 1. Install the dependencies 90 | 91 | ```bash 92 | yarn install 93 | ``` 94 | 95 | 2. Setup the environmental variables. Refer to [.env.example](.env.example) for the list of environmental variables and store them in `.env` at the root level of the repository. 96 | 97 | 3. Start the server 98 | 99 | ```bash 100 | yarn dev 101 | ``` 102 | 103 | The development server will start on the URL - `http://localhost:8080` (port number may change based on the `.env`, 8080 is default). 104 | 105 | ### 2. API Endpoints 106 | 107 | 1. `/api/v1`: The default endpoint for the v1 of the server. A header of the key `x-covalent-api-key` with the value as the [Covalent API key](https://www.covalenthq.com/platform/apikey/) is **mandatory** for the Decoder to work. 108 | 109 | 1. `/tx/decode`: Decodes a transaction of a chain. 110 | 111 | Expects the JSON body: 112 | 113 | 1. `chain_name`: The chain name of the transaction 114 | 2. `tx_hash`: Hash of the transaction to be decoded. 115 | 116 | ```bash 117 | curl --location 'http://localhost:/api/v1/tx/decode' \ 118 | --header 'x-covalent-api-key: ' \ 119 | --header 'Content-Type: application/json' \ 120 | --data '{ 121 | "chain_name": "", 122 | "tx_hash": "" 123 | }' 124 | ``` 125 | 126 | ### 3. Adding a Decoder 127 | 128 | Follow the following steps to add a Decoding logic for an event from a contract of a chain. 129 | 130 | 1. Run this on your terminal 131 | ```bash 132 | yarn add-config 133 | ``` 134 | 2. Add a Protocol Name for which you want to add an config. If the protocol does not exist, a new protocol will be created. However, if it does exist, another config will be added for that protocol. 135 | 3. Input data as per the prompts. The data required after the `protocol_name` is 136 | 137 | - `address`: This is the contract address. It can either be a standalone contract or a factory contract. 138 | - `is_factory`: If the input address is a factory contract or not. 139 | - `chain_name`: The chain for which the config is added. 140 | 141 | This will modify the configs added to the [Protocols](services/protocols) folder. A config will be added to `${protocol_name}.configs.ts`. A sample decoder with a dummy event name (``) will be added to `${protocol_name}.decoders.ts`. Along with this, a test file `${protocol_name}.test.ts` will also be created which needs to be fixed so that the test passes. 142 | 143 | 4. In `${protocol_name}.decoders.ts`, a decoding logic declaration (`Decoder.on(...) {...}`) will be exposed wherein the decoding logic needs to be implemented. The return type of the decoding function expects: 144 | 145 | ```ts 146 | export interface EventType { 147 | category: DECODED_EVENT_CATEGORY; 148 | action: DECODED_ACTION; 149 | name: string; 150 | protocol?: { 151 | name: string; 152 | logo: string; 153 | }; 154 | tokens?: { 155 | heading: string; 156 | value: string; 157 | decimals: number; 158 | ticker_symbol: string | null; 159 | ticker_logo: string | null; 160 | pretty: string; 161 | }[]; 162 | nfts?: { 163 | heading: string; 164 | collection_name: string | null; 165 | token_identifier: string | null; 166 | collection_address: string; 167 | images: { 168 | default: string | null; 169 | 256: string | null; 170 | 512: string | null; 171 | 1024: string | null; 172 | }; 173 | }[]; 174 | details?: { 175 | heading: string; 176 | value: string; 177 | type: "address" | "text"; 178 | }[]; 179 | } 180 | ``` 181 | 182 | ## Contributing 183 | 184 | Contributions, issues and feature requests are welcome! 185 | Feel free to check [issues](https://github.com/covalenthq/goldrush-decoder/issues) page. 186 | 187 | ## Show your support 188 | 189 | Give a ⭐️ if this project helped you! 190 | 191 | ## License 192 | 193 | This project is [MIT](LICENSE) licensed. 194 | -------------------------------------------------------------------------------- /STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | To maintain a consistent and high-quality codebase, please adhere to the following guidelines: 4 | 5 | ## File Conventions 6 | 7 | - All the files will be a `TypeScript` file (`.ts | .tsx`). 8 | - Use kebab-case for files (e.g., `my-file.ts`). 9 | 10 | ## Code Formatting 11 | 12 | The repository is loaded with [ESLint](https://github.com/covalenthq/goldrush-decoder/blob/main/eslintrc.json) and [Prettier](https://github.com/covalenthq/goldrush-decoder/blob/main/prettierrc.json) with their own specific configurations. It is **mandatory** to follow these configurations. 13 | 14 | ```bash 15 | yarn pretty 16 | yarn lint 17 | ``` 18 | 19 | ## Variable Names 20 | 21 | - Use meaningful and descriptive variable names. 22 | - Follow camelCase for variable names (e.g., `myVariable`). 23 | 24 | ## Typecasting 25 | 26 | - Fully typecast your code using TypeScript. 27 | - Clearly define the types for variables, function parameters, and explicit return values. 28 | 29 | ## Comments 30 | 31 | - Include comments for code that might be unclear to others. 32 | - Use comments sparingly and focus on explaining why, not what (code should be self-explanatory). 33 | - Follow the [Better Comments](https://bettercomments.com/) convention for improved comment readability. 34 | - Use different comment styles to convey the significance of comments. 35 | 36 | 1. // add example comments 37 | 38 | ## Commit Messages 39 | 40 | - Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) convention for writing commit messages. This convention helps in generating a meaningful changelog and automating versioning. 41 | - Prefix your commit messages with the relevant type (e.g., "feat(decoder): add new feature", "fix(decoder): resolve bug"). 42 | - Be concise and provide enough information for others to understand the changes. 43 | 44 | ## Pull Requests 45 | 46 | - Ensure your changes pass the Prettier and ESLint checks before submitting a pull request. 47 | - Clearly state the purpose and context of your changes in the pull request description. 48 | - Follow the Pull Request template diligently. 49 | 50 | ## Testing 51 | 52 | - Include unit tests for new features and bug fixes. 53 | - Ensure all tests pass before submitting changes. 54 | 55 | By following these style guide principles, we can collectively maintain a clean and easily maintainable codebase. Thank you for your commitment to code quality! 56 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | type Express, 3 | type Request, 4 | type Response, 5 | type NextFunction, 6 | } from "express"; 7 | import cors from "cors"; 8 | import { config as dotenvConfig } from "dotenv"; 9 | import { GoldRushDecoder } from "../services"; 10 | import { txRouter } from "../microservices/tx/tx.routes"; 11 | import { timestampParser } from "../utils/functions"; 12 | 13 | dotenvConfig(); 14 | 15 | const app: Express = express(); 16 | app.use(cors()); 17 | app.use(express.json()); 18 | 19 | app.get("/api/v1/healthcheck", (_req: Request, res: Response) => { 20 | const now = new Date(); 21 | res.json({ 22 | success: true, 23 | timestamp: now.toISOString(), 24 | uptime: process.uptime(), 25 | }); 26 | }); 27 | app.use("/api/v1/tx", txRouter); 28 | app.use("*", (_req: Request, res: Response) => { 29 | res.status(404).json({ 30 | success: false, 31 | message: "Not Found", 32 | }); 33 | }); 34 | app.use( 35 | (err: Error | any, _req: Request, res: Response, _next: NextFunction) => { 36 | const now = new Date(); 37 | console.error("Server Error"); 38 | console.error( 39 | `${now.toISOString()}: ${timestampParser(now, "descriptive")}` 40 | ); 41 | console.error(err); 42 | if (err.errorCode) { 43 | res.status(err.errorCode).json({ 44 | success: false, 45 | message: `${err.name || "Server Error"}: ${err.message}`, 46 | }); 47 | } else { 48 | res.status(500).json({ 49 | success: false, 50 | message: "Internal Server Error", 51 | }); 52 | } 53 | } 54 | ); 55 | 56 | (async () => { 57 | try { 58 | await Promise.all([GoldRushDecoder.initDecoder()]); 59 | const env: string = process.env.NODE_ENV || "development"; 60 | if (env !== "test") { 61 | const port: number = +(process.env.PORT || 8080); 62 | app.listen(port, () => { 63 | console.info( 64 | `Server listening on Port ${port} in the ${env} environment` 65 | ); 66 | }); 67 | } 68 | } catch (error) { 69 | console.error(error); 70 | process.exit(1); 71 | } 72 | })(); 73 | 74 | process.on("SIGINT", () => { 75 | process.exit(0); 76 | }); 77 | process.on("SIGHUP", () => { 78 | process.exit(0); 79 | }); 80 | 81 | export default app; 82 | -------------------------------------------------------------------------------- /assets/goldrush-decoder-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zikc2023/goldrush-decoder/ce54e36c5f414f093ca6298c3e6047bf0d90a808/assets/goldrush-decoder-banner.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | coveragePathIgnorePatterns: ["./dist/*"], 6 | maxWorkers: 10, 7 | testTimeout: 60000, 8 | }; 9 | -------------------------------------------------------------------------------- /microservices/tx/tx.routes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Router, 3 | type Request, 4 | type Response, 5 | type NextFunction, 6 | } from "express"; 7 | import { validateQuery } from "../../middlewares"; 8 | import { 9 | type DecodeTXRequest, 10 | decodeTXBodySchema, 11 | decodeTXHeadersSchema, 12 | type DecodeTXHeaders, 13 | decodeTXQuerySchema, 14 | type DecodeTXQuery, 15 | } from "./tx.schema"; 16 | import { decodeLogsFromTx, fetchTxFromHash } from "./tx.service"; 17 | import { type Chain } from "@covalenthq/client-sdk"; 18 | 19 | export const txRouter = Router(); 20 | 21 | const handleDecode = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | try { 27 | const covalentApiKey = (req.headers as DecodeTXHeaders)[ 28 | "x-covalent-api-key" 29 | ]; 30 | const raw_logs = (req.query as DecodeTXQuery)["raw_logs"] === "true"; 31 | const min_usd = (req.query as DecodeTXQuery)["min_usd"] ?? 0; 32 | const { chain_name, tx_hash } = req.body as DecodeTXRequest; 33 | const tx = await fetchTxFromHash( 34 | chain_name as Chain, 35 | tx_hash, 36 | covalentApiKey 37 | ); 38 | const { 39 | log_events, 40 | dex_details, 41 | nft_sale_details, 42 | lending_details, 43 | safe_details, 44 | ...tx_metadata 45 | } = tx; 46 | const events = await decodeLogsFromTx( 47 | chain_name as Chain, 48 | tx, 49 | covalentApiKey, 50 | { 51 | raw_logs, 52 | min_usd, 53 | } 54 | ); 55 | const parsedTx = JSON.parse( 56 | JSON.stringify(tx_metadata, (_key, value) => { 57 | return typeof value === "bigint" ? value.toString() : value; 58 | }) 59 | ); 60 | res.json({ 61 | success: true, 62 | events: events, 63 | tx_metadata: parsedTx, 64 | }); 65 | } catch (error) { 66 | next(error); 67 | } 68 | }; 69 | 70 | txRouter.post( 71 | "/decode", 72 | validateQuery("headers", decodeTXHeadersSchema), 73 | validateQuery("query", decodeTXQuerySchema), 74 | validateQuery("body", decodeTXBodySchema), 75 | handleDecode 76 | ); 77 | -------------------------------------------------------------------------------- /microservices/tx/tx.schema.ts: -------------------------------------------------------------------------------- 1 | import { Chains } from "@covalenthq/client-sdk"; 2 | import * as yup from "yup"; 3 | 4 | export const decodeTXBodySchema = yup.object({ 5 | chain_name: yup 6 | .mixed() 7 | .oneOf(Object.values(Chains), "chain_name is incorrect") 8 | .required("chain_name is required"), 9 | tx_hash: yup.string().trim().required("tx_hash is required"), 10 | }); 11 | 12 | export type DecodeTXRequest = yup.InferType; 13 | 14 | export const decodeTXHeadersSchema = yup.object({ 15 | "x-covalent-api-key": yup 16 | .string() 17 | .trim() 18 | .required("x-covalent-api-key is required"), 19 | }); 20 | 21 | export type DecodeTXHeaders = yup.InferType; 22 | 23 | export const decodeTXQuerySchema = yup.object({ 24 | raw_logs: yup.string().oneOf(["false", "true"]), 25 | min_usd: yup.number().min(0), 26 | }); 27 | 28 | export type DecodeTXQuery = yup.InferType; 29 | -------------------------------------------------------------------------------- /microservices/tx/tx.service.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../services"; 2 | import { 3 | CovalentClient, 4 | type Chain, 5 | type Transaction, 6 | } from "@covalenthq/client-sdk"; 7 | import { type QueryOptions } from "../../services/decoder/decoder.types"; 8 | 9 | export const fetchTxFromHash = async ( 10 | chain_name: Chain, 11 | tx_hash: string, 12 | covalentApiKey: string 13 | ): Promise => { 14 | const covalentClient = new CovalentClient(covalentApiKey); 15 | const { data, error_code, error_message } = 16 | await covalentClient.TransactionService.getTransaction( 17 | chain_name, 18 | tx_hash, 19 | { 20 | noLogs: false, 21 | quoteCurrency: "USD", 22 | withDex: false, 23 | withLending: false, 24 | withNftSales: false, 25 | withSafe: false, 26 | } 27 | ); 28 | const tx = data?.items?.[0]; 29 | if (tx) { 30 | return tx; 31 | } else { 32 | throw { 33 | errorCode: error_code, 34 | message: error_message, 35 | }; 36 | } 37 | }; 38 | 39 | export const decodeLogsFromTx = async ( 40 | chain_name: Chain, 41 | tx: Transaction, 42 | covalentApiKey: string, 43 | options: QueryOptions 44 | ) => { 45 | const events = await GoldRushDecoder.decode( 46 | chain_name, 47 | tx, 48 | covalentApiKey, 49 | options 50 | ); 51 | return events; 52 | }; 53 | -------------------------------------------------------------------------------- /middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { validateQuery } from "./validate-query"; 2 | -------------------------------------------------------------------------------- /middlewares/validate-query.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { type ObjectSchema, type ValidationError } from "yup"; 4 | import { type Request, type Response, type NextFunction } from "express"; 5 | type RequestLocations = "query" | "body" | "params" | "headers"; 6 | 7 | /** 8 | * Generic Request Validator 9 | * @param {RequestLocations} location The parameter of the req object to be validated. 10 | * @param {yup.ObjectSchema} schema The schema against which validation is to be done. 11 | */ 12 | export const validateQuery = ( 13 | location: RequestLocations, 14 | schema: ObjectSchema 15 | ) => { 16 | return async (req: Request, res: Response, next: NextFunction) => { 17 | let _location; 18 | switch (location) { 19 | case "query": 20 | _location = req.query; 21 | break; 22 | case "body": 23 | _location = req.body; 24 | break; 25 | case "params": 26 | _location = req.params; 27 | break; 28 | case "headers": 29 | _location = req.headers; 30 | break; 31 | } 32 | try { 33 | await schema.validate(_location, { abortEarly: false }); 34 | next(); 35 | } catch (error: Error | ValidationError | any) { 36 | let message: string = ""; 37 | error.errors.forEach((e: string) => { 38 | message += `${e}. `; 39 | }); 40 | res.status(400).json({ 41 | name: "Validation Error", 42 | message: message.trim(), 43 | }); 44 | } 45 | }; 46 | }; 47 | 48 | export default validateQuery; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@covalent/goldrush-decoder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "add-config": "ts-node ./scripts/add-config.ts", 7 | "build": "tsc", 8 | "dev": "tsc-watch --onSuccess \"yarn start\"", 9 | "lint": "eslint .", 10 | "pretty": "prettier . --write", 11 | "start": "node ./dist/api/index.js", 12 | "test": "jest --coverage" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@covalenthq/client-sdk": "^1.0.2", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.3.1", 20 | "enquirer": "^2.4.1", 21 | "express": "^4.19.2", 22 | "tsc-watch": "^6.0.4", 23 | "viem": "^1.16.6", 24 | "yup": "^1.3.2" 25 | }, 26 | "devDependencies": { 27 | "@types/cors": "^2.8.14", 28 | "@types/express": "^4.17.19", 29 | "@types/jest": "^29.5.8", 30 | "@types/node": "^20.8.2", 31 | "@types/supertest": "^2.0.16", 32 | "@typescript-eslint/eslint-plugin": "^6.7.5", 33 | "@typescript-eslint/parser": "^6.7.5", 34 | "eslint": "^8.51.0", 35 | "eslint-plugin-prettier": "^5.0.1", 36 | "jest": "^29.7.0", 37 | "prettier": "^3.0.3", 38 | "supertest": "^6.3.3", 39 | "ts-jest": "^29.1.1", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^5.2.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/add-config.ts: -------------------------------------------------------------------------------- 1 | import { Chains, type Chain } from "@covalenthq/client-sdk"; 2 | import { prompt } from "enquirer"; 3 | import { existsSync, mkdirSync, writeFileSync } from "fs"; 4 | import { join } from "path"; 5 | import { format, type Options } from "prettier"; 6 | import prettierConfig from "../.prettierrc.json"; 7 | import { slugify } from "../utils/functions"; 8 | import { type Configs } from "../services/decoder/decoder.types"; 9 | import * as yup from "yup"; 10 | 11 | const writeInFile = async ( 12 | path: string, 13 | name: string, 14 | content: string, 15 | pretty: boolean 16 | ) => { 17 | if (!existsSync(path)) { 18 | mkdirSync(path); 19 | } 20 | const formattedContent = pretty 21 | ? await format(content, { 22 | parser: "typescript", 23 | ...(prettierConfig as Options), 24 | }) 25 | : content; 26 | writeFileSync(join(path, name), formattedContent, "utf-8"); 27 | }; 28 | 29 | const reset = "\x1b[0m"; 30 | const bright = "\x1b[1m"; 31 | const fgGreen = "\x1b[32m"; 32 | const fgYellow = "\x1b[33m"; 33 | const customLog = (message: string, type: "success" | "info") => { 34 | console.info( 35 | `${bright}${type === "info" ? fgYellow : fgGreen}${message}${reset}` 36 | ); 37 | }; 38 | 39 | const nameSchema = yup.string().trim().required("name is required"); 40 | const addressSchema = yup.string().trim().required("address is required"); 41 | const isFactorySchema = yup.boolean().required("is_factory is required"); 42 | const chainNameSchema = yup 43 | .mixed() 44 | .oneOf(Object.values(Chains), "chain_name is incorrect") 45 | .required("chain_name is required"); 46 | (async () => { 47 | const { protocol_name } = (await prompt({ 48 | type: "input", 49 | name: "protocol_name", 50 | message: "What is the Protocol Name?", 51 | format: (value) => slugify(value), 52 | result: (value) => slugify(value), 53 | validate: async (value) => { 54 | try { 55 | await nameSchema.validate(value, { 56 | abortEarly: false, 57 | }); 58 | return true; 59 | } catch (error: yup.ValidationError | any) { 60 | return `Invalid Input: ${error.errors.join(". ")}`; 61 | } 62 | }, 63 | })) as { 64 | protocol_name: string; 65 | }; 66 | 67 | const protocolDir: string = join( 68 | __dirname, 69 | "..", 70 | "services", 71 | "decoder", 72 | "protocols", 73 | protocol_name 74 | ); 75 | 76 | const exists: boolean = existsSync(protocolDir); 77 | if (exists) { 78 | customLog( 79 | `'${protocol_name}' already exists. Adding another config to it.`, 80 | "info" 81 | ); 82 | } else { 83 | customLog( 84 | `'${protocol_name}' does not exist. Creating a new config template.`, 85 | "info" 86 | ); 87 | } 88 | 89 | const { address, is_factory, chain_name } = (await prompt([ 90 | { 91 | type: "input", 92 | name: "chain_name", 93 | message: "What is the Chain on which the contract is deployed?", 94 | format: (value) => slugify(value), 95 | result: (value) => slugify(value), 96 | validate: async (value) => { 97 | try { 98 | await chainNameSchema.validate(value, { 99 | abortEarly: false, 100 | }); 101 | return true; 102 | } catch (error: yup.ValidationError | any) { 103 | return `Invalid Input: ${error.errors.join(". ")}`; 104 | } 105 | }, 106 | }, 107 | { 108 | type: "input", 109 | name: "address", 110 | message: "What is the Contract Address?", 111 | format: (value) => value.toLowerCase(), 112 | result: (value) => value.toLowerCase(), 113 | validate: async (value) => { 114 | try { 115 | await addressSchema.validate(value, { 116 | abortEarly: false, 117 | }); 118 | return true; 119 | } catch (error: yup.ValidationError | any) { 120 | return `Invalid Input: ${error.errors.join(". ")}`; 121 | } 122 | }, 123 | }, 124 | { 125 | type: "toggle", 126 | name: "is_factory", 127 | message: "Is it a Factory Address?", 128 | enabled: "Yes", 129 | disabled: "No", 130 | validate: async (value) => { 131 | try { 132 | await isFactorySchema.validate(value, { 133 | abortEarly: false, 134 | }); 135 | return true; 136 | } catch (error: yup.ValidationError | any) { 137 | return `Invalid Input: ${error.errors.join(". ")}`; 138 | } 139 | }, 140 | }, 141 | ])) as { 142 | address: string; 143 | is_factory: boolean; 144 | chain_name: Chain; 145 | }; 146 | 147 | if (!exists) { 148 | const eventName: string = ""; 149 | const abiContent: string = `[]`; 150 | const configsContent: string = `import{type Configs}from"../../decoder.types";\n\nconst configs:Configs=[{address:"${address}",is_factory:${is_factory},protocol_name:"${protocol_name}",chain_name:"${chain_name}"}];\n\nexport default configs;`; 151 | const decodersContent: string = `import{GoldRushDecoder}from"../../decoder";import{type EventType}from"../../decoder.types";import{DECODED_ACTION,DECODED_EVENT_CATEGORY}from"../../decoder.constants";import{decodeEventLog,type Abi}from"viem";import ABI from "./abis/${protocol_name}.abi.json";\n\nGoldRushDecoder.on("${protocol_name}:${eventName}",["${chain_name}"],ABI as Abi,async(log_event,tx,chain_name,covalent_client,options):Promise =>{const{raw_log_data,raw_log_topics}=log_event;\n\nconst{args:decoded}=decodeEventLog({abi:ABI,topics:raw_log_topics as[],data:raw_log_data as \`0x\${string}\`,eventName:"${eventName}"})as{eventName:"${eventName}";args:{}};\n\nreturn{action:DECODED_ACTION.SWAPPED,category:DECODED_EVENT_CATEGORY.DEX,name:"${eventName}",protocol:{logo:log_event.sender_logo_url as string,name:log_event.sender_name as string},...(options.raw_logs?{raw_log:log_event}:{})};});`; 152 | const testContent: string = `import request from"supertest";import app from"../../../../api";import{type EventType}from"../../decoder.types";\n\ndescribe("${protocol_name}",()=>{test("${chain_name}:${eventName}",async()=>{const res=await request(app).post("/api/v1/tx/decode").set({"x-covalent-api-key":process.env.TEST_COVALENT_API_KEY}).send({chain_name:"${chain_name}",tx_hash:""});const{events}=res.body as{events:EventType[]};const event=events.find(({name})=>name==="${eventName}");if(!event){throw Error("Event not found")}const testAdded:boolean=false;expect(testAdded).toEqual(true)})});`; 153 | await writeInFile( 154 | protocolDir, 155 | `${protocol_name}.decoders.ts`, 156 | decodersContent, 157 | true 158 | ); 159 | await writeInFile( 160 | protocolDir, 161 | `${protocol_name}.configs.ts`, 162 | configsContent, 163 | true 164 | ); 165 | await writeInFile( 166 | protocolDir, 167 | `${protocol_name}.test.ts`, 168 | testContent, 169 | true 170 | ); 171 | await writeInFile( 172 | join(protocolDir, "abis"), 173 | `${protocol_name}.abi.json`, 174 | abiContent, 175 | false 176 | ); 177 | customLog(`Created '${protocol_name}' successfully!`, "success"); 178 | } else { 179 | const configFile = join(protocolDir, `${protocol_name}.configs.ts`); 180 | const configs = require(configFile).default as Configs; 181 | configs.push({ 182 | address: address, 183 | is_factory: is_factory, 184 | protocol_name: protocol_name, 185 | chain_name: chain_name, 186 | }); 187 | const configsContent: string = `import{type Configs}from"../../decoder.types";\n\nconst configs:Configs=${JSON.stringify( 188 | configs 189 | )};\n\nexport default configs;`; 190 | await writeInFile( 191 | protocolDir, 192 | `${protocol_name}.configs.ts`, 193 | configsContent, 194 | true 195 | ); 196 | customLog( 197 | `Added a config to '${protocol_name}' successfully!`, 198 | "success" 199 | ); 200 | } 201 | })(); 202 | -------------------------------------------------------------------------------- /services/decoder/decoder.constants.ts: -------------------------------------------------------------------------------- 1 | export enum DECODED_EVENT_CATEGORY { 2 | NFT = "NFT Transaction", 3 | LENDING = "Lending", 4 | SAFE = "SAFE", 5 | DEX = "DEX", 6 | TOKEN = "Token", 7 | SWAP = "Swap", 8 | DEFI = "DeFi", 9 | BRIDGE = "Bridge", 10 | GAMING = "Gaming", 11 | SOCIAL = "Social", 12 | STAKING = "Staking", 13 | OTHERS = "Others", 14 | } 15 | 16 | export enum DECODED_ACTION { 17 | SWAPPED = "Swapped", 18 | MULTISIG_ACTION = "MultiSig", 19 | APPROVAL = "Approval", 20 | TRANSFERRED = "Transferred", 21 | NATIVE_TRANSFER = "Native Token Transfer", 22 | RECEIVED_BRIDGE = "Received Bridge", 23 | ACCOUNT_ABSTRACTION = "Account Abstraction Transaction", 24 | SALE = "Sale", 25 | MINT = "Mint", 26 | BURN = "Burn", 27 | WITHDRAW = "Withdraw", 28 | DEPOSIT = "Deposit", 29 | ADD_LIQUIDITY = "Add Liquidity", 30 | REMOVE_LIQUIDITY = "Remove Liquidity", 31 | UPDATE = "Update", 32 | FLASHLOAN = "Flashloan", 33 | REPAY = "Repay", 34 | BORROW = "Borrow", 35 | LIQUIDATE = "Liquidate", 36 | CLAIM_REWARDS = "Claim Rewards", 37 | GAME_ACTION = "Game Action", 38 | CREATE = "Create Pool", 39 | INIT_ROUTER = "Init Router", 40 | ADD_ROUTER = "Add Router", 41 | REMOVE_ROUTER = "Remove Router", 42 | } 43 | -------------------------------------------------------------------------------- /services/decoder/decoder.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "fs"; 2 | import { join } from "path"; 3 | import { 4 | CovalentClient, 5 | type Chain, 6 | type Transaction, 7 | } from "@covalenthq/client-sdk"; 8 | import { 9 | type Configs, 10 | type DecodingFunction, 11 | type Decoders, 12 | type DecodingFunctions, 13 | type EventType, 14 | type DecoderConfig, 15 | type Fallbacks, 16 | type NativeDecodingFunction, 17 | type QueryOptions, 18 | } from "./decoder.types"; 19 | import { encodeEventTopics, type Abi } from "viem"; 20 | import { chunkify } from "../../utils/functions"; 21 | 22 | export class GoldRushDecoder { 23 | private static configs: DecoderConfig = {}; 24 | private static decoders: Decoders = {}; 25 | private static fallbacks: Fallbacks = {}; 26 | private static native_decoder: NativeDecodingFunction; 27 | private static decoding_functions: DecodingFunctions = []; 28 | private static fallback_functions: DecodingFunctions = []; 29 | private static fileExtension: "js" | "ts" = 30 | process.env.NODE_ENV !== "test" ? "js" : "ts"; 31 | 32 | public static initDecoder = (): void => { 33 | console.info("Initializing GoldrushDecoder Service..."); 34 | 35 | const protocolsDirectoryPath: string = join(__dirname, "/protocols"); 36 | const protocols = readdirSync(protocolsDirectoryPath); 37 | let protocolsCount: number = 0; 38 | let configsCount: number = 0; 39 | for (const protocol of protocols) { 40 | const protocolPath = join(protocolsDirectoryPath, protocol); 41 | const files = readdirSync(protocolPath); 42 | let configFile: string | null = null, 43 | decodersFile: string | null = null; 44 | files.forEach((file) => { 45 | if (file.endsWith(`.configs.${this.fileExtension}`)) { 46 | configFile = file; 47 | } 48 | if (file.endsWith(`.decoders.${this.fileExtension}`)) { 49 | decodersFile = file; 50 | } 51 | }); 52 | if (configFile && decodersFile) { 53 | protocolsCount++; 54 | const configs = require(join(protocolPath, configFile)) 55 | .default as Configs; 56 | configs.forEach( 57 | ({ address, is_factory, chain_name, protocol_name }) => { 58 | this.configs[chain_name] ??= {}; 59 | this.configs[chain_name][protocol_name] ??= {}; 60 | this.configs[chain_name][protocol_name][address] = { 61 | is_factory: is_factory, 62 | }; 63 | configsCount++; 64 | } 65 | ); 66 | require(join(protocolPath, decodersFile)); 67 | } 68 | } 69 | 70 | const fallbacksDirectoryPath: string = join(__dirname, "/fallbacks"); 71 | const fallbacks = readdirSync(fallbacksDirectoryPath); 72 | for (const fallback of fallbacks) { 73 | const fallbackPath = join(fallbacksDirectoryPath, fallback); 74 | const files = readdirSync(fallbackPath); 75 | let fallbackFile: string | null = null; 76 | files.forEach((file) => { 77 | if (file.endsWith(`.fallback.${this.fileExtension}`)) { 78 | fallbackFile = file; 79 | } 80 | }); 81 | if (fallbackFile) { 82 | require(join(fallbackPath, fallbackFile)); 83 | } 84 | } 85 | 86 | const nativeDecoderPath: string = join( 87 | __dirname, 88 | "native", 89 | `native.decoder.${this.fileExtension}` 90 | ); 91 | require(join(nativeDecoderPath)); 92 | 93 | const decodersCount = this.decoding_functions.length; 94 | const fallbacksCount = this.fallback_functions.length; 95 | 96 | console.info("1 native decoder added"); 97 | console.info(`${protocolsCount.toLocaleString()} protocols found`); 98 | console.info(`${configsCount.toLocaleString()} configs generated`); 99 | console.info(`${decodersCount.toLocaleString()} decoders generated`); 100 | console.info(`${fallbacksCount.toLocaleString()} fallbacks generated`); 101 | }; 102 | 103 | public static on = ( 104 | event_id: string, 105 | chain_names: Chain[], 106 | abi: Abi, 107 | decoding_function: DecodingFunction 108 | ): void => { 109 | const [protocol, event_name] = event_id.split(":"); 110 | const [topic0_hash] = encodeEventTopics({ 111 | abi: abi, 112 | eventName: event_name, 113 | }); 114 | this.decoding_functions.push(decoding_function); 115 | const decoding_function_index: number = 116 | this.decoding_functions.length - 1; 117 | chain_names.forEach((chain_name) => { 118 | const configExists = this.configs[chain_name]?.[protocol] 119 | ? true 120 | : false; 121 | if (!configExists) { 122 | throw Error( 123 | `config for ${protocol} does not exist on ${chain_name}` 124 | ); 125 | } 126 | Object.keys(this.configs[chain_name][protocol]).forEach( 127 | (address) => { 128 | const lowercaseChainName = 129 | chain_name.toLowerCase() as Chain; 130 | const lowercaseAddress = address.toLowerCase(); 131 | const lowercaseTopic0Hash = topic0_hash.toLowerCase(); 132 | 133 | this.decoders[lowercaseChainName] ??= {}; 134 | this.decoders[lowercaseChainName][lowercaseAddress] ??= {}; 135 | this.decoders[lowercaseChainName][lowercaseAddress][ 136 | lowercaseTopic0Hash 137 | ] = decoding_function_index; 138 | } 139 | ); 140 | }); 141 | }; 142 | 143 | public static fallback = ( 144 | event_name: string, 145 | abi: Abi, 146 | decoding_function: DecodingFunction 147 | ): void => { 148 | const [topic0_hash] = encodeEventTopics({ 149 | abi: abi, 150 | eventName: event_name, 151 | }); 152 | const lowercaseTopic0Hash = topic0_hash.toLowerCase(); 153 | this.fallback_functions.push(decoding_function); 154 | const fallback_function_index: number = 155 | this.fallback_functions.length - 1; 156 | this.fallbacks[lowercaseTopic0Hash] = fallback_function_index; 157 | }; 158 | 159 | public static native = (native_decoder: NativeDecodingFunction): void => { 160 | this.native_decoder = native_decoder; 161 | }; 162 | 163 | public static decode = async ( 164 | chain_name: Chain, 165 | tx: Transaction, 166 | covalent_api_key: string, 167 | options: QueryOptions 168 | ): Promise => { 169 | const covalent_client = new CovalentClient(covalent_api_key); 170 | let events: (EventType | null)[] = []; 171 | if (tx.value) { 172 | const nativeEvent = this.native_decoder(tx, options); 173 | events.push(nativeEvent); 174 | } 175 | const logChunks = chunkify(tx.log_events ?? [], 100); 176 | for (const logChunk of logChunks) { 177 | const decodedChunk = await Promise.all( 178 | logChunk.map((log_event) => { 179 | const { 180 | raw_log_topics: [topic0_hash], 181 | sender_address, 182 | sender_factory_address, 183 | } = log_event; 184 | const lowercaseChainName = 185 | chain_name.toLowerCase() as Chain; 186 | const lowercaseSenderAddress = 187 | sender_address?.toLowerCase(); 188 | const lowercaseSenderFactoryAddress = 189 | sender_factory_address?.toLowerCase(); 190 | const lowercaseTopic0Hash = topic0_hash?.toLowerCase(); 191 | const decoding_index = 192 | this.decoders[lowercaseChainName]?.[ 193 | lowercaseSenderAddress 194 | ]?.[lowercaseTopic0Hash] ?? 195 | this.decoders[lowercaseChainName]?.[ 196 | lowercaseSenderFactoryAddress 197 | ]?.[lowercaseTopic0Hash]; 198 | const fallback_index = this.fallbacks[lowercaseTopic0Hash]; 199 | const logFunction = 200 | (decoding_index !== undefined && 201 | this.decoding_functions[decoding_index]) || 202 | (fallback_index !== undefined && 203 | this.fallback_functions[fallback_index]) || 204 | null; 205 | return logFunction 206 | ? logFunction( 207 | log_event, 208 | tx, 209 | chain_name, 210 | covalent_client, 211 | options 212 | ) 213 | : null; 214 | }) 215 | ); 216 | events = [...events, ...decodedChunk]; 217 | } 218 | return events.filter(Boolean) as EventType[]; 219 | }; 220 | } 221 | -------------------------------------------------------------------------------- /services/decoder/decoder.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type CovalentClient, 3 | type Chain, 4 | type LogEvent, 5 | type Transaction, 6 | } from "@covalenthq/client-sdk"; 7 | import { 8 | type DECODED_ACTION, 9 | type DECODED_EVENT_CATEGORY, 10 | } from "./decoder.constants"; 11 | 12 | export type Configs = { 13 | protocol_name: string; 14 | chain_name: Chain; 15 | address: string; 16 | is_factory: boolean; 17 | }[]; 18 | 19 | export type EventDetails = { 20 | heading: string; 21 | value: string; 22 | type: "address" | "text"; 23 | }[]; 24 | 25 | export type EventNFTs = { 26 | heading: string; 27 | collection_name: string | null; 28 | token_identifier: string | null; 29 | collection_address: string; 30 | images: { 31 | default: string | null; 32 | 256: string | null; 33 | 512: string | null; 34 | 1024: string | null; 35 | }; 36 | }[]; 37 | 38 | export type EventTokens = { 39 | heading: string; 40 | value: string; 41 | decimals: number; 42 | ticker_symbol: string | null; 43 | ticker_logo: string | null; 44 | pretty_quote: string; 45 | }[]; 46 | 47 | export interface EventType { 48 | category: DECODED_EVENT_CATEGORY; 49 | action: DECODED_ACTION; 50 | name: string; 51 | protocol?: { 52 | name: string; 53 | logo: string; 54 | }; 55 | tokens?: EventTokens; 56 | nfts?: EventNFTs; 57 | details?: EventDetails; 58 | raw_log?: LogEvent; 59 | } 60 | 61 | export interface QueryOptions { 62 | raw_logs?: boolean; 63 | min_usd?: number; 64 | } 65 | 66 | export type DecodingFunction = ( 67 | log_event: LogEvent, 68 | tx: Transaction, 69 | chain_name: Chain, 70 | covalent_client: CovalentClient, 71 | options: QueryOptions 72 | ) => Promise; 73 | 74 | export type NativeDecodingFunction = ( 75 | tx: Transaction, 76 | options: QueryOptions 77 | ) => EventType | null; 78 | 79 | export type DecoderConfig = 80 | | { 81 | [chain_name in Chain]: { 82 | [protocol_name: string]: { 83 | [address: string]: { 84 | is_factory: boolean; 85 | }; 86 | }; 87 | }; 88 | } 89 | | Record; 90 | 91 | export type Decoders = 92 | | { 93 | [chain_name in Chain]: { 94 | [address: string]: { 95 | [topic0_hash: string]: number; 96 | }; 97 | }; 98 | } 99 | | Record; 100 | 101 | export type Fallbacks = 102 | | { 103 | [topic0_hash: string]: number; 104 | } 105 | | Record; 106 | 107 | export type DecodingFunctions = DecodingFunction[]; 108 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval-for-all/abis/approval-for-all.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "owner", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "operator", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "bool", 20 | "name": "approved", 21 | "type": "bool" 22 | } 23 | ], 24 | "name": "ApprovalForAll", 25 | "type": "event" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval-for-all/approval-for-all.fallback.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { type EventType } from "../../decoder.types"; 3 | import { 4 | DECODED_ACTION, 5 | DECODED_EVENT_CATEGORY, 6 | } from "../../decoder.constants"; 7 | import { decodeEventLog, type Abi } from "viem"; 8 | import ABI from "./abis/approval-for-all.abi.json"; 9 | 10 | GoldRushDecoder.fallback( 11 | "ApprovalForAll", 12 | ABI as Abi, 13 | async ( 14 | log_event, 15 | tx, 16 | chain_name, 17 | covalent_client, 18 | options 19 | ): Promise => { 20 | const { raw_log_data, raw_log_topics, sender_logo_url, sender_name } = 21 | log_event; 22 | 23 | const { args: decoded } = decodeEventLog({ 24 | abi: ABI, 25 | topics: raw_log_topics as [], 26 | data: raw_log_data as `0x${string}`, 27 | eventName: "ApprovalForAll", 28 | }) as { 29 | eventName: "ApprovalForAll"; 30 | args: { 31 | owner: string; 32 | operator: string; 33 | approved: boolean; 34 | }; 35 | }; 36 | 37 | return { 38 | action: DECODED_ACTION.APPROVAL, 39 | category: DECODED_EVENT_CATEGORY.DEX, 40 | name: "Approval For All", 41 | protocol: { 42 | logo: sender_logo_url as string, 43 | name: sender_name as string, 44 | }, 45 | ...(options.raw_logs ? { raw_log: log_event } : {}), 46 | details: [ 47 | { 48 | heading: "Owner", 49 | value: decoded.owner, 50 | type: "address", 51 | }, 52 | { 53 | heading: "Operator", 54 | value: decoded.operator, 55 | type: "address", 56 | }, 57 | { 58 | heading: "Approved", 59 | value: decoded.approved ? "Yes" : "No", 60 | type: "text", 61 | }, 62 | ], 63 | }; 64 | } 65 | ); 66 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval-for-all/approval-for-all.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("fallback", () => { 6 | test("ApprovalForAll", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xb9ccede37d9324af16b91b8c00ff6cbe0d426c6baf2ae0cf30a0bfe6b96c38ef", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Approval For All"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.details?.length).toEqual(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval/abis/approval-erc20.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "name": "owner", 8 | "type": "address" 9 | }, 10 | { 11 | "indexed": true, 12 | "name": "spender", 13 | "type": "address" 14 | }, 15 | { 16 | "indexed": false, 17 | "name": "value", 18 | "type": "uint256" 19 | } 20 | ], 21 | "name": "Approval", 22 | "type": "event" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval/abis/approval-erc721.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "name": "owner", 8 | "type": "address" 9 | }, 10 | { 11 | "indexed": true, 12 | "name": "spender", 13 | "type": "address" 14 | }, 15 | { 16 | "indexed": true, 17 | "name": "tokenId", 18 | "type": "uint256" 19 | } 20 | ], 21 | "name": "Approval", 22 | "type": "event" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval/approval.fallback.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { type EventDetails, type EventType } from "../../decoder.types"; 3 | import { 4 | DECODED_ACTION, 5 | DECODED_EVENT_CATEGORY, 6 | } from "../../decoder.constants"; 7 | import { decodeEventLog, type Abi } from "viem"; 8 | import ERC20ABI from "./abis/approval-erc20.abi.json"; 9 | import ERC721ABI from "./abis/approval-erc721.abi.json"; 10 | import { currencyToNumber, timestampParser } from "../../../../utils/functions"; 11 | import { prettifyCurrency } from "@covalenthq/client-sdk"; 12 | 13 | GoldRushDecoder.fallback( 14 | "Approval", 15 | ERC20ABI as Abi, 16 | async ( 17 | log_event, 18 | tx, 19 | chain_name, 20 | covalent_client, 21 | options 22 | ): Promise => { 23 | const { 24 | block_signed_at, 25 | raw_log_data, 26 | raw_log_topics, 27 | sender_address, 28 | sender_logo_url, 29 | sender_name, 30 | sender_contract_ticker_symbol, 31 | sender_contract_decimals, 32 | } = log_event; 33 | 34 | let decoded: 35 | | { 36 | owner: string; 37 | spender: string; 38 | value: bigint; 39 | tokenId?: never; 40 | } 41 | | { 42 | owner: string; 43 | spender: string; 44 | tokenId: bigint; 45 | value?: never; 46 | }; 47 | 48 | try { 49 | const { args } = decodeEventLog({ 50 | abi: ERC20ABI, 51 | topics: raw_log_topics as [], 52 | data: raw_log_data as `0x${string}`, 53 | eventName: "Approval", 54 | }) as { 55 | eventName: "Approval"; 56 | args: { 57 | owner: string; 58 | spender: string; 59 | value: bigint; 60 | }; 61 | }; 62 | decoded = args; 63 | } catch (error) { 64 | const { args } = decodeEventLog({ 65 | abi: ERC721ABI, 66 | topics: raw_log_topics as [], 67 | data: raw_log_data as `0x${string}`, 68 | eventName: "Approval", 69 | }) as { 70 | eventName: "Approval"; 71 | args: { 72 | owner: string; 73 | spender: string; 74 | tokenId: bigint; 75 | }; 76 | }; 77 | decoded = args; 78 | } 79 | 80 | const details: EventDetails = [ 81 | { 82 | heading: "Owner", 83 | value: decoded.owner, 84 | type: "address", 85 | }, 86 | { 87 | heading: "Spender", 88 | value: decoded.spender, 89 | type: "address", 90 | }, 91 | ]; 92 | 93 | const parsedData: EventType = { 94 | action: DECODED_ACTION.APPROVAL, 95 | category: DECODED_EVENT_CATEGORY.DEX, 96 | name: "Approval", 97 | protocol: { 98 | logo: sender_logo_url as string, 99 | name: sender_name as string, 100 | }, 101 | ...(options.raw_logs ? { raw_log: log_event } : {}), 102 | details: details, 103 | }; 104 | 105 | if (decoded.value) { 106 | const unlimitedValue: boolean = 107 | decoded.value.toString() === 108 | "115792089237316195423570985008687907853269984665640564039457584007913129639935"; 109 | 110 | if (unlimitedValue) { 111 | details.push({ 112 | heading: "Value", 113 | value: "Unlimited", 114 | type: "text", 115 | }); 116 | } else { 117 | const date = timestampParser(block_signed_at, "YYYY-MM-DD"); 118 | const { data } = 119 | await covalent_client.PricingService.getTokenPrices( 120 | chain_name, 121 | "USD", 122 | sender_address, 123 | { 124 | from: date, 125 | to: date, 126 | } 127 | ); 128 | 129 | const pretty_quote = prettifyCurrency( 130 | data?.[0]?.items?.[0]?.price * 131 | (Number(decoded.value) / 132 | Math.pow( 133 | 10, 134 | data?.[0]?.items?.[0]?.contract_metadata 135 | ?.contract_decimals ?? 18 136 | )) 137 | ); 138 | 139 | if (currencyToNumber(pretty_quote) < options.min_usd!) { 140 | return null; 141 | } 142 | 143 | parsedData.tokens = [ 144 | { 145 | heading: "Value", 146 | value: decoded.value.toString(), 147 | ticker_symbol: sender_contract_ticker_symbol, 148 | ticker_logo: sender_logo_url, 149 | decimals: sender_contract_decimals ?? 18, 150 | pretty_quote: pretty_quote, 151 | }, 152 | ]; 153 | } 154 | } else if (decoded.tokenId) { 155 | const { data } = 156 | await covalent_client.NftService.getNftMetadataForGivenTokenIdForContract( 157 | chain_name, 158 | sender_address, 159 | decoded.tokenId.toString(), 160 | { 161 | withUncached: true, 162 | } 163 | ); 164 | 165 | parsedData.nfts = [ 166 | { 167 | heading: "NFT Transferred", 168 | collection_address: data?.items?.[0]?.contract_address, 169 | collection_name: 170 | data?.items?.[0]?.nft_data?.external_data?.name || null, 171 | token_identifier: 172 | data?.items?.[0]?.nft_data?.token_id?.toString() || 173 | null, 174 | images: { 175 | "1024": 176 | data?.items?.[0]?.nft_data?.external_data 177 | ?.image_1024 || null, 178 | "512": 179 | data?.items?.[0]?.nft_data?.external_data 180 | ?.image_512 || null, 181 | "256": 182 | data?.items?.[0]?.nft_data?.external_data 183 | ?.image_256 || null, 184 | default: 185 | data?.items?.[0]?.nft_data?.external_data?.image || 186 | null, 187 | }, 188 | }, 189 | ]; 190 | } 191 | 192 | return parsedData; 193 | } 194 | ); 195 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/approval/approval.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("fallback", () => { 6 | test("Approval", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xa16a05cb1d0d37e75e52fa15e4d69307272ae922e098a5023017729ce95c358c", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Approval"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | if (event.tokens?.length) { 21 | expect(event.tokens?.length).toEqual(1); 22 | expect(event.details?.length).toEqual(2); 23 | } else if (event.nfts?.length) { 24 | expect(event.nfts?.length).toEqual(1); 25 | expect(event.details?.length).toEqual(2); 26 | } else { 27 | expect(event.details?.length).toEqual(3); 28 | } 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/transfer/abis/transfer-erc20.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "from", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "to", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "value", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Transfer", 25 | "type": "event" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/transfer/abis/transfer-erc721.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "from", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "to", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "uint256", 20 | "name": "tokenId", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Transfer", 25 | "type": "event" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/transfer/transfer.fallback.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { type EventDetails, type EventType } from "../../decoder.types"; 3 | import { 4 | DECODED_ACTION, 5 | DECODED_EVENT_CATEGORY, 6 | } from "../../decoder.constants"; 7 | import { decodeEventLog, type Abi } from "viem"; 8 | import ERC20ABI from "./abis/transfer-erc20.abi.json"; 9 | import ERC721ABI from "./abis/transfer-erc721.abi.json"; 10 | import { currencyToNumber, timestampParser } from "../../../../utils/functions"; 11 | import { prettifyCurrency } from "@covalenthq/client-sdk"; 12 | 13 | GoldRushDecoder.fallback( 14 | "Transfer", 15 | ERC20ABI as Abi, 16 | async ( 17 | log_event, 18 | tx, 19 | chain_name, 20 | covalent_client, 21 | options 22 | ): Promise => { 23 | const { raw_log_data, raw_log_topics } = log_event; 24 | 25 | let decoded: 26 | | { 27 | from: string; 28 | to: string; 29 | value: bigint; 30 | tokenId?: never; 31 | } 32 | | { 33 | from: string; 34 | to: string; 35 | tokenId: bigint; 36 | value?: never; 37 | }; 38 | 39 | try { 40 | const { args } = decodeEventLog({ 41 | abi: ERC20ABI, 42 | topics: raw_log_topics as [], 43 | data: raw_log_data as `0x${string}`, 44 | eventName: "Transfer", 45 | }) as { 46 | eventName: "Transfer"; 47 | args: { 48 | from: string; 49 | to: string; 50 | value: bigint; 51 | }; 52 | }; 53 | decoded = args; 54 | } catch (error) { 55 | const { args } = decodeEventLog({ 56 | abi: ERC721ABI, 57 | topics: raw_log_topics as [], 58 | data: raw_log_data as `0x${string}`, 59 | eventName: "Transfer", 60 | }) as { 61 | eventName: "Transfer"; 62 | args: { 63 | from: string; 64 | to: string; 65 | tokenId: bigint; 66 | }; 67 | }; 68 | decoded = args; 69 | } 70 | 71 | const details: EventDetails = [ 72 | { 73 | heading: "From", 74 | value: decoded.from, 75 | type: "address", 76 | }, 77 | { 78 | heading: "To", 79 | value: decoded.to, 80 | type: "address", 81 | }, 82 | ]; 83 | 84 | const parsedData: EventType = { 85 | action: DECODED_ACTION.TRANSFERRED, 86 | category: DECODED_EVENT_CATEGORY.TOKEN, 87 | name: "Transfer", 88 | protocol: { 89 | logo: log_event.sender_logo_url as string, 90 | name: log_event.sender_name as string, 91 | }, 92 | ...(options.raw_logs ? { raw_log: log_event } : {}), 93 | details: details, 94 | }; 95 | 96 | if (decoded.value) { 97 | const date = timestampParser( 98 | log_event.block_signed_at, 99 | "YYYY-MM-DD" 100 | ); 101 | const { data } = 102 | await covalent_client.PricingService.getTokenPrices( 103 | chain_name, 104 | "USD", 105 | log_event.sender_address, 106 | { 107 | from: date, 108 | to: date, 109 | } 110 | ); 111 | 112 | const pretty_quote = prettifyCurrency( 113 | data?.[0]?.items?.[0]?.price * 114 | (Number(decoded.value) / 115 | Math.pow( 116 | 10, 117 | data?.[0]?.items?.[0]?.contract_metadata 118 | ?.contract_decimals ?? 18 119 | )) ?? 0 120 | ); 121 | 122 | if (currencyToNumber(pretty_quote) < options.min_usd!) { 123 | return null; 124 | } 125 | 126 | parsedData.tokens = [ 127 | { 128 | decimals: data?.[0]?.contract_decimals ?? 18, 129 | heading: "Token Amount", 130 | pretty_quote: pretty_quote, 131 | ticker_logo: data?.[0]?.logo_urls?.token_logo_url, 132 | ticker_symbol: data?.[0]?.contract_ticker_symbol, 133 | value: decoded.value.toString(), 134 | }, 135 | ]; 136 | } else if (decoded.tokenId) { 137 | const { data } = 138 | await covalent_client.NftService.getNftMetadataForGivenTokenIdForContract( 139 | chain_name, 140 | log_event.sender_address, 141 | decoded.tokenId.toString(), 142 | { 143 | withUncached: true, 144 | } 145 | ); 146 | 147 | parsedData.nfts = [ 148 | { 149 | heading: "NFT Transferred", 150 | collection_address: data?.items?.[0]?.contract_address, 151 | collection_name: 152 | data?.items?.[0]?.nft_data?.external_data?.name || null, 153 | token_identifier: 154 | data?.items?.[0]?.nft_data?.token_id?.toString() || 155 | null, 156 | images: { 157 | "1024": 158 | data?.items?.[0]?.nft_data?.external_data 159 | ?.image_1024 || null, 160 | "512": 161 | data?.items?.[0]?.nft_data?.external_data 162 | ?.image_512 || null, 163 | "256": 164 | data?.items?.[0]?.nft_data?.external_data 165 | ?.image_256 || null, 166 | default: 167 | data?.items?.[0]?.nft_data?.external_data?.image || 168 | null, 169 | }, 170 | }, 171 | ]; 172 | } 173 | 174 | return parsedData; 175 | } 176 | ); 177 | -------------------------------------------------------------------------------- /services/decoder/fallbacks/transfer/transfer.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("fallback", () => { 6 | test("Transfer", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xe7b894fdac8c037fa69bbabe168fe7984033226e1b1871bd9f70c861b6f6a35d", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Transfer"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.details?.length).toEqual(2); 21 | if (event.tokens) { 22 | expect(event.tokens?.length).toEqual(1); 23 | } 24 | if (event.nfts) { 25 | expect(event.nfts?.length).toEqual(1); 26 | } 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /services/decoder/native/native.decoder.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../decoder"; 2 | import { type EventType } from "../decoder.types"; 3 | import { currencyToNumber } from "../../../utils/functions"; 4 | import { DECODED_ACTION, DECODED_EVENT_CATEGORY } from "../decoder.constants"; 5 | 6 | GoldRushDecoder.native((tx, options): EventType | null => { 7 | if (currencyToNumber(tx.pretty_value_quote) < options.min_usd!) { 8 | return null; 9 | } 10 | 11 | return { 12 | action: DECODED_ACTION.NATIVE_TRANSFER, 13 | category: DECODED_EVENT_CATEGORY.DEX, 14 | name: "Native Transfer", 15 | protocol: { 16 | logo: tx.gas_metadata.logo_url, 17 | name: tx.gas_metadata.contract_name, 18 | }, 19 | details: [ 20 | { 21 | heading: "From", 22 | value: tx.from_address, 23 | type: "address", 24 | }, 25 | { 26 | heading: "To", 27 | value: tx.to_address, 28 | type: "address", 29 | }, 30 | ], 31 | tokens: [ 32 | { 33 | heading: "Value", 34 | value: tx.value?.toString() || "0", 35 | decimals: tx.gas_metadata.contract_decimals, 36 | pretty_quote: tx.pretty_value_quote, 37 | ticker_logo: tx.gas_metadata.logo_url, 38 | ticker_symbol: tx.gas_metadata.contract_ticker_symbol, 39 | }, 40 | ], 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /services/decoder/native/native.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../api"; 3 | import { type EventType } from "../decoder.types"; 4 | 5 | describe("Native", () => { 6 | test("Native Transfer", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xfa6d5bd3041f6d904e96e592d5d339907637d1c445b8464184dba92d728e7234", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Native Transfer"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.details?.length).toEqual(2); 21 | expect(event.tokens?.length).toEqual(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /services/decoder/protocols/4337-entry-point/4337-entry-point.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | protocol_name: "4337-entry-point", 6 | address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789", 7 | is_factory: false, 8 | chain_name: "matic-mainnet", 9 | }, 10 | { 11 | protocol_name: "4337-entry-point", 12 | address: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789", 13 | is_factory: false, 14 | chain_name: "avalanche-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/4337-entry-point/4337-entry-point.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { type EventType } from "../../decoder.types"; 3 | import { 4 | DECODED_ACTION, 5 | DECODED_EVENT_CATEGORY, 6 | } from "../../decoder.constants"; 7 | import { decodeEventLog, type Abi } from "viem"; 8 | import ABI from "./abis/4337-entry-point.abi.json"; 9 | 10 | GoldRushDecoder.on( 11 | "4337-entry-point:UserOperationEvent", 12 | ["matic-mainnet", "avalanche-mainnet"], 13 | ABI as Abi, 14 | async ( 15 | log_event, 16 | tx, 17 | chain_name, 18 | covalent_client, 19 | options 20 | ): Promise => { 21 | const { raw_log_data, raw_log_topics, sender_contract_decimals } = 22 | log_event; 23 | 24 | const { args: decoded } = decodeEventLog({ 25 | abi: ABI, 26 | topics: raw_log_topics as [], 27 | data: raw_log_data as `0x${string}`, 28 | eventName: "UserOperationEvent", 29 | }) as { 30 | eventName: "UserOperationEvent"; 31 | args: { 32 | userOpHash: string; 33 | sender: string; 34 | paymaster: string; 35 | nonce: bigint; 36 | success: boolean; 37 | actualGasCost: bigint; 38 | actualGasUsed: bigint; 39 | }; 40 | }; 41 | 42 | return { 43 | action: DECODED_ACTION.ACCOUNT_ABSTRACTION, 44 | category: DECODED_EVENT_CATEGORY.OTHERS, 45 | name: "User Operation Event", 46 | protocol: { 47 | logo: log_event.sender_logo_url as string, 48 | name: "4337 Entry Point", 49 | }, 50 | ...(options.raw_logs ? { raw_log: log_event } : {}), 51 | details: [ 52 | { 53 | heading: "Gas Cost", 54 | value: ( 55 | decoded.actualGasCost / 56 | BigInt(Math.pow(10, sender_contract_decimals)) 57 | ).toString(), 58 | type: "text", 59 | }, 60 | { 61 | heading: "Gas Used", 62 | value: ( 63 | decoded.actualGasUsed / 64 | BigInt(Math.pow(10, sender_contract_decimals)) 65 | ).toString(), 66 | type: "text", 67 | }, 68 | { 69 | heading: "Paymaster", 70 | value: decoded.paymaster, 71 | type: "address", 72 | }, 73 | { 74 | heading: "Sender", 75 | value: decoded.sender, 76 | type: "address", 77 | }, 78 | { 79 | heading: "User Operation Hash", 80 | value: decoded.userOpHash, 81 | type: "address", 82 | }, 83 | ], 84 | }; 85 | } 86 | ); 87 | -------------------------------------------------------------------------------- /services/decoder/protocols/4337-entry-point/4337-entry-point.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("4337-entry-point", () => { 6 | test("matic-mainnet:UserOperationEvent", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "matic-mainnet", 12 | tx_hash: 13 | "0x8070ea41ed0dcb4f52a6033c0357b2700d689412a2f32effed839df240f37175", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find( 17 | ({ name }) => name === "User Operation Event" 18 | ); 19 | if (!event) { 20 | throw Error("Event not found"); 21 | } 22 | expect(event.details?.length).toEqual(5); 23 | }); 24 | 25 | test("avalanche-mainnet:UserOperationEvent", async () => { 26 | const res = await request(app) 27 | .post("/api/v1/tx/decode") 28 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 29 | .send({ 30 | chain_name: "avalanche-mainnet", 31 | tx_hash: 32 | "0xc244be4710c3ad34e120c596555ce75c40356c3d9de9b141a8d5ce0ed056e0d2", 33 | }); 34 | const { events } = res.body as { events: EventType[] }; 35 | const event = events.find( 36 | ({ name }) => name === "User Operation Event" 37 | ); 38 | if (!event) { 39 | throw Error("Event not found"); 40 | } 41 | expect(event.details?.length).toEqual(5); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /services/decoder/protocols/aave-v3/aave-v3.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2", 6 | is_factory: false, 7 | protocol_name: "aave-v3", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x794a61358d6845594f94dc1db02a252b5b4814ad", 12 | is_factory: false, 13 | protocol_name: "aave-v3", 14 | chain_name: "avalanche-mainnet", 15 | }, 16 | { 17 | address: "0x794a61358d6845594f94dc1db02a252b5b4814ad", 18 | is_factory: false, 19 | protocol_name: "aave-v3", 20 | chain_name: "arbitrum-mainnet", 21 | }, 22 | { 23 | address: "0x794a61358d6845594f94dc1db02a252b5b4814ad", 24 | is_factory: false, 25 | protocol_name: "aave-v3", 26 | chain_name: "optimism-mainnet", 27 | }, 28 | { 29 | address: "0x794a61358d6845594f94dc1db02a252b5b4814ad", 30 | is_factory: false, 31 | protocol_name: "aave-v3", 32 | chain_name: "matic-mainnet", 33 | }, 34 | { 35 | address: "0x90df02551bb792286e8d4f13e0e357b4bf1d6a57", 36 | is_factory: false, 37 | protocol_name: "aave-v3", 38 | chain_name: "metis-mainnet", 39 | }, 40 | { 41 | address: "0xa238dd80c259a72e81d7e4664a9801593f98d1c5", 42 | is_factory: false, 43 | protocol_name: "aave-v3", 44 | chain_name: "base-mainnet", 45 | }, 46 | { 47 | address: "0x6807dc923806fe8fd134338eabca509979a7e0cb", 48 | is_factory: false, 49 | protocol_name: "aave-v3", 50 | chain_name: "bsc-mainnet", 51 | }, 52 | ]; 53 | 54 | export default configs; 55 | -------------------------------------------------------------------------------- /services/decoder/protocols/blur/blur.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x000000000000ad05ccc4f10045630fb830b95127", 6 | is_factory: false, 7 | protocol_name: "blur", 8 | chain_name: "eth-mainnet", 9 | }, 10 | ]; 11 | 12 | export default configs; 13 | -------------------------------------------------------------------------------- /services/decoder/protocols/blur/blur.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { 3 | type EventDetails, 4 | type EventNFTs, 5 | type EventTokens, 6 | type EventType, 7 | } from "../../decoder.types"; 8 | import { 9 | DECODED_ACTION, 10 | DECODED_EVENT_CATEGORY, 11 | } from "../../decoder.constants"; 12 | import { decodeEventLog, type Abi } from "viem"; 13 | import ABI from "./abis/blur.BlurExchange.abi.json"; 14 | import { timestampParser } from "../../../../utils/functions"; 15 | import { prettifyCurrency } from "@covalenthq/client-sdk"; 16 | 17 | GoldRushDecoder.on( 18 | "blur:OrdersMatched", 19 | ["eth-mainnet"], 20 | ABI as Abi, 21 | async ( 22 | log_event, 23 | tx, 24 | chain_name, 25 | covalent_client, 26 | options 27 | ): Promise => { 28 | const { block_signed_at, raw_log_data, raw_log_topics } = log_event; 29 | 30 | enum SIDE { 31 | "BUY" = 0, 32 | "SELL" = 1, 33 | } 34 | 35 | const { args: decoded } = decodeEventLog({ 36 | abi: ABI, 37 | topics: raw_log_topics as [], 38 | data: raw_log_data as `0x${string}`, 39 | eventName: "OrdersMatched", 40 | }) as { 41 | eventName: "OrdersMatched"; 42 | args: { 43 | maker: string; 44 | taker: string; 45 | sell: { 46 | trader: string; 47 | side: SIDE; 48 | matchingPolicy: string; 49 | collection: string; 50 | tokenId: bigint; 51 | amount: bigint; 52 | paymentToken: string; 53 | price: bigint; 54 | listingTime: bigint; 55 | expirationTime: bigint; 56 | fees: { 57 | rate: number; 58 | recipient: string; 59 | }[]; 60 | salt: bigint; 61 | extraParams: string; 62 | }; 63 | sellHash: string; 64 | buy: { 65 | trader: string; 66 | side: SIDE; 67 | matchingPolicy: string; 68 | collection: string; 69 | tokenId: bigint; 70 | amount: bigint; 71 | paymentToken: string; 72 | price: bigint; 73 | listingTime: bigint; 74 | expirationTime: bigint; 75 | fees: { 76 | rate: number; 77 | recipient: string; 78 | }[]; 79 | salt: bigint; 80 | extraParams: string; 81 | }; 82 | buyHash: string; 83 | }; 84 | }; 85 | 86 | const tokens: EventTokens = []; 87 | const nfts: EventNFTs = []; 88 | const details: EventDetails = [ 89 | { 90 | heading: "Maker", 91 | value: decoded.maker, 92 | type: "address", 93 | }, 94 | { 95 | heading: "Taker", 96 | value: decoded.taker, 97 | type: "address", 98 | }, 99 | { 100 | heading: "Sell Hash", 101 | value: decoded.sellHash, 102 | type: "address", 103 | }, 104 | { 105 | heading: "Buy Hash", 106 | value: decoded.buyHash, 107 | type: "address", 108 | }, 109 | { 110 | heading: "Sell Fees Rate", 111 | value: decoded.sell.fees[0].rate.toLocaleString(), 112 | type: "text", 113 | }, 114 | { 115 | heading: "Sell Fees Recipient", 116 | value: decoded.sell.fees[0].recipient, 117 | type: "address", 118 | }, 119 | { 120 | heading: "Expiration Time", 121 | value: timestampParser( 122 | new Date(Number(decoded.sell.expirationTime) * 1000), 123 | "descriptive" 124 | ), 125 | type: "text", 126 | }, 127 | { 128 | heading: "Listing Time", 129 | value: timestampParser( 130 | new Date(Number(decoded.sell.listingTime) * 1000), 131 | "descriptive" 132 | ), 133 | type: "text", 134 | }, 135 | { 136 | heading: "Extra Params", 137 | value: decoded.sell.extraParams, 138 | type: "text", 139 | }, 140 | { 141 | heading: "Matching Policy", 142 | value: decoded.sell.matchingPolicy, 143 | type: "address", 144 | }, 145 | { 146 | heading: "Sell Salt", 147 | value: decoded.sell.salt.toLocaleString(), 148 | type: "text", 149 | }, 150 | { 151 | heading: "Buy Salt", 152 | value: decoded.buy.salt.toLocaleString(), 153 | type: "text", 154 | }, 155 | ]; 156 | 157 | const date = timestampParser(block_signed_at, "YYYY-MM-DD"); 158 | const { data: tokenPriceData } = 159 | await covalent_client.PricingService.getTokenPrices( 160 | chain_name, 161 | "USD", 162 | decoded.sell.collection, 163 | { 164 | from: date, 165 | to: date, 166 | } 167 | ); 168 | tokens.push({ 169 | heading: `Match Amount`, 170 | value: decoded.sell.amount.toString(), 171 | decimals: tokenPriceData?.[0]?.contract_decimals ?? 18, 172 | pretty_quote: prettifyCurrency( 173 | tokenPriceData?.[0]?.items?.[0]?.price * 174 | (Number(decoded.sell.amount) / 175 | Math.pow( 176 | 10, 177 | tokenPriceData?.[0]?.items?.[0]?.contract_metadata 178 | ?.contract_decimals ?? 18 179 | )) 180 | ), 181 | ticker_symbol: tokenPriceData?.[0]?.contract_ticker_symbol, 182 | ticker_logo: tokenPriceData?.[0]?.logo_urls?.token_logo_url, 183 | }); 184 | 185 | const { data } = 186 | await covalent_client.NftService.getNftMetadataForGivenTokenIdForContract( 187 | chain_name, 188 | decoded.sell.collection, 189 | decoded.sell.tokenId.toString(), 190 | { 191 | withUncached: true, 192 | } 193 | ); 194 | nfts.push({ 195 | heading: `Matched to ${decoded.buy.trader}`, 196 | collection_address: data?.items?.[0]?.contract_address, 197 | collection_name: 198 | data?.items?.[0]?.nft_data?.external_data?.name || null, 199 | token_identifier: 200 | data?.items?.[0]?.nft_data?.token_id?.toString() || null, 201 | images: { 202 | "1024": 203 | data?.items?.[0]?.nft_data?.external_data?.image_1024 || 204 | null, 205 | "512": 206 | data?.items?.[0]?.nft_data?.external_data?.image_512 || 207 | null, 208 | "256": 209 | data?.items?.[0]?.nft_data?.external_data?.image_256 || 210 | null, 211 | default: 212 | data?.items?.[0]?.nft_data?.external_data?.image || null, 213 | }, 214 | }); 215 | 216 | return { 217 | action: DECODED_ACTION.SWAPPED, 218 | category: DECODED_EVENT_CATEGORY.DEX, 219 | name: "Orders Matched", 220 | protocol: { 221 | logo: log_event.sender_logo_url as string, 222 | name: log_event.sender_name as string, 223 | }, 224 | ...(options.raw_logs ? { raw_log: log_event } : {}), 225 | details: details, 226 | nfts: nfts, 227 | tokens: tokens, 228 | }; 229 | } 230 | ); 231 | -------------------------------------------------------------------------------- /services/decoder/protocols/blur/blur.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("blur", () => { 6 | test("eth-mainnet:OrdersMatched", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xb7664c23d72d66ae56d7c51fee4b04968d33af513e1c2d52f1b6fc583374d0cb", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Orders Matched"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event?.details?.length).toEqual(12); 21 | expect(event?.tokens?.length).toEqual(1); 22 | expect(event?.nfts?.length).toEqual(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /services/decoder/protocols/connext/connext.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x8898b472c54c31894e3b9bb83cea802a5d0e63c6", 6 | is_factory: false, 7 | protocol_name: "connext", 8 | chain_name: "eth-mainnet", 9 | }, 10 | ]; 11 | 12 | export default configs; 13 | -------------------------------------------------------------------------------- /services/decoder/protocols/connext/connext.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("connext", () => { 6 | test("eth-mainnet:RouterLiquidityAdded", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xe22220e29611e9c78d0e778cb2acd473e7d7fb073778dd868e2c368598ebc579", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find( 17 | ({ name }) => name === "RouterLiquidityAdded" 18 | ); 19 | if (!event) { 20 | throw Error("Event not found"); 21 | } 22 | expect(event.details?.length).toEqual(5); 23 | }); 24 | }); 25 | 26 | describe("connext", () => { 27 | test("eth-mainnet:TransferRelayerFeesIncreased", async () => { 28 | const res = await request(app) 29 | .post("/api/v1/tx/decode") 30 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 31 | .send({ 32 | chain_name: "eth-mainnet", 33 | tx_hash: 34 | "0x3ac23b56813b3268e1a55fc06d815178b572a3d7ee20ab06aab18e8fa7d0d56a", 35 | }); 36 | const { events } = res.body as { events: EventType[] }; 37 | const event = events.find( 38 | ({ name }) => name === "TransferRelayerFeesIncreased" 39 | ); 40 | if (!event) { 41 | throw Error("Event not found"); 42 | } 43 | expect(event.details?.length).toEqual(3); 44 | }); 45 | }); 46 | 47 | describe("connext", () => { 48 | test("eth-mainnet:XCalled", async () => { 49 | const res = await request(app) 50 | .post("/api/v1/tx/decode") 51 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 52 | .send({ 53 | chain_name: "eth-mainnet", 54 | tx_hash: 55 | "0x3ac23b56813b3268e1a55fc06d815178b572a3d7ee20ab06aab18e8fa7d0d56a", 56 | }); 57 | const { events } = res.body as { events: EventType[] }; 58 | const event = events.find(({ name }) => name === "XCalled"); 59 | if (!event) { 60 | throw Error("Event not found"); 61 | } 62 | expect(event.details?.length).toEqual(17); 63 | if (event.tokens) { 64 | expect(event.tokens.length).toEqual(2); 65 | } 66 | }); 67 | }); 68 | 69 | describe("connext", () => { 70 | test("eth-mainnet:ExternalCalldataExecuted", async () => { 71 | const res = await request(app) 72 | .post("/api/v1/tx/decode") 73 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 74 | .send({ 75 | chain_name: "eth-mainnet", 76 | tx_hash: 77 | "0xf56c61fc2e3b8ee038b5bd8d32baa88fd0e1539ee9c7dce919651bcfe11e1c43", 78 | }); 79 | const { events } = res.body as { events: EventType[] }; 80 | const event = events.find( 81 | ({ name }) => name === "ExternalCalldataExecuted" 82 | ); 83 | if (!event) { 84 | throw Error("Event not found"); 85 | } 86 | expect(event.details?.length).toEqual(3); 87 | }); 88 | }); 89 | 90 | describe("connext", () => { 91 | test("eth-mainnet:SlippageUpdated", async () => { 92 | const res = await request(app) 93 | .post("/api/v1/tx/decode") 94 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 95 | .send({ 96 | chain_name: "eth-mainnet", 97 | tx_hash: 98 | "0x24eacc95b4c3610dcac9ca766aedc807605afba77d5ceee69258ccb16438d5a4", 99 | }); 100 | const { events } = res.body as { events: EventType[] }; 101 | const event = events.find(({ name }) => name === "SlippageUpdated"); 102 | if (!event) { 103 | throw Error("Event not found"); 104 | } 105 | expect(event.details?.length).toEqual(2); 106 | }); 107 | }); 108 | 109 | describe("connext", () => { 110 | test("eth-mainnet:RouterLiquidityRemoved", async () => { 111 | const res = await request(app) 112 | .post("/api/v1/tx/decode") 113 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 114 | .send({ 115 | chain_name: "eth-mainnet", 116 | tx_hash: 117 | "0x1d47976272cf1317118ec241160c9efc576a41cb6c9f651dbdb23e7160526a0a", 118 | }); 119 | const { events } = res.body as { events: EventType[] }; 120 | const event = events.find( 121 | ({ name }) => name === "RouterLiquidityRemoved" 122 | ); 123 | if (!event) { 124 | throw Error("Event not found"); 125 | } 126 | expect(event.details?.length).toEqual(6); 127 | }); 128 | }); 129 | 130 | describe("connext", () => { 131 | test("eth-mainnet:RouterRecipientSet", async () => { 132 | const res = await request(app) 133 | .post("/api/v1/tx/decode") 134 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 135 | .send({ 136 | chain_name: "eth-mainnet", 137 | tx_hash: 138 | "0x91c3161728f270969a5aaffe33046d357826c6f067a00004b5eb579a4d4e9183", 139 | }); 140 | const { events } = res.body as { events: EventType[] }; 141 | const event = events.find(({ name }) => name === "RouterRecipientSet"); 142 | if (!event) { 143 | throw Error("Event not found"); 144 | } 145 | expect(event.details?.length).toEqual(3); 146 | }); 147 | }); 148 | 149 | describe("connext", () => { 150 | test("eth-mainnet:RouterOwnerAccepted", async () => { 151 | const res = await request(app) 152 | .post("/api/v1/tx/decode") 153 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 154 | .send({ 155 | chain_name: "eth-mainnet", 156 | tx_hash: 157 | "0x91c3161728f270969a5aaffe33046d357826c6f067a00004b5eb579a4d4e9183", 158 | }); 159 | const { events } = res.body as { events: EventType[] }; 160 | const event = events.find(({ name }) => name === "RouterOwnerAccepted"); 161 | if (!event) { 162 | throw Error("Event not found"); 163 | } 164 | expect(event.details?.length).toEqual(3); 165 | }); 166 | }); 167 | 168 | describe("connext", () => { 169 | test("eth-mainnet:RouterInitialized", async () => { 170 | const res = await request(app) 171 | .post("/api/v1/tx/decode") 172 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 173 | .send({ 174 | chain_name: "eth-mainnet", 175 | tx_hash: 176 | "0x91c3161728f270969a5aaffe33046d357826c6f067a00004b5eb579a4d4e9183", 177 | }); 178 | const { events } = res.body as { events: EventType[] }; 179 | const event = events.find(({ name }) => name === "RouterInitialized"); 180 | if (!event) { 181 | throw Error("Event not found"); 182 | } 183 | expect(event.details?.length).toEqual(1); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /services/decoder/protocols/covalent-network/covalent-network.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | protocol_name: "covalent-network", 6 | address: "0x4f2e285227d43d9eb52799d0a28299540452446e", 7 | is_factory: false, 8 | chain_name: "moonbeam-mainnet", 9 | }, 10 | { 11 | protocol_name: "covalent-network", 12 | address: "0x7487b04899c2572a223a8c6ec9ba919e27bbcd36", 13 | is_factory: false, 14 | chain_name: "moonbeam-mainnet", 15 | }, 16 | { 17 | protocol_name: "covalent-network", 18 | address: "0x8ebba081291b908096d19f6614df041c95fc4469", 19 | is_factory: false, 20 | chain_name: "moonbeam-mainnet", 21 | }, 22 | { 23 | protocol_name: "covalent-network", 24 | address: "0xfe97b0C517a84F98fc6eDe3CD26B43012d31992a", 25 | is_factory: false, 26 | chain_name: "eth-mainnet", 27 | }, 28 | ]; 29 | 30 | export default configs; 31 | -------------------------------------------------------------------------------- /services/decoder/protocols/covalent-network/covalent-network.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("covalent-network", () => { 6 | test("moonbeam-mainnet:BlockSpecimenProductionProofSubmitted", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ 10 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 11 | }) 12 | .send({ 13 | chain_name: "moonbeam-mainnet", 14 | tx_hash: 15 | "0x62ca953422e00605ce8561a4cee863e063a892ba69b578875747c4d54f6e353e", 16 | }); 17 | const { events } = res.body as { events: EventType[] }; 18 | const event = events.find( 19 | ({ name }) => name === "Block Specimen Production Proof Submitted" 20 | ); 21 | if (!event) { 22 | throw Error("Event not found"); 23 | } 24 | expect(event.details?.length).toEqual(3); 25 | }); 26 | }); 27 | 28 | describe("covalent-network", () => { 29 | test("moonbeam-mainnet:Unstaked", async () => { 30 | const res = await request(app) 31 | .post("/api/v1/tx/decode") 32 | .set({ 33 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 34 | }) 35 | .send({ 36 | chain_name: "moonbeam-mainnet", 37 | tx_hash: 38 | "0x0f165e83c3f353ee14bb78c01f88a0620aa7b4c9de24bb69ba1653ad90fda93d", 39 | }); 40 | const { events } = res.body as { events: EventType[] }; 41 | const event = events.find(({ name }) => name === "Unstaked"); 42 | if (!event) { 43 | throw Error("Event not found"); 44 | } 45 | expect(event.details?.length).toEqual(3); 46 | expect(event.tokens?.length).toEqual(1); 47 | }); 48 | }); 49 | 50 | describe("covalent-network", () => { 51 | test("moonbeam-mainnet:RewardRedeemed", async () => { 52 | const res = await request(app) 53 | .post("/api/v1/tx/decode") 54 | .set({ 55 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 56 | }) 57 | .send({ 58 | chain_name: "moonbeam-mainnet", 59 | tx_hash: 60 | "0x90a20c7f7d1bfbda38c1532131232ed056cb9ef49ccba8b28498974dd837eed1", 61 | }); 62 | const { events } = res.body as { events: EventType[] }; 63 | const event = events.find(({ name }) => name === "Reward Redeemed"); 64 | if (!event) { 65 | throw Error("Event not found"); 66 | } 67 | expect(event.details?.length).toEqual(2); 68 | expect(event.tokens?.length).toEqual(1); 69 | }); 70 | }); 71 | 72 | describe("covalent-network", () => { 73 | test("moonbeam-mainnet:CommissionRewardRedeemed", async () => { 74 | const res = await request(app) 75 | .post("/api/v1/tx/decode") 76 | .set({ 77 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 78 | }) 79 | .send({ 80 | chain_name: "moonbeam-mainnet", 81 | tx_hash: 82 | "0x9d0d9ddc5e06aafcd8c1ab6e8b5dc0cb81d4e62a247b2d3e364bcf650ad61594", 83 | }); 84 | const { events } = res.body as { events: EventType[] }; 85 | const event = events.find( 86 | ({ name }) => name === "Commission Reward Redeemed" 87 | ); 88 | if (!event) { 89 | throw Error("Event not found"); 90 | } 91 | expect(event.details?.length).toEqual(2); 92 | expect(event.tokens?.length).toEqual(1); 93 | }); 94 | }); 95 | 96 | describe("covalent-network", () => { 97 | test("moonbeam-mainnet:BlockSpecimenProductionProofSubmitted", async () => { 98 | const res = await request(app) 99 | .post("/api/v1/tx/decode") 100 | .set({ 101 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 102 | }) 103 | .send({ 104 | chain_name: "moonbeam-mainnet", 105 | tx_hash: 106 | "0x2c601a9a34f906769b241567e6c778920d14c8884740dea31a75de58c22cf663", 107 | }); 108 | const { events } = res.body as { events: EventType[] }; 109 | const event = events.find( 110 | ({ name }) => name === "Block Specimen Production Proof Submitted" 111 | ); 112 | if (!event) { 113 | throw Error("Event not found"); 114 | } 115 | expect(event.details?.length).toEqual(2); 116 | }); 117 | }); 118 | 119 | describe("covalent-network", () => { 120 | test("eth-mainnet:Staked", async () => { 121 | const res = await request(app) 122 | .post("/api/v1/tx/decode") 123 | .set({ 124 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 125 | }) 126 | .send({ 127 | chain_name: "eth-mainnet", 128 | tx_hash: 129 | "0xd8f4de64159857c609f6ab92a5d6914b149fcd02b49e31839c6cea5d78c1667d", 130 | }); 131 | const { events } = res.body as { events: EventType[] }; 132 | const event = events.find(({ name }) => name === "Staked"); 133 | if (!event) { 134 | throw Error("Event not found"); 135 | } 136 | expect(event.details?.length).toEqual(2); 137 | expect(event.tokens?.length).toEqual(1); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /services/decoder/protocols/defi-kingdoms/defi-kingdoms.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x1990f87d6bc9d9385917e3eda0a7674411c3cd7f", 6 | is_factory: false, 7 | protocol_name: "defi-kingdoms", 8 | chain_name: "defi-kingdoms-mainnet", 9 | }, 10 | { 11 | address: "0xc390faa4c7f66e4d62e59c231d5bed32ff77bef0", 12 | is_factory: false, 13 | protocol_name: "defi-kingdoms", 14 | chain_name: "defi-kingdoms-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/defi-kingdoms/defi-kingdoms.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("defi-kingdoms", () => { 6 | test("defi-kingdoms-mainnet:Pet Fed", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "defi-kingdoms-mainnet", 12 | tx_hash: 13 | "0x038746ca8d77f7df46d669da7108fb618afc927cbcb64615846e91286e2327a8", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Pet Fed"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | if (event.nfts) { 21 | expect(event.nfts?.length).toBeGreaterThan(0); 22 | } 23 | expect(event?.details?.length).toEqual(3); 24 | }); 25 | 26 | test("defi-kingdoms-mainnet:Auction Created", async () => { 27 | const res = await request(app) 28 | .post("/api/v1/tx/decode") 29 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 30 | .send({ 31 | chain_name: "defi-kingdoms-mainnet", 32 | tx_hash: 33 | "0xa716c7e699fb26703b74c5cdae4a1d4930ef31353594185a9e7b40d881c56a57", 34 | }); 35 | const { events } = res.body as { events: EventType[] }; 36 | const event = events.find(({ name }) => name === "Auction Created"); 37 | if (!event) { 38 | throw Error("Event not found"); 39 | } 40 | if (event.nfts) { 41 | expect(event.nfts?.length).toBeGreaterThan(0); 42 | } 43 | if (event.tokens) { 44 | expect(event.tokens?.length).toBeGreaterThan(0); 45 | } 46 | expect(event?.details?.length).toEqual(5); 47 | }); 48 | 49 | test("defi-kingdoms-mainnet:Auction Cancelled", async () => { 50 | const res = await request(app) 51 | .post("/api/v1/tx/decode") 52 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 53 | .send({ 54 | chain_name: "defi-kingdoms-mainnet", 55 | tx_hash: 56 | "0x8a162e78fa5da11670555233052b31366c9784e78e18d849fe1edac06ad46c2c", 57 | }); 58 | const { events } = res.body as { events: EventType[] }; 59 | const event = events.find(({ name }) => name === "Auction Cancelled"); 60 | if (!event) { 61 | throw Error("Event not found"); 62 | } 63 | if (event.nfts) { 64 | expect(event.nfts?.length).toBeGreaterThan(0); 65 | } 66 | expect(event?.details?.length).toEqual(2); 67 | }); 68 | 69 | test("defi-kingdoms-mainnet:Auction Successful", async () => { 70 | const res = await request(app) 71 | .post("/api/v1/tx/decode") 72 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 73 | .send({ 74 | chain_name: "defi-kingdoms-mainnet", 75 | tx_hash: 76 | "0xe313388c1d1f6e190abc93c30c48698d671da83ca0898646e3573a0c90d892db", 77 | }); 78 | const { events } = res.body as { events: EventType[] }; 79 | const event = events.find(({ name }) => name === "Auction Successful"); 80 | if (!event) { 81 | throw Error("Event not found"); 82 | } 83 | if (event.nfts) { 84 | expect(event.nfts?.length).toBeGreaterThan(0); 85 | } 86 | if (event.tokens) { 87 | expect(event.tokens?.length).toBeGreaterThan(0); 88 | } 89 | expect(event?.details?.length).toEqual(3); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /services/decoder/protocols/lido/lido.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", 6 | is_factory: false, 7 | protocol_name: "lido", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x889edc2edab5f40e902b864ad4d7ade8e412f9b1", 12 | is_factory: false, 13 | protocol_name: "lido", 14 | chain_name: "eth-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/lido/lido.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("lido", () => { 6 | test("eth-mainnet:Transfer Shares", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xe9c8e3d7629f0305d75f6bd3171cde37baa8f4d38b1c1f1bb5e158420e4bb144", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Transfer Shares"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.details?.length).toEqual(3); 21 | }); 22 | 23 | test("eth-mainnet:Submitted", async () => { 24 | const res = await request(app) 25 | .post("/api/v1/tx/decode") 26 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 27 | .send({ 28 | chain_name: "eth-mainnet", 29 | tx_hash: 30 | "0x7b820fc2409217a65ad6b7a4a06eeb412470b94821c07f7ddd4b75c8cbc172c4", 31 | }); 32 | const { events } = res.body as { events: EventType[] }; 33 | const event = events.find(({ name }) => name === "Submitted"); 34 | if (!event) { 35 | throw Error("Event not found"); 36 | } 37 | expect(event.details?.length).toEqual(2); 38 | expect(event.tokens?.length).toEqual(1); 39 | }); 40 | 41 | test("eth-mainnet:Token Rebased", async () => { 42 | const res = await request(app) 43 | .post("/api/v1/tx/decode") 44 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 45 | .send({ 46 | chain_name: "eth-mainnet", 47 | tx_hash: 48 | "0x406f1e0feb4259f667ef0ae2270933b2249fe8bd35f1eec36b8f15e987ee6e32", 49 | }); 50 | const { events } = res.body as { events: EventType[] }; 51 | const event = events.find(({ name }) => name === "Token Rebased"); 52 | if (!event) { 53 | throw Error("Event not found"); 54 | } 55 | expect(event.details?.length).toEqual(5); 56 | expect(event.tokens?.length).toEqual(2); 57 | }); 58 | 59 | test("eth-mainnet:Shares Burnt", async () => { 60 | const res = await request(app) 61 | .post("/api/v1/tx/decode") 62 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 63 | .send({ 64 | chain_name: "eth-mainnet", 65 | tx_hash: 66 | "0x8235c55ee3e43973710fbad71934c29e1b635e346e22a3923f3760d2c8d0b759", 67 | }); 68 | const { events } = res.body as { events: EventType[] }; 69 | const event = events.find(({ name }) => name === "Shares Burnt"); 70 | if (!event) { 71 | throw Error("Event not found"); 72 | } 73 | expect(event.details?.length).toEqual(2); 74 | expect(event.tokens?.length).toEqual(2); 75 | }); 76 | 77 | test("eth-mainnet:ETH Distributed", async () => { 78 | const res = await request(app) 79 | .post("/api/v1/tx/decode") 80 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 81 | .send({ 82 | chain_name: "eth-mainnet", 83 | tx_hash: 84 | "0x588d3ae5e31dd66788a4e9ac7f677a02c2e7b691dce014a7a6702153ef440e49", 85 | }); 86 | const { events } = res.body as { events: EventType[] }; 87 | const event = events.find(({ name }) => name === "ETH Distributed"); 88 | if (!event) { 89 | throw Error("Event not found"); 90 | } 91 | expect(event.details?.length).toEqual(1); 92 | expect(event.tokens?.length).toEqual(5); 93 | }); 94 | 95 | test("eth-mainnet:Withdrawals Received", async () => { 96 | const res = await request(app) 97 | .post("/api/v1/tx/decode") 98 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 99 | .send({ 100 | chain_name: "eth-mainnet", 101 | tx_hash: 102 | "0x71eb6c994ba206d35e9625c0139a94e85665fb2ac7a5ac3d2c346bc9f5a9de40", 103 | }); 104 | const { events } = res.body as { events: EventType[] }; 105 | const event = events.find( 106 | ({ name }) => name === "Withdrawals Received" 107 | ); 108 | if (!event) { 109 | throw Error("Event not found"); 110 | } 111 | expect(event.tokens?.length).toEqual(1); 112 | }); 113 | 114 | test("eth-mainnet:EL Rewards Received", async () => { 115 | const res = await request(app) 116 | .post("/api/v1/tx/decode") 117 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 118 | .send({ 119 | chain_name: "eth-mainnet", 120 | tx_hash: 121 | "0x9ca6a5bda1d982c871c74c6603c7c96d1559140bd69a64b692b59b2fc1a91293", 122 | }); 123 | const { events } = res.body as { events: EventType[] }; 124 | const event = events.find(({ name }) => name === "EL Rewards Received"); 125 | if (!event) { 126 | throw Error("Event not found"); 127 | } 128 | expect(event.tokens?.length).toEqual(1); 129 | }); 130 | 131 | test("eth-mainnet:CL Validators Updated", async () => { 132 | const res = await request(app) 133 | .post("/api/v1/tx/decode") 134 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 135 | .send({ 136 | chain_name: "eth-mainnet", 137 | tx_hash: 138 | "0xfcf4097cc6b61801e5c9f3a11f16e1d7f7fa76921d182e8b0c4ff88c1a6b5b3f", 139 | }); 140 | const { events } = res.body as { events: EventType[] }; 141 | const event = events.find( 142 | ({ name }) => name === "CL Validators Updated" 143 | ); 144 | if (!event) { 145 | throw Error("Event not found"); 146 | } 147 | expect(event.details?.length).toEqual(2); 148 | }); 149 | 150 | test("eth-mainnet:Withdrawal Requested", async () => { 151 | const res = await request(app) 152 | .post("/api/v1/tx/decode") 153 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 154 | .send({ 155 | chain_name: "eth-mainnet", 156 | tx_hash: 157 | "0x1cdcd47924e59bab5026da93fa5f5f7dd96718fee5e9e2924a4ba39e23da3a0a", 158 | }); 159 | const { events } = res.body as { events: EventType[] }; 160 | const event = events.find( 161 | ({ name }) => name === "Withdrawal Requested" 162 | ); 163 | if (!event) { 164 | throw Error("Event not found"); 165 | } 166 | expect(event.details?.length).toEqual(4); 167 | expect(event.tokens?.length).toEqual(1); 168 | }); 169 | 170 | test("eth-mainnet:Withdrawal Claimed", async () => { 171 | const res = await request(app) 172 | .post("/api/v1/tx/decode") 173 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 174 | .send({ 175 | chain_name: "eth-mainnet", 176 | tx_hash: 177 | "0xfadc5ea23b04adf5a55f4933a464ed2deb03b9cbf42257831e3e5c93203123d7", 178 | }); 179 | const { events } = res.body as { events: EventType[] }; 180 | const event = events.find(({ name }) => name === "Withdrawal Claimed"); 181 | if (!event) { 182 | throw Error("Event not found"); 183 | } 184 | expect(event.details?.length).toEqual(3); 185 | expect(event.tokens?.length).toEqual(1); 186 | }); 187 | 188 | test("eth-mainnet:Batch Metadata Update", async () => { 189 | const res = await request(app) 190 | .post("/api/v1/tx/decode") 191 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 192 | .send({ 193 | chain_name: "eth-mainnet", 194 | tx_hash: 195 | "0x54e8178be9dad3ae0a7ec4e52bdd231b86d5a0a8b76e07d76b75f0a1144388c7", 196 | }); 197 | const { events } = res.body as { events: EventType[] }; 198 | const event = events.find( 199 | ({ name }) => name === "Batch Metadata Update" 200 | ); 201 | if (!event) { 202 | throw Error("Event not found"); 203 | } 204 | expect(event.details?.length).toEqual(2); 205 | }); 206 | 207 | test("eth-mainnet:Withdrawals Finalized", async () => { 208 | const res = await request(app) 209 | .post("/api/v1/tx/decode") 210 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 211 | .send({ 212 | chain_name: "eth-mainnet", 213 | tx_hash: 214 | "0xaaa7a76022040bfc7d93c52db52f58d6187906c0b71bb9b0a002e8860ce9a465", 215 | }); 216 | const { events } = res.body as { events: EventType[] }; 217 | const event = events.find( 218 | ({ name }) => name === "Withdrawals Finalized" 219 | ); 220 | if (!event) { 221 | throw Error("Event not found"); 222 | } 223 | expect(event.details?.length).toEqual(4); 224 | expect(event.tokens?.length).toEqual(1); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /services/decoder/protocols/opensea/opensea.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | protocol_name: "opensea", 6 | address: "0x00000000006c3852cbef3e08e8df289169ede581", 7 | is_factory: false, 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | protocol_name: "opensea", 12 | address: "0x00000000006c3852cbef3e08e8df289169ede581", 13 | is_factory: false, 14 | chain_name: "matic-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/opensea/opensea.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { 3 | type EventDetails, 4 | type EventNFTs, 5 | type EventTokens, 6 | type EventType, 7 | } from "../../decoder.types"; 8 | import { 9 | DECODED_ACTION, 10 | DECODED_EVENT_CATEGORY, 11 | } from "../../decoder.constants"; 12 | import { decodeEventLog, type Abi } from "viem"; 13 | import Seaport from "./abis/seaport-1.1.abi.json"; 14 | import { timestampParser } from "../../../../utils/functions"; 15 | import { prettifyCurrency } from "@covalenthq/client-sdk"; 16 | 17 | GoldRushDecoder.on( 18 | "opensea:OrderFulfilled", 19 | ["eth-mainnet", "matic-mainnet"], 20 | Seaport as Abi, 21 | async ( 22 | log_event, 23 | tx, 24 | chain_name, 25 | covalent_client, 26 | options 27 | ): Promise => { 28 | const { block_signed_at, raw_log_data, raw_log_topics } = log_event; 29 | 30 | enum ITEM_TYPE { 31 | "NATIVE" = 0, 32 | "ERC20" = 1, 33 | "ERC721" = 2, 34 | "ERC1155" = 3, 35 | "ERC721_WITH_CRITERIA" = 4, 36 | "ERC1155_WITH_CRITERIA" = 5, 37 | } 38 | 39 | const { args: decoded } = decodeEventLog({ 40 | abi: Seaport, 41 | topics: raw_log_topics as [], 42 | data: raw_log_data as `0x${string}`, 43 | eventName: "OrderFulfilled", 44 | }) as { 45 | eventName: "OrderFulfilled"; 46 | args: { 47 | offerer: string; 48 | zone: string; 49 | orderHash: string; 50 | recipient: string; 51 | offer: { 52 | itemType: ITEM_TYPE; 53 | token: string; 54 | identifier: bigint; 55 | amount: bigint; 56 | }[]; 57 | consideration: { 58 | itemType: ITEM_TYPE; 59 | token: string; 60 | identifier: bigint; 61 | amount: bigint; 62 | recipient: string; 63 | }[]; 64 | }; 65 | }; 66 | 67 | const tokens: EventTokens = []; 68 | const nfts: EventNFTs = []; 69 | const details: EventDetails = [ 70 | { 71 | heading: "Offerer", 72 | value: decoded.offerer, 73 | type: "address", 74 | }, 75 | { 76 | heading: "Recipient", 77 | value: decoded.recipient, 78 | type: "address", 79 | }, 80 | { 81 | heading: "Order Hash", 82 | value: decoded.orderHash, 83 | type: "address", 84 | }, 85 | ]; 86 | 87 | const parseItem = async ( 88 | itemType: ITEM_TYPE, 89 | token: string, 90 | identifier: bigint, 91 | amount: bigint, 92 | recipient?: string 93 | ) => { 94 | switch (itemType) { 95 | case ITEM_TYPE.NATIVE: 96 | case ITEM_TYPE.ERC20: { 97 | const date = timestampParser(block_signed_at, "YYYY-MM-DD"); 98 | const { data } = 99 | await covalent_client.PricingService.getTokenPrices( 100 | chain_name, 101 | "USD", 102 | token, 103 | { 104 | from: date, 105 | to: date, 106 | } 107 | ); 108 | tokens.push({ 109 | heading: recipient 110 | ? `Sent to ${recipient}` 111 | : `Offered to ${decoded.recipient}`, 112 | value: amount.toString(), 113 | decimals: data?.[0]?.contract_decimals ?? 18, 114 | pretty_quote: prettifyCurrency( 115 | data?.[0]?.items?.[0]?.price * 116 | (Number(amount) / 117 | Math.pow( 118 | 10, 119 | data?.[0]?.items?.[0]?.contract_metadata 120 | ?.contract_decimals ?? 18 121 | )) 122 | ), 123 | ticker_symbol: data?.[0]?.contract_ticker_symbol, 124 | ticker_logo: data?.[0]?.logo_urls?.token_logo_url, 125 | }); 126 | break; 127 | } 128 | case ITEM_TYPE.ERC721: { 129 | const { data } = 130 | await covalent_client.NftService.getNftMetadataForGivenTokenIdForContract( 131 | chain_name, 132 | token, 133 | identifier.toString(), 134 | { 135 | withUncached: true, 136 | } 137 | ); 138 | nfts.push({ 139 | heading: recipient 140 | ? `Sent to ${recipient}` 141 | : `Offered to ${decoded.recipient}`, 142 | collection_address: data?.items?.[0]?.contract_address, 143 | collection_name: 144 | data?.items?.[0]?.nft_data?.external_data?.name || 145 | null, 146 | token_identifier: 147 | data?.items?.[0]?.nft_data?.token_id?.toString() || 148 | null, 149 | images: { 150 | "1024": 151 | data?.items?.[0]?.nft_data?.external_data 152 | ?.image_1024 || null, 153 | "512": 154 | data?.items?.[0]?.nft_data?.external_data 155 | ?.image_512 || null, 156 | "256": 157 | data?.items?.[0]?.nft_data?.external_data 158 | ?.image_256 || null, 159 | default: 160 | data?.items?.[0]?.nft_data?.external_data 161 | ?.image || null, 162 | }, 163 | }); 164 | break; 165 | } 166 | } 167 | }; 168 | 169 | for (const { itemType, token, identifier, amount } of decoded.offer) { 170 | await parseItem(itemType, token, identifier, amount); 171 | } 172 | for (const { 173 | itemType, 174 | token, 175 | identifier, 176 | amount, 177 | recipient, 178 | } of decoded.consideration) { 179 | await parseItem(itemType, token, identifier, amount, recipient); 180 | } 181 | 182 | return { 183 | action: DECODED_ACTION.SALE, 184 | category: DECODED_EVENT_CATEGORY.NFT, 185 | name: "Basic Order Fulfilled", 186 | protocol: { 187 | logo: log_event.sender_logo_url as string, 188 | name: "Opensea", 189 | }, 190 | ...(options.raw_logs ? { raw_log: log_event } : {}), 191 | details: details, 192 | nfts: nfts, 193 | tokens: tokens, 194 | }; 195 | } 196 | ); 197 | -------------------------------------------------------------------------------- /services/decoder/protocols/opensea/opensea.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("opensea", () => { 6 | test("eth-mainnet:OrderFulfilled", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ 10 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 11 | }) 12 | .send({ 13 | chain_name: "eth-mainnet", 14 | tx_hash: 15 | "0x7a038d2f5be4d196a3ff389497f8d61a639e4a32d353758b4f062cafbc5d475c", 16 | }); 17 | const { events } = res.body as { events: EventType[] }; 18 | const event = events.find( 19 | ({ name }) => name === "Basic Order Fulfilled" 20 | ); 21 | if (!event) { 22 | throw Error("Event not found"); 23 | } 24 | if (event.nfts) { 25 | expect(event.nfts?.length).toBeGreaterThan(0); 26 | } 27 | if (event.tokens) { 28 | expect(event.tokens?.length).toBeGreaterThan(0); 29 | } 30 | expect(event.details?.length).toEqual(3); 31 | }); 32 | 33 | test("matic-mainnet:OrderFulfilled", async () => { 34 | const res = await request(app) 35 | .post("/api/v1/tx/decode") 36 | .set({ 37 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 38 | }) 39 | .send({ 40 | chain_name: "matic-mainnet", 41 | tx_hash: 42 | "0xbb0849b132f97174bd1f0c41ef39b4105ddb0e07b8f6730910d56d48dfffa0e8", 43 | }); 44 | const { events } = res.body as { events: EventType[] }; 45 | const event = events.find( 46 | ({ name }) => name === "Basic Order Fulfilled" 47 | ); 48 | if (!event) { 49 | throw Error("Event not found"); 50 | } 51 | if (event.nfts) { 52 | expect(event.nfts?.length).toBeGreaterThan(0); 53 | } 54 | if (event.tokens) { 55 | expect(event.tokens?.length).toBeGreaterThan(0); 56 | } 57 | expect(event.details?.length).toEqual(3); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /services/decoder/protocols/paraswap-v5/paraswap-v5.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0xdef171fe48cf0115b1d80b88dc8eab59176fee57", 6 | is_factory: false, 7 | protocol_name: "paraswap-v5", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0xdef171fe48cf0115b1d80b88dc8eab59176fee57", 12 | is_factory: false, 13 | protocol_name: "paraswap-v5", 14 | chain_name: "matic-mainnet", 15 | }, 16 | { 17 | address: "0xdef171fe48cf0115b1d80b88dc8eab59176fee57", 18 | is_factory: false, 19 | protocol_name: "paraswap-v5", 20 | chain_name: "avalanche-mainnet", 21 | }, 22 | ]; 23 | 24 | export default configs; 25 | -------------------------------------------------------------------------------- /services/decoder/protocols/paraswap-v5/paraswap-v5.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import { 3 | type EventTokens, 4 | type EventDetails, 5 | type EventType, 6 | } from "../../decoder.types"; 7 | import { 8 | DECODED_ACTION, 9 | DECODED_EVENT_CATEGORY, 10 | } from "../../decoder.constants"; 11 | import { decodeEventLog, type Abi } from "viem"; 12 | import SimpleSwapABI from "./abis/paraswap-v5.simple-swap.abi.json"; 13 | import { 14 | calculatePrettyBalance, 15 | prettifyCurrency, 16 | } from "@covalenthq/client-sdk"; 17 | 18 | GoldRushDecoder.on( 19 | "paraswap-v5:SwappedV3", 20 | ["eth-mainnet", "matic-mainnet", "avalanche-mainnet"], 21 | SimpleSwapABI as Abi, 22 | async ( 23 | log_event, 24 | tx, 25 | chain_name, 26 | covalent_client, 27 | options 28 | ): Promise => { 29 | const { raw_log_data, raw_log_topics } = log_event; 30 | 31 | const { args: decoded } = decodeEventLog({ 32 | abi: SimpleSwapABI, 33 | topics: raw_log_topics as [], 34 | data: raw_log_data as `0x${string}`, 35 | eventName: "SwappedV3", 36 | }) as { 37 | eventName: "SwappedV3"; 38 | args: { 39 | beneficiary: string; 40 | srcToken: string; 41 | destToken: string; 42 | uuid: string; 43 | partner: string; 44 | feePercent: bigint; 45 | initiator: string; 46 | srcAmount: bigint; 47 | receivedAmount: bigint; 48 | expectedAmount: bigint; 49 | }; 50 | }; 51 | 52 | const [{ data: srcToken }, { data: destToken }] = await Promise.all( 53 | [decoded.srcToken, decoded.destToken].map((address) => { 54 | return covalent_client.PricingService.getTokenPrices( 55 | chain_name, 56 | "USD", 57 | address 58 | ); 59 | }) 60 | ); 61 | 62 | const details: EventDetails = [ 63 | { 64 | heading: "UUID", 65 | value: decoded.uuid, 66 | type: "address", 67 | }, 68 | { 69 | heading: "Beneficiary", 70 | value: decoded.beneficiary, 71 | type: "address", 72 | }, 73 | { 74 | heading: "Initiator", 75 | value: decoded.initiator, 76 | type: "address", 77 | }, 78 | { 79 | heading: "Expected Amount", 80 | value: calculatePrettyBalance( 81 | decoded.expectedAmount, 82 | destToken?.[0]?.contract_decimals ?? 0 83 | ), 84 | type: "text", 85 | }, 86 | ]; 87 | 88 | const tokens: EventTokens = [ 89 | { 90 | decimals: srcToken?.[0]?.contract_decimals ?? 0, 91 | heading: "Input", 92 | pretty_quote: prettifyCurrency( 93 | srcToken?.[0]?.prices?.[0].price * 94 | (Number(decoded.srcAmount) / 95 | Math.pow(10, srcToken?.[0]?.contract_decimals ?? 0)) 96 | ), 97 | ticker_logo: srcToken?.[0]?.logo_url ?? null, 98 | ticker_symbol: srcToken?.[0]?.contract_ticker_symbol ?? null, 99 | value: decoded.srcAmount.toString(), 100 | }, 101 | { 102 | decimals: destToken?.[0]?.contract_decimals ?? 0, 103 | heading: "Output", 104 | pretty_quote: prettifyCurrency( 105 | destToken?.[0]?.prices?.[0].price * 106 | (Number(decoded.receivedAmount) / 107 | Math.pow( 108 | 10, 109 | destToken?.[0]?.contract_decimals ?? 0 110 | )) 111 | ), 112 | ticker_logo: destToken?.[0]?.logo_url ?? null, 113 | ticker_symbol: destToken?.[0]?.contract_ticker_symbol ?? null, 114 | value: decoded.receivedAmount.toString(), 115 | }, 116 | ]; 117 | 118 | return { 119 | action: DECODED_ACTION.SWAPPED, 120 | category: DECODED_EVENT_CATEGORY.DEX, 121 | name: "Swap V3", 122 | protocol: { 123 | logo: log_event.sender_logo_url as string, 124 | name: "Paraswap V5", 125 | }, 126 | ...(options.raw_logs ? { raw_log: log_event } : {}), 127 | details: details, 128 | tokens: tokens, 129 | }; 130 | } 131 | ); 132 | -------------------------------------------------------------------------------- /services/decoder/protocols/paraswap-v5/paraswap-v5.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("paraswap-v5", () => { 6 | test("eth-mainnet:SwappedV3", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0x7b0e0718e211149bdd480fe372e0cfec2e8c3c2737ace1dc969674843e313258", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Swap V3"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.details?.length).toEqual(4); 21 | expect(event.tokens?.length).toEqual(2); 22 | }); 23 | 24 | test("matic-mainnet:SwappedV3", async () => { 25 | const res = await request(app) 26 | .post("/api/v1/tx/decode") 27 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 28 | .send({ 29 | chain_name: "matic-mainnet", 30 | tx_hash: 31 | "0xbd0f211af42276a79dca5a5bd5a9b27c95eaa8403083171fa2a129c35a74996f", 32 | }); 33 | const { events } = res.body as { events: EventType[] }; 34 | const event = events.find(({ name }) => name === "Swap V3"); 35 | if (!event) { 36 | throw Error("Event not found"); 37 | } 38 | expect(event.details?.length).toEqual(4); 39 | expect(event.tokens?.length).toEqual(2); 40 | }); 41 | 42 | test("avalanche-mainnet:SwappedV3", async () => { 43 | const res = await request(app) 44 | .post("/api/v1/tx/decode") 45 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 46 | .send({ 47 | chain_name: "avalanche-mainnet", 48 | tx_hash: 49 | "0x41525d4a5790d110ec0816397cafeab5d777e8a8c21f07b06a800d5c567d2804", 50 | }); 51 | const { events } = res.body as { events: EventType[] }; 52 | const event = events.find(({ name }) => name === "Swap V3"); 53 | if (!event) { 54 | throw Error("Event not found"); 55 | } 56 | expect(event.details?.length).toEqual(4); 57 | expect(event.tokens?.length).toEqual(2); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /services/decoder/protocols/pendle/pendle.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x00000000005bbb0ef59571e58418f9a4357b68a0", 6 | is_factory: false, 7 | protocol_name: "pendle", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x4f30A9D41B80ecC5B94306AB4364951AE3170210", 12 | is_factory: false, 13 | protocol_name: "pendle", 14 | chain_name: "eth-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/pendle/pendle.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("pendle", () => { 6 | test("eth-mainnet:SwapPtAndToken", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0x3a6536890e00ed665eb39c36aa3073c4211de39cfc8c751ceaaf352c40a56fb0", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "SwapPtAndToken"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | if (event.tokens) { 21 | expect(event.tokens?.length).toEqual(4); 22 | } 23 | expect(event?.details?.length).toEqual(3); 24 | }); 25 | }); 26 | 27 | describe("pendle", () => { 28 | test("eth-mainnet:SwapYtAndSy", async () => { 29 | const res = await request(app) 30 | .post("/api/v1/tx/decode") 31 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 32 | .send({ 33 | chain_name: "eth-mainnet", 34 | tx_hash: 35 | "0x1fa6bd1d4718b540eb1b2dd80edcca9710262ad9960eadb2743c354f1f4aa4aa", 36 | }); 37 | const { events } = res.body as { events: EventType[] }; 38 | const event = events.find(({ name }) => name === "SwapYtAndSy"); 39 | if (!event) { 40 | throw Error("Event not found"); 41 | } 42 | if (event.tokens) { 43 | expect(event.tokens?.length).toEqual(2); 44 | } 45 | expect(event?.details?.length).toEqual(3); 46 | }); 47 | }); 48 | 49 | describe("pendle", () => { 50 | test("eth-mainnet:SwapYtAndToken", async () => { 51 | const res = await request(app) 52 | .post("/api/v1/tx/decode") 53 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 54 | .send({ 55 | chain_name: "eth-mainnet", 56 | tx_hash: 57 | "0x52f579402c1dc41b626a2c71755283266e267cfb4b747e2f7195dd6bde0726fc", 58 | }); 59 | const { events } = res.body as { events: EventType[] }; 60 | const event = events.find(({ name }) => name === "SwapYtAndToken"); 61 | if (!event) { 62 | throw Error("Event not found"); 63 | } 64 | if (event.tokens) { 65 | expect(event.tokens?.length).toEqual(4); 66 | } 67 | expect(event?.details?.length).toEqual(3); 68 | }); 69 | }); 70 | 71 | describe("pendle", () => { 72 | test("eth-mainnet:NewLockPosition", async () => { 73 | const res = await request(app) 74 | .post("/api/v1/tx/decode") 75 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 76 | .send({ 77 | chain_name: "eth-mainnet", 78 | tx_hash: 79 | "0x4056e54c6f6cc1788830be72eacc13edfdd1f2af7c67715b429c50a9d94176e6", 80 | }); 81 | const { events } = res.body as { events: EventType[] }; 82 | const event = events.find(({ name }) => name === "NewLockPosition"); 83 | if (!event) { 84 | throw Error("Event not found"); 85 | } 86 | if (event.tokens) { 87 | expect(event.tokens?.length).toEqual(1); 88 | } 89 | expect(event?.details?.length).toEqual(2); 90 | }); 91 | }); 92 | 93 | describe("pendle", () => { 94 | test("eth-mainnet:BroadcastUserPosition", async () => { 95 | const res = await request(app) 96 | .post("/api/v1/tx/decode") 97 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 98 | .send({ 99 | chain_name: "eth-mainnet", 100 | tx_hash: 101 | "0x4056e54c6f6cc1788830be72eacc13edfdd1f2af7c67715b429c50a9d94176e6", 102 | }); 103 | const { events } = res.body as { events: EventType[] }; 104 | const event = events.find( 105 | ({ name }) => name === "BroadcastUserPosition" 106 | ); 107 | if (!event) { 108 | throw Error("Event not found"); 109 | } 110 | expect(event?.details?.length).toEqual(2); 111 | }); 112 | }); 113 | 114 | describe("pendle", () => { 115 | test("eth-mainnet:BroadcastTotalSupply", async () => { 116 | const res = await request(app) 117 | .post("/api/v1/tx/decode") 118 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 119 | .send({ 120 | chain_name: "eth-mainnet", 121 | tx_hash: 122 | "0x4056e54c6f6cc1788830be72eacc13edfdd1f2af7c67715b429c50a9d94176e6", 123 | }); 124 | const { events } = res.body as { events: EventType[] }; 125 | const event = events.find( 126 | ({ name }) => name === "BroadcastTotalSupply" 127 | ); 128 | if (!event) { 129 | throw Error("Event not found"); 130 | } 131 | expect(event?.details?.length).toEqual(3); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /services/decoder/protocols/renzo/abis/renzo.deposit-queue-abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, 3 | { "inputs": [], "name": "InvalidZeroInput", "type": "error" }, 4 | { "inputs": [], "name": "NotERC20RewardsAdmin", "type": "error" }, 5 | { "inputs": [], "name": "NotNativeEthRestakeAdmin", "type": "error" }, 6 | { "inputs": [], "name": "NotRestakeManager", "type": "error" }, 7 | { "inputs": [], "name": "NotRestakeManagerAdmin", "type": "error" }, 8 | { "inputs": [], "name": "OverMaxBasisPoints", "type": "error" }, 9 | { "inputs": [], "name": "TransferFailed", "type": "error" }, 10 | { 11 | "anonymous": false, 12 | "inputs": [ 13 | { 14 | "indexed": false, 15 | "internalType": "uint256", 16 | "name": "amount", 17 | "type": "uint256" 18 | } 19 | ], 20 | "name": "ETHDepositedFromProtocol", 21 | "type": "event" 22 | }, 23 | { 24 | "anonymous": false, 25 | "inputs": [ 26 | { 27 | "indexed": false, 28 | "internalType": "contract IOperatorDelegator", 29 | "name": "operatorDelegator", 30 | "type": "address" 31 | }, 32 | { 33 | "indexed": false, 34 | "internalType": "bytes", 35 | "name": "pubkey", 36 | "type": "bytes" 37 | }, 38 | { 39 | "indexed": false, 40 | "internalType": "uint256", 41 | "name": "amountStaked", 42 | "type": "uint256" 43 | }, 44 | { 45 | "indexed": false, 46 | "internalType": "uint256", 47 | "name": "amountQueued", 48 | "type": "uint256" 49 | } 50 | ], 51 | "name": "ETHStakedFromQueue", 52 | "type": "event" 53 | }, 54 | { 55 | "anonymous": false, 56 | "inputs": [ 57 | { 58 | "indexed": false, 59 | "internalType": "address", 60 | "name": "feeAddress", 61 | "type": "address" 62 | }, 63 | { 64 | "indexed": false, 65 | "internalType": "uint256", 66 | "name": "feeBasisPoints", 67 | "type": "uint256" 68 | } 69 | ], 70 | "name": "FeeConfigUpdated", 71 | "type": "event" 72 | }, 73 | { 74 | "anonymous": false, 75 | "inputs": [ 76 | { 77 | "indexed": false, 78 | "internalType": "uint8", 79 | "name": "version", 80 | "type": "uint8" 81 | } 82 | ], 83 | "name": "Initialized", 84 | "type": "event" 85 | }, 86 | { 87 | "anonymous": false, 88 | "inputs": [ 89 | { 90 | "indexed": false, 91 | "internalType": "contract IERC20", 92 | "name": "token", 93 | "type": "address" 94 | }, 95 | { 96 | "indexed": false, 97 | "internalType": "uint256", 98 | "name": "amount", 99 | "type": "uint256" 100 | }, 101 | { 102 | "indexed": false, 103 | "internalType": "address", 104 | "name": "destination", 105 | "type": "address" 106 | } 107 | ], 108 | "name": "ProtocolFeesPaid", 109 | "type": "event" 110 | }, 111 | { 112 | "anonymous": false, 113 | "inputs": [ 114 | { 115 | "indexed": false, 116 | "internalType": "contract IRestakeManager", 117 | "name": "restakeManager", 118 | "type": "address" 119 | } 120 | ], 121 | "name": "RestakeManagerUpdated", 122 | "type": "event" 123 | }, 124 | { 125 | "anonymous": false, 126 | "inputs": [ 127 | { 128 | "indexed": false, 129 | "internalType": "contract IERC20", 130 | "name": "token", 131 | "type": "address" 132 | }, 133 | { 134 | "indexed": false, 135 | "internalType": "uint256", 136 | "name": "amount", 137 | "type": "uint256" 138 | } 139 | ], 140 | "name": "RewardsDeposited", 141 | "type": "event" 142 | }, 143 | { 144 | "inputs": [], 145 | "name": "depositETHFromProtocol", 146 | "outputs": [], 147 | "stateMutability": "payable", 148 | "type": "function" 149 | }, 150 | { 151 | "inputs": [], 152 | "name": "feeAddress", 153 | "outputs": [ 154 | { "internalType": "address", "name": "", "type": "address" } 155 | ], 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [], 161 | "name": "feeBasisPoints", 162 | "outputs": [ 163 | { "internalType": "uint256", "name": "", "type": "uint256" } 164 | ], 165 | "stateMutability": "view", 166 | "type": "function" 167 | }, 168 | { 169 | "inputs": [ 170 | { 171 | "internalType": "contract IRoleManager", 172 | "name": "_roleManager", 173 | "type": "address" 174 | } 175 | ], 176 | "name": "initialize", 177 | "outputs": [], 178 | "stateMutability": "nonpayable", 179 | "type": "function" 180 | }, 181 | { 182 | "inputs": [], 183 | "name": "restakeManager", 184 | "outputs": [ 185 | { 186 | "internalType": "contract IRestakeManager", 187 | "name": "", 188 | "type": "address" 189 | } 190 | ], 191 | "stateMutability": "view", 192 | "type": "function" 193 | }, 194 | { 195 | "inputs": [], 196 | "name": "roleManager", 197 | "outputs": [ 198 | { 199 | "internalType": "contract IRoleManager", 200 | "name": "", 201 | "type": "address" 202 | } 203 | ], 204 | "stateMutability": "view", 205 | "type": "function" 206 | }, 207 | { 208 | "inputs": [ 209 | { 210 | "internalType": "address", 211 | "name": "_feeAddress", 212 | "type": "address" 213 | }, 214 | { 215 | "internalType": "uint256", 216 | "name": "_feeBasisPoints", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "setFeeConfig", 221 | "outputs": [], 222 | "stateMutability": "nonpayable", 223 | "type": "function" 224 | }, 225 | { 226 | "inputs": [ 227 | { 228 | "internalType": "contract IRestakeManager", 229 | "name": "_restakeManager", 230 | "type": "address" 231 | } 232 | ], 233 | "name": "setRestakeManager", 234 | "outputs": [], 235 | "stateMutability": "nonpayable", 236 | "type": "function" 237 | }, 238 | { 239 | "inputs": [ 240 | { 241 | "internalType": "contract IOperatorDelegator", 242 | "name": "operatorDelegator", 243 | "type": "address" 244 | }, 245 | { "internalType": "bytes", "name": "pubkey", "type": "bytes" }, 246 | { "internalType": "bytes", "name": "signature", "type": "bytes" }, 247 | { 248 | "internalType": "bytes32", 249 | "name": "depositDataRoot", 250 | "type": "bytes32" 251 | } 252 | ], 253 | "name": "stakeEthFromQueue", 254 | "outputs": [], 255 | "stateMutability": "nonpayable", 256 | "type": "function" 257 | }, 258 | { 259 | "inputs": [ 260 | { 261 | "internalType": "contract IERC20", 262 | "name": "token", 263 | "type": "address" 264 | } 265 | ], 266 | "name": "sweepERC20", 267 | "outputs": [], 268 | "stateMutability": "nonpayable", 269 | "type": "function" 270 | }, 271 | { 272 | "inputs": [ 273 | { "internalType": "address", "name": "", "type": "address" } 274 | ], 275 | "name": "totalEarned", 276 | "outputs": [ 277 | { "internalType": "uint256", "name": "", "type": "uint256" } 278 | ], 279 | "stateMutability": "view", 280 | "type": "function" 281 | }, 282 | { "stateMutability": "payable", "type": "receive" } 283 | ] 284 | -------------------------------------------------------------------------------- /services/decoder/protocols/renzo/renzo.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x858646372cc42e1a627fce94aa7a7033e7cf075a", 6 | is_factory: false, 7 | protocol_name: "renzo", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x74a09653A083691711cF8215a6ab074BB4e99ef5", 12 | is_factory: false, 13 | protocol_name: "renzo", 14 | chain_name: "eth-mainnet", 15 | }, 16 | { 17 | address: "0xf2F305D14DCD8aaef887E0428B3c9534795D0d60", 18 | is_factory: false, 19 | protocol_name: "renzo", 20 | chain_name: "eth-mainnet", 21 | }, 22 | ]; 23 | 24 | export default configs; 25 | -------------------------------------------------------------------------------- /services/decoder/protocols/renzo/renzo.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import type { EventDetails, EventTokens } from "../../decoder.types"; 3 | import { type EventType } from "../../decoder.types"; 4 | import { 5 | DECODED_ACTION, 6 | DECODED_EVENT_CATEGORY, 7 | } from "../../decoder.constants"; 8 | import { decodeEventLog, type Abi } from "viem"; 9 | import STRATEGY_MANAGER_ABI from "./abis/renzo.eigen-layer-strategy-manager.json"; 10 | import RESTAKE_STRATEGY_ABI from "./abis/renzo.restake-manager-abi.json"; 11 | import { timestampParser } from "../../../../utils/functions"; 12 | import { prettifyCurrency } from "@covalenthq/client-sdk"; 13 | 14 | GoldRushDecoder.on( 15 | "renzo:ShareWithdrawalQueued", 16 | ["eth-mainnet"], 17 | STRATEGY_MANAGER_ABI as Abi, 18 | async ( 19 | log_event, 20 | tx, 21 | chain_name, 22 | covalent_client, 23 | options 24 | ): Promise => { 25 | const { raw_log_data, raw_log_topics } = log_event; 26 | 27 | const { args: decoded } = decodeEventLog({ 28 | abi: STRATEGY_MANAGER_ABI, 29 | topics: raw_log_topics as [], 30 | data: raw_log_data as `0x${string}`, 31 | eventName: "ShareWithdrawalQueued", 32 | }) as { 33 | eventName: "ShareWithdrawalQueued"; 34 | args: { 35 | depositor: string; 36 | nonce: bigint; 37 | strategy: string; 38 | shares: bigint; 39 | }; 40 | }; 41 | 42 | const details: EventDetails = [ 43 | { 44 | heading: "Depositor", 45 | value: decoded.depositor, 46 | type: "address", 47 | }, 48 | { 49 | heading: "Nonce", 50 | value: decoded.nonce.toString(), 51 | type: "text", 52 | }, 53 | { 54 | heading: "Strategy", 55 | value: decoded.strategy, 56 | type: "address", 57 | }, 58 | { 59 | heading: "Shares", 60 | value: decoded.shares.toString(), 61 | type: "text", 62 | }, 63 | ]; 64 | 65 | return { 66 | action: DECODED_ACTION.WITHDRAW, 67 | category: DECODED_EVENT_CATEGORY.STAKING, 68 | name: "ShareWithdrawalQueued", 69 | protocol: { 70 | logo: log_event.sender_logo_url as string, 71 | name: log_event.sender_name as string, 72 | }, 73 | ...(options.raw_logs ? { raw_log: log_event } : {}), 74 | details, 75 | }; 76 | } 77 | ); 78 | 79 | GoldRushDecoder.on( 80 | "renzo:WithdrawalQueued", 81 | ["eth-mainnet"], 82 | STRATEGY_MANAGER_ABI as Abi, 83 | async ( 84 | log_event, 85 | tx, 86 | chain_name, 87 | covalent_client, 88 | options 89 | ): Promise => { 90 | const { raw_log_data, raw_log_topics } = log_event; 91 | 92 | const { args: decoded } = decodeEventLog({ 93 | abi: STRATEGY_MANAGER_ABI, 94 | topics: raw_log_topics as [], 95 | data: raw_log_data as `0x${string}`, 96 | eventName: "WithdrawalQueued", 97 | }) as { 98 | eventName: "WithdrawalQueued"; 99 | args: { 100 | depositor: string; 101 | nonce: bigint; 102 | withdrawer: string; 103 | delegatedAddress: string; 104 | withdrawalRoot: string; 105 | }; 106 | }; 107 | 108 | const details: EventDetails = [ 109 | { 110 | heading: "Depositor", 111 | value: decoded.depositor, 112 | type: "address", 113 | }, 114 | { 115 | heading: "Nonce", 116 | value: decoded.nonce.toString(), 117 | type: "text", 118 | }, 119 | { 120 | heading: "Withdrawer", 121 | value: decoded.withdrawer, 122 | type: "address", 123 | }, 124 | { 125 | heading: "Delegated Address", 126 | value: decoded.delegatedAddress, 127 | type: "address", 128 | }, 129 | { 130 | heading: "Withdrawal Root", 131 | value: decoded.withdrawalRoot, 132 | type: "text", 133 | }, 134 | ]; 135 | 136 | return { 137 | action: DECODED_ACTION.WITHDRAW, 138 | category: DECODED_EVENT_CATEGORY.STAKING, 139 | name: "WithdrawalQueued", 140 | protocol: { 141 | logo: log_event.sender_logo_url as string, 142 | name: log_event.sender_name as string, 143 | }, 144 | ...(options.raw_logs ? { raw_log: log_event } : {}), 145 | details, 146 | }; 147 | } 148 | ); 149 | 150 | GoldRushDecoder.on( 151 | "renzo:WithdrawalCompleted", 152 | ["eth-mainnet"], 153 | STRATEGY_MANAGER_ABI as Abi, 154 | async ( 155 | log_event, 156 | tx, 157 | chain_name, 158 | covalent_client, 159 | options 160 | ): Promise => { 161 | const { raw_log_data, raw_log_topics } = log_event; 162 | 163 | const { args: decoded } = decodeEventLog({ 164 | abi: STRATEGY_MANAGER_ABI, 165 | topics: raw_log_topics as [], 166 | data: raw_log_data as `0x${string}`, 167 | eventName: "WithdrawalCompleted", 168 | }) as { 169 | eventName: "WithdrawalCompleted"; 170 | args: { 171 | depositor: string; 172 | nonce: bigint; 173 | withdrawer: string; 174 | withdrawalRoot: string; 175 | }; 176 | }; 177 | 178 | const details: EventDetails = [ 179 | { 180 | heading: "Depositor", 181 | value: decoded.depositor, 182 | type: "address", 183 | }, 184 | { 185 | heading: "Nonce", 186 | value: decoded.nonce.toString(), 187 | type: "text", 188 | }, 189 | { 190 | heading: "Withdrawer", 191 | value: decoded.withdrawer, 192 | type: "address", 193 | }, 194 | { 195 | heading: "Withdrawal Root", 196 | value: decoded.withdrawalRoot, 197 | type: "text", 198 | }, 199 | ]; 200 | 201 | return { 202 | action: DECODED_ACTION.WITHDRAW, 203 | category: DECODED_EVENT_CATEGORY.STAKING, 204 | name: "WithdrawalCompleted", 205 | protocol: { 206 | logo: log_event.sender_logo_url as string, 207 | name: log_event.sender_name as string, 208 | }, 209 | ...(options.raw_logs ? { raw_log: log_event } : {}), 210 | details, 211 | }; 212 | } 213 | ); 214 | 215 | GoldRushDecoder.on( 216 | "renzo:Deposit", 217 | ["eth-mainnet"], 218 | RESTAKE_STRATEGY_ABI as Abi, 219 | async ( 220 | log_event, 221 | tx, 222 | chain_name, 223 | covalent_client, 224 | options 225 | ): Promise => { 226 | const { raw_log_data, raw_log_topics } = log_event; 227 | 228 | const { args: decoded } = decodeEventLog({ 229 | abi: RESTAKE_STRATEGY_ABI, 230 | topics: raw_log_topics as [], 231 | data: raw_log_data as `0x${string}`, 232 | eventName: "Deposit", 233 | }) as { 234 | eventName: "Deposit"; 235 | args: { 236 | depositor: string; 237 | token: string; 238 | amount: bigint; 239 | ezETHMinted: bigint; 240 | referralId: bigint; 241 | }; 242 | }; 243 | 244 | const details: EventDetails = [ 245 | { 246 | heading: "Depositor", 247 | value: decoded.depositor, 248 | type: "address", 249 | }, 250 | { 251 | heading: "Token", 252 | value: decoded.token, 253 | type: "address", 254 | }, 255 | { 256 | heading: "Referral ID", 257 | value: decoded.referralId.toString(), 258 | type: "text", 259 | }, 260 | ]; 261 | 262 | const date = timestampParser(tx.block_signed_at, "YYYY-MM-DD"); 263 | 264 | const { data: TokenData } = 265 | await covalent_client.PricingService.getTokenPrices( 266 | chain_name, 267 | "USD", 268 | decoded.token, 269 | { 270 | from: date, 271 | to: date, 272 | } 273 | ); 274 | 275 | const { data: ezETHData } = 276 | await covalent_client.PricingService.getTokenPrices( 277 | chain_name, 278 | "USD", 279 | "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", 280 | { 281 | from: date, 282 | to: date, 283 | } 284 | ); 285 | 286 | const tokens: EventTokens = [ 287 | { 288 | decimals: TokenData?.[0]?.contract_decimals, 289 | heading: "Deposit Amount", 290 | value: String(decoded.amount), 291 | pretty_quote: prettifyCurrency( 292 | TokenData?.[0]?.prices?.[0]?.price * 293 | (Number(decoded.amount) / 294 | Math.pow( 295 | 10, 296 | TokenData?.[0]?.contract_decimals ?? 0 297 | )) 298 | ), 299 | ticker_logo: TokenData?.[0]?.logo_urls?.token_logo_url, 300 | ticker_symbol: TokenData?.[0]?.contract_ticker_symbol, 301 | }, 302 | { 303 | decimals: ezETHData?.[0]?.contract_decimals, 304 | heading: "ezETH Minted", 305 | value: String(decoded.ezETHMinted), 306 | pretty_quote: prettifyCurrency( 307 | ezETHData?.[0]?.prices?.[0]?.price * 308 | (Number(decoded.ezETHMinted) / 309 | Math.pow( 310 | 10, 311 | ezETHData?.[0]?.contract_decimals ?? 0 312 | )) 313 | ), 314 | ticker_logo: ezETHData?.[0]?.logo_urls?.token_logo_url, 315 | ticker_symbol: ezETHData?.[0]?.contract_ticker_symbol, 316 | }, 317 | ]; 318 | 319 | return { 320 | action: DECODED_ACTION.DEPOSIT, 321 | category: DECODED_EVENT_CATEGORY.STAKING, 322 | name: "Deposit", 323 | protocol: { 324 | logo: log_event.sender_logo_url as string, 325 | name: log_event.sender_name as string, 326 | }, 327 | ...(options.raw_logs ? { raw_log: log_event } : {}), 328 | details, 329 | tokens, 330 | }; 331 | } 332 | ); 333 | -------------------------------------------------------------------------------- /services/decoder/protocols/renzo/renzo.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("renzo", () => { 6 | test("eth-mainnet:ShareWithdrawalQueued", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0x398cc4c5dd16f5e6fda2a14186c85692ea94de99c200df3665841be1bf9f2af4", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find( 17 | ({ name }) => name === "ShareWithdrawalQueued" 18 | ); 19 | if (!event) { 20 | throw Error("Event not found"); 21 | } 22 | expect(event?.details?.length).toEqual(4); 23 | }); 24 | }); 25 | 26 | describe("renzo", () => { 27 | test("eth-mainnet:WithdrawalQueued", async () => { 28 | const res = await request(app) 29 | .post("/api/v1/tx/decode") 30 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 31 | .send({ 32 | chain_name: "eth-mainnet", 33 | tx_hash: 34 | "0x398cc4c5dd16f5e6fda2a14186c85692ea94de99c200df3665841be1bf9f2af4", 35 | }); 36 | const { events } = res.body as { events: EventType[] }; 37 | const event = events.find(({ name }) => name === "WithdrawalQueued"); 38 | if (!event) { 39 | throw Error("Event not found"); 40 | } 41 | expect(event?.details?.length).toEqual(5); 42 | }); 43 | }); 44 | 45 | describe("renzo", () => { 46 | test("eth-mainnet:WithdrawalCompleted", async () => { 47 | const res = await request(app) 48 | .post("/api/v1/tx/decode") 49 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 50 | .send({ 51 | chain_name: "eth-mainnet", 52 | tx_hash: 53 | "0xf9842f803f7373eed7b0ad96bf91ea80e85c82074cc2855ccb0ba3d2d778bda7", 54 | }); 55 | const { events } = res.body as { events: EventType[] }; 56 | const event = events.find(({ name }) => name === "WithdrawalCompleted"); 57 | if (!event) { 58 | throw Error("Event not found"); 59 | } 60 | expect(event?.details?.length).toEqual(4); 61 | }); 62 | }); 63 | 64 | describe("renzo", () => { 65 | test("eth-mainnet:Deposit", async () => { 66 | const res = await request(app) 67 | .post("/api/v1/tx/decode") 68 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 69 | .send({ 70 | chain_name: "eth-mainnet", 71 | tx_hash: 72 | "0xdfe1f29b8dfbdd3c6a0d8379e118225f936a4ea74c1e468048d1adbc18510cb0", 73 | }); 74 | const { events } = res.body as { events: EventType[] }; 75 | const event = events.find(({ name }) => name === "Deposit"); 76 | if (!event) { 77 | throw Error("Event not found"); 78 | } 79 | expect(event?.details?.length).toEqual(3); 80 | expect(event.tokens?.length).toEqual(2); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v2/abis/uniswap-v2.factory.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_feeToSetter", 7 | "type": "address" 8 | } 9 | ], 10 | "payable": false, 11 | "stateMutability": "nonpayable", 12 | "type": "constructor" 13 | }, 14 | { 15 | "anonymous": false, 16 | "inputs": [ 17 | { 18 | "indexed": true, 19 | "internalType": "address", 20 | "name": "token0", 21 | "type": "address" 22 | }, 23 | { 24 | "indexed": true, 25 | "internalType": "address", 26 | "name": "token1", 27 | "type": "address" 28 | }, 29 | { 30 | "indexed": false, 31 | "internalType": "address", 32 | "name": "pair", 33 | "type": "address" 34 | }, 35 | { 36 | "indexed": false, 37 | "internalType": "uint256", 38 | "name": "allPairsLength", 39 | "type": "uint256" 40 | } 41 | ], 42 | "name": "PairCreated", 43 | "type": "event" 44 | }, 45 | { 46 | "constant": true, 47 | "inputs": [ 48 | { 49 | "internalType": "uint256", 50 | "name": "", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "allPairs", 55 | "outputs": [ 56 | { 57 | "internalType": "address", 58 | "name": "", 59 | "type": "address" 60 | } 61 | ], 62 | "payable": false, 63 | "stateMutability": "view", 64 | "type": "function" 65 | }, 66 | { 67 | "constant": true, 68 | "inputs": [], 69 | "name": "allPairsLength", 70 | "outputs": [ 71 | { 72 | "internalType": "uint256", 73 | "name": "", 74 | "type": "uint256" 75 | } 76 | ], 77 | "payable": false, 78 | "stateMutability": "view", 79 | "type": "function" 80 | }, 81 | { 82 | "constant": false, 83 | "inputs": [ 84 | { 85 | "internalType": "address", 86 | "name": "tokenA", 87 | "type": "address" 88 | }, 89 | { 90 | "internalType": "address", 91 | "name": "tokenB", 92 | "type": "address" 93 | } 94 | ], 95 | "name": "createPair", 96 | "outputs": [ 97 | { 98 | "internalType": "address", 99 | "name": "pair", 100 | "type": "address" 101 | } 102 | ], 103 | "payable": false, 104 | "stateMutability": "nonpayable", 105 | "type": "function" 106 | }, 107 | { 108 | "constant": true, 109 | "inputs": [], 110 | "name": "feeTo", 111 | "outputs": [ 112 | { 113 | "internalType": "address", 114 | "name": "", 115 | "type": "address" 116 | } 117 | ], 118 | "payable": false, 119 | "stateMutability": "view", 120 | "type": "function" 121 | }, 122 | { 123 | "constant": true, 124 | "inputs": [], 125 | "name": "feeToSetter", 126 | "outputs": [ 127 | { 128 | "internalType": "address", 129 | "name": "", 130 | "type": "address" 131 | } 132 | ], 133 | "payable": false, 134 | "stateMutability": "view", 135 | "type": "function" 136 | }, 137 | { 138 | "constant": true, 139 | "inputs": [ 140 | { 141 | "internalType": "address", 142 | "name": "", 143 | "type": "address" 144 | }, 145 | { 146 | "internalType": "address", 147 | "name": "", 148 | "type": "address" 149 | } 150 | ], 151 | "name": "getPair", 152 | "outputs": [ 153 | { 154 | "internalType": "address", 155 | "name": "", 156 | "type": "address" 157 | } 158 | ], 159 | "payable": false, 160 | "stateMutability": "view", 161 | "type": "function" 162 | }, 163 | { 164 | "constant": false, 165 | "inputs": [ 166 | { 167 | "internalType": "address", 168 | "name": "_feeTo", 169 | "type": "address" 170 | } 171 | ], 172 | "name": "setFeeTo", 173 | "outputs": [], 174 | "payable": false, 175 | "stateMutability": "nonpayable", 176 | "type": "function" 177 | }, 178 | { 179 | "constant": false, 180 | "inputs": [ 181 | { 182 | "internalType": "address", 183 | "name": "_feeToSetter", 184 | "type": "address" 185 | } 186 | ], 187 | "name": "setFeeToSetter", 188 | "outputs": [], 189 | "payable": false, 190 | "stateMutability": "nonpayable", 191 | "type": "function" 192 | } 193 | ] 194 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v2/uniswap-v2.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f", 6 | is_factory: true, 7 | protocol_name: "uniswap-v2", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x794c07912474351b3134e6d6b3b7b3b4a07cbaaa", 12 | is_factory: true, 13 | protocol_name: "uniswap-v2", 14 | chain_name: "defi-kingdoms-mainnet", 15 | }, 16 | ]; 17 | 18 | export default configs; 19 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v2/uniswap-v2.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("uniswap-v2", () => { 6 | test("eth-mainnet:Swap", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ 10 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 11 | }) 12 | .send({ 13 | chain_name: "eth-mainnet", 14 | tx_hash: 15 | "0x7c0d75a2c4407917a0f70c48655f8a66f35f9aba7d36e615bcabc2c191ac2658", 16 | }); 17 | const { events } = res.body as { events: EventType[] }; 18 | const event = events.find(({ name }) => name === "Swap"); 19 | if (!event) { 20 | throw Error("Event not found"); 21 | } 22 | expect(event.tokens?.length).toEqual(2); 23 | expect(event.details?.length).toEqual(2); 24 | }); 25 | 26 | test("defi-kingdoms-mainnet:Swap", async () => { 27 | const res = await request(app) 28 | .post("/api/v1/tx/decode") 29 | .set({ 30 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 31 | }) 32 | .send({ 33 | chain_name: "defi-kingdoms-mainnet", 34 | tx_hash: 35 | "0x9327e7e7ba43fdb276e6b098e5ef7eb114640f14ce528f0419716d950ee9f947", 36 | }); 37 | const { events } = res.body as { events: EventType[] }; 38 | const event = events.find(({ name }) => name === "Swap"); 39 | if (!event) { 40 | throw Error("Event not found"); 41 | } 42 | expect(event.tokens?.length).toEqual(2); 43 | expect(event.details?.length).toEqual(2); 44 | }); 45 | 46 | test("eth-mainnet:Mint", async () => { 47 | const res = await request(app) 48 | .post("/api/v1/tx/decode") 49 | .set({ 50 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 51 | }) 52 | .send({ 53 | chain_name: "eth-mainnet", 54 | tx_hash: 55 | "0x020468ae7052596fdd72deac82891de9bdd581f4bb12631c729d1825ad7ba2b6", 56 | }); 57 | const { events } = res.body as { events: EventType[] }; 58 | const event = events.find(({ name }) => name === "Mint"); 59 | if (!event) { 60 | throw Error("Event not found"); 61 | } 62 | expect(event.tokens?.length).toEqual(2); 63 | expect(event.details?.length).toEqual(1); 64 | }); 65 | 66 | test("eth-mainnet:Burn", async () => { 67 | const res = await request(app) 68 | .post("/api/v1/tx/decode") 69 | .set({ 70 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 71 | }) 72 | .send({ 73 | chain_name: "eth-mainnet", 74 | tx_hash: 75 | "0xf419cd1a89b928cb93f38237e9b1e6743218fbb87aaac678cb1f950951b7476e", 76 | }); 77 | const { events } = res.body as { events: EventType[] }; 78 | const event = events.find(({ name }) => name === "Burn"); 79 | if (!event) { 80 | throw Error("Event not found"); 81 | } 82 | expect(event.tokens?.length).toEqual(2); 83 | }); 84 | 85 | test("eth-mainnet:Sync", async () => { 86 | const res = await request(app) 87 | .post("/api/v1/tx/decode") 88 | .set({ 89 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 90 | }) 91 | .send({ 92 | chain_name: "eth-mainnet", 93 | tx_hash: 94 | "0xf419cd1a89b928cb93f38237e9b1e6743218fbb87aaac678cb1f950951b7476e", 95 | }); 96 | const { events } = res.body as { events: EventType[] }; 97 | const event = events.find(({ name }) => name === "Sync"); 98 | if (!event) { 99 | throw Error("Event not found"); 100 | } 101 | expect(event.tokens?.length).toEqual(2); 102 | }); 103 | 104 | test("eth-mainnet:PairCreated", async () => { 105 | const res = await request(app) 106 | .post("/api/v1/tx/decode") 107 | .set({ 108 | "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY, 109 | }) 110 | .send({ 111 | chain_name: "eth-mainnet", 112 | tx_hash: 113 | "0x9584cdf7d99a22e18843cf26c484018bfb11ab4ce4f2d898ec69075ed8e3c8dc", 114 | }); 115 | const { events } = res.body as { events: EventType[] }; 116 | const event = events.find(({ name }) => name === "Pair Created"); 117 | if (!event) { 118 | throw Error("Event not found"); 119 | } 120 | expect(event.details?.length).toEqual(9); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v3/abis/uniswap-v3.factory.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "uint24", 13 | "name": "fee", 14 | "type": "uint24" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "int24", 19 | "name": "tickSpacing", 20 | "type": "int24" 21 | } 22 | ], 23 | "name": "FeeAmountEnabled", 24 | "type": "event" 25 | }, 26 | { 27 | "anonymous": false, 28 | "inputs": [ 29 | { 30 | "indexed": true, 31 | "internalType": "address", 32 | "name": "oldOwner", 33 | "type": "address" 34 | }, 35 | { 36 | "indexed": true, 37 | "internalType": "address", 38 | "name": "newOwner", 39 | "type": "address" 40 | } 41 | ], 42 | "name": "OwnerChanged", 43 | "type": "event" 44 | }, 45 | { 46 | "anonymous": false, 47 | "inputs": [ 48 | { 49 | "indexed": true, 50 | "internalType": "address", 51 | "name": "token0", 52 | "type": "address" 53 | }, 54 | { 55 | "indexed": true, 56 | "internalType": "address", 57 | "name": "token1", 58 | "type": "address" 59 | }, 60 | { 61 | "indexed": true, 62 | "internalType": "uint24", 63 | "name": "fee", 64 | "type": "uint24" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "int24", 69 | "name": "tickSpacing", 70 | "type": "int24" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "address", 75 | "name": "pool", 76 | "type": "address" 77 | } 78 | ], 79 | "name": "PoolCreated", 80 | "type": "event" 81 | }, 82 | { 83 | "inputs": [ 84 | { 85 | "internalType": "address", 86 | "name": "tokenA", 87 | "type": "address" 88 | }, 89 | { 90 | "internalType": "address", 91 | "name": "tokenB", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "uint24", 96 | "name": "fee", 97 | "type": "uint24" 98 | } 99 | ], 100 | "name": "createPool", 101 | "outputs": [ 102 | { 103 | "internalType": "address", 104 | "name": "pool", 105 | "type": "address" 106 | } 107 | ], 108 | "stateMutability": "nonpayable", 109 | "type": "function" 110 | }, 111 | { 112 | "inputs": [ 113 | { 114 | "internalType": "uint24", 115 | "name": "fee", 116 | "type": "uint24" 117 | }, 118 | { 119 | "internalType": "int24", 120 | "name": "tickSpacing", 121 | "type": "int24" 122 | } 123 | ], 124 | "name": "enableFeeAmount", 125 | "outputs": [], 126 | "stateMutability": "nonpayable", 127 | "type": "function" 128 | }, 129 | { 130 | "inputs": [ 131 | { 132 | "internalType": "uint24", 133 | "name": "", 134 | "type": "uint24" 135 | } 136 | ], 137 | "name": "feeAmountTickSpacing", 138 | "outputs": [ 139 | { 140 | "internalType": "int24", 141 | "name": "", 142 | "type": "int24" 143 | } 144 | ], 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "inputs": [ 150 | { 151 | "internalType": "address", 152 | "name": "", 153 | "type": "address" 154 | }, 155 | { 156 | "internalType": "address", 157 | "name": "", 158 | "type": "address" 159 | }, 160 | { 161 | "internalType": "uint24", 162 | "name": "", 163 | "type": "uint24" 164 | } 165 | ], 166 | "name": "getPool", 167 | "outputs": [ 168 | { 169 | "internalType": "address", 170 | "name": "", 171 | "type": "address" 172 | } 173 | ], 174 | "stateMutability": "view", 175 | "type": "function" 176 | }, 177 | { 178 | "inputs": [], 179 | "name": "owner", 180 | "outputs": [ 181 | { 182 | "internalType": "address", 183 | "name": "", 184 | "type": "address" 185 | } 186 | ], 187 | "stateMutability": "view", 188 | "type": "function" 189 | }, 190 | { 191 | "inputs": [], 192 | "name": "parameters", 193 | "outputs": [ 194 | { 195 | "internalType": "address", 196 | "name": "factory", 197 | "type": "address" 198 | }, 199 | { 200 | "internalType": "address", 201 | "name": "token0", 202 | "type": "address" 203 | }, 204 | { 205 | "internalType": "address", 206 | "name": "token1", 207 | "type": "address" 208 | }, 209 | { 210 | "internalType": "uint24", 211 | "name": "fee", 212 | "type": "uint24" 213 | }, 214 | { 215 | "internalType": "int24", 216 | "name": "tickSpacing", 217 | "type": "int24" 218 | } 219 | ], 220 | "stateMutability": "view", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { 226 | "internalType": "address", 227 | "name": "_owner", 228 | "type": "address" 229 | } 230 | ], 231 | "name": "setOwner", 232 | "outputs": [], 233 | "stateMutability": "nonpayable", 234 | "type": "function" 235 | } 236 | ] 237 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v3/uniswap-v3.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x1f98431c8ad98523631ae4a59f267346ea31f984", 6 | is_factory: true, 7 | protocol_name: "uniswap-v3", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0xcbcdf9626bc03e24f779434178a73a0b4bad62ed", 12 | is_factory: false, 13 | protocol_name: "uniswap-v3", 14 | chain_name: "eth-mainnet", 15 | }, 16 | { 17 | address: "0xc36442b4a4522e871399cd717abdd847ab11fe88", 18 | is_factory: false, 19 | protocol_name: "uniswap-v3", 20 | chain_name: "eth-mainnet", 21 | }, 22 | ]; 23 | 24 | export default configs; 25 | -------------------------------------------------------------------------------- /services/decoder/protocols/uniswap-v3/uniswap-v3.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("uniswap-v3", () => { 6 | test("eth-mainnet:PoolCreated", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0xf87d91f3d72a8e912c020c2e316151f3557b1217b44d4f6b6bec126448318530", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "Pool Created"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event.tokens?.length).toEqual(2); 21 | expect(event.details?.length).toEqual(5); 22 | }); 23 | 24 | test("eth-mainnet:Burn", async () => { 25 | const res = await request(app) 26 | .post("/api/v1/tx/decode") 27 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 28 | .send({ 29 | chain_name: "eth-mainnet", 30 | tx_hash: 31 | "0x3d1748ea19a9c6c3b7690652fca03c54f6636f1403b9df25e4965ddfa765f06c", 32 | }); 33 | const { events } = res.body as { events: EventType[] }; 34 | const event = events.find(({ name }) => name === "Burn"); 35 | if (!event) { 36 | throw Error("Event not found"); 37 | } 38 | expect(event.details?.length).toEqual(6); 39 | }); 40 | 41 | test("eth-mainnet:Mint", async () => { 42 | const res = await request(app) 43 | .post("/api/v1/tx/decode") 44 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 45 | .send({ 46 | chain_name: "eth-mainnet", 47 | tx_hash: 48 | "0x509ffb3e2e1338991b27284d6365a93bdf36ac50a9a89e6260b5f791bf0e50e6", 49 | }); 50 | const { events } = res.body as { events: EventType[] }; 51 | const event = events.find(({ name }) => name === "Mint"); 52 | if (!event) { 53 | throw Error("Event not found"); 54 | } 55 | expect(event.details?.length).toEqual(7); 56 | }); 57 | 58 | test("eth-mainnet:Swap", async () => { 59 | const res = await request(app) 60 | .post("/api/v1/tx/decode") 61 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 62 | .send({ 63 | chain_name: "eth-mainnet", 64 | tx_hash: 65 | "0xf0c18bcdeb3b167a7323499307b6a18031450bf955cf9ec1153231f97898f391", 66 | }); 67 | const { events } = res.body as { events: EventType[] }; 68 | const event = events.find(({ name }) => name === "Swap"); 69 | if (!event) { 70 | throw Error("Event not found"); 71 | } 72 | expect(event.details?.length).toEqual(7); 73 | }); 74 | 75 | test("eth-mainnet:Collect", async () => { 76 | const res = await request(app) 77 | .post("/api/v1/tx/decode") 78 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 79 | .send({ 80 | chain_name: "eth-mainnet", 81 | tx_hash: 82 | "0x7c927bbab8a2f60f0a36ee9425c03db556a44c87dddf855d5641f5f1c2270ebd", 83 | }); 84 | const { events } = res.body as { events: EventType[] }; 85 | const event = events.find(({ name }) => name === "Collect Fees"); 86 | if (!event) { 87 | throw Error("Event not found"); 88 | } 89 | expect(event.details?.length).toEqual(6); 90 | }); 91 | 92 | test("eth-mainnet:Flash", async () => { 93 | const res = await request(app) 94 | .post("/api/v1/tx/decode") 95 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 96 | .send({ 97 | chain_name: "eth-mainnet", 98 | tx_hash: 99 | "0xe3fcabe33a5ebf9ed6450f11b907da4a5d72f2e58917e8b2ae20fb259be385d4", 100 | }); 101 | const { events } = res.body as { events: EventType[] }; 102 | const event = events.find(({ name }) => name === "Flash Loan"); 103 | if (!event) { 104 | throw Error("Event not found"); 105 | } 106 | expect(event.details?.length).toEqual(6); 107 | }); 108 | 109 | test("eth-mainnet:DecreaseLiquidity", async () => { 110 | const res = await request(app) 111 | .post("/api/v1/tx/decode") 112 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 113 | .send({ 114 | chain_name: "eth-mainnet", 115 | tx_hash: 116 | "0x3d1748ea19a9c6c3b7690652fca03c54f6636f1403b9df25e4965ddfa765f06c", 117 | }); 118 | const { events } = res.body as { events: EventType[] }; 119 | const event = events.find(({ name }) => name === "Decrease Liquidity"); 120 | if (!event) { 121 | throw Error("Event not found"); 122 | } 123 | expect(event.details?.length).toEqual(4); 124 | }); 125 | 126 | test("eth-mainnet:IncreaseLiquidity", async () => { 127 | const res = await request(app) 128 | .post("/api/v1/tx/decode") 129 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 130 | .send({ 131 | chain_name: "eth-mainnet", 132 | tx_hash: 133 | "0x509ffb3e2e1338991b27284d6365a93bdf36ac50a9a89e6260b5f791bf0e50e6", 134 | }); 135 | const { events } = res.body as { events: EventType[] }; 136 | const event = events.find(({ name }) => name === "Increase Liquidity"); 137 | if (!event) { 138 | throw Error("Event not found"); 139 | } 140 | expect(event.details?.length).toEqual(4); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /services/decoder/protocols/wormhole/wormhole.configs.ts: -------------------------------------------------------------------------------- 1 | import { type Configs } from "../../decoder.types"; 2 | 3 | const configs: Configs = [ 4 | { 5 | address: "0x3ee18b2214aff97000d974cf647e7c347e8fa585", 6 | is_factory: false, 7 | protocol_name: "wormhole", 8 | chain_name: "eth-mainnet", 9 | }, 10 | { 11 | address: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B", 12 | is_factory: false, 13 | protocol_name: "wormhole", 14 | chain_name: "eth-mainnet", 15 | }, 16 | { 17 | address: "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7", 18 | is_factory: false, 19 | protocol_name: "wormhole", 20 | chain_name: "bsc-mainnet", 21 | }, 22 | { 23 | address: "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B", 24 | is_factory: false, 25 | protocol_name: "wormhole", 26 | chain_name: "bsc-mainnet", 27 | }, 28 | { 29 | address: "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE", 30 | is_factory: false, 31 | protocol_name: "wormhole", 32 | chain_name: "matic-mainnet", 33 | }, 34 | { 35 | address: "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7", 36 | is_factory: false, 37 | protocol_name: "wormhole", 38 | chain_name: "matic-mainnet", 39 | }, 40 | { 41 | address: "0x0e082F06FF657D94310cB8cE8B0D9a04541d8052", 42 | is_factory: false, 43 | protocol_name: "wormhole", 44 | chain_name: "avalanche-mainnet", 45 | }, 46 | { 47 | address: "0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c", 48 | is_factory: false, 49 | protocol_name: "wormhole", 50 | chain_name: "avalanche-mainnet", 51 | }, 52 | { 53 | address: "0x5848C791e09901b40A9Ef749f2a6735b418d7564", 54 | is_factory: false, 55 | protocol_name: "wormhole", 56 | chain_name: "emerald-paratime-mainnet", 57 | }, 58 | { 59 | address: "0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585", 60 | is_factory: false, 61 | protocol_name: "wormhole", 62 | chain_name: "emerald-paratime-mainnet", 63 | }, 64 | { 65 | address: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E", 66 | is_factory: false, 67 | protocol_name: "wormhole", 68 | chain_name: "aurora-mainnet", 69 | }, 70 | { 71 | address: "0x51b5123a7b0F9b2bA265f9c4C8de7D78D52f510F", 72 | is_factory: false, 73 | protocol_name: "wormhole", 74 | chain_name: "aurora-mainnet", 75 | }, 76 | { 77 | address: "0x796Dff6D74F3E27060B71255Fe517BFb23C93eed", 78 | is_factory: false, 79 | protocol_name: "wormhole", 80 | chain_name: "celo-mainnet", 81 | }, 82 | { 83 | address: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E", 84 | is_factory: false, 85 | protocol_name: "wormhole", 86 | chain_name: "celo-mainnet", 87 | }, 88 | { 89 | address: "0xb1731c586ca89a23809861c6103f0b96b3f57d92", 90 | is_factory: false, 91 | protocol_name: "wormhole", 92 | chain_name: "moonbeam-mainnet", 93 | }, 94 | { 95 | address: "0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3", 96 | is_factory: false, 97 | protocol_name: "wormhole", 98 | chain_name: "moonbeam-mainnet", 99 | }, 100 | { 101 | address: "0xa5f208e072434bC67592E4C49C1B991BA79BCA46", 102 | is_factory: false, 103 | protocol_name: "wormhole", 104 | chain_name: "arbitrum-mainnet", 105 | }, 106 | { 107 | address: "0x0b2402144Bb366A632D14B83F244D2e0e21bD39c", 108 | is_factory: false, 109 | protocol_name: "wormhole", 110 | chain_name: "arbitrum-mainnet", 111 | }, 112 | { 113 | address: "0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b", 114 | is_factory: false, 115 | protocol_name: "wormhole", 116 | chain_name: "optimism-mainnet", 117 | }, 118 | { 119 | address: "0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722", 120 | is_factory: false, 121 | protocol_name: "wormhole", 122 | chain_name: "optimism-mainnet", 123 | }, 124 | { 125 | address: "0xa321448d90d4e5b0A732867c18eA198e75CAC48E", 126 | is_factory: false, 127 | protocol_name: "wormhole", 128 | chain_name: "gnosis-mainnet", 129 | }, 130 | { 131 | address: "0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627", 132 | is_factory: false, 133 | protocol_name: "wormhole", 134 | chain_name: "base-mainnet", 135 | }, 136 | { 137 | address: "0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6", 138 | is_factory: false, 139 | protocol_name: "wormhole", 140 | chain_name: "base-mainnet", 141 | }, 142 | ]; 143 | 144 | export default configs; 145 | -------------------------------------------------------------------------------- /services/decoder/protocols/wormhole/wormhole.decoders.ts: -------------------------------------------------------------------------------- 1 | import { GoldRushDecoder } from "../../decoder"; 2 | import type { EventDetails } from "../../decoder.types"; 3 | import { type EventType } from "../../decoder.types"; 4 | import { 5 | DECODED_ACTION, 6 | DECODED_EVENT_CATEGORY, 7 | } from "../../decoder.constants"; 8 | import { decodeEventLog, type Abi } from "viem"; 9 | import PORTAL_BRIDGE_ABI from "./abis/wormhole-portal-bridge.abi.json"; 10 | import ETH_CORE_ABI from "./abis/wormhole-eth-core.abi.json"; 11 | 12 | GoldRushDecoder.on( 13 | "wormhole:TransferRedeemed", 14 | [ 15 | "eth-mainnet", 16 | "arbitrum-mainnet", 17 | "bsc-mainnet", 18 | "matic-mainnet", 19 | "avalanche-mainnet", 20 | "emerald-paratime-mainnet", 21 | "aurora-mainnet", 22 | "celo-mainnet", 23 | "moonbeam-mainnet", 24 | "optimism-mainnet", 25 | "base-mainnet", 26 | ], 27 | PORTAL_BRIDGE_ABI as Abi, 28 | async ( 29 | log_event, 30 | tx, 31 | chain_name, 32 | covalent_client, 33 | options 34 | ): Promise => { 35 | const { raw_log_data, raw_log_topics } = log_event; 36 | 37 | const { args: decoded } = decodeEventLog({ 38 | abi: PORTAL_BRIDGE_ABI, 39 | topics: raw_log_topics as [], 40 | data: raw_log_data as `0x${string}`, 41 | eventName: "TransferRedeemed", 42 | }) as { 43 | eventName: "TransferRedeemed"; 44 | args: { 45 | emitterChainId: string; 46 | emitterAddress: string; 47 | sequence: bigint; 48 | }; 49 | }; 50 | 51 | const details: EventDetails = [ 52 | { 53 | heading: "Emitter Chain ID", 54 | value: decoded.emitterChainId, 55 | type: "text", 56 | }, 57 | { 58 | heading: "Emitter Address", 59 | value: decoded.emitterAddress, 60 | type: "address", 61 | }, 62 | { 63 | heading: "Sequence", 64 | value: decoded.sequence.toLocaleString(), 65 | type: "text", 66 | }, 67 | ]; 68 | 69 | return { 70 | action: DECODED_ACTION.RECEIVED_BRIDGE, 71 | category: DECODED_EVENT_CATEGORY.BRIDGE, 72 | name: "TransferRedeemed", 73 | protocol: { 74 | logo: log_event.sender_logo_url as string, 75 | name: log_event.sender_name as string, 76 | }, 77 | ...(options.raw_logs ? { raw_log: log_event } : {}), 78 | details, 79 | }; 80 | } 81 | ); 82 | 83 | GoldRushDecoder.on( 84 | "wormhole:LogMessagePublished", 85 | [ 86 | "eth-mainnet", 87 | "arbitrum-mainnet", 88 | "bsc-mainnet", 89 | "matic-mainnet", 90 | "avalanche-mainnet", 91 | "emerald-paratime-mainnet", 92 | "aurora-mainnet", 93 | "celo-mainnet", 94 | "moonbeam-mainnet", 95 | "optimism-mainnet", 96 | "base-mainnet", 97 | "gnosis-mainnet", 98 | ], 99 | ETH_CORE_ABI as Abi, 100 | async ( 101 | log_event, 102 | tx, 103 | chain_name, 104 | covalent_client, 105 | options 106 | ): Promise => { 107 | const { raw_log_data, raw_log_topics } = log_event; 108 | 109 | const { args: decoded } = decodeEventLog({ 110 | abi: ETH_CORE_ABI, 111 | topics: raw_log_topics as [], 112 | data: raw_log_data as `0x${string}`, 113 | eventName: "LogMessagePublished", 114 | }) as { 115 | eventName: "LogMessagePublished"; 116 | args: { 117 | sender: string; 118 | consistencyLevel: bigint; 119 | sequence: bigint; 120 | nonce: bigint; 121 | payload: string; 122 | }; 123 | }; 124 | 125 | const details: EventDetails = [ 126 | { 127 | heading: "Sender", 128 | value: decoded.sender, 129 | type: "address", 130 | }, 131 | { 132 | heading: "Consistency Level", 133 | value: decoded.consistencyLevel.toLocaleString(), 134 | type: "text", 135 | }, 136 | { 137 | heading: "Sequence", 138 | value: decoded.sequence.toLocaleString(), 139 | type: "text", 140 | }, 141 | { 142 | heading: "Nonce", 143 | value: decoded.nonce.toLocaleString(), 144 | type: "text", 145 | }, 146 | { 147 | heading: "Payload", 148 | value: decoded.payload, 149 | type: "text", 150 | }, 151 | ]; 152 | 153 | return { 154 | action: DECODED_ACTION.TRANSFERRED, 155 | category: DECODED_EVENT_CATEGORY.BRIDGE, 156 | name: "LogMessagePublished", 157 | protocol: { 158 | logo: log_event.sender_logo_url as string, 159 | name: log_event.sender_name as string, 160 | }, 161 | ...(options.raw_logs ? { raw_log: log_event } : {}), 162 | details, 163 | }; 164 | } 165 | ); 166 | -------------------------------------------------------------------------------- /services/decoder/protocols/wormhole/wormhole.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../../../api"; 3 | import { type EventType } from "../../decoder.types"; 4 | 5 | describe("wormhole", () => { 6 | test("eth-mainnet:LogMessagePublished", async () => { 7 | const res = await request(app) 8 | .post("/api/v1/tx/decode") 9 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 10 | .send({ 11 | chain_name: "eth-mainnet", 12 | tx_hash: 13 | "0x126f334fc80dc36189b2b1ef6c0fce2fcca4b16b287cf5ce8a7394a3c6710ba3", 14 | }); 15 | const { events } = res.body as { events: EventType[] }; 16 | const event = events.find(({ name }) => name === "LogMessagePublished"); 17 | if (!event) { 18 | throw Error("Event not found"); 19 | } 20 | expect(event?.details?.length).toEqual(5); 21 | }); 22 | }); 23 | 24 | describe("wormhole", () => { 25 | test("eth-mainnet:TransferRedeemed", async () => { 26 | const res = await request(app) 27 | .post("/api/v1/tx/decode") 28 | .set({ "x-covalent-api-key": process.env.TEST_COVALENT_API_KEY }) 29 | .send({ 30 | chain_name: "eth-mainnet", 31 | tx_hash: 32 | "0x3fbb9deb7b0e93bc0d474dbbea82371199430f560439851cdf5a64034344ef2c", 33 | }); 34 | const { events } = res.body as { events: EventType[] }; 35 | const event = events.find(({ name }) => name === "TransferRedeemed"); 36 | if (!event) { 37 | throw Error("Event not found"); 38 | } 39 | expect(event?.details?.length).toEqual(3); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /services/index.ts: -------------------------------------------------------------------------------- 1 | export { GoldRushDecoder } from "./decoder/decoder"; 2 | -------------------------------------------------------------------------------- /utils/functions/chunkify.ts: -------------------------------------------------------------------------------- 1 | export const chunkify = (arr: Array, size: number) => { 2 | const chunks: Array> = []; 3 | for (let i = 0; i < arr.length; i += size) { 4 | chunks.push(arr.slice(i, i + size)); 5 | } 6 | return chunks; 7 | }; 8 | -------------------------------------------------------------------------------- /utils/functions/currency-to-number.ts: -------------------------------------------------------------------------------- 1 | export const currencyToNumber = (priceString: string): number => 2 | parseFloat(priceString.replace(/[$, ]/g, "")); 3 | -------------------------------------------------------------------------------- /utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export { chunkify } from "./chunkify"; 2 | export { currencyToNumber } from "./currency-to-number"; 3 | export { slugify } from "./slugify"; 4 | export { timestampParser } from "./timestamp-parser"; 5 | export { isNullAddress } from "./is-null-address"; 6 | -------------------------------------------------------------------------------- /utils/functions/is-null-address.ts: -------------------------------------------------------------------------------- 1 | export const isNullAddress = (address: string): boolean => { 2 | return ( 3 | address === "0x0000000000000000000000000000000000000000" || 4 | address === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" 5 | ); 6 | }; 7 | -------------------------------------------------------------------------------- /utils/functions/slugify.ts: -------------------------------------------------------------------------------- 1 | export const slugify = (input: string): string => { 2 | return input 3 | .replace(/\s+/g, "-") 4 | .replace(/([a-z])([A-Z])/g, "$1-$2") 5 | .toLowerCase(); 6 | }; 7 | -------------------------------------------------------------------------------- /utils/functions/timestamp-parser.ts: -------------------------------------------------------------------------------- 1 | const months: string[] = [ 2 | "January", 3 | "February", 4 | "March", 5 | "April", 6 | "May", 7 | "June", 8 | "July", 9 | "August", 10 | "September", 11 | "October", 12 | "November", 13 | "December", 14 | ]; 15 | 16 | export const timestampParser = ( 17 | timestamp: Date, 18 | type: "descriptive" | "YYYY-MM-DD" | "relative" 19 | ): string => { 20 | const _unix: Date = new Date(timestamp); 21 | 22 | switch (type) { 23 | case "descriptive": { 24 | const _minutes = _unix.getMinutes(); 25 | const _hours = _unix.getHours(); 26 | const _seconds = _unix.getSeconds(); 27 | const _parsedSeconds: string = `${ 28 | _seconds <= 9 ? "0" : "" 29 | }${_seconds}`; 30 | const _parsedMinutes: string = `${ 31 | _minutes <= 9 ? "0" : "" 32 | }${_minutes}`; 33 | const _parsedHours: string = `${_hours <= 9 ? "0" : ""}${_hours}`; 34 | 35 | return `${ 36 | months[_unix.getMonth()] 37 | } ${_unix.getDate()} ${_unix.getFullYear()} at ${_parsedHours}:${_parsedMinutes}:${_parsedSeconds}`; 38 | } 39 | 40 | case "YYYY-MM-DD": { 41 | const date = new Date(timestamp); 42 | 43 | const year = date.getFullYear(); 44 | const month = String(date.getMonth() + 1).padStart(2, "0"); 45 | const day = String(date.getDate()).padStart(2, "0"); 46 | 47 | return `${year}-${month}-${day}`; 48 | } 49 | 50 | case "relative": { 51 | const currentTime = new Date(); 52 | const timeDifference = currentTime.getTime() - _unix.getTime(); 53 | 54 | const secondsDifference = Math.floor(timeDifference / 1000); 55 | const minutesDifference = Math.floor(secondsDifference / 60); 56 | const hoursDifference = Math.floor(minutesDifference / 60); 57 | const daysDifference = Math.floor(hoursDifference / 24); 58 | const monthsDifference = Math.floor(daysDifference / 30); 59 | const yearsDifference = Math.floor(monthsDifference / 12); 60 | 61 | if (yearsDifference > 0) { 62 | return `${yearsDifference} year${ 63 | yearsDifference > 1 ? "s" : "" 64 | } ago`; 65 | } else if (monthsDifference > 0) { 66 | return `${monthsDifference} month${ 67 | monthsDifference > 1 ? "s" : "" 68 | } ago`; 69 | } else if (daysDifference > 0) { 70 | return `${daysDifference} day${ 71 | daysDifference > 1 ? "s" : "" 72 | } ago`; 73 | } else if (hoursDifference > 0) { 74 | return `${hoursDifference} hour${ 75 | hoursDifference > 1 ? "s" : "" 76 | } ago`; 77 | } else if (minutesDifference > 0) { 78 | return `${minutesDifference} minute${ 79 | minutesDifference > 1 ? "s" : "" 80 | } ago`; 81 | } else { 82 | return `just now`; 83 | } 84 | } 85 | 86 | default: { 87 | return "error"; 88 | } 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/api/(.*)", 5 | "destination": "/api" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------