├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── index.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── grpc-block.test.case.ts │ ├── grpc-block.test.ts │ ├── grpc-tx.test.case.ts │ ├── grpc-tx.test.ts │ ├── liquidity-meteora-damm.test.ts │ ├── liquidity-meteora-pools.test.ts │ ├── liquidity-meteora.test.ts │ ├── liquidity-orca.test.ts │ ├── liquidity-raydium-cl.test.ts │ ├── liquidity-raydium-cpmm.test.ts │ ├── liquidity-raydium.test.ts │ ├── parse-finalswap.test.ts │ ├── parse-jupiter-dca.test.ts │ ├── parse-jupiter-limit-v2.test.ts │ ├── parser-block.test.ts │ ├── parser-boopfun.test.ts │ ├── parser-pumpfun.test.ts │ ├── parser-pumpswap.test.ts │ ├── parser-raydium-lcp.test.ts │ ├── parser-trade.test.ts │ ├── parser.test.case.ts │ ├── parser.test.ts │ └── utils.test.ts ├── constants │ ├── discriminators.ts │ ├── index.ts │ ├── instruction-types.ts │ ├── programId.ts │ └── token.ts ├── dex-parser.ts ├── instruction-classifier.ts ├── parsers │ ├── base-liquidity-parser.ts │ ├── base-parser.ts │ ├── binary-reader.ts │ ├── boopfun │ │ ├── index.ts │ │ ├── parser-boopfun-event.ts │ │ ├── parser-boopfun.ts │ │ └── util.ts │ ├── index.ts │ ├── jupiter │ │ ├── index.ts │ │ ├── layouts │ │ │ ├── index.ts │ │ │ ├── jupiter-dca.layout.ts │ │ │ ├── jupiter-limit.layout.ts │ │ │ ├── jupiter-v6.layout.ts │ │ │ └── jupiter-va.layout.ts │ │ ├── parser-jupiter-dca.ts │ │ ├── parser-jupiter-limit-v2.ts │ │ ├── parser-jupiter-limit.ts │ │ ├── parser-jupiter-va.ts │ │ └── parser-jupiter.ts │ ├── meteora │ │ ├── index.ts │ │ ├── liquidity-meteora-damm-v2.ts │ │ ├── liquidity-meteora-dlmm.ts │ │ ├── liquidity-meteora-pools.ts │ │ ├── parser-meteora-liquidity-base.ts │ │ └── parser-meteora.ts │ ├── moonshot │ │ ├── index.ts │ │ └── parser-moonshot.ts │ ├── orca │ │ ├── index.ts │ │ ├── parser-orca-liquidity.ts │ │ └── parser-orca.ts │ ├── pumpfun │ │ ├── index.ts │ │ ├── parser-pumpfun-event.ts │ │ ├── parser-pumpfun.ts │ │ ├── parser-pumpswap-event.ts │ │ ├── parser-pumpswap-liquidity.ts │ │ ├── parser-pumpswap.ts │ │ └── util.ts │ └── raydium │ │ ├── index.ts │ │ ├── layouts │ │ ├── raydium-lcp-create.layout.ts │ │ └── raydium-lcp-trade.layout.ts │ │ ├── liquidity-raydium-cl.ts │ │ ├── liquidity-raydium-cpmm.ts │ │ ├── liquidity-raydium-v4.ts │ │ ├── parser-raydium-launchpad-event.ts │ │ ├── parser-raydium-launchpad.ts │ │ ├── parser-raydium-liquidity-base.ts │ │ ├── parser-raydium-logs.ts │ │ ├── parser-raydium.ts │ │ └── util.ts ├── transaction-adapter.ts ├── transaction-utils.ts ├── transfer-compiled-utils.ts ├── transfer-utils.ts ├── types │ ├── boopfun.ts │ ├── common.ts │ ├── index.ts │ ├── jupiter.ts │ ├── pool.ts │ ├── pumpfun.ts │ ├── pumpswap.ts │ ├── raydium.ts │ └── trade.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js','**/*.test.ts'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | "max-len": ["error", { 25 | code: 120, 26 | tabWidth: 2, 27 | ignoreComments: true, 28 | ignoreUrls: true, 29 | ignoreStrings: true, 30 | ignoreTemplateLiterals: true, 31 | ignoreRegExpLiterals: true 32 | }] 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "always", 13 | "proseWrap": "preserve", 14 | "htmlWhitespaceSensitivity": "css", 15 | "vueIndentScriptAndStyle": false, 16 | "endOfLine": "lf", 17 | "embeddedLanguageFormatting": "auto", 18 | "singleAttributePerLine": false 19 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Jest Tests", 8 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 9 | "args": [ 10 | "--runInBand", 11 | "--no-cache" 12 | ], 13 | "cwd": "${workspaceFolder}", 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen", 16 | "env": { 17 | "NODE_ENV": "test" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Caspod 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 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/constants'; 2 | export * from './src/types'; 3 | export * from './src/utils'; 4 | export * from './src/parsers'; 5 | export * from './src/transaction-adapter'; 6 | export * from './src/transaction-utils'; 7 | export * from './src/transfer-utils'; 8 | export * from './src/transfer-compiled-utils'; 9 | export * from './src/instruction-classifier'; 10 | export * from './src/dex-parser'; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | transform: { 6 | '^.+\\.tsx?$': [ 7 | 'ts-jest', 8 | { 9 | tsconfig: 'tsconfig.json' 10 | } 11 | ] 12 | }, 13 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 15 | collectCoverage: true, 16 | coverageDirectory: 'coverage', 17 | coverageReporters: ['text', 'lcov'], 18 | testTimeout: 30000 19 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-dex-parser", 3 | "version": "2.5.4", 4 | "description": "Solana Dex Transaction Parser", 5 | "author": "cxcx", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "github:cxcx-ai/solana-dex-parser" 10 | }, 11 | "keywords": [ 12 | "solana", 13 | "dex", 14 | "parser", 15 | "swap", 16 | "transaction", 17 | "blockchain", 18 | "liquidity", 19 | "raydium", 20 | "raydiumv4", 21 | "raydium launchpad", 22 | "jupiter", 23 | "moonshot", 24 | "meteora", 25 | "orca", 26 | "pumpfun", 27 | "pumpswap", 28 | "boopfun", 29 | "okx" 30 | ], 31 | "main": "dist/index.js", 32 | "types": "dist/index.d.ts", 33 | "files": [ 34 | "dist/**/*.js", 35 | "dist/**/*.d.ts", 36 | "dist/**/*.js.map", 37 | "!dist/**/*.test.*", 38 | "!dist/**/__tests__/**" 39 | ], 40 | "scripts": { 41 | "build": "tsc", 42 | "pretest": "npm run build", 43 | "prepublishOnly": "npm run build", 44 | "test": "jest", 45 | "lint": "eslint src --ext .ts", 46 | "format": "prettier --write \"src/**/*.ts\"" 47 | }, 48 | "dependencies": { 49 | "@solana/web3.js": "^1.87.0", 50 | "dotenv": "^16.4.7" 51 | }, 52 | "devDependencies": { 53 | "@types/bs58": "^4.0.4", 54 | "@types/jest": "^29.5.0", 55 | "@types/json-bigint": "^1.0.4", 56 | "@types/node": "^20.0.0", 57 | "@typescript-eslint/eslint-plugin": "^6.0.0", 58 | "@typescript-eslint/parser": "^6.0.0", 59 | "eslint": "^8.0.0", 60 | "eslint-config-prettier": "^9.0.0", 61 | "eslint-plugin-prettier": "^5.0.0", 62 | "jest": "^29.5.0", 63 | "json-bigint": "^1.0.0", 64 | "prettier": "^3.0.0", 65 | "ts-jest": "^29.1.0", 66 | "typescript": "^5.0.0" 67 | }, 68 | "overrides": { 69 | "bs58": "4.0.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/__tests__/grpc-block.test.ts: -------------------------------------------------------------------------------- 1 | import { DexParser } from "../dex-parser"; 2 | import { blockSubscribe } from "./grpc-block.test.case"; 3 | 4 | describe('Parser', () => { 5 | it("grpc-block", async () => { 6 | const parser = new DexParser(); 7 | const block = blockSubscribe.block; 8 | block.transactions.forEach((tx, idx) => { 9 | 10 | if (tx) { 11 | const result = parser.parseAll({ 12 | ...tx!, 13 | slot: Number(block.slot), 14 | blockTime: Number(block.blockTime.timestamp) 15 | } as any 16 | ); 17 | 18 | console.log(`tx-${tx.signature} > index:${idx}`, JSON.stringify(result, null, 2)); 19 | } 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/grpc-tx.test.ts: -------------------------------------------------------------------------------- 1 | import { DexParser } from "../dex-parser"; 2 | import { tx } from "./grpc-tx.test.case"; 3 | 4 | const rs = new DexParser().parseAll(tx.transaction.transaction as any); 5 | 6 | console.log(rs, JSON.stringify(rs, null, 2)); -------------------------------------------------------------------------------- /src/__tests__/liquidity-meteora-pools.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | 5 | dotenv.config(); 6 | 7 | const tests = { 8 | CREATE: [ 9 | { 10 | signature: '2GWLwbEjyR7moFYK5JfapbsDBrBz3298BVWsAebhUECPaXjLbTZ6DkbEN34BF57jdGot7GkwDnrzszFB3H9AJxmS', 11 | type: 'CREATE', 12 | desc: 'Meteora Pools Program: initializePermissionlessConstantProductPoolWithConfig', 13 | name: 'STRIKE', 14 | poolId: 'BCXjm4FfSoquZQJV5Wcje1g1pSHW2hFMU9wDE98Nyatb', 15 | token0Mint: 'STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw', 16 | token0Amount: 100000000, 17 | token1Mint: 'So11111111111111111111111111111111111111112', 18 | token1Amount: 2740, 19 | }, 20 | ], 21 | ADD: [ 22 | { 23 | signature: 'LaocVd6PpfdH1KTdQuRTf5WwnUzmyf3gdAy16xro747nzrhpgXg1oxFrpgBk31tPh24ksVAyiSkNW7vncoKTGyH', 24 | type: 'ADD', 25 | desc: 'Meteora Pools Program: addBalanceLiquidity', 26 | name: 'STRIKE', 27 | poolId: 'BCXjm4FfSoquZQJV5Wcje1g1pSHW2hFMU9wDE98Nyatb', 28 | token0Mint: 'STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw', 29 | token0Amount: 720.780405, 30 | token1Mint: 'So11111111111111111111111111111111111111112', 31 | token1Amount: 0.029097874, 32 | }, 33 | ], 34 | REMOVE: [ 35 | { 36 | signature: '2xEAewTjtSHgpEHHzaNjHiuoMHNZQXz5vySXHCSL8omujjvaxq9JsfGWjusz43ndmzcu5riESKm1UH4riWDX9v1v', 37 | type: 'REMOVE', 38 | desc: ' Meteora Pools Program: removeBalanceLiquidity', 39 | name: 'STRIKE', 40 | poolId: 'BCXjm4FfSoquZQJV5Wcje1g1pSHW2hFMU9wDE98Nyatb', 41 | token0Mint: 'STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw', 42 | token0Amount: 14938.609562, 43 | token1Mint: 'So11111111111111111111111111111111111111112', 44 | token1Amount: 0.578534516, 45 | }, 46 | ], 47 | }; 48 | 49 | describe('Liquidity', () => { 50 | let connection: Connection; 51 | beforeAll(async () => { 52 | // Initialize connection 53 | const rpcUrl = process.env.SOLANA_RPC_URL; 54 | if (!rpcUrl) { 55 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 56 | } 57 | connection = new Connection(rpcUrl); 58 | }); 59 | 60 | describe('Meteora Pools', () => { 61 | Object.values(tests) 62 | .flat() 63 | .forEach((test) => { 64 | it(`${test.type} > ${test.name} > ${test.desc} `, async () => { 65 | const tx = await connection.getTransaction(test.signature, { 66 | maxSupportedTransactionVersion: 0, 67 | }); 68 | if (!tx) throw new Error('Transaction not found'); 69 | const parser = new DexParser(); 70 | const events = parser.parseLiquidity(tx); 71 | expect(events.length).toEqual(1); 72 | expect(events[0].type).toEqual(test.type); 73 | expect(events[0].poolId).toEqual(test.poolId); 74 | expect(events[0].token0Mint).toEqual(test.token0Mint); 75 | expect(events[0].token0Amount).toEqual(test.token0Amount); 76 | expect(events[0].token1Mint).toEqual(test.token1Mint); 77 | expect(events[0].token1Amount).toEqual(test.token1Amount); 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/__tests__/liquidity-orca.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | 5 | dotenv.config(); 6 | 7 | const tests = { 8 | ADD: [ 9 | { 10 | signature: '3mZcyeDJysgs79nLcvtN4XQ6iepyERqG93P2F2ZYgUX4ZF1Yr1XFBMKR8DHd7z4gN2EmvAqMc3KhQTQpGMbtvhF7', 11 | type: 'ADD', 12 | desc: ' Whirlpools Program: increaseLiquidity', 13 | name: 'JUP', 14 | poolId: 'C1MgLojNLWBKADvu9BHdtgzz1oZX4dZ5zGdGcgvvW8Wz', 15 | token0Mint: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', 16 | token0Amount: 6931.015285, 17 | token1Mint: 'So11111111111111111111111111111111111111112', 18 | token1Amount: 45.407996732, 19 | }, 20 | { 21 | signature: '4Kv6gQgdSsCPSxRApiCNMHFE1dKKGVugrJTzdzSYX5a2aXho4o7jaQDSHLH3RTsr5aVwpkzWL1o5mSCyDtHeZKZr', 22 | type: 'ADD', 23 | desc: 'Whirlpools Program: increaseLiquidityV2', 24 | name: 'STRIKE', 25 | poolId: 'Djf5NYkwhdipTW3ZScPeUkjf7BLtDdxLvEGtNyWMtw3d', 26 | token0Mint: 'STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw', 27 | token0Amount: 2000000, 28 | token1Mint: 'So11111111111111111111111111111111111111112', 29 | token1Amount: 6.315579644, 30 | }, 31 | ], 32 | REMOVE: [ 33 | { 34 | signature: '23zkGAorUC3aHSk7zJYiUvvs6gEXPPzp8xRiWfACzkrqBEaQrKiH9QCgrmwSTD6hxKKEjryEGbEvurt6xSBpuBMC', 35 | type: 'REMOVE', 36 | desc: 'Whirlpools Program: decreaseLiquidity', 37 | name: 'JUP', 38 | poolId: 'C1MgLojNLWBKADvu9BHdtgzz1oZX4dZ5zGdGcgvvW8Wz', 39 | token0Mint: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', 40 | token0Amount: 106.470976, 41 | token1Mint: 'So11111111111111111111111111111111111111112', 42 | token1Amount: 0.475676716, 43 | }, 44 | ], 45 | }; 46 | 47 | describe('Liquidity', () => { 48 | let connection: Connection; 49 | beforeAll(async () => { 50 | // Initialize connection 51 | const rpcUrl = process.env.SOLANA_RPC_URL; 52 | if (!rpcUrl) { 53 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 54 | } 55 | connection = new Connection(rpcUrl); 56 | }); 57 | 58 | describe('Orca', () => { 59 | Object.values(tests) 60 | .flat() 61 | .forEach((test) => { 62 | it(`${test.type} > ${test.name} > ${test.desc} `, async () => { 63 | const tx = await connection.getTransaction(test.signature, { 64 | maxSupportedTransactionVersion: 0, 65 | }); 66 | if(!tx) throw new Error('Transaction not found'); 67 | const parser = new DexParser(); 68 | const events = parser.parseLiquidity(tx); 69 | expect(events.length).toEqual(1); 70 | expect(events[0].type).toEqual(test.type); 71 | expect(events[0].poolId).toEqual(test.poolId); 72 | expect(events[0].token0Mint).toEqual(test.token0Mint); 73 | expect(events[0].token0Amount).toEqual(test.token0Amount); 74 | expect(events[0].token1Mint).toEqual(test.token1Mint); 75 | expect(events[0].token1Amount).toEqual(test.token1Amount); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/__tests__/liquidity-raydium-cpmm.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | 5 | dotenv.config(); 6 | 7 | const tests = { 8 | CREATE: [ 9 | { 10 | signature: 'xZmKodPHYxesDJzPLHfjqG4VvKJcQpwuo3fTa2T64LY9nePfAa97xtSmzCvSWJ5EFzYjAURLD666zqV8oJL8kTp', 11 | type: 'CREATE', 12 | desc: 'Raydium CPMM: initialize', 13 | name: 'IMG', 14 | poolId: 'CXgcuECqdaBpvJWH5cwEir9Y5FY9SKTjhGutMc95bGy3', 15 | token0Mint: 'znv3FZt2HFAvzYf5LxzVyryh3mBXWuTRRng25gEZAjh', 16 | token0Amount: 1000000000, 17 | token1Mint: 'So11111111111111111111111111111111111111112', 18 | token1Amount: 20, 19 | }, 20 | { 21 | signature: '55JBLGRP6Zcd4t8gxsQ27EbLN3kzzMMnwxA9iD1CUTsax2C2cJLNbW3nuRg1gCH56ojJ4YNpRcuzpDL1PbnRvh18', 22 | type: 'CREATE', 23 | desc: 'Raydium CPMM: initialize', 24 | name: 'TRUMP', 25 | poolId: 'HKuJrP5tYQLbEUdjKwjgnHs2957QKjR2iWhJKTtMa1xs', 26 | token0Mint: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', 27 | token0Amount: 423.320786, 28 | token1Mint: 'So11111111111111111111111111111111111111112', 29 | token1Amount: 0.352896784, 30 | }, 31 | ], 32 | ADD: [ 33 | { 34 | signature: '2uVjffn9bwatwRnGZYRpJk5fhxTcGcGfvtc971FBdt1CgUhxESrG8qPN3DiahgaLDULC4JTqc1whSCfvZ5gzJRmL', 35 | type: 'ADD', 36 | desc: 'Raydium CPMM: deposit', 37 | name: 'IMG', 38 | poolId: 'CXgcuECqdaBpvJWH5cwEir9Y5FY9SKTjhGutMc95bGy3', 39 | token0Mint: 'znv3FZt2HFAvzYf5LxzVyryh3mBXWuTRRng25gEZAjh', 40 | token0Amount: 928.616834, 41 | token1Mint: 'So11111111111111111111111111111111111111112', 42 | token1Amount: 0.064818705, 43 | }, 44 | { 45 | signature: '38Bf6HotXPuGcErvexLS4tCQVnLSuD1PtvhgNuCmhbbNwwDKXNSw8Gc6MQwgWwpU5FNUJwGac1ziSS6kKDJdW1Rr', 46 | type: 'ADD', 47 | desc: 'Raydium CPMM: deposit', 48 | name: 'TRUMP', 49 | poolId: 'HKuJrP5tYQLbEUdjKwjgnHs2957QKjR2iWhJKTtMa1xs', 50 | token0Mint: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', 51 | token0Amount: 972.595817, 52 | token1Mint: 'So11111111111111111111111111111111111111112', 53 | token1Amount: 95.855640586, 54 | }, 55 | ], 56 | REMOVE: [ 57 | { 58 | signature: '5sYGW9tkSuh4UHUNGLwtLRSuAGHTTKQMqgaGtF7jB4r8EzADkVmq3nPfiWiQ7UhPn4vj2bpqNXvcKpqgMX1uWc97', 59 | type: 'REMOVE', 60 | desc: 'Raydium CPMM: withdraw', 61 | name: 'IMG', 62 | poolId: 'CXgcuECqdaBpvJWH5cwEir9Y5FY9SKTjhGutMc95bGy3', 63 | token0Mint: 'znv3FZt2HFAvzYf5LxzVyryh3mBXWuTRRng25gEZAjh', 64 | token0Amount: 1735.661744, 65 | token1Mint: 'So11111111111111111111111111111111111111112', 66 | token1Amount: 0.03501825, 67 | }, 68 | { 69 | signature: '5eVe9LMAHgz6Ze7VrUn1XHgoaWJXinM2uoEvRciVoJsADCDc8v4HrewQorp4JUx3jAzBSw5p3RrSAYFqd95udWxR', 70 | type: 'REMOVE', 71 | desc: 'Raydium CPMM: withdraw', 72 | name: 'TRUMP', 73 | poolId: 'HKuJrP5tYQLbEUdjKwjgnHs2957QKjR2iWhJKTtMa1xs', 74 | token0Mint: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', 75 | token0Amount: 977.537752, 76 | token1Mint: 'So11111111111111111111111111111111111111112', 77 | token1Amount: 95.417034698, 78 | }, 79 | ], 80 | }; 81 | 82 | describe('Liquidity', () => { 83 | let connection: Connection; 84 | beforeAll(async () => { 85 | // Initialize connection 86 | const rpcUrl = process.env.SOLANA_RPC_URL; 87 | if (!rpcUrl) { 88 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 89 | } 90 | connection = new Connection(rpcUrl); 91 | }); 92 | 93 | describe('Raydium CPMM', () => { 94 | Object.values(tests) 95 | .flat() 96 | .forEach((test) => { 97 | it(`${test.type} > ${test.name} > ${test.desc} `, async () => { 98 | const tx = await connection.getTransaction(test.signature, { 99 | maxSupportedTransactionVersion: 0, 100 | }); 101 | if(!tx) throw new Error('Transaction not found'); 102 | const parser = new DexParser(); 103 | const events = parser.parseLiquidity(tx); 104 | expect(events.length).toEqual(1); 105 | expect(events[0].type).toEqual(test.type); 106 | expect(events[0].poolId).toEqual(test.poolId); 107 | expect(events[0].token0Mint).toEqual(test.token0Mint); 108 | expect(events[0].token0Amount).toEqual(test.token0Amount); 109 | expect(events[0].token1Mint).toEqual(test.token1Mint); 110 | expect(events[0].token1Amount).toEqual(test.token1Amount); 111 | }); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/__tests__/parse-jupiter-dca.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | 5 | dotenv.config(); 6 | 7 | const tests = [ 8 | { 9 | "type": "OpenDca", 10 | "programId": "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M", 11 | "info": { 12 | "source": "E4qe2XfHQ5AKVdPU33MugNPD2T6RgnzqH2QFP9b7J1Wy", 13 | "destination": "tSJjJ8TtRTe4KZVCezs1vNBw5WS9AFZkA5gJqTtgYvr", 14 | "mint": "So11111111111111111111111111111111111111112", 15 | "tokenAmount": { 16 | "amount": "-509072160", 17 | "uiAmount": -0.50907216, 18 | "decimals": 9 19 | }, 20 | "sourceBalance": { 21 | "amount": "490937840", 22 | "uiAmount": 0.49093784, 23 | "decimals": 9 24 | }, 25 | "sourcePreBalance": { 26 | "amount": "1000010000", 27 | "uiAmount": 1.00001, 28 | "decimals": 9 29 | } 30 | }, 31 | "idx": "5-0", 32 | "timestamp": 1733744710, 33 | "signature": "4PvkHqhgTJa61cChu52gCyPcBK1rXGoKSJk4vRStXteXNTvuw7o9VoH5aqAgoYKGjQWSVdNvpwfEbDvHi7tZQZqw" 34 | }, 35 | { 36 | "type": "CloseDca", 37 | "programId": "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M", 38 | "info": { 39 | "destination": "E4qe2XfHQ5AKVdPU33MugNPD2T6RgnzqH2QFP9b7J1Wy", 40 | "mint": "So11111111111111111111111111111111111111112", 41 | "source": "tSJjJ8TtRTe4KZVCezs1vNBw5WS9AFZkA5gJqTtgYvr", 42 | "tokenAmount": { 43 | "amount": "256931275", 44 | "uiAmount": 0.256931275, 45 | "decimals": 9 46 | }, 47 | "destinationBalance": { 48 | "amount": "747869515", 49 | "uiAmount": 0.747869515, 50 | "decimals": 9 51 | }, 52 | "destinationPreBalance": { 53 | "amount": "490938240", 54 | "uiAmount": 0.49093824, 55 | "decimals": 9 56 | } 57 | }, 58 | "idx": "0-0", 59 | "timestamp": 1733744880, 60 | "signature": "2oSCggfbBGsATMvzB5CBh4aF1r7E78dox6Hc7eTkt9FW7RCKAN7stZVhTLHUA2pBadDFEBpYsgmJBmCKu5rX3wez" 61 | }, 62 | { 63 | "type": "CloseDca", 64 | "programId": "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M", 65 | "info": { 66 | "destination": "E4qe2XfHQ5AKVdPU33MugNPD2T6RgnzqH2QFP9b7J1Wy", 67 | "mint": "So11111111111111111111111111111111111111112", 68 | "source": "HasGZUp8RFFjPhAzb9eBN12CPnj7ZpFXjZqPXD6cfptU", 69 | "tokenAmount": { 70 | "amount": "6933880", 71 | "uiAmount": 0.00693388, 72 | "decimals": 9 73 | }, 74 | "destinationBalance": { 75 | "amount": "695568225", 76 | "uiAmount": 0.695568225, 77 | "decimals": 9 78 | }, 79 | "destinationPreBalance": { 80 | "amount": "688634345", 81 | "uiAmount": 0.688634345, 82 | "decimals": 9 83 | } 84 | }, 85 | "idx": "1-0", 86 | "timestamp": 1733746114, 87 | "signature": "42A1smk5Trd8cdU7Vz19E17xtWXFsypBzMLkRE6UdccHA2nyRohZoT6GNXAzviuFGLfUT9ANhxHroTFQMkxJvNCU" 88 | }, { 89 | "type": "OpenDca", 90 | "programId": "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M", 91 | "info": { 92 | "source": "ByBxpqTdJUQt5NpnJJp9GzovBmnT3hmMx1CqhtRAKaK1", 93 | "destination": "CfBLHEJkCUqn5LrST6ptAG96UrkjB5pfZFc98LUQUY3g", 94 | "mint": "So11111111111111111111111111111111111111112", 95 | "tokenAmount": { 96 | "amount": "-11985880", 97 | "uiAmount": -0.01198588, 98 | "decimals": 9 99 | }, 100 | "sourceBalance": { 101 | "amount": "1065026245", 102 | "uiAmount": 1.065026245, 103 | "decimals": 9 104 | }, 105 | "sourcePreBalance": { 106 | "amount": "1077012125", 107 | "uiAmount": 1.077012125, 108 | "decimals": 9 109 | } 110 | }, 111 | "idx": "2-0", 112 | "timestamp": 1739688566, 113 | "signature": "4vL1piuminnprE9PJ7eXkxAnsvH2fQTxSya1yBdAkx6mq5M9DeeZiLwsTP565pYXoQxQziPwRzL53MDAybRVsD2A" 114 | }, 115 | { 116 | "type": "withdraw", 117 | "programId": "VALaaymxQh2mNy2trH9jUqHT1mTow76wpTcGmSWSwJe", 118 | "info": { 119 | "destination": "E4qe2XfHQ5AKVdPU33MugNPD2T6RgnzqH2QFP9b7J1Wy", 120 | "mint": "So11111111111111111111111111111111111111112", 121 | "source": "7FFJfHZQ3ZSdMf3ZHNVMnTcQuWnsbVMQaJYsEWa79xGz", 122 | "tokenAmount": { 123 | "amount": "64248301", 124 | "decimals": 9, 125 | "uiAmount": 0.064248301 126 | }, 127 | "sourceBalance": { 128 | "amount": "0", 129 | "uiAmount": 0, 130 | "decimals": 9 131 | }, 132 | "sourcePreBalance": { 133 | "amount": "0", 134 | "uiAmount": 0, 135 | "decimals": 9 136 | }, 137 | "destinationBalance": { 138 | "amount": "249535139", 139 | "uiAmount": 0.249535139, 140 | "decimals": 9 141 | }, 142 | "destinationPreBalance": { 143 | "amount": "178029324", 144 | "uiAmount": 0.178029324, 145 | "decimals": 9 146 | } 147 | }, 148 | "idx": "0-9", 149 | "timestamp": 1733762197, 150 | "signature": "2BhdRHDAtPY4Cb8qSFHZTeQXKKenTQjuoCGCBe6y45pPbEDKKPRmLo2rxiTEssU7wXMJFztRWcUQUF5s8E8XD4Wi" 151 | } 152 | ]; 153 | 154 | describe('Jupiter DCA Transfers', () => { 155 | let connection: Connection; 156 | beforeAll(async () => { 157 | // Initialize connection 158 | const rpcUrl = process.env.SOLANA_RPC_URL; 159 | if (!rpcUrl) { 160 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 161 | } 162 | connection = new Connection(rpcUrl); 163 | }); 164 | 165 | Object.values(tests) 166 | .flat() 167 | .forEach((test) => { 168 | it(`${test.type} > ${test.programId} > ${test.signature} `, async () => { 169 | const tx = await connection.getTransaction(test.signature, { 170 | maxSupportedTransactionVersion: 0, 171 | }); 172 | if (!tx) throw new Error('Transaction not found'); 173 | const parser = new DexParser(); 174 | const { transfers } = parser.parseAll(tx, { tryUnknowDEX: false }); 175 | const transfer = transfers[0]; 176 | console.log('transfers', JSON.stringify(transfers, null, 2)); 177 | expect(transfer.type).toEqual(test.type); 178 | expect(transfer.programId).toEqual(test.programId); 179 | expect(transfer.info.mint).toEqual(test.info.mint); 180 | expect(transfer.info.tokenAmount.amount).toEqual(test.info.tokenAmount.amount); 181 | expect(transfer.info.tokenAmount.uiAmount).toEqual(test.info.tokenAmount.uiAmount); 182 | expect(transfer.info.tokenAmount.decimals).toEqual(test.info.tokenAmount.decimals); 183 | expect(transfer.info.source).toEqual(test.info.source); 184 | expect(transfer.info.destination).toEqual(test.info.destination); 185 | 186 | expect(transfer.timestamp).toEqual(test.timestamp); 187 | expect(transfer.signature).toEqual(test.signature); 188 | expect(transfer.idx).toEqual(test.idx); 189 | }); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /src/__tests__/parser-block.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import * as fs from 'fs'; 4 | import { DexParser } from '../dex-parser'; 5 | 6 | dotenv.config(); 7 | 8 | describe('Parser', () => { 9 | let connection: Connection; 10 | beforeAll(async () => { 11 | // Initialize connection 12 | const rpcUrl = process.env.SOLANA_RPC_URL; 13 | if (!rpcUrl) { 14 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 15 | } 16 | connection = new Connection(rpcUrl, { 17 | commitment: 'confirmed', 18 | }); 19 | }); 20 | 21 | describe('Dex', () => { 22 | 23 | describe('parseTransaction', () => { 24 | it("block", async () => { 25 | const parser = new DexParser(); 26 | 27 | const s1 = Date.now(); 28 | const block = await connection.getBlock(337441395, { 29 | commitment: 'confirmed', 30 | maxSupportedTransactionVersion: 0, 31 | transactionDetails: 'full', 32 | }) 33 | const s2 = Date.now(); 34 | if (!block) { 35 | throw new Error("Block not found"); 36 | } 37 | const ts: any[] = [], liqs: any[] = []; 38 | console.log('>>>', block.transactions.length); 39 | block.transactions.forEach((tx,idx) => { 40 | // if (tx.meta?.err) { 41 | // return; 42 | // } 43 | if(idx==1504 || idx==1264) { 44 | console.log('>K>', tx.transaction.signatures[0]); 45 | // fs.writeFileSync(`./src/__tests__/tx-${tx.transaction.signatures[0]}.json`, JSON.stringify(tx, null, 2)); 46 | } 47 | // const { trades, liquidities } = parser.parseAll({ ...tx!, slot: (block.parentSlot + 1), blockTime: block.blockTime } as any, { tryUnknowDEX: false }); 48 | 49 | // ts.push(...trades); 50 | // liqs.push(...liquidities); 51 | }) 52 | // const s3 = Date.now(); 53 | 54 | // console.log(`Fetch block: ${(s2 - s1) / 1000} s > Parser: ${(s3 - s2) / 1000} s > Hits: ${ts.length + liqs.length} / ${block.transactions.length}`); 55 | 56 | }); 57 | 58 | // it("json-block", async () => { 59 | // const parser = new DexParser(); 60 | // const data = fs.readFileSync("./src/__tests__/tx-55gZaGoRSov1S9yQE3azRhteZrxwswk9YWW4E7z3XBBaLL9VFdoMnJTYfcixGXJTpp2yGmuCznqw8tj78E9po8UC.json", { encoding: "utf8" }); 61 | // const tx = JSON.parse(data); 62 | // const { trades, liquidities } = parser.parseAll(tx as any, { tryUnknowDEX: false }); 63 | // console.log('trades', trades); 64 | // console.log('liquidities', liquidities); 65 | // }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/__tests__/parser-boopfun.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | import { BoopfunEventParser } from '../parsers'; 5 | import { TransactionAdapter } from '../transaction-adapter'; 6 | import { TransactionUtils } from '../transaction-utils'; 7 | 8 | dotenv.config(); 9 | 10 | describe('Parser', () => { 11 | let connection: Connection; 12 | beforeAll(async () => { 13 | // Initialize connection 14 | const rpcUrl = process.env.SOLANA_RPC_URL; 15 | if (!rpcUrl) { 16 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 17 | } 18 | connection = new Connection(rpcUrl); 19 | }); 20 | 21 | describe('Boopfun', () => { 22 | it('boopfun events', async () => { 23 | const tx = await connection.getTransaction( 24 | '4YxPRX9p3rdN7H6cbjC6pKqyQfTu589nkVH3PqxFQyaoP5cZxEgfLK2SJmHFzUTXoJceGtxC8eGXeDqFjLE2UycH', // create & complete 25 | // "28S2MakapF1zTrnqYHdMxdnN9uqAfKV2fa5ez9HpE466L3xWz8AXwsz4eKXXnpvX8p49Ckbp26doG5fgW5f6syk9", // buy 26 | // "3Lyh3wAPkcLGKydqT6VdjMsorLUJqEuDeppxh79sQjGxuLiMqMgB75aSJyZsM3y3jJRqdLJYZhNUBaLeKQ8vL4An", // sell 27 | // "3yLq2ECkAtzFrvAH3V5nhQirZMNRj28EXfFifBYoeJmfAhutVfjqVnjewAExkSaz9ENfUXf511T5zSMfnFiVj1Jy", // complete 28 | { 29 | maxSupportedTransactionVersion: 0, 30 | } 31 | ); 32 | if(!tx) throw new Error('Transaction not found'); 33 | 34 | // parse Boopfun trades (buy, sell) 35 | const dexParser = new DexParser(); 36 | const result = dexParser.parseAll(tx); 37 | console.log('result', JSON.stringify(result, null, 2)); 38 | 39 | // parse Boopfun events (create, buy, sell, complete) 40 | const adapter = new TransactionAdapter(tx); 41 | const utils = new TransactionUtils(adapter); 42 | const transferActions = utils.getTransferActions(); 43 | const parser = new BoopfunEventParser(adapter,transferActions); 44 | const events = parser.processEvents(); 45 | 46 | console.log('events', events); 47 | expect(events.length).toBeGreaterThan(0); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/parser-pumpfun.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { PumpfunEventParser } from '../parsers'; 4 | import { TransactionAdapter } from '../transaction-adapter'; 5 | 6 | dotenv.config(); 7 | 8 | describe('Parser', () => { 9 | let connection: Connection; 10 | beforeAll(async () => { 11 | // Initialize connection 12 | const rpcUrl = process.env.SOLANA_RPC_URL; 13 | if (!rpcUrl) { 14 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 15 | } 16 | connection = new Connection(rpcUrl); 17 | }); 18 | 19 | describe('Pumpfun', () => { 20 | it('pumpfun events', async () => { 21 | const tx = await connection.getTransaction( 22 | '2CYBHseAoZy1WHTNnVj1cTV9gnDeXE5WHAq6xXP62RL6h54uN1ft1AM1r5VkhMXYtav54CaP4nbR2rDe5TZdPzbR', // create & complete 23 | // "4Cod1cNGv6RboJ7rSB79yeVCR4Lfd25rFgLY3eiPJfTJjTGyYP1r2i1upAYZHQsWDqUbGd1bhTRm1bpSQcpWMnEz", // create 24 | // "v8s37Srj6QPMtRC1HfJcrSenCHvYebHiGkHVuFFiQ6UviqHnoVx4U77M3TZhQQXewXadHYh5t35LkesJi3ztPZZ", // complete 25 | { 26 | maxSupportedTransactionVersion: 0, 27 | } 28 | ); 29 | if(!tx) throw new Error('Transaction not found'); 30 | const parser = new PumpfunEventParser(new TransactionAdapter(tx)); 31 | const events = parser.processEvents(); 32 | console.log(events); 33 | expect(events.length).toBeGreaterThan(1); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/parser-raydium-lcp.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { RaydiumLaunchpadEventParser } from '../parsers'; 4 | import { TransactionAdapter } from '../transaction-adapter'; 5 | import { ConstantCurve, PoolStatus, RaydiumLCPCompleteEvent, RaydiumLCPCreateEvent, RaydiumLCPTradeEvent, TradeDirection } from '../types'; 6 | 7 | dotenv.config(); 8 | 9 | describe('Parser', () => { 10 | let connection: Connection; 11 | beforeAll(async () => { 12 | // Initialize connection 13 | const rpcUrl = process.env.SOLANA_RPC_URL; 14 | if (!rpcUrl) { 15 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 16 | } 17 | connection = new Connection(rpcUrl); 18 | }); 19 | 20 | describe('RaydiumLaunchpad', () => { 21 | it('create', async () => { 22 | const tx = await connection.getTransaction( 23 | '4x8k2aQKevA8yuCVX1V8EaH2GBqdbZ1dgYxwtkwZJ7SmCQeng7CCs17AvyjFv6nMoUkBgpBwLHAABdCxGHbAWxo4', // create & complete 24 | { 25 | maxSupportedTransactionVersion: 0, 26 | } 27 | ); 28 | if (!tx) throw new Error('Transaction not found'); 29 | const parser = new RaydiumLaunchpadEventParser(new TransactionAdapter(tx)); 30 | const events = parser.processEvents(); 31 | 32 | const data = events[1].data as RaydiumLCPCreateEvent; 33 | const buy = events[0].data as RaydiumLCPTradeEvent; 34 | console.log('create events', events); 35 | // create 36 | expect(data.poolState).toEqual("CPTNvVYT7qCzX3HnRRtSRAFpMipVgSP3eynXrW9p9YgD"); 37 | expect(data.creator).toEqual("J88snVaNTCW7T6saPvAmYDmjnhPiSpkw8uJ8FFCyfcGA"); 38 | expect(data.config).toEqual("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"); 39 | expect(data.baseMintParam.symbol).toEqual("TOAST"); 40 | expect(data.curveParam.variant).toEqual("Constant"); 41 | expect(data.curveParam.data.supply.toString()).toEqual("1000000000000000"); 42 | expect((data.curveParam.data as ConstantCurve).totalBaseSell.toString()).toEqual("793100000000000"); 43 | expect(data.vestingParam.totalLockedAmount.toString()).toEqual("0"); 44 | expect(data.vestingParam.cliffPeriod.toString()).toEqual("0"); 45 | expect(data.vestingParam.unlockPeriod.toString()).toEqual("0"); 46 | // buy 47 | expect(buy.poolState).toEqual("CPTNvVYT7qCzX3HnRRtSRAFpMipVgSP3eynXrW9p9YgD"); 48 | expect(buy.amountIn.toString()).toEqual("10000000"); 49 | expect(buy.amountOut.toString()).toEqual("353971575213"); 50 | expect(buy.tradeDirection).toEqual(TradeDirection.Buy); 51 | expect(buy.poolStatus).toEqual(PoolStatus.Fund); 52 | 53 | }); 54 | 55 | it('migrate_to_amm', async () => { 56 | const tx = await connection.getTransaction( 57 | '2yD4a9fKXkPvEndSFKwnYUCeHe8yfb6KGdVKopc8ZJDwqQZzxEB42hyspbmUYAp2MofcdCxD8YduZdepHsC2cMFd', // create & complete 58 | { 59 | maxSupportedTransactionVersion: 0, 60 | } 61 | ); 62 | if (!tx) throw new Error('Transaction not found'); 63 | const parser = new RaydiumLaunchpadEventParser(new TransactionAdapter(tx)); 64 | const event = parser.processEvents()[0]; 65 | const data = event.data as RaydiumLCPCompleteEvent; 66 | 67 | expect(data.baseMint).toEqual("GGiHEB7CtBe2pCsotGMBPgTFzFhXm6cjWrnSgNqVUray"); 68 | expect(data.quoteMint).toEqual("So11111111111111111111111111111111111111112"); 69 | expect(data.poolMint).toEqual("J6VesUgku4yr31wA9m2YZKNpoD8iGBiuoMMpEAo7NXU7"); 70 | expect(data.lpMint).toEqual("4hF3cktcf5nXFt8wmNsVVUZwRdcgBvn36gLrud6Ypyc3"); 71 | expect(data.amm).toEqual("RaydiumV4"); 72 | }); 73 | 74 | it('migrate_to_cpswap', async () => { 75 | const tx = await connection.getTransaction( 76 | '2gWHLTb1utduUkZCTo9GZpcCZr7hVPXTJajdoVjMURgVG6eJdKJQY6jF954XN15sSmDvsPCmMD7XSRyofLrQWuFv', // create & complete 77 | { 78 | maxSupportedTransactionVersion: 0, 79 | } 80 | ); 81 | if (!tx) throw new Error('Transaction not found'); 82 | const parser = new RaydiumLaunchpadEventParser(new TransactionAdapter(tx)); 83 | const event = parser.processEvents()[0]; 84 | const data = event.data as RaydiumLCPCompleteEvent; 85 | console.log(event); 86 | expect(data.baseMint).toEqual("Em8DYuvdQ28PNZqSiAvUxjG32XbpFPm9kwu2y5pdTray"); 87 | expect(data.quoteMint).toEqual("So11111111111111111111111111111111111111112"); 88 | expect(data.poolMint).toEqual("9N82SeWs9cFrThpNyU8dngUjRHe9vzVjDnQrgQ115tEy"); 89 | expect(data.lpMint).toEqual("5Jg51sVNevcDeuzoHcfJFGMcYszuWSqSsZuDjiakXuXq"); 90 | expect(data.amm).toEqual("RaydiumCPMM"); 91 | }); 92 | 93 | it('buy_exact_in', async () => { 94 | const tx = await connection.getTransaction( 95 | 'Gi44zBwsd8eUGEVPS1jstts457hKLbm8SSMLrRVHVK2McrhJjosiszb65U1LdrjsF1WfCXoesLMhm8RX3dchx4s', // create & complete 96 | { 97 | maxSupportedTransactionVersion: 0, 98 | } 99 | ); 100 | if (!tx) throw new Error('Transaction not found'); 101 | const parser = new RaydiumLaunchpadEventParser(new TransactionAdapter(tx)); 102 | const event = parser.processEvents()[0]; 103 | const data = event.data as RaydiumLCPTradeEvent; 104 | console.log(event); 105 | expect(data.poolState).toEqual("GeSSWHbFkeYknLX3edkTP3JcsjHRnCJG3SymEkBzaFDo"); 106 | expect(data.amountIn.toString()).toEqual("50000000"); 107 | expect(data.amountOut.toString()).toEqual("353067172960"); 108 | expect(data.tradeDirection).toEqual(TradeDirection.Buy); 109 | expect(data.poolStatus).toEqual(PoolStatus.Fund); 110 | }); 111 | 112 | it('sell_exact_in', async () => { 113 | const tx = await connection.getTransaction( 114 | '36n8GMHRMSyX8kRSgaUfcE5jpjWNWhjAu7YPeYFX2fMVzirJT4YhvYMo4dS5VoCVj5H47qZ8FzSEDLc6ui78HcAh', // create & complete 115 | { 116 | maxSupportedTransactionVersion: 0, 117 | } 118 | ); 119 | if (!tx) throw new Error('Transaction not found'); 120 | const parser = new RaydiumLaunchpadEventParser(new TransactionAdapter(tx)); 121 | const event = parser.processEvents()[0]; 122 | const data = event.data as RaydiumLCPTradeEvent; 123 | console.log(event); 124 | expect(data.poolState).toEqual("7SgAC6oe5jwb58JaK2KMXDnAL7JxnaH1DX5nc6BEp7Ng"); 125 | expect(data.amountIn.toString()).toEqual("26252327418406"); 126 | expect(data.amountOut.toString()).toEqual("744875999"); 127 | expect(data.tradeDirection).toEqual(TradeDirection.Sell); 128 | expect(data.poolStatus).toEqual(PoolStatus.Fund); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/__tests__/parser-trade.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | import { getFinalSwap } from '../utils'; 5 | import JSONbig from 'json-bigint'; // for bigint serialization in json 6 | import fs from 'fs'; 7 | 8 | dotenv.config(); 9 | 10 | describe('Dex Parser', () => { 11 | let connection: Connection; 12 | beforeAll(async () => { 13 | // Initialize connection 14 | const rpcUrl = process.env.SOLANA_RPC_URL; 15 | if (!rpcUrl) { 16 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 17 | } 18 | connection = new Connection(rpcUrl, { 19 | commitment: 'confirmed', 20 | // httpAgent: new https.Agent({ host: '127.0.0.1', port: 7890 }) 21 | }); 22 | }); 23 | 24 | describe('Parse Trades', () => { 25 | const parser = new DexParser(); 26 | 27 | [ 28 | 29 | "2fCNJ8hUhwayV8aZfPYhTUNKoFyVTvzUL53UvyMyk6eiXHqgEzG5PVBnW8h46mW14hDR8cZgDKBD6kdBBj7vcrKC", 30 | // "3874qjiBkmSNk3rRMEst2fAfwSx9jPNNi3sCcFBxETzEYxpPeRnU9emKz26M2x3ttxJGJmjV4ctZziQMFmDgKBkZ", // multiple signers 31 | // "3Dd6Hr9AFFearu8MZ8V3Ukm2dAbWLQ3ZUbxTvfLBw1UtghqSc1mEsrgdcbqVYQrfozTy9wNYaHQoE5FqXqfTvHA", // pumpfun 32 | // "5pBu3T3iguqLpgtKTmhfiik13EruLVKNa28ZMtkrE2hhcM1hM1D7aNn7vgiqQsahFTaw6kiJiPre6suJAJdKrK2y", //pumpswap 33 | // "4YxPRX9p3rdN7H6cbjC6pKqyQfTu589nkVH3PqxFQyaoP5cZxEgfLK2SJmHFzUTXoJceGtxC8eGXeDqFjLE2UycH", //Boopfun 34 | // "4x8k2aQKevA8yuCVX1V8EaH2GBqdbZ1dgYxwtkwZJ7SmCQeng7CCs17AvyjFv6nMoUkBgpBwLHAABdCxGHbAWxo4", // raydium launchpad 35 | // "4WGyuUf65j9ojW6zrKf9zBEQsEfW5WiuKjdh6K2dxQAn7ggMkmT1cn1v9GuFs3Ew1d7oMJGh2z1VNvwdLQqJoC9s" // transfer 36 | ] 37 | .forEach((signature) => { 38 | it(`${signature} `, async () => { 39 | const tx = await connection.getParsedTransaction(signature, { 40 | commitment: 'confirmed', 41 | maxSupportedTransactionVersion: 0, 42 | }); 43 | if (!tx) { throw new Error(`Transaction not found > ${signature}`); } 44 | const { fee, trades, liquidities, transfers, solBalanceChange, tokenBalanceChange, moreEvents } = parser.parseAll(tx); 45 | // fs.writeFileSync(`./src/__tests__/tx-${signature}-parsed.json`, JSON.stringify(tx, null, 2)); 46 | const swap = getFinalSwap(trades); 47 | console.log('fee', fee); 48 | console.log('solBalanceChange', solBalanceChange, 'tokenBalanceChange', tokenBalanceChange); 49 | 50 | console.log('finalSwap', JSON.stringify(swap, null, 2)); 51 | console.log('trades', JSON.stringify(trades, null, 2)); 52 | console.log('liquidity', liquidities); 53 | console.log('transfer', JSON.stringify(transfers, null, 2)); 54 | console.log('moreEvents', JSONbig.stringify(moreEvents, null, 2)); 55 | expect(trades.length + liquidities.length + transfers.length).toBeGreaterThanOrEqual(1); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/__tests__/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import dotenv from 'dotenv'; 3 | import { DexParser } from '../dex-parser'; 4 | import { tests } from './parser.test.case'; 5 | 6 | dotenv.config(); 7 | 8 | describe('Dex Parser', () => { 9 | let connection: Connection; 10 | let fetchTime = 0, processTime = 0; 11 | beforeAll(async () => { 12 | // Initialize connection 13 | const rpcUrl = process.env.SOLANA_RPC_URL; 14 | if (!rpcUrl) { 15 | throw new Error('SOLANA_RPC_URL environment variable is not set'); 16 | } 17 | connection = new Connection(rpcUrl, { 18 | commitment: 'confirmed', 19 | }); 20 | }); 21 | 22 | describe('Parse Trades', () => { 23 | const parser = new DexParser(); 24 | const expectItem = (item: any, test: any) => { 25 | expect(item.type).toEqual(test.type); 26 | expect(item.user).toEqual(test.user); 27 | expect(item.inputToken.mint).toEqual(test.inputToken.mint); 28 | expect(item.inputToken.amount).toEqual(test.inputToken.amount); 29 | expect(item.inputToken.amountRaw / Math.pow(10, item.inputToken.decimals)).toEqual(test.inputToken.amount); 30 | expect(item.inputToken.decimals).toEqual(test.inputToken.decimals); 31 | expect(item.outputToken.mint).toEqual(test.outputToken.mint); 32 | expect(item.outputToken.amount).toEqual(test.outputToken.amount); 33 | expect(item.outputToken.amountRaw / Math.pow(10, item.outputToken.decimals)).toEqual(test.outputToken.amount); 34 | expect(item.outputToken.decimals).toEqual(test.outputToken.decimals); 35 | expect(item.amm).toEqual(test.amm); 36 | expect(item.route).toEqual(test.route); 37 | expect(item.programId).toEqual(test.programId); 38 | expect(item.slot).toEqual(test.slot); 39 | expect(item.timestamp).toEqual(test.timestamp); 40 | expect(item.signature).toEqual(test.signature); 41 | } 42 | 43 | Object.values(tests) 44 | .flat() 45 | // .filter((test: any) => test.test == true) // test only 46 | .forEach((test) => { 47 | it(`${test.type} > ${test.amm} > ${test.signature} `, async () => { 48 | const s1 = Date.now(); 49 | const tx = await connection.getTransaction(test.signature, { 50 | commitment: 'confirmed', 51 | maxSupportedTransactionVersion: 0, 52 | }); 53 | if (!tx) { throw new Error(`Transaction not found > ${test.signature}`); } 54 | const s2 = Date.now(); 55 | fetchTime += s2 - s1; 56 | const s3 = Date.now(); 57 | 58 | const trades = parser.parseTrades(tx); 59 | 60 | const s4 = Date.now(); 61 | processTime += s4 - s3; 62 | // console.log('fetchTime', fetchTime); 63 | // console.log('processTime', processTime); 64 | // console.log('trades', trades); 65 | expect(trades.length).toBeGreaterThanOrEqual(1); 66 | expectItem(trades[0], test); 67 | if (test.items) { 68 | expect(trades.length).toBeGreaterThan(1); 69 | expectItem(trades[1], test.items[0]); 70 | } 71 | 72 | }); 73 | }); 74 | }); 75 | 76 | afterAll(async () => { 77 | console.log(`Fetch time: ${fetchTime / 1000} s > avg: ${(fetchTime) / 1000 / tests.length} s >`, '\n', 78 | `Process time: ${processTime / 1000} s > avg: ${(processTime) / 1000 / tests.length} s >`); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { hexToUint8Array } from '../utils'; 3 | import base58 from 'bs58'; 4 | 5 | dotenv.config(); 6 | 7 | describe('Utils', () => { 8 | describe('Base58', () => { 9 | it('Get discriminator', async () => { 10 | const hex = 11 | //'c1209b3341d69c810e030000003d016400011a64010234640203402c420600000000e953780100000000500000'; // instruction discriminator 12 | '856e4aaf709ff59fdd8d3f6d0b01000095901b040000000000'; // event discriminator 13 | 14 | const data = hexToUint8Array(hex); 15 | 16 | console.log(data.slice(0, 8)); // instruction discriminator 17 | // console.log(data.slice(0, 16)); // event discriminator 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './programId'; 2 | export * from './token'; 3 | export * from './discriminators'; 4 | export * from './instruction-types'; 5 | -------------------------------------------------------------------------------- /src/constants/instruction-types.ts: -------------------------------------------------------------------------------- 1 | export const SPL_TOKEN_INSTRUCTION_TYPES = { 2 | InitializeMint: 0, 3 | InitializeAccount: 1, 4 | InitializeMultisig: 2, 5 | Transfer: 3, 6 | Approve: 4, 7 | Revoke: 5, 8 | SetAuthority: 6, 9 | MintTo: 7, 10 | Burn: 8, 11 | CloseAccount: 9, 12 | FreezeAccount: 10, 13 | ThawAccount: 11, 14 | TransferChecked: 12, 15 | ApproveChecked: 13, 16 | MintToChecked: 14, 17 | BurnChecked: 15, 18 | } as const; 19 | 20 | export const SYSTEM_INSTRUCTION_TYPES = { 21 | CreateAccount: 0, 22 | Assign: 1, 23 | Transfer: 2, 24 | CreateAccountWithSeed: 3, 25 | AdvanceNonceAccount: 4, 26 | WithdrawNonceAccount: 5, 27 | InitializeNonceAccount: 6, 28 | AuthorizeNonceAccount: 7, 29 | Allocate: 8, 30 | AllocateWithSeed: 9, 31 | AssignWithSeed: 10, 32 | TransferWithSeed: 11, 33 | UpgradeNonceAccount: 12, 34 | CreateAccountWithSeedChecked: 13, 35 | CreateIdempotent: 14, 36 | } as const; 37 | -------------------------------------------------------------------------------- /src/constants/token.ts: -------------------------------------------------------------------------------- 1 | // Known token addresses 2 | export const TOKENS = { 3 | NATIVE: '11111111111111111111111111111111', 4 | SOL: 'So11111111111111111111111111111111111111112', // Wrapped SOL 5 | USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 6 | USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 7 | }; 8 | 9 | export const TOKEN_DECIMALS = { 10 | [TOKENS.SOL]: 9, 11 | [TOKENS.USDC]: 6, 12 | [TOKENS.USDT]: 6, 13 | }; 14 | -------------------------------------------------------------------------------- /src/instruction-classifier.ts: -------------------------------------------------------------------------------- 1 | import { SYSTEM_PROGRAMS } from './constants'; 2 | import { TransactionAdapter } from './transaction-adapter'; 3 | import { ClassifiedInstruction } from './types/common'; 4 | 5 | export class InstructionClassifier { 6 | private instructionMap: Map = new Map(); 7 | 8 | constructor(private adapter: TransactionAdapter) { 9 | this.classifyInstructions(); 10 | } 11 | 12 | private classifyInstructions() { 13 | // outer instructions 14 | this.adapter.instructions.forEach((instruction: any, outerIndex: any) => { 15 | const programId = this.adapter.getInstructionProgramId(instruction); 16 | this.addInstruction({ 17 | instruction, 18 | programId, 19 | outerIndex, 20 | }); 21 | }); 22 | 23 | // innerInstructions 24 | const innerInstructions = this.adapter.innerInstructions; 25 | if (innerInstructions) { 26 | innerInstructions.forEach((set) => { 27 | set.instructions.forEach((instruction, innerIndex) => { 28 | const programId = this.adapter.getInstructionProgramId(instruction); 29 | this.addInstruction({ 30 | instruction, 31 | programId, 32 | outerIndex: set.index, 33 | innerIndex, 34 | }); 35 | }); 36 | }); 37 | } 38 | } 39 | 40 | private addInstruction(classified: ClassifiedInstruction) { 41 | if (!classified.programId) return; 42 | 43 | const instructions = this.instructionMap.get(classified.programId) || []; 44 | instructions.push(classified); 45 | this.instructionMap.set(classified.programId, instructions); 46 | } 47 | 48 | public getInstructions(programId: string): ClassifiedInstruction[] { 49 | return this.instructionMap.get(programId) || []; 50 | } 51 | 52 | public getAllProgramIds(): string[] { 53 | return Array.from(this.instructionMap.keys()).filter((it) => !SYSTEM_PROGRAMS.includes(it)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/parsers/base-liquidity-parser.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../transaction-adapter'; 2 | import { TransactionUtils } from '../transaction-utils'; 3 | import { ClassifiedInstruction, PoolEvent, TransferData } from '../types'; 4 | import { getInstructionData } from '../utils'; 5 | 6 | export abstract class BaseLiquidityParser { 7 | protected readonly utils: TransactionUtils; 8 | 9 | constructor( 10 | protected readonly adapter: TransactionAdapter, 11 | protected readonly transferActions: Record, 12 | protected readonly classifiedInstructions: ClassifiedInstruction[] 13 | ) { 14 | this.utils = new TransactionUtils(adapter); 15 | } 16 | 17 | abstract processLiquidity(): PoolEvent[]; 18 | 19 | protected getTransfersForInstruction( 20 | programId: string, 21 | outerIndex: number, 22 | innerIndex?: number, 23 | filterTypes?: string[] 24 | ): TransferData[] { 25 | const key = `${programId}:${outerIndex}${innerIndex == undefined ? '' : `-${innerIndex}`}`; 26 | const transfers = this.transferActions[key] || []; 27 | 28 | if (filterTypes) { 29 | return transfers.filter((t) => filterTypes.includes(t.type)); 30 | } 31 | return transfers; 32 | } 33 | 34 | protected getInstructionByDiscriminator(discriminator: Uint8Array, slice: number): ClassifiedInstruction | undefined { 35 | const instruction = this.classifiedInstructions.find((i) => { 36 | const data = getInstructionData(i.instruction); 37 | return data.slice(0, slice).equals(discriminator); 38 | }); 39 | return instruction; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/parsers/base-parser.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../transaction-adapter'; 2 | import { TransactionUtils } from '../transaction-utils'; 3 | import { ClassifiedInstruction, DexInfo, TradeInfo, TransferData } from '../types'; 4 | 5 | export abstract class BaseParser { 6 | protected readonly utils: TransactionUtils; 7 | 8 | constructor( 9 | protected readonly adapter: TransactionAdapter, 10 | protected readonly dexInfo: DexInfo, 11 | protected readonly transferActions: Record, 12 | protected readonly classifiedInstructions: ClassifiedInstruction[] 13 | ) { 14 | this.utils = new TransactionUtils(adapter); 15 | } 16 | 17 | abstract processTrades(): TradeInfo[]; 18 | 19 | protected getTransfersForInstruction( 20 | programId: string, 21 | outerIndex: number, 22 | innerIndex?: number, 23 | extraTypes?: string[] 24 | ): TransferData[] { 25 | const key = `${programId}:${outerIndex}${innerIndex == undefined ? '' : `-${innerIndex}`}`; 26 | const transfers = this.transferActions[key] || []; 27 | return transfers.filter((t) => ['transfer', 'transferChecked', ...(extraTypes || [])].includes(t.type)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/parsers/binary-reader.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | 3 | export class BinaryReader { 4 | private offset = 0; 5 | 6 | constructor(private buffer: Buffer) {} 7 | 8 | readFixedArray(length: number): Buffer { 9 | this.checkBounds(length); 10 | const array = this.buffer.slice(this.offset, this.offset + length); 11 | this.offset += length; 12 | return array; 13 | } 14 | 15 | readU8(): number { 16 | this.checkBounds(1); 17 | const value = this.buffer.readUInt8(this.offset); 18 | this.offset += 1; 19 | return value; 20 | } 21 | 22 | readU16(): number { 23 | this.checkBounds(2); 24 | const value = this.buffer.readUint16LE(this.offset); 25 | this.offset += 2; 26 | return value; 27 | } 28 | 29 | readU64(): bigint { 30 | this.checkBounds(8); 31 | const value = this.buffer.readBigUInt64LE(this.offset); 32 | this.offset += 8; 33 | return value; 34 | } 35 | 36 | readI64(): bigint { 37 | this.checkBounds(8); 38 | const value = this.buffer.readBigInt64LE(this.offset); 39 | this.offset += 8; 40 | return value; 41 | } 42 | 43 | readString(): string { 44 | // Read 4-byte (32-bit) length instead of 1 byte 45 | const length = this.buffer.readUInt32LE(this.offset); 46 | this.offset += 4; 47 | 48 | this.checkBounds(length); 49 | const strBuffer = this.buffer.slice(this.offset, this.offset + length); 50 | const content = strBuffer.toString('utf8'); 51 | this.offset += length; 52 | 53 | return content; 54 | } 55 | 56 | readPubkey(): string { 57 | return base58.encode(Buffer.from(this.readFixedArray(32))); 58 | } 59 | 60 | private checkBounds(length: number) { 61 | if (this.offset + length > this.buffer.length) { 62 | throw new Error( 63 | `Buffer overflow: trying to read ${length} bytes at offset ${this.offset} in buffer of length ${this.buffer.length}` 64 | ); 65 | } 66 | } 67 | 68 | getOffset(): number { 69 | return this.offset; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/parsers/boopfun/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-boopfun-event'; 2 | export * from './parser-boopfun'; 3 | export * from './util'; 4 | -------------------------------------------------------------------------------- /src/parsers/boopfun/parser-boopfun-event.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants'; 2 | import { InstructionClassifier } from '../../instruction-classifier'; 3 | import { TransactionAdapter } from '../../transaction-adapter'; 4 | import { 5 | BoopfunCompleteEvent, 6 | BoopfunCreateEvent, 7 | BoopfunEvent, 8 | BoopfunTradeEvent, 9 | ClassifiedInstruction, 10 | EventsParser, 11 | TransferData, 12 | } from '../../types'; 13 | import { getInstructionData, sortByIdx } from '../../utils'; 14 | import { BinaryReader } from '../binary-reader'; 15 | 16 | /** 17 | * Parse Boopfun events (CREATE/BUY/SELL/COMPLETE) 18 | */ 19 | export class BoopfunEventParser { 20 | constructor( 21 | private readonly adapter: TransactionAdapter, 22 | private readonly transferActions: Record 23 | ) {} 24 | 25 | private readonly eventParsers: Record> = { 26 | BUY: { 27 | discriminators: [DISCRIMINATORS.BOOPFUN.BUY], 28 | slice: 8, 29 | decode: this.decodeBuyEvent.bind(this), 30 | }, 31 | SELL: { 32 | discriminators: [DISCRIMINATORS.BOOPFUN.SELL], 33 | slice: 8, 34 | decode: this.decodeSellEvent.bind(this), 35 | }, 36 | CREATE: { 37 | discriminators: [DISCRIMINATORS.BOOPFUN.CREATE], 38 | slice: 8, 39 | decode: this.decodeCreateEvent.bind(this), 40 | }, 41 | COMPLETE: { 42 | discriminators: [DISCRIMINATORS.BOOPFUN.COMPLETE], 43 | slice: 8, 44 | decode: this.decodeCompleteEvent.bind(this), 45 | }, 46 | }; 47 | 48 | public processEvents(): BoopfunEvent[] { 49 | const instructions = new InstructionClassifier(this.adapter).getInstructions(DEX_PROGRAMS.BOOP_FUN.id); 50 | return this.parseInstructions(instructions); 51 | } 52 | 53 | public parseInstructions(instructions: ClassifiedInstruction[]): BoopfunEvent[] { 54 | return sortByIdx( 55 | instructions 56 | .map(({ instruction, outerIndex, innerIndex }) => { 57 | try { 58 | const data = getInstructionData(instruction); 59 | 60 | for (const [type, parser] of Object.entries(this.eventParsers)) { 61 | const discriminator = Buffer.from(data.slice(0, parser.slice)); 62 | if (parser.discriminators.some((it) => discriminator.equals(it))) { 63 | const options = { 64 | instruction, 65 | outerIndex, 66 | innerIndex, 67 | }; 68 | const eventData = parser.decode(data.slice(parser.slice), options); 69 | if (!eventData) return null; 70 | 71 | return { 72 | type: type as 'BUY' | 'SELL' | 'CREATE' | 'COMPLETE', 73 | data: eventData, 74 | slot: this.adapter.slot, 75 | timestamp: this.adapter.blockTime || 0, 76 | signature: this.adapter.signature, 77 | idx: `${outerIndex}-${innerIndex ?? 0}`, 78 | }; 79 | } 80 | } 81 | } catch (error) { 82 | console.error('Failed to parse Boopfun event:', error); 83 | throw error; 84 | } 85 | return null; 86 | }) 87 | .filter((event): event is BoopfunEvent => event !== null) 88 | ); 89 | } 90 | 91 | private decodeBuyEvent(data: Buffer, options: any): BoopfunTradeEvent { 92 | const { instruction, outerIndex, innerIndex } = options; 93 | // get instruction accounts 94 | const accounts = this.adapter.getInstructionAccounts(instruction); 95 | const reader = new BinaryReader(data); 96 | 97 | const transfers = this.getTransfersForInstruction( 98 | this.adapter.getInstructionProgramId(instruction), 99 | outerIndex, 100 | innerIndex 101 | ); 102 | const transfer = transfers.find((transfer) => transfer.info.mint == accounts[0]); 103 | 104 | return { 105 | mint: accounts[0], 106 | solAmount: reader.readU64(), 107 | tokenAmount: BigInt(transfer?.info.tokenAmount.amount || '0'), 108 | isBuy: true, 109 | user: accounts[6], 110 | bondingCurve: accounts[1], 111 | }; 112 | } 113 | 114 | private decodeSellEvent(data: Buffer, options: any): BoopfunTradeEvent { 115 | const { instruction, outerIndex, innerIndex } = options; 116 | // get instruction accounts 117 | const accounts = this.adapter.getInstructionAccounts(instruction); 118 | const reader = new BinaryReader(data); 119 | 120 | const transfers = this.getTransfersForInstruction( 121 | this.adapter.getInstructionProgramId(instruction), 122 | outerIndex, 123 | innerIndex 124 | ); 125 | const transfer = transfers.find((transfer) => transfer.info.mint == TOKENS.SOL); 126 | 127 | return { 128 | mint: accounts[0], 129 | solAmount: BigInt(transfer?.info.tokenAmount.amount || '0'), 130 | tokenAmount: reader.readU64(), 131 | isBuy: false, 132 | user: accounts[6], 133 | bondingCurve: accounts[1], 134 | }; 135 | } 136 | 137 | private decodeCreateEvent(data: Buffer, options: any): BoopfunCreateEvent { 138 | const { instruction } = options; 139 | // get instruction accounts 140 | const accounts = this.adapter.getInstructionAccounts(instruction); 141 | const reader = new BinaryReader(data); 142 | reader.readU64(); 143 | return { 144 | name: reader.readString(), 145 | symbol: reader.readString(), 146 | uri: reader.readString(), 147 | mint: accounts[2], 148 | user: accounts[3], 149 | }; 150 | } 151 | 152 | private decodeCompleteEvent(data: Buffer, options: any): BoopfunCompleteEvent { 153 | const { instruction, outerIndex, innerIndex } = options; 154 | // get instruction accounts 155 | const accounts = this.adapter.getInstructionAccounts(instruction); 156 | const transfers = this.getTransfersForInstruction( 157 | this.adapter.getInstructionProgramId(instruction), 158 | outerIndex, 159 | innerIndex 160 | ); 161 | const sols = transfers 162 | .filter((transfer) => transfer.info.mint == TOKENS.SOL) 163 | .sort((a, b) => b.info.tokenAmount.uiAmount - a.info.tokenAmount.uiAmount); 164 | 165 | return { 166 | user: accounts[10], 167 | mint: accounts[0], 168 | bondingCurve: accounts[7], 169 | solAmount: BigInt(sols[0].info.tokenAmount.amount), 170 | feeAmount: sols.length > 1 ? BigInt(sols[1].info.tokenAmount.amount) : BigInt(0), 171 | }; 172 | } 173 | 174 | protected getTransfersForInstruction(programId: string, outerIndex: number, innerIndex?: number): TransferData[] { 175 | const key = `${programId}:${outerIndex}${innerIndex == undefined ? '' : `-${innerIndex}`}`; 176 | const transfers = this.transferActions[key] || []; 177 | return transfers.filter((t) => ['transfer', 'transferChecked'].includes(t.type)); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/parsers/boopfun/parser-boopfun.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../../transaction-adapter'; 2 | import { BoopfunEvent, BoopfunTradeEvent, ClassifiedInstruction, DexInfo, TradeInfo, TransferData } from '../../types'; 3 | import { BaseParser } from '../base-parser'; 4 | import { BoopfunEventParser } from './parser-boopfun-event'; 5 | import { getBoopfunTradeInfo } from './util'; 6 | 7 | /** 8 | * Parse Boopfun trades (BUY/SELL) 9 | */ 10 | export class BoopfunParser extends BaseParser { 11 | private eventParser: BoopfunEventParser; 12 | 13 | constructor( 14 | adapter: TransactionAdapter, 15 | dexInfo: DexInfo, 16 | transferActions: Record, 17 | classifiedInstructions: ClassifiedInstruction[] 18 | ) { 19 | super(adapter, dexInfo, transferActions, classifiedInstructions); 20 | this.eventParser = new BoopfunEventParser(adapter, transferActions); 21 | } 22 | 23 | public processTrades(): TradeInfo[] { 24 | const events = this.eventParser 25 | .parseInstructions(this.classifiedInstructions) 26 | .filter((event) => event.type === 'BUY' || event.type === 'SELL'); 27 | return events.map((event) => this.createTradeInfo(event)); 28 | } 29 | 30 | private createTradeInfo(data: BoopfunEvent): TradeInfo { 31 | const event = data.data as BoopfunTradeEvent; 32 | const trade = getBoopfunTradeInfo(event, { 33 | slot: data.slot, 34 | signature: data.signature, 35 | timestamp: data.timestamp, 36 | idx: data.idx, 37 | dexInfo: this.dexInfo, 38 | }); 39 | 40 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/parsers/boopfun/util.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, TOKENS } from '../../constants'; 2 | import { BoopfunTradeEvent, DexInfo, TradeInfo, TradeType, convertToUiAmount } from '../../types'; 3 | 4 | export const getBoopfunTradeInfo = ( 5 | event: BoopfunTradeEvent, 6 | info: { 7 | slot: number; 8 | signature: string; 9 | timestamp: number; 10 | idx?: string; 11 | dexInfo?: DexInfo; 12 | } 13 | ): TradeInfo => { 14 | const tradeType: TradeType = event.isBuy ? 'BUY' : 'SELL'; 15 | const isBuy = tradeType === 'BUY'; 16 | return { 17 | type: tradeType, 18 | inputToken: { 19 | mint: isBuy ? TOKENS.SOL : event.mint, 20 | amount: isBuy ? convertToUiAmount(event.solAmount) : convertToUiAmount(event.tokenAmount, 6), 21 | amountRaw: isBuy ? event.solAmount.toString() : event.tokenAmount.toString(), 22 | decimals: isBuy ? 9 : 6, 23 | }, 24 | outputToken: { 25 | mint: isBuy ? event.mint : TOKENS.SOL, 26 | amount: isBuy ? convertToUiAmount(event.tokenAmount, 6) : convertToUiAmount(event.solAmount), 27 | amountRaw: isBuy ? event.tokenAmount.toString() : event.solAmount.toString(), 28 | decimals: isBuy ? 6 : 9, 29 | }, 30 | user: event.user, 31 | programId: DEX_PROGRAMS.BOOP_FUN.id, 32 | amm: info.dexInfo?.amm || DEX_PROGRAMS.BOOP_FUN.name, 33 | route: info.dexInfo?.route || '', 34 | slot: info.slot, 35 | timestamp: info.timestamp, 36 | signature: info.signature, 37 | idx: info.idx || '', 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jupiter'; 2 | export * from './meteora'; 3 | export * from './moonshot'; 4 | export * from './orca'; 5 | export * from './pumpfun'; 6 | export * from './raydium'; 7 | export * from './boopfun'; 8 | export * from './binary-reader'; 9 | -------------------------------------------------------------------------------- /src/parsers/jupiter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-jupiter'; 2 | export * from './parser-jupiter-dca'; 3 | export * from './parser-jupiter-va'; 4 | export * from './parser-jupiter-limit'; 5 | export * from './parser-jupiter-limit-v2'; 6 | export * from './layouts'; 7 | -------------------------------------------------------------------------------- /src/parsers/jupiter/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jupiter-dca.layout'; 2 | export * from './jupiter-va.layout'; 3 | export * from './jupiter-limit.layout'; 4 | export * from './jupiter-v6.layout'; 5 | -------------------------------------------------------------------------------- /src/parsers/jupiter/layouts/jupiter-dca.layout.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | 3 | export class JupiterDCAFilledLayout { 4 | userKey: Uint8Array; 5 | dcaKey: Uint8Array; 6 | inputMint: Uint8Array; 7 | outputMint: Uint8Array; 8 | inAmount: bigint; 9 | outAmount: bigint; 10 | feeMint: Uint8Array; 11 | fee: bigint; 12 | 13 | constructor(fields: { 14 | userKey: Uint8Array; 15 | dcaKey: Uint8Array; 16 | inputMint: Uint8Array; 17 | outputMint: Uint8Array; 18 | inAmount: bigint; 19 | outAmount: bigint; 20 | feeMint: Uint8Array; 21 | fee: bigint; 22 | }) { 23 | this.userKey = fields.userKey; 24 | this.dcaKey = fields.dcaKey; 25 | this.inputMint = fields.inputMint; 26 | this.outputMint = fields.outputMint; 27 | this.inAmount = fields.inAmount; 28 | this.outAmount = fields.outAmount; 29 | this.feeMint = fields.feeMint; 30 | this.fee = fields.fee; 31 | } 32 | 33 | static schema = new Map([ 34 | [ 35 | JupiterDCAFilledLayout, 36 | { 37 | kind: 'struct', 38 | fields: [ 39 | ['userKey', [32]], 40 | ['dcaKey', [32]], 41 | ['inputMint', [32]], 42 | ['outputMint', [32]], 43 | ['inAmount', 'u64'], 44 | ['outAmount', 'u64'], 45 | ['feeMint', [32]], 46 | ['fee', 'u64'], 47 | ], 48 | }, 49 | ], 50 | ]); 51 | 52 | toObject() { 53 | return { 54 | userKey: base58.encode(this.userKey), 55 | dcaKey: base58.encode(this.dcaKey), 56 | inputMint: base58.encode(this.inputMint), 57 | outputMint: base58.encode(this.outputMint), 58 | inAmount: this.inAmount, 59 | outAmount: this.outAmount, 60 | feeMint: base58.encode(this.feeMint), 61 | fee: this.fee, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/parsers/jupiter/layouts/jupiter-limit.layout.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | import { BinaryReader } from '../../binary-reader'; 3 | 4 | export class JupiterLimitOrderV2TradeLayout { 5 | orderKey: Uint8Array; 6 | taker: Uint8Array; 7 | remainingMakingAmount: bigint; 8 | remainingTakingAmount: bigint; 9 | makingAmount: bigint; 10 | takingAmount: bigint; 11 | 12 | constructor(fields: { 13 | orderKey: Uint8Array; 14 | taker: Uint8Array; 15 | remainingMakingAmount: bigint; 16 | remainingTakingAmount: bigint; 17 | makingAmount: bigint; 18 | takingAmount: bigint; 19 | }) { 20 | this.orderKey = fields.orderKey; 21 | this.taker = fields.taker; 22 | this.remainingMakingAmount = fields.remainingMakingAmount; 23 | this.remainingTakingAmount = fields.remainingTakingAmount; 24 | this.makingAmount = fields.makingAmount; 25 | this.takingAmount = fields.takingAmount; 26 | } 27 | 28 | static schema = new Map([ 29 | [ 30 | JupiterLimitOrderV2TradeLayout, 31 | { 32 | kind: 'struct', 33 | fields: [ 34 | ['orderKey', [32]], 35 | ['taker', [32]], 36 | ['remainingMakingAmount', 'u64'], 37 | ['remainingTakingAmount', 'u64'], 38 | ['makingAmount', 'u64'], 39 | ['takingAmount', 'u64'], 40 | ], 41 | }, 42 | ], 43 | ]); 44 | 45 | toObject() { 46 | return { 47 | orderKey: base58.encode(this.orderKey), 48 | taker: base58.encode(this.taker), 49 | remainingMakingAmount: this.remainingMakingAmount, 50 | remainingTakingAmount: this.remainingTakingAmount, 51 | makingAmount: this.makingAmount, 52 | takingAmount: this.takingAmount, 53 | }; 54 | } 55 | } 56 | 57 | export class JupiterLimitOrderV2CreateOrderLayout { 58 | orderKey: Uint8Array; 59 | maker: Uint8Array; 60 | inputMint: Uint8Array; 61 | outputMint: Uint8Array; 62 | inputTokenProgram: Uint8Array; 63 | outputTokenProgram: Uint8Array; 64 | makingAmount: bigint; 65 | takingAmount: bigint; 66 | expiredAt: bigint | null; 67 | feeBps: number; 68 | feeAccount: Uint8Array; 69 | 70 | constructor(fields: { 71 | orderKey: Uint8Array; 72 | maker: Uint8Array; 73 | inputMint: Uint8Array; 74 | outputMint: Uint8Array; 75 | inputTokenProgram: Uint8Array; 76 | outputTokenProgram: Uint8Array; 77 | makingAmount: bigint; 78 | takingAmount: bigint; 79 | expiredAt: bigint | null; 80 | feeBps: number; 81 | feeAccount: Uint8Array; 82 | }) { 83 | this.orderKey = fields.orderKey; 84 | this.maker = fields.maker; 85 | this.inputMint = fields.inputMint; 86 | this.outputMint = fields.outputMint; 87 | this.inputTokenProgram = fields.inputTokenProgram; 88 | this.outputTokenProgram = fields.outputTokenProgram; 89 | this.makingAmount = fields.makingAmount; 90 | this.takingAmount = fields.takingAmount; 91 | this.expiredAt = fields.expiredAt; 92 | this.feeBps = fields.feeBps; 93 | this.feeAccount = fields.feeAccount; 94 | } 95 | 96 | static schema = new Map([ 97 | [ 98 | JupiterLimitOrderV2CreateOrderLayout, 99 | { 100 | kind: 'struct', 101 | fields: [ 102 | ['orderKey', [32]], 103 | ['maker', [32]], 104 | ['inputMint', [32]], 105 | ['outputMint', [32]], 106 | ['inputTokenProgram', [32]], 107 | ['outputTokenProgram', [32]], 108 | ['makingAmount', 'u64'], 109 | ['takingAmount', 'u64'], 110 | ['expiredAt', { option: 'i64' }], 111 | ['feeBps', 'u16'], 112 | ['feeAccount', [32]], 113 | ], 114 | }, 115 | ], 116 | ]); 117 | 118 | static deserialize(data: Buffer): JupiterLimitOrderV2CreateOrderLayout { 119 | const reader = new BinaryReader(data); 120 | 121 | const orderKey = reader.readFixedArray(32); 122 | const maker = reader.readFixedArray(32); 123 | const inputMint = reader.readFixedArray(32); 124 | const outputMint = reader.readFixedArray(32); 125 | const inputTokenProgram = reader.readFixedArray(32); 126 | const outputTokenProgram = reader.readFixedArray(32); 127 | const makingAmount = reader.readU64(); 128 | const takingAmount = reader.readU64(); 129 | 130 | // Handle optional expiredAt 131 | const expiredAtDiscriminator = reader.readU8(); // Read 1-byte discriminator 132 | let expiredAt: bigint | null = null; 133 | if (expiredAtDiscriminator === 1) { 134 | expiredAt = reader.readI64(); // Read i64 only if value is present 135 | } 136 | const feeBps = reader.readU16(); 137 | const feeAccount = reader.readFixedArray(32); 138 | 139 | return new JupiterLimitOrderV2CreateOrderLayout({ 140 | orderKey, 141 | maker, 142 | inputMint, 143 | outputMint, 144 | inputTokenProgram, 145 | outputTokenProgram, 146 | makingAmount, 147 | takingAmount, 148 | expiredAt, 149 | feeBps, 150 | feeAccount, 151 | }); 152 | } 153 | 154 | toObject() { 155 | return { 156 | orderKey: base58.encode(this.orderKey), 157 | maker: base58.encode(this.maker), 158 | inputMint: base58.encode(this.inputMint), 159 | outputMint: base58.encode(this.outputMint), 160 | inputTokenProgram: base58.encode(this.inputTokenProgram), 161 | outputTokenProgram: base58.encode(this.outputTokenProgram), 162 | makingAmount: this.makingAmount, 163 | takingAmount: this.takingAmount, 164 | expiredAt: this.expiredAt !== null ? this.expiredAt.toString() : null, 165 | feeBps: this.feeBps, 166 | feeAccount: base58.encode(this.feeAccount), 167 | }; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/parsers/jupiter/layouts/jupiter-v6.layout.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { JupiterSwapEvent } from '../../../types/jupiter'; 3 | 4 | export class JupiterSwapLayout { 5 | amm: Uint8Array; 6 | inputMint: Uint8Array; 7 | inputAmount: bigint; 8 | outputMint: Uint8Array; 9 | outputAmount: bigint; 10 | 11 | constructor(fields: { 12 | amm: Uint8Array; 13 | inputMint: Uint8Array; 14 | inputAmount: bigint; 15 | outputMint: Uint8Array; 16 | outputAmount: bigint; 17 | }) { 18 | this.amm = fields.amm; 19 | this.inputMint = fields.inputMint; 20 | this.inputAmount = fields.inputAmount; 21 | this.outputMint = fields.outputMint; 22 | this.outputAmount = fields.outputAmount; 23 | } 24 | 25 | static schema = new Map([ 26 | [ 27 | JupiterSwapLayout, 28 | { 29 | kind: 'struct', 30 | fields: [ 31 | ['amm', [32]], 32 | ['inputMint', [32]], 33 | ['inputAmount', 'u64'], 34 | ['outputMint', [32]], 35 | ['outputAmount', 'u64'], 36 | ], 37 | }, 38 | ], 39 | ]); 40 | 41 | toSwapEvent(): JupiterSwapEvent { 42 | return { 43 | amm: new PublicKey(this.amm), 44 | inputMint: new PublicKey(this.inputMint), 45 | inputAmount: this.inputAmount, 46 | outputMint: new PublicKey(this.outputMint), 47 | outputAmount: this.outputAmount, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/parsers/jupiter/parser-jupiter-dca.ts: -------------------------------------------------------------------------------- 1 | import { deserializeUnchecked } from 'borsh'; 2 | import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants'; 3 | import { convertToUiAmount, TradeInfo, TradeType, TransferData } from '../../types'; 4 | import { getAMMs, getInstructionData, getTradeType } from '../../utils'; 5 | import { BaseParser } from '../base-parser'; 6 | import { JupiterDCAFilledLayout } from './layouts/jupiter-dca.layout'; 7 | 8 | export class JupiterDcaParser extends BaseParser { 9 | public processTrades(): TradeInfo[] { 10 | const trades: TradeInfo[] = []; 11 | 12 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 13 | if (programId == DEX_PROGRAMS.JUPITER_DCA.id) { 14 | const data = getInstructionData(instruction); 15 | const discriminator = Buffer.from(data.slice(0, 16)); 16 | if (discriminator.equals(DISCRIMINATORS.JUPITER_DCA.FILLED)) { 17 | trades.push(this.parseFullFilled(instruction, `${outerIndex}-${innerIndex ?? 0}`)); 18 | } 19 | } 20 | }); 21 | 22 | return trades; 23 | } 24 | 25 | private parseFullFilled(instruction: any, idx: string): TradeInfo { 26 | const eventData = getInstructionData(instruction).slice(16); 27 | const layout = deserializeUnchecked(JupiterDCAFilledLayout.schema, JupiterDCAFilledLayout, Buffer.from(eventData)); 28 | const event = layout.toObject(); 29 | 30 | const tradeType: TradeType = getTradeType(event.inputMint, event.outputMint); 31 | 32 | const [inputDecimal, outputDecimal, feeDecimal] = [ 33 | this.adapter.splDecimalsMap.get(event.inputMint), 34 | this.adapter.splDecimalsMap.get(event.outputMint), 35 | this.adapter.splDecimalsMap.get(event.feeMint), 36 | ]; 37 | 38 | const trade = { 39 | type: tradeType, 40 | inputToken: { 41 | mint: event.inputMint, 42 | amount: convertToUiAmount(event.inAmount, inputDecimal), 43 | amountRaw: event.inAmount.toString(), 44 | decimals: inputDecimal ?? 0, 45 | }, 46 | outputToken: { 47 | mint: event.outputMint, 48 | amount: convertToUiAmount(event.outAmount, outputDecimal), 49 | amountRaw: event.outAmount.toString(), 50 | decimals: outputDecimal ?? 0, 51 | }, 52 | fee: { 53 | mint: event.feeMint, 54 | amount: convertToUiAmount(event.fee, feeDecimal), 55 | amountRaw: event.fee.toString(), 56 | decimals: feeDecimal ?? 0, 57 | }, 58 | user: event.userKey, 59 | programId: DEX_PROGRAMS.JUPITER_DCA.id, 60 | amm: this.getAmm(), 61 | route: this.dexInfo?.route || '', 62 | slot: this.adapter.slot, 63 | timestamp: this.adapter.blockTime || 0, 64 | signature: this.adapter.signature, 65 | idx: idx || '', 66 | }; 67 | 68 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 69 | } 70 | 71 | private getAmm(): string { 72 | const amms = getAMMs(Object.keys(this.transferActions)); 73 | return amms.length > 0 ? amms[0] : this.dexInfo?.amm || DEX_PROGRAMS.JUPITER_DCA.name; 74 | } 75 | 76 | public processTransfers(): TransferData[] { 77 | const transfers: TransferData[] = []; 78 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 79 | if (programId == DEX_PROGRAMS.JUPITER_DCA.id) { 80 | const data = getInstructionData(instruction); 81 | const discriminator = Buffer.from(data.slice(0, 8)); 82 | if (discriminator.equals(DISCRIMINATORS.JUPITER_DCA.CLOSE_DCA)) { 83 | transfers.push(...this.parseCloseDca(instruction, programId, `${outerIndex}-${innerIndex ?? 0}`)); 84 | } else if ( 85 | discriminator.equals(DISCRIMINATORS.JUPITER_DCA.OPEN_DCA) || 86 | discriminator.equals(DISCRIMINATORS.JUPITER_DCA.OPEN_DCA_V2) 87 | ) { 88 | transfers.push(...this.parseOpenDca(instruction, programId, `${outerIndex}-${innerIndex ?? 0}`)); 89 | } 90 | } 91 | }); 92 | return transfers; 93 | } 94 | 95 | private parseCloseDca(instruction: any, programId: string, idx: string): TransferData[] { 96 | const transfers: TransferData[] = []; 97 | const user = this.adapter.signer; 98 | const balance = this.adapter.getAccountSolBalanceChanges().get(user); 99 | if (!balance) return []; 100 | 101 | const accounts = this.adapter.getInstructionAccounts(instruction); 102 | transfers.push({ 103 | type: 'CloseDca', 104 | programId: programId, 105 | info: { 106 | authority: this.adapter.getTokenAccountOwner(accounts[1]), 107 | destination: user, 108 | destinationOwner: this.adapter.getTokenAccountOwner(user), 109 | mint: TOKENS.SOL, 110 | source: accounts[1], 111 | tokenAmount: { 112 | amount: balance.change.amount, 113 | uiAmount: balance.change.uiAmount ?? 0, 114 | decimals: balance.change.decimals, 115 | }, 116 | destinationBalance: balance.post, 117 | destinationPreBalance: balance.pre, 118 | }, 119 | idx: idx, 120 | timestamp: this.adapter.blockTime, 121 | signature: this.adapter.signature, 122 | }); 123 | 124 | return transfers; 125 | } 126 | 127 | private parseOpenDca(instruction: any, programId: string, idx: string): TransferData[] { 128 | const transfers: TransferData[] = []; 129 | const user = this.adapter.signer; 130 | const balances = this.adapter.getAccountSolBalanceChanges(); 131 | const balance = balances.get(user); 132 | 133 | if (!balance) return []; 134 | 135 | const accounts = this.adapter.getInstructionAccounts(instruction); 136 | transfers.push({ 137 | type: 'OpenDca', 138 | programId: programId, 139 | info: { 140 | authority: this.adapter.getTokenAccountOwner(user), 141 | source: user, 142 | destination: accounts[0], 143 | destinationOwner: this.adapter.getTokenAccountOwner(accounts[0]), 144 | mint: TOKENS.SOL, 145 | tokenAmount: { 146 | amount: balance.change.amount, 147 | uiAmount: balance.change.uiAmount ?? 0, 148 | decimals: balance.change.decimals, 149 | }, 150 | sourceBalance: balance.post, 151 | sourcePreBalance: balance.pre, 152 | }, 153 | idx: idx, 154 | timestamp: this.adapter.blockTime, 155 | signature: this.adapter.signature, 156 | }); 157 | 158 | return transfers; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/parsers/jupiter/parser-jupiter-limit.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants'; 2 | import { convertToUiAmount, TradeInfo, TransferData } from '../../types'; 3 | import { getInstructionData } from '../../utils'; 4 | import { BaseParser } from '../base-parser'; 5 | 6 | export class JupiterLimitOrderParser extends BaseParser { 7 | public processTrades(): TradeInfo[] { 8 | const trades: TradeInfo[] = []; 9 | return trades; 10 | } 11 | 12 | public processTransfers(): TransferData[] { 13 | const transfers: TransferData[] = []; 14 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 15 | if (programId == DEX_PROGRAMS.JUPITER_LIMIT_ORDER.id) { 16 | const data = getInstructionData(instruction); 17 | 18 | if (Buffer.from(data.slice(0, 8)).equals(DISCRIMINATORS.JUPITER_LIMIT_ORDER.CREATE_ORDER)) { 19 | transfers.push(...this.parseInitializeOrder(instruction, programId, outerIndex, innerIndex)); 20 | } else if (Buffer.from(data.slice(0, 8)).equals(DISCRIMINATORS.JUPITER_LIMIT_ORDER.CANCEL_ORDER)) { 21 | transfers.push(...this.parseCancelOrder(instruction, programId, outerIndex, innerIndex)); 22 | } 23 | } 24 | }); 25 | // Deduplicate transfers 26 | if (transfers.length > 1) { 27 | return [...new Map(transfers.map((item) => [`${item.idx}-${item.signature}=${item.isFee}`, item])).values()]; 28 | } 29 | return transfers; 30 | } 31 | 32 | private parseInitializeOrder( 33 | instruction: any, 34 | programId: string, 35 | outerIndex: number, 36 | innerIndex?: number 37 | ): TransferData[] { 38 | // get instruction accounts 39 | const accounts = this.adapter.getInstructionAccounts(instruction); 40 | 41 | const [user, mint, source] = [accounts[1], accounts[5], accounts[4]]; 42 | const destination = mint == TOKENS.SOL ? user : accounts[3]; 43 | 44 | const balance = 45 | mint == TOKENS.SOL 46 | ? this.adapter.getAccountSolBalanceChanges(true).get(user) 47 | : this.adapter.getAccountTokenBalanceChanges().get(source)?.get(mint); 48 | const solBalance = this.adapter.getAccountSolBalanceChanges(true).get(user); 49 | 50 | if (!balance) return []; 51 | 52 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 53 | const transfer = transfers.find((t) => t.info.mint == mint); 54 | 55 | const decimals = transfer?.info.tokenAmount.decimals || this.adapter.getTokenDecimals(mint); 56 | const tokenAmount = transfer?.info.tokenAmount.amount || balance.change.amount || '0'; 57 | 58 | return [ 59 | { 60 | type: 'initializeOrder', 61 | programId: programId, 62 | info: { 63 | authority: this.adapter.getTokenAccountOwner(source) || user, 64 | source: source, 65 | destination: destination, 66 | destinationOwner: this.adapter.getTokenAccountOwner(source), 67 | mint: mint, 68 | tokenAmount: { 69 | amount: tokenAmount, 70 | uiAmount: convertToUiAmount(tokenAmount, decimals), 71 | decimals: decimals, 72 | }, 73 | sourceBalance: balance.post, 74 | sourcePreBalance: balance.pre, 75 | solBalanceChange: solBalance?.change.amount || '0', 76 | }, 77 | idx: `${outerIndex}-${innerIndex ?? 0}`, 78 | timestamp: this.adapter.blockTime, 79 | signature: this.adapter.signature, 80 | }, 81 | ]; 82 | } 83 | 84 | private parseCancelOrder( 85 | instruction: any, 86 | programId: string, 87 | outerIndex: number, 88 | innerIndex?: number 89 | ): TransferData[] { 90 | // get instruction accounts 91 | const accounts = this.adapter.getInstructionAccounts(instruction); 92 | 93 | const [user, mint, source, authority] = [accounts[2], accounts[6], accounts[1], accounts[0]]; 94 | const destination = mint == TOKENS.SOL ? user : accounts[3]; 95 | 96 | const balance = 97 | mint == TOKENS.SOL 98 | ? this.adapter.getAccountSolBalanceChanges().get(destination) 99 | : this.adapter.getAccountTokenBalanceChanges().get(destination)?.get(mint); 100 | 101 | if (!balance) throw new Error('Balance not found'); 102 | 103 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 104 | const transfer = transfers.find((t) => t.info.mint == mint); 105 | 106 | const decimals = transfer?.info.tokenAmount.decimals || this.adapter.getTokenDecimals(mint); 107 | const tokenAmount = transfer?.info.tokenAmount.amount || balance.change.amount || '0'; 108 | 109 | const tokens: TransferData[] = []; 110 | tokens.push({ 111 | type: 'cancelOrder', 112 | programId: programId, 113 | info: { 114 | authority: transfer?.info.authority || authority, 115 | source: transfer?.info.source || source, 116 | destination: transfer ? transfer?.info.destination || destination : user, 117 | destinationOwner: this.adapter.getTokenAccountOwner(destination), 118 | mint: mint, 119 | tokenAmount: { 120 | amount: tokenAmount, 121 | uiAmount: convertToUiAmount(tokenAmount, decimals), 122 | decimals: decimals, 123 | }, 124 | destinationBalance: balance.post, 125 | destinationPreBalance: balance.pre, 126 | }, 127 | idx: `${outerIndex}-${innerIndex ?? 0}`, 128 | timestamp: this.adapter.blockTime, 129 | signature: this.adapter.signature, 130 | }); 131 | 132 | if (mint !== TOKENS.SOL) { 133 | const solBalance = this.adapter.getAccountSolBalanceChanges().get(user); 134 | if (solBalance) { 135 | tokens.push({ 136 | type: 'cancelOrder', 137 | programId: programId, 138 | info: { 139 | authority: transfer?.info.authority || authority, 140 | source: transfer?.info.source || source, 141 | destination: user, 142 | mint: TOKENS.SOL, 143 | tokenAmount: { 144 | amount: solBalance.change.amount, 145 | uiAmount: solBalance.change.uiAmount || 0, 146 | decimals: solBalance.change.decimals, 147 | }, 148 | destinationBalance: solBalance.post, 149 | destinationPreBalance: solBalance.pre, 150 | }, 151 | idx: `${outerIndex}-${innerIndex ?? 0}`, 152 | timestamp: this.adapter.blockTime, 153 | signature: this.adapter.signature, 154 | isFee: true, 155 | }); 156 | } 157 | } 158 | 159 | return tokens; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/parsers/jupiter/parser-jupiter-va.ts: -------------------------------------------------------------------------------- 1 | import { deserializeUnchecked } from 'borsh'; 2 | import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants'; 3 | import { convertToUiAmount, TradeInfo, TradeType, TransferData } from '../../types'; 4 | import { getAMMs, getInstructionData, getTradeType } from '../../utils'; 5 | import { BaseParser } from '../base-parser'; 6 | import { JupiterVAFillLayout, JupiterVAOpenLayout, JupiterVAWithdrawLayout } from './layouts/jupiter-va.layout'; 7 | 8 | export class JupiterVAParser extends BaseParser { 9 | public processTrades(): TradeInfo[] { 10 | const trades: TradeInfo[] = []; 11 | 12 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 13 | if (programId == DEX_PROGRAMS.JUPITER_VA.id) { 14 | const data = getInstructionData(instruction); 15 | const discriminator = Buffer.from(data.slice(0, 16)); 16 | if (discriminator.equals(DISCRIMINATORS.JUPITER_VA.FILL_EVENT)) { 17 | trades.push(this.parseFullFilled(instruction, `${outerIndex}-${innerIndex ?? 0}`)); 18 | } 19 | } 20 | }); 21 | 22 | return trades; 23 | } 24 | 25 | private parseFullFilled(instruction: any, idx: string): TradeInfo { 26 | const eventData = getInstructionData(instruction).slice(16); 27 | const layout = deserializeUnchecked(JupiterVAFillLayout.schema, JupiterVAFillLayout, Buffer.from(eventData)); 28 | const event = layout.toObject(); 29 | 30 | const tradeType: TradeType = getTradeType(event.inputMint, event.outputMint); 31 | 32 | const [inputDecimal, outputDecimal] = [ 33 | this.adapter.splDecimalsMap.get(event.inputMint), 34 | this.adapter.splDecimalsMap.get(event.outputMint), 35 | ]; 36 | 37 | const trade = { 38 | type: tradeType, 39 | inputToken: { 40 | mint: event.inputMint, 41 | amount: convertToUiAmount(event.inputAmount, inputDecimal), 42 | amountRaw: event.inputAmount.toString(), 43 | decimals: inputDecimal ?? 0, 44 | }, 45 | outputToken: { 46 | mint: event.outputMint, 47 | amount: convertToUiAmount(event.outputAmount, outputDecimal), 48 | amountRaw: event.outputAmount.toString(), 49 | decimals: outputDecimal ?? 0, 50 | }, 51 | fee: { 52 | mint: event.outputMint, 53 | amount: convertToUiAmount(event.fee, outputDecimal), 54 | amountRaw: event.fee.toString(), 55 | decimals: outputDecimal ?? 0, 56 | }, 57 | user: event.user, 58 | programId: DEX_PROGRAMS.JUPITER_VA.id, 59 | amm: this.getAmm(), 60 | route: this.dexInfo?.route || '', 61 | slot: this.adapter.slot, 62 | timestamp: this.adapter.blockTime || 0, 63 | signature: this.adapter.signature, 64 | idx: idx || '', 65 | }; 66 | 67 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 68 | } 69 | 70 | private getAmm(): string { 71 | const amms = getAMMs(Object.keys(this.transferActions)); 72 | return amms.length > 0 ? amms[0] : this.dexInfo?.amm || DEX_PROGRAMS.JUPITER_VA.name; 73 | } 74 | 75 | public processTransfers(): TransferData[] { 76 | const transfers: TransferData[] = []; 77 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 78 | if (programId == DEX_PROGRAMS.JUPITER_VA.id) { 79 | const data = getInstructionData(instruction); 80 | const discriminator = Buffer.from(data.slice(0, 16)); 81 | 82 | if (discriminator.equals(DISCRIMINATORS.JUPITER_VA.OPEN_EVENT)) { 83 | transfers.push(...this.parseOpen(data, programId, outerIndex, `${outerIndex}-${innerIndex ?? 0}`)); 84 | } else if (discriminator.equals(DISCRIMINATORS.JUPITER_VA.WITHDRAW_EVENT)) { 85 | transfers.push(...this.parseWithdraw(data, programId, outerIndex, `${outerIndex}-${innerIndex ?? 0}`)); 86 | } 87 | } 88 | }); 89 | return transfers; 90 | } 91 | 92 | private parseOpen(data: any, programId: string, outerIndex: number, idx: string): TransferData[] { 93 | // find outer instruction 94 | const eventInstruction = this.adapter.instructions[outerIndex]; 95 | if (!eventInstruction) { 96 | throw new Error('Event instruction not found'); 97 | } 98 | // parse event data 99 | const eventData = data.slice(16); 100 | const event = JupiterVAOpenLayout.deserialize(eventData).toObject(); 101 | 102 | // get outer instruction accounts 103 | const accounts = this.adapter.getInstructionAccounts(eventInstruction); 104 | const user = event.user; 105 | const [source, destination] = [accounts[5], accounts[6]]; 106 | 107 | const balance = 108 | event.inputMint == TOKENS.SOL 109 | ? this.adapter.getAccountSolBalanceChanges().get(user) 110 | : this.adapter.getAccountTokenBalanceChanges().get(user)?.get(event.inputMint); 111 | if (!balance) return []; 112 | 113 | return [ 114 | { 115 | type: 'open', 116 | programId: programId, 117 | info: { 118 | authority: user, 119 | source: source, 120 | destination: destination, 121 | destinationOwner: this.adapter.getTokenAccountOwner(destination), 122 | mint: event.inputMint, 123 | tokenAmount: { 124 | amount: balance.change.amount, 125 | uiAmount: balance.change.uiAmount ?? 0, 126 | decimals: balance.change.decimals, 127 | }, 128 | sourceBalance: balance.post, 129 | sourcePreBalance: balance.pre, 130 | }, 131 | idx: idx, 132 | timestamp: this.adapter.blockTime, 133 | signature: this.adapter.signature, 134 | }, 135 | ]; 136 | } 137 | 138 | private parseWithdraw(data: any, programId: string, outerIndex: number, idx: string): TransferData[] { 139 | // find outer instruction 140 | const eventInstruction = this.adapter.instructions[outerIndex]; 141 | if (!eventInstruction) { 142 | throw new Error('Event instruction not found'); 143 | } 144 | // parse event data 145 | const eventData = data.slice(16); 146 | const event = JupiterVAWithdrawLayout.deserialize(eventData).toObject(); 147 | 148 | // get outer instruction accounts 149 | const accounts = this.adapter.getInstructionAccounts(eventInstruction); 150 | const user = accounts[1]; 151 | const source = accounts[8]; 152 | 153 | const balance = 154 | event.mint == TOKENS.SOL 155 | ? this.adapter.getAccountSolBalanceChanges().get(user) 156 | : this.adapter.getAccountTokenBalanceChanges().get(user)?.get(event.mint); 157 | 158 | if (!balance) return []; 159 | 160 | return [ 161 | { 162 | type: 'withdraw', 163 | programId: programId, 164 | info: { 165 | authority: this.adapter.getTokenAccountOwner(source), 166 | source: source, 167 | destination: user, 168 | destinationOwner: this.adapter.getTokenAccountOwner(user), 169 | mint: event.mint, 170 | tokenAmount: { 171 | amount: balance.change.amount, 172 | uiAmount: balance.change.uiAmount ?? 0, 173 | decimals: balance.change.decimals, 174 | }, 175 | sourceBalance: balance.post, 176 | sourcePreBalance: balance.pre, 177 | }, 178 | idx: idx, 179 | timestamp: this.adapter.blockTime, 180 | signature: this.adapter.signature, 181 | }, 182 | ]; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/parsers/meteora/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-meteora'; 2 | export * from './parser-meteora-liquidity-base'; 3 | export * from './liquidity-meteora-dlmm'; 4 | export * from './liquidity-meteora-pools'; 5 | export * from './liquidity-meteora-damm-v2'; 6 | -------------------------------------------------------------------------------- /src/parsers/meteora/liquidity-meteora-damm-v2.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS, TOKENS } from '../../constants'; 2 | import { PoolEvent, PoolEventType, TransferData } from '../../types'; 3 | import { MeteoraLiquidityParserBase } from './parser-meteora-liquidity-base'; 4 | 5 | export class MeteoraDAMMPoolParser extends MeteoraLiquidityParserBase { 6 | public getPoolAction(data: Buffer): PoolEventType | null { 7 | const instructionType = data.slice(0, 8); 8 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.INITIALIZE_POOL)) return 'CREATE'; 9 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.INITIALIZE_CUSTOM_POOL)) return 'CREATE'; 10 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.ADD_LIQUIDITY)) return 'ADD'; 11 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.CLAIM_POSITION_FEE)) return 'REMOVE'; 12 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.REMOVE_LIQUIDITY)) return 'REMOVE'; 13 | if (instructionType.equals(DISCRIMINATORS.METEORA_DAMM.REMOVE_ALL_LIQUIDITY)) return 'REMOVE'; 14 | return null; 15 | } 16 | 17 | protected parseCreateLiquidityEvent( 18 | instruction: any, 19 | index: number, 20 | data: Buffer, 21 | transfers: TransferData[] 22 | ): PoolEvent { 23 | const discriminator = data.slice(0, 8); 24 | const eventInstruction = this.getInstructionByDiscriminator(DISCRIMINATORS.METEORA_DAMM.CREATE_POSITION_EVENT, 16); 25 | if (!eventInstruction) throw new Error('Event instruction not found'); 26 | const eventTransfers = this.getTransfersForInstruction( 27 | eventInstruction.programId, 28 | eventInstruction.outerIndex, 29 | eventInstruction.innerIndex 30 | ); 31 | const [token0, token1] = this.utils.getLPTransfers(eventTransfers); 32 | const lpToken = transfers.find((t) => t.type === 'mintTo'); 33 | 34 | const accounts = this.adapter.getInstructionAccounts(instruction); 35 | const token0Mint = 36 | token0?.info.mint || 37 | (discriminator.equals(DISCRIMINATORS.METEORA_DAMM.INITIALIZE_CUSTOM_POOL) ? accounts[7] : accounts[8]); 38 | const token1Mint = 39 | token1?.info.mint || 40 | (discriminator.equals(DISCRIMINATORS.METEORA_DAMM.INITIALIZE_CUSTOM_POOL) ? accounts[8] : accounts[9]); 41 | 42 | const programId = this.adapter.getInstructionProgramId(instruction); 43 | const [token0Decimals, token1Decimals] = [ 44 | this.adapter.getTokenDecimals(token0Mint), 45 | this.adapter.getTokenDecimals(token1Mint), 46 | ]; 47 | const poolId = discriminator.equals(DISCRIMINATORS.METEORA_DAMM.INITIALIZE_CUSTOM_POOL) ? accounts[5] : accounts[6]; 48 | return { 49 | ...this.adapter.getPoolEventBase('CREATE', programId), 50 | idx: index.toString(), 51 | poolId: poolId, 52 | poolLpMint: lpToken?.info.mint || accounts[1], 53 | token0Mint, 54 | token1Mint, 55 | token0Amount: token0?.info.tokenAmount.uiAmount, 56 | token0AmountRaw: token0?.info.tokenAmount.amount, 57 | token1Amount: token1?.info.tokenAmount.uiAmount, 58 | token1AmountRaw: token1?.info.tokenAmount.amount, 59 | token0Decimals, 60 | token1Decimals, 61 | lpAmount: lpToken?.info.tokenAmount.uiAmount || 1, 62 | lpAmountRaw: lpToken?.info.tokenAmount.amount || '1', 63 | }; 64 | } 65 | 66 | protected parseAddLiquidityEvent( 67 | instruction: any, 68 | index: number, 69 | data: Buffer, 70 | transfers: TransferData[] 71 | ): PoolEvent { 72 | const [token0, token1] = this.normalizeTokens(transfers); 73 | const programId = this.adapter.getInstructionProgramId(instruction); 74 | const accounts = this.adapter.getInstructionAccounts(instruction); 75 | return { 76 | ...this.adapter.getPoolEventBase('ADD', programId), 77 | idx: index.toString(), 78 | poolId: accounts[0], 79 | poolLpMint: accounts[1], 80 | token0Mint: token0?.info.mint, 81 | token1Mint: token1?.info.mint, 82 | token0Amount: token0?.info.tokenAmount.uiAmount || 0, 83 | token0AmountRaw: token0?.info.tokenAmount.amount, 84 | token1Amount: token1?.info.tokenAmount.uiAmount || 0, 85 | token1AmountRaw: token1?.info.tokenAmount.amount, 86 | token0Decimals: token0 && this.adapter.getTokenDecimals(token0?.info.mint), 87 | token1Decimals: token1 && this.adapter.getTokenDecimals(token1?.info.mint), 88 | }; 89 | } 90 | 91 | protected parseRemoveLiquidityEvent( 92 | instruction: any, 93 | index: number, 94 | data: Buffer, 95 | transfers: TransferData[] 96 | ): PoolEvent { 97 | const accounts = this.adapter.getInstructionAccounts(instruction); 98 | let [token0, token1] = this.normalizeTokens(transfers); 99 | 100 | if (token1 == undefined && token0?.info.mint == accounts[8]) { 101 | token1 = token0; 102 | token0 = undefined; 103 | } else if (token0 == undefined && token1?.info.mint == accounts[7]) { 104 | token0 = token1; 105 | token1 = undefined; 106 | } 107 | const token0Mint = token0?.info.mint || accounts[7]; 108 | const token1Mint = token1?.info.mint || accounts[8]; 109 | const programId = this.adapter.getInstructionProgramId(instruction); 110 | return { 111 | ...this.adapter.getPoolEventBase('REMOVE', programId), 112 | idx: index.toString(), 113 | poolId: accounts[1], 114 | poolLpMint: accounts[2], 115 | token0Mint: token0?.info.mint || accounts[7], 116 | token1Mint: token1?.info.mint || accounts[8], 117 | token0Amount: token0?.info.tokenAmount.uiAmount || 0, 118 | token0AmountRaw: token0?.info.tokenAmount.amount, 119 | token1Amount: token1?.info.tokenAmount.uiAmount || 0, 120 | token1AmountRaw: token1?.info.tokenAmount.amount, 121 | token0Decimals: this.adapter.getTokenDecimals(token0Mint), 122 | token1Decimals: this.adapter.getTokenDecimals(token1Mint), 123 | }; 124 | } 125 | 126 | private normalizeTokens(transfers: TransferData[]): [TransferData | undefined, TransferData | undefined] { 127 | let [token0, token1] = this.utils.getLPTransfers(transfers); 128 | if (transfers.length === 1 && transfers[0].info.mint == TOKENS.SOL) { 129 | token1 = transfers[0]; 130 | token0 = null as unknown as TransferData; 131 | } 132 | return [token0, token1]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/parsers/meteora/liquidity-meteora-dlmm.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS, TOKENS } from '../../constants'; 2 | import { PoolEvent, PoolEventType, TransferData } from '../../types'; 3 | import { MeteoraLiquidityParserBase } from './parser-meteora-liquidity-base'; 4 | 5 | export class MeteoraDLMMPoolParser extends MeteoraLiquidityParserBase { 6 | public getPoolAction(data: Buffer): { name: string; type: PoolEventType } | null { 7 | const instructionType = data.slice(0, 8); 8 | 9 | for (const [name, discriminator] of Object.entries(DISCRIMINATORS.METEORA_DLMM.ADD_LIQUIDITY)) { 10 | if (instructionType.equals(discriminator)) { 11 | return { name, type: 'ADD' }; 12 | } 13 | } 14 | 15 | for (const [name, discriminator] of Object.entries(DISCRIMINATORS.METEORA_DLMM.REMOVE_LIQUIDITY)) { 16 | if (instructionType.equals(discriminator)) { 17 | return { name, type: 'REMOVE' }; 18 | } 19 | } 20 | 21 | return null; 22 | } 23 | 24 | protected parseAddLiquidityEvent( 25 | instruction: any, 26 | index: number, 27 | data: Buffer, 28 | transfers: TransferData[] 29 | ): PoolEvent { 30 | const [token0, token1] = this.normalizeTokens(transfers); 31 | const programId = this.adapter.getInstructionProgramId(instruction); 32 | const accounts = this.adapter.getInstructionAccounts(instruction); 33 | return { 34 | ...this.adapter.getPoolEventBase('ADD', programId), 35 | idx: index.toString(), 36 | poolId: accounts[1], 37 | poolLpMint: accounts[1], 38 | token0Mint: token0?.info.mint, 39 | token1Mint: token1?.info.mint, 40 | token0Amount: token0?.info.tokenAmount.uiAmount || 0, 41 | token0AmountRaw: token0?.info.tokenAmount.amount, 42 | token1Amount: token1?.info.tokenAmount.uiAmount || 0, 43 | token1AmountRaw: token1?.info.tokenAmount.amount, 44 | token0Decimals: token0 && this.adapter.getTokenDecimals(token0?.info.mint), 45 | token1Decimals: token1 && this.adapter.getTokenDecimals(token1?.info.mint), 46 | }; 47 | } 48 | 49 | protected parseRemoveLiquidityEvent( 50 | instruction: any, 51 | index: number, 52 | data: Buffer, 53 | transfers: TransferData[] 54 | ): PoolEvent { 55 | const accounts = this.adapter.getInstructionAccounts(instruction); 56 | let [token0, token1] = this.normalizeTokens(transfers); 57 | 58 | if (token1 == undefined && token0?.info.mint == accounts[8]) { 59 | token1 = token0; 60 | token0 = undefined; 61 | } else if (token0 == undefined && token1?.info.mint == accounts[7]) { 62 | token0 = token1; 63 | token1 = undefined; 64 | } 65 | const token0Mint = token0?.info.mint || accounts[7]; 66 | const token1Mint = token1?.info.mint || accounts[8]; 67 | const programId = this.adapter.getInstructionProgramId(instruction); 68 | return { 69 | ...this.adapter.getPoolEventBase('REMOVE', programId), 70 | idx: index.toString(), 71 | poolId: accounts[1], 72 | poolLpMint: accounts[1], 73 | token0Mint: token0?.info.mint || accounts[7], 74 | token1Mint: token1?.info.mint || accounts[8], 75 | token0Amount: token0?.info.tokenAmount.uiAmount || 0, 76 | token0AmountRaw: token0?.info.tokenAmount.amount, 77 | token1Amount: token1?.info.tokenAmount.uiAmount || 0, 78 | token1AmountRaw: token1?.info.tokenAmount.amount, 79 | token0Decimals: this.adapter.getTokenDecimals(token0Mint), 80 | token1Decimals: this.adapter.getTokenDecimals(token1Mint), 81 | }; 82 | } 83 | 84 | private normalizeTokens(transfers: TransferData[]): [TransferData | undefined, TransferData | undefined] { 85 | let [token0, token1] = this.utils.getLPTransfers(transfers); 86 | if (transfers.length === 1 && transfers[0].info.mint == TOKENS.SOL) { 87 | token1 = transfers[0]; 88 | token0 = null as unknown as TransferData; 89 | } 90 | return [token0, token1]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/parsers/meteora/liquidity-meteora-pools.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS } from '../../constants'; 2 | import { convertToUiAmount, PoolEvent, PoolEventType, TransferData } from '../../types'; 3 | import { MeteoraLiquidityParserBase } from './parser-meteora-liquidity-base'; 4 | 5 | export class MeteoraPoolsParser extends MeteoraLiquidityParserBase { 6 | public getPoolAction(data: Buffer): PoolEventType | null { 7 | const instructionType = data.slice(0, 8); 8 | if (instructionType.equals(DISCRIMINATORS.METEORA_POOLS.CREATE)) return 'CREATE'; 9 | if ( 10 | [DISCRIMINATORS.METEORA_POOLS.ADD_LIQUIDITY, DISCRIMINATORS.METEORA_POOLS.ADD_IMBALANCE_LIQUIDITY].some((it) => 11 | instructionType.equals(it) 12 | ) 13 | ) 14 | return 'ADD'; 15 | if (instructionType.equals(DISCRIMINATORS.METEORA_POOLS.REMOVE_LIQUIDITY)) return 'REMOVE'; 16 | return null; 17 | } 18 | 19 | protected parseCreateLiquidityEvent( 20 | instruction: any, 21 | index: number, 22 | data: Buffer, 23 | transfers: TransferData[] 24 | ): PoolEvent { 25 | const accounts = this.adapter.getInstructionAccounts(instruction); 26 | const [token0, token1] = this.utils.getLPTransfers(transfers); 27 | const lpToken = transfers.find((t) => t.type === 'mintTo'); 28 | const token0Mint = token0?.info.mint || accounts[3]; 29 | const token1Mint = token1?.info.mint || accounts[4]; 30 | const programId = this.adapter.getInstructionProgramId(instruction); 31 | const [token0Decimals, token1Decimals] = [ 32 | this.adapter.getTokenDecimals(token0Mint), 33 | this.adapter.getTokenDecimals(token1Mint), 34 | ]; 35 | 36 | return { 37 | ...this.adapter.getPoolEventBase('CREATE', programId), 38 | idx: index.toString(), 39 | poolId: accounts[0], 40 | poolLpMint: accounts[2], 41 | token0Mint, 42 | token1Mint, 43 | token0Amount: token0?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(16), token0Decimals), 44 | token0AmountRaw: token0?.info.tokenAmount.amount || data.readBigUInt64LE(16).toString(), 45 | token1Amount: token1?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(8), token1Decimals), 46 | token1AmountRaw: token1?.info.tokenAmount.amount || data.readBigUInt64LE(8).toString(), 47 | token0Decimals, 48 | token1Decimals, 49 | lpAmount: lpToken?.info.tokenAmount.uiAmount || 0, 50 | lpAmountRaw: lpToken?.info.tokenAmount.amount, 51 | }; 52 | } 53 | 54 | protected parseAddLiquidityEvent( 55 | instruction: any, 56 | index: number, 57 | data: Buffer, 58 | transfers: TransferData[] 59 | ): PoolEvent { 60 | const accounts = this.adapter.getInstructionAccounts(instruction); 61 | const [token0, token1] = this.utils.getLPTransfers(transfers); 62 | const lpToken = transfers.find((t) => t.type === 'mintTo'); 63 | const token0Mint = token0?.info.mint; 64 | const token1Mint = token1?.info.mint; 65 | const programId = this.adapter.getInstructionProgramId(instruction); 66 | const [token0Decimals, token1Decimals] = [ 67 | this.adapter.getTokenDecimals(token0Mint), 68 | this.adapter.getTokenDecimals(token1Mint), 69 | ]; 70 | 71 | return { 72 | ...this.adapter.getPoolEventBase('ADD', programId), 73 | idx: index.toString(), 74 | poolId: accounts[0], 75 | poolLpMint: accounts[1], 76 | token0Mint, 77 | token1Mint, 78 | token0Amount: token0?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(24), token0Decimals), 79 | token0AmountRaw: token0?.info.tokenAmount.amount || data.readBigUInt64LE(24).toString(), 80 | token1Amount: token1?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(16), token1Decimals), 81 | token1AmountRaw: token1?.info.tokenAmount.amount || data.readBigUInt64LE(16).toString(), 82 | token0Decimals, 83 | token1Decimals, 84 | lpAmount: 85 | lpToken?.info.tokenAmount.uiAmount || 86 | convertToUiAmount(data.readBigUInt64LE(8), this.adapter.getTokenDecimals(accounts[1])), 87 | lpAmountRaw: lpToken?.info.tokenAmount.amount || data.readBigUInt64LE(8).toString(), 88 | }; 89 | } 90 | 91 | protected parseRemoveLiquidityEvent( 92 | instruction: any, 93 | index: number, 94 | data: Buffer, 95 | transfers: TransferData[] 96 | ): PoolEvent { 97 | const accounts = this.adapter.getInstructionAccounts(instruction); 98 | const [token0, token1] = this.utils.getLPTransfers(transfers); 99 | const lpToken = transfers.find((t) => t.type === 'burn'); 100 | const token0Mint = token0?.info.mint; 101 | const token1Mint = token1?.info.mint; 102 | const programId = this.adapter.getInstructionProgramId(instruction); 103 | const [token0Decimals, token1Decimals] = [ 104 | this.adapter.getTokenDecimals(token0Mint), 105 | this.adapter.getTokenDecimals(token1Mint), 106 | ]; 107 | 108 | return { 109 | ...this.adapter.getPoolEventBase('REMOVE', programId), 110 | idx: index.toString(), 111 | poolId: accounts[0], 112 | poolLpMint: accounts[1], 113 | token0Mint, 114 | token1Mint, 115 | token0Amount: token0?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(24), token0Decimals), 116 | token0AmountRaw: token0?.info.tokenAmount.amount || data.readBigUInt64LE(24).toString(), 117 | token1Amount: token1?.info.tokenAmount.uiAmount || convertToUiAmount(data.readBigUInt64LE(16), token1Decimals), 118 | token1AmountRaw: token1?.info.tokenAmount.amount || data.readBigUInt64LE(16).toString(), 119 | token0Decimals, 120 | token1Decimals, 121 | lpAmount: 122 | lpToken?.info.tokenAmount.uiAmount || 123 | convertToUiAmount(data.readBigUInt64LE(8), this.adapter.getTokenDecimals(accounts[1])), 124 | lpAmountRaw: lpToken?.info.tokenAmount.amount || data.readBigUInt64LE(8).toString(), 125 | }; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/parsers/meteora/parser-meteora-liquidity-base.ts: -------------------------------------------------------------------------------- 1 | import { PoolEvent, PoolEventType, TransferData } from '../../types'; 2 | import { BaseLiquidityParser } from '../base-liquidity-parser'; 3 | import { getInstructionData } from '../../utils'; 4 | 5 | export abstract class MeteoraLiquidityParserBase extends BaseLiquidityParser { 6 | abstract getPoolAction(data: Buffer): PoolEventType | { name: string; type: PoolEventType } | null; 7 | 8 | public processLiquidity(): PoolEvent[] { 9 | const events: PoolEvent[] = []; 10 | 11 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 12 | const event = this.parseInstruction(instruction, programId, outerIndex, innerIndex); 13 | if (event) { 14 | events.push(event); 15 | } 16 | }); 17 | 18 | return events; 19 | } 20 | 21 | protected parseInstruction( 22 | instruction: any, 23 | programId: string, 24 | outerIndex: number, 25 | innerIndex?: number 26 | ): PoolEvent | null { 27 | try { 28 | const data = getInstructionData(instruction); 29 | const action = this.getPoolAction(data); 30 | if (!action) return null; 31 | 32 | let transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 33 | if (transfers.length === 0) transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex ?? 0); 34 | const type = typeof action === 'string' ? action : action.type; 35 | switch (type) { 36 | case 'CREATE': 37 | return this.parseCreateLiquidityEvent?.(instruction, outerIndex, data, transfers) ?? null; 38 | case 'ADD': 39 | return this.parseAddLiquidityEvent(instruction, outerIndex, data, transfers); 40 | case 'REMOVE': 41 | return this.parseRemoveLiquidityEvent(instruction, outerIndex, data, transfers); 42 | } 43 | } catch (error) { 44 | console.error('parseInstruction error:', error); 45 | throw error; 46 | } 47 | } 48 | 49 | protected abstract parseAddLiquidityEvent( 50 | instruction: any, 51 | index: number, 52 | data: Buffer, 53 | transfers: TransferData[] 54 | ): PoolEvent; 55 | 56 | protected abstract parseRemoveLiquidityEvent( 57 | instruction: any, 58 | index: number, 59 | data: Buffer, 60 | transfers: TransferData[] 61 | ): PoolEvent; 62 | 63 | protected parseCreateLiquidityEvent?( 64 | instruction: any, 65 | index: number, 66 | data: Buffer, 67 | transfers: TransferData[] 68 | ): PoolEvent; 69 | } 70 | -------------------------------------------------------------------------------- /src/parsers/meteora/parser-meteora.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, DISCRIMINATORS } from '../../constants'; 2 | import { TradeInfo } from '../../types'; 3 | import { getInstructionData, getProgramName } from '../../utils'; 4 | import { BaseParser } from '../base-parser'; 5 | 6 | export class MeteoraParser extends BaseParser { 7 | public processTrades(): TradeInfo[] { 8 | const trades: TradeInfo[] = []; 9 | 10 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 11 | if ( 12 | [DEX_PROGRAMS.METEORA.id, DEX_PROGRAMS.METEORA_POOLS.id, DEX_PROGRAMS.METEORA_DAMM.id].includes(programId) && 13 | this.notLiquidityEvent(instruction) 14 | ) { 15 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 16 | if (transfers.length >= 2) { 17 | const trade = this.utils.processSwapData(transfers, { 18 | ...this.dexInfo, 19 | amm: this.dexInfo.amm || getProgramName(programId), 20 | }); 21 | if (trade) { 22 | trades.push(this.utils.attachTokenTransferInfo(trade, this.transferActions)); 23 | } 24 | } 25 | } 26 | }); 27 | 28 | return trades; 29 | } 30 | 31 | private notLiquidityEvent(instruction: any): boolean { 32 | const data = getInstructionData(instruction); 33 | if (!data) return true; 34 | 35 | const isDLMMLiquidity = Object.values(DISCRIMINATORS.METEORA_DLMM) 36 | .flatMap((it) => Object.values(it)) 37 | .some((it) => data.slice(0, it.length).equals(it)); 38 | 39 | const isPoolsLiquidity = Object.values(DISCRIMINATORS.METEORA_POOLS).some((it) => 40 | data.slice(0, it.length).equals(it) 41 | ); 42 | 43 | const isDAMMLiquidity = Object.values(DISCRIMINATORS.METEORA_DAMM).some((it) => 44 | data.slice(0, it.length).equals(it) 45 | ); 46 | 47 | return !isDLMMLiquidity && !isPoolsLiquidity && !isDAMMLiquidity; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/parsers/moonshot/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-moonshot'; 2 | -------------------------------------------------------------------------------- /src/parsers/moonshot/parser-moonshot.ts: -------------------------------------------------------------------------------- 1 | import { TokenAmount } from '@solana/web3.js'; 2 | import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants'; 3 | import { convertToUiAmount, TradeInfo, TradeType } from '../../types'; 4 | import { absBigInt, getInstructionData } from '../../utils'; 5 | import { BaseParser } from '../base-parser'; 6 | 7 | export class MoonshotParser extends BaseParser { 8 | public processTrades(): TradeInfo[] { 9 | const trades: TradeInfo[] = []; 10 | 11 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 12 | if (this.isTradeInstruction(instruction, programId)) { 13 | const trade = this.parseTradeInstruction(instruction, `${outerIndex}-${innerIndex ?? 0}`); 14 | if (trade) { 15 | trades.push(trade); 16 | } 17 | } 18 | }); 19 | 20 | return trades; 21 | } 22 | 23 | private isTradeInstruction(instruction: any, programId: string): boolean { 24 | const accounts = this.adapter.getInstructionAccounts(instruction); 25 | return programId === DEX_PROGRAMS.MOONSHOT.id && accounts && accounts.length === 11; 26 | } 27 | 28 | private parseTradeInstruction(instruction: any, idx: string): TradeInfo | null { 29 | try { 30 | if (!('data' in instruction)) return null; 31 | 32 | const data = getInstructionData(instruction); 33 | const discriminator = data.slice(0, 8); 34 | let tradeType: TradeType; 35 | 36 | if (discriminator.equals(DISCRIMINATORS.MOONSHOT.BUY)) { 37 | tradeType = 'BUY'; 38 | } else if (discriminator.equals(DISCRIMINATORS.MOONSHOT.SELL)) { 39 | tradeType = 'SELL'; 40 | } else { 41 | return null; 42 | } 43 | 44 | const moonshotTokenMint = this.adapter.getInstructionAccounts(instruction)[6]; 45 | const accountKeys = this.adapter.accountKeys; 46 | const collateralMint = this.detectCollateralMint(accountKeys); 47 | const { tokenAmount, collateralAmount } = this.calculateAmounts(moonshotTokenMint, collateralMint); 48 | 49 | const trade: TradeInfo = { 50 | type: tradeType, 51 | inputToken: { 52 | mint: tradeType === 'BUY' ? collateralMint : moonshotTokenMint, 53 | amount: tradeType === 'BUY' ? (collateralAmount.uiAmount ?? 0) : (tokenAmount.uiAmount ?? 0), 54 | amountRaw: tradeType === 'BUY' ? collateralAmount.amount : tokenAmount.amount, 55 | decimals: tradeType === 'BUY' ? collateralAmount.decimals : tokenAmount.decimals, 56 | }, 57 | outputToken: { 58 | mint: tradeType === 'BUY' ? moonshotTokenMint : collateralMint, 59 | amount: tradeType === 'BUY' ? (tokenAmount.uiAmount ?? 0) : (collateralAmount.uiAmount ?? 0), 60 | amountRaw: tradeType === 'BUY' ? tokenAmount.amount : collateralAmount.amount, 61 | decimals: tradeType === 'BUY' ? tokenAmount.decimals : collateralAmount.decimals, 62 | }, 63 | user: this.adapter.signer, 64 | programId: DEX_PROGRAMS.MOONSHOT.id, 65 | amm: DEX_PROGRAMS.MOONSHOT.name, 66 | route: this.dexInfo.route || '', 67 | slot: this.adapter.slot, 68 | timestamp: this.adapter.blockTime, 69 | signature: this.adapter.signature, 70 | idx, 71 | }; 72 | 73 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 74 | } catch (error) { 75 | console.error('Failed to parse Moonshot trade:', error); 76 | throw error; 77 | } 78 | } 79 | 80 | private detectCollateralMint(accountKeys: string[]): string { 81 | if (accountKeys.some((key) => key === TOKENS.USDC)) return TOKENS.USDC; 82 | if (accountKeys.some((key) => key === TOKENS.USDT)) return TOKENS.USDT; 83 | return TOKENS.SOL; 84 | } 85 | 86 | private calculateAmounts(tokenMint: string, collateralMint: string) { 87 | const tokenBalanceChanges = this.getTokenBalanceChanges(tokenMint); 88 | const collateralBalanceChanges = this.getTokenBalanceChanges(collateralMint); 89 | 90 | return { 91 | tokenAmount: this.createTokenAmount(absBigInt(tokenBalanceChanges), tokenMint), 92 | collateralAmount: this.createTokenAmount(absBigInt(collateralBalanceChanges), collateralMint), 93 | }; 94 | } 95 | 96 | private getTokenBalanceChanges(mint: string): bigint { 97 | const signer = this.adapter.signer; 98 | 99 | if (mint === TOKENS.SOL) { 100 | if (!this.adapter.postBalances?.[0] || !this.adapter.preBalances?.[0]) { 101 | throw new Error('Insufficient balance information for SOL'); 102 | } 103 | return BigInt(this.adapter.postBalances[0] - this.adapter.preBalances[0]); 104 | } 105 | 106 | let preAmount = BigInt(0); 107 | let postAmount = BigInt(0); 108 | let balanceFound = false; 109 | 110 | this.adapter.preTokenBalances?.forEach((preBalance) => { 111 | if (preBalance.mint === mint && preBalance.owner === signer) { 112 | preAmount = BigInt(preBalance.uiTokenAmount.amount); 113 | balanceFound = true; 114 | } 115 | }); 116 | 117 | this.adapter.postTokenBalances?.forEach((postBalance) => { 118 | if (postBalance.mint === mint && postBalance.owner === signer) { 119 | postAmount = BigInt(postBalance.uiTokenAmount.amount); 120 | balanceFound = true; 121 | } 122 | }); 123 | 124 | if (!balanceFound) { 125 | throw new Error('Could not find balance for specified mint and signer'); 126 | } 127 | 128 | return postAmount - preAmount; 129 | } 130 | 131 | private createTokenAmount(amount: bigint, mint: string): TokenAmount { 132 | const decimals = this.adapter.getTokenDecimals(mint); 133 | return { 134 | amount: amount.toString(), 135 | uiAmount: convertToUiAmount(amount, decimals), 136 | decimals, 137 | }; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/parsers/orca/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-orca'; 2 | export * from './parser-orca-liquidity'; 3 | -------------------------------------------------------------------------------- /src/parsers/orca/parser-orca-liquidity.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, DISCRIMINATORS } from '../../constants'; 2 | import { convertToUiAmount, PoolEvent, PoolEventType, TransferData } from '../../types'; 3 | import { getInstructionData } from '../../utils'; 4 | import { BaseLiquidityParser } from '../base-liquidity-parser'; 5 | 6 | export class OrcaLiquidityParser extends BaseLiquidityParser { 7 | public processLiquidity(): PoolEvent[] { 8 | const events: PoolEvent[] = []; 9 | 10 | this.classifiedInstructions 11 | .filter(({ programId }) => programId === DEX_PROGRAMS.ORCA.id) 12 | .forEach(({ instruction, programId, outerIndex, innerIndex }) => { 13 | const event = this.parseInstruction(instruction, programId, outerIndex, innerIndex); 14 | if (event) { 15 | events.push(event); 16 | } 17 | }); 18 | 19 | return events; 20 | } 21 | 22 | private parseInstruction( 23 | instruction: any, 24 | programId: string, 25 | outerIndex: number, 26 | innerIndex?: number 27 | ): PoolEvent | null { 28 | try { 29 | const data = getInstructionData(instruction); 30 | const action = this.getPoolAction(data); 31 | if (!action) return null; 32 | 33 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 34 | 35 | switch (action) { 36 | case 'ADD': 37 | return this.parseAddLiquidityEvent(instruction, outerIndex, data, transfers); 38 | case 'REMOVE': 39 | return this.parseRemoveLiquidityEvent(instruction, outerIndex, data, transfers); 40 | } 41 | return null; 42 | } catch (error) { 43 | console.error('parseInstruction error:', error); 44 | throw error; 45 | } 46 | } 47 | 48 | private getPoolAction(data: Buffer): PoolEventType | null { 49 | const instructionType = data.slice(0, 8); 50 | if ( 51 | instructionType.equals(DISCRIMINATORS.ORCA.ADD_LIQUIDITY) || 52 | instructionType.equals(DISCRIMINATORS.ORCA.ADD_LIQUIDITY2) 53 | ) { 54 | return 'ADD'; 55 | } else if (instructionType.equals(DISCRIMINATORS.ORCA.REMOVE_LIQUIDITY)) { 56 | return 'REMOVE'; 57 | } 58 | return null; 59 | } 60 | 61 | private parseAddLiquidityEvent(instruction: any, index: number, data: any, transfers: TransferData[]): PoolEvent { 62 | const [token0, token1] = this.utils.getLPTransfers(transfers); 63 | const token0Mint = token0?.info.mint; 64 | const token1Mint = token1?.info.mint; 65 | const programId = this.adapter.getInstructionProgramId(instruction); 66 | const accounts = this.adapter.getInstructionAccounts(instruction); 67 | const [token0Decimals, token1Decimals] = [ 68 | this.adapter.getTokenDecimals(token0Mint), 69 | this.adapter.getTokenDecimals(token1Mint), 70 | ]; 71 | 72 | return { 73 | ...this.adapter.getPoolEventBase('ADD', programId), 74 | idx: index.toString(), 75 | poolId: accounts[0], 76 | poolLpMint: accounts[0], 77 | token0Mint: token0Mint, 78 | token1Mint: token1Mint, 79 | token0Amount: token0?.info.tokenAmount.uiAmount, 80 | token0AmountRaw: token0?.info.tokenAmount.amount, 81 | token1Amount: token1?.info.tokenAmount.uiAmount, 82 | token1AmountRaw: token1?.info.tokenAmount.amount, 83 | token0Decimals: token0Decimals, 84 | token1Decimals: token1Decimals, 85 | lpAmount: convertToUiAmount(data.readBigUInt64LE(8), this.adapter.getTokenDecimals(accounts[1])), 86 | lpAmountRaw: data.readBigUInt64LE(8).toString(), 87 | }; 88 | } 89 | 90 | private parseRemoveLiquidityEvent(instruction: any, index: number, data: any, transfers: TransferData[]): PoolEvent { 91 | const [token0, token1] = this.utils.getLPTransfers(transfers); 92 | const token0Mint = token0?.info.mint; 93 | const token1Mint = token1?.info.mint; 94 | const programId = this.adapter.getInstructionProgramId(instruction); 95 | const accounts = this.adapter.getInstructionAccounts(instruction); 96 | const [token0Decimals, token1Decimals] = [ 97 | this.adapter.getTokenDecimals(token0Mint), 98 | this.adapter.getTokenDecimals(token1Mint), 99 | ]; 100 | 101 | return { 102 | ...this.adapter.getPoolEventBase('REMOVE', programId), 103 | idx: index.toString(), 104 | poolId: accounts[0], 105 | poolLpMint: accounts[0], 106 | token0Mint: token0Mint, 107 | token1Mint: token1Mint, 108 | token0Amount: token0?.info.tokenAmount.uiAmount, 109 | token0AmountRaw: token0?.info.tokenAmount.amount, 110 | token1Amount: token1?.info.tokenAmount.uiAmount, 111 | token1AmountRaw: token1?.info.tokenAmount.amount, 112 | token0Decimals: token0Decimals, 113 | token1Decimals: token1Decimals, 114 | lpAmount: convertToUiAmount(data.readBigUInt64LE(8), this.adapter.getTokenDecimals(accounts[1])), 115 | lpAmountRaw: data.readBigUInt64LE(8).toString(), 116 | }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/parsers/orca/parser-orca.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, DISCRIMINATORS } from '../../constants'; 2 | import { TradeInfo } from '../../types'; 3 | import { getInstructionData, getProgramName } from '../../utils'; 4 | import { BaseParser } from '../base-parser'; 5 | 6 | export class OrcaParser extends BaseParser { 7 | public processTrades(): TradeInfo[] { 8 | const trades: TradeInfo[] = []; 9 | 10 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 11 | if (DEX_PROGRAMS.ORCA.id === programId && this.notLiquidityEvent(instruction)) { 12 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 13 | if (transfers.length >= 2) { 14 | const trade = this.utils.processSwapData(transfers, { 15 | ...this.dexInfo, 16 | amm: this.dexInfo.amm || getProgramName(programId), 17 | }); 18 | if (trade) { 19 | trades.push(this.utils.attachTokenTransferInfo(trade, this.transferActions)); 20 | } 21 | } 22 | } 23 | }); 24 | 25 | return trades; 26 | } 27 | 28 | private notLiquidityEvent(instruction: any): boolean { 29 | if (instruction.data) { 30 | const instructionType = getInstructionData(instruction).slice(0, 8); 31 | return !Object.values(DISCRIMINATORS.ORCA).some((it) => instructionType.equals(it)); 32 | } 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-pumpfun'; 2 | export * from './parser-pumpfun-event'; 3 | export * from './parser-pumpswap'; 4 | export * from './parser-pumpswap-event'; 5 | export * from './parser-pumpswap-liquidity'; 6 | export * from './util'; 7 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/parser-pumpfun-event.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | import { Buffer } from 'buffer'; 3 | import { DEX_PROGRAMS, DISCRIMINATORS } from '../../constants'; 4 | import { InstructionClassifier } from '../../instruction-classifier'; 5 | import { TransactionAdapter } from '../../transaction-adapter'; 6 | import { 7 | ClassifiedInstruction, 8 | EventParser, 9 | PumpfunCompleteEvent, 10 | PumpfunCreateEvent, 11 | PumpfunEvent, 12 | PumpfunTradeEvent, 13 | } from '../../types'; 14 | import { getInstructionData, sortByIdx } from '../../utils'; 15 | import { BinaryReader } from '../binary-reader'; 16 | 17 | export class PumpfunEventParser { 18 | constructor(private readonly adapter: TransactionAdapter) {} 19 | 20 | private readonly eventParsers: Record> = { 21 | TRADE: { 22 | discriminator: DISCRIMINATORS.PUMPFUN.TRADE_EVENT, 23 | decode: this.decodeTradeEvent.bind(this), 24 | }, 25 | CREATE: { 26 | discriminator: DISCRIMINATORS.PUMPFUN.CREATE_EVENT, 27 | decode: this.decodeCreateEvent.bind(this), 28 | }, 29 | COMPLETE: { 30 | discriminator: DISCRIMINATORS.PUMPFUN.COMPLETE_EVENT, 31 | decode: this.decodeCompleteEvent.bind(this), 32 | }, 33 | }; 34 | 35 | public processEvents(): PumpfunEvent[] { 36 | const instructions = new InstructionClassifier(this.adapter).getInstructions(DEX_PROGRAMS.PUMP_FUN.id); 37 | return this.parseInstructions(instructions); 38 | } 39 | 40 | public parseInstructions(instructions: ClassifiedInstruction[]): PumpfunEvent[] { 41 | return sortByIdx( 42 | instructions 43 | .map(({ instruction, outerIndex, innerIndex }) => { 44 | try { 45 | const data = getInstructionData(instruction); 46 | const discriminator = Buffer.from(data.slice(0, 16)); 47 | 48 | for (const [type, parser] of Object.entries(this.eventParsers)) { 49 | if (discriminator.equals(parser.discriminator)) { 50 | const eventData = parser.decode(data.slice(16)); 51 | if (!eventData) return null; 52 | 53 | return { 54 | type: type as 'TRADE' | 'CREATE' | 'COMPLETE', 55 | data: eventData, 56 | slot: this.adapter.slot, 57 | timestamp: this.adapter.blockTime || 0, 58 | signature: this.adapter.signature, 59 | idx: `${outerIndex}-${innerIndex ?? 0}`, 60 | }; 61 | } 62 | } 63 | } catch (error) { 64 | console.error('Failed to parse Pumpfun event:', error); 65 | throw error; 66 | } 67 | return null; 68 | }) 69 | .filter((event): event is PumpfunEvent => event !== null) 70 | ); 71 | } 72 | 73 | private decodeTradeEvent(data: Buffer): PumpfunTradeEvent { 74 | const reader = new BinaryReader(data); 75 | 76 | return { 77 | mint: base58.encode(Buffer.from(reader.readFixedArray(32))), 78 | solAmount: reader.readU64(), 79 | tokenAmount: reader.readU64(), 80 | isBuy: reader.readU8() === 1, 81 | user: base58.encode(reader.readFixedArray(32)), 82 | timestamp: reader.readI64(), 83 | virtualSolReserves: reader.readU64(), 84 | virtualTokenReserves: reader.readU64(), 85 | }; 86 | } 87 | 88 | private decodeCreateEvent(data: Buffer): PumpfunCreateEvent { 89 | const reader = new BinaryReader(data); 90 | return { 91 | name: reader.readString(), 92 | symbol: reader.readString(), 93 | uri: reader.readString(), 94 | mint: base58.encode(Buffer.from(reader.readFixedArray(32))), 95 | bondingCurve: base58.encode(reader.readFixedArray(32)), 96 | user: base58.encode(reader.readFixedArray(32)), 97 | }; 98 | } 99 | 100 | private decodeCompleteEvent(data: Buffer): PumpfunCompleteEvent { 101 | const reader = new BinaryReader(data); 102 | return { 103 | user: base58.encode(reader.readFixedArray(32)), 104 | mint: base58.encode(Buffer.from(reader.readFixedArray(32))), 105 | bondingCurve: base58.encode(reader.readFixedArray(32)), 106 | timestamp: reader.readI64(), 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/parser-pumpfun.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../../transaction-adapter'; 2 | import { ClassifiedInstruction, DexInfo, PumpfunEvent, PumpfunTradeEvent, TradeInfo, TransferData } from '../../types'; 3 | import { BaseParser } from '../base-parser'; 4 | import { PumpfunEventParser } from './parser-pumpfun-event'; 5 | import { getPumpfunTradeInfo } from './util'; 6 | 7 | export class PumpfunParser extends BaseParser { 8 | private eventParser: PumpfunEventParser; 9 | 10 | constructor( 11 | adapter: TransactionAdapter, 12 | dexInfo: DexInfo, 13 | transferActions: Record, 14 | classifiedInstructions: ClassifiedInstruction[] 15 | ) { 16 | super(adapter, dexInfo, transferActions, classifiedInstructions); 17 | this.eventParser = new PumpfunEventParser(adapter); 18 | } 19 | 20 | public processTrades(): TradeInfo[] { 21 | const events = this.eventParser 22 | .parseInstructions(this.classifiedInstructions) 23 | .filter((event) => event.type === 'TRADE'); 24 | 25 | return events.map((event) => this.createTradeInfo(event)); 26 | } 27 | 28 | private createTradeInfo(data: PumpfunEvent): TradeInfo { 29 | const event = data.data as PumpfunTradeEvent; 30 | const trade = getPumpfunTradeInfo(event, { 31 | slot: data.slot, 32 | signature: data.signature, 33 | timestamp: data.timestamp, 34 | idx: data.idx, 35 | dexInfo: this.dexInfo, 36 | }); 37 | 38 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/parser-pumpswap-liquidity.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS } from '../../constants'; 2 | import { TransactionAdapter } from '../../transaction-adapter'; 3 | import { 4 | ClassifiedInstruction, 5 | convertToUiAmount, 6 | PoolEvent, 7 | PumpswapCreatePoolEvent, 8 | PumpswapDepositEvent, 9 | PumpswapEvent, 10 | PumpswapWithdrawEvent, 11 | TransferData, 12 | } from '../../types'; 13 | import { BaseLiquidityParser } from '../base-liquidity-parser'; 14 | import { PumpswapEventParser } from './parser-pumpswap-event'; 15 | 16 | export class PumpswapLiquidityParser extends BaseLiquidityParser { 17 | private eventParser: PumpswapEventParser; 18 | 19 | constructor( 20 | adapter: TransactionAdapter, 21 | transferActions: Record, 22 | classifiedInstructions: ClassifiedInstruction[] 23 | ) { 24 | super(adapter, transferActions, classifiedInstructions); 25 | this.eventParser = new PumpswapEventParser(adapter); 26 | } 27 | 28 | public processLiquidity(): PoolEvent[] { 29 | const events = this.eventParser 30 | .parseInstructions(this.classifiedInstructions) 31 | .filter((event) => ['CREATE', 'ADD', 'REMOVE'].includes(event.type)); 32 | 33 | return events.length > 0 ? this.parseLiquidityEvents(events) : []; 34 | } 35 | 36 | private parseLiquidityEvents(events: PumpswapEvent[]): PoolEvent[] { 37 | if (!events.length) return []; 38 | return events 39 | .map((event) => { 40 | switch (event.type) { 41 | case 'CREATE': 42 | return this.parseCreateEvent(event); 43 | case 'ADD': 44 | return this.parseDepositEvent(event); 45 | case 'REMOVE': 46 | return this.parseWithdrawEvent(event); 47 | default: 48 | return null; 49 | } 50 | }) 51 | .filter((it) => it != null); 52 | } 53 | 54 | private parseCreateEvent(data: PumpswapEvent): PoolEvent { 55 | const event = data.data as PumpswapCreatePoolEvent; 56 | 57 | return { 58 | ...this.adapter.getPoolEventBase('CREATE', DEX_PROGRAMS.PUMP_SWAP.id), 59 | idx: data.idx, 60 | poolId: event.pool, 61 | poolLpMint: event.lpMint, 62 | token0Mint: event.baseMint, 63 | token1Mint: event.quoteMint, 64 | token0Amount: convertToUiAmount(event.baseAmountIn, event.baseMintDecimals), 65 | token0AmountRaw: event.baseAmountIn.toString(), 66 | token1Amount: convertToUiAmount(event.quoteAmountIn, event.quoteMintDecimals), 67 | token1AmountRaw: event.quoteAmountIn.toString(), 68 | token0Decimals: event.baseMintDecimals, 69 | token1Decimals: event.quoteMintDecimals, 70 | }; 71 | } 72 | 73 | private parseDepositEvent(data: PumpswapEvent): PoolEvent { 74 | const event = data.data as PumpswapDepositEvent; 75 | const token0Mint = this.adapter.splTokenMap.get(event.userBaseTokenAccount)!.mint; 76 | const token0Decimals = this.adapter.getTokenDecimals(token0Mint); 77 | const token1Mint = this.adapter.splTokenMap.get(event.userQuoteTokenAccount)!.mint; 78 | const token1Decimals = this.adapter.getTokenDecimals(token1Mint); 79 | return { 80 | ...this.adapter.getPoolEventBase('ADD', DEX_PROGRAMS.PUMP_SWAP.id), 81 | idx: data.idx, 82 | poolId: event.pool, 83 | poolLpMint: this.adapter.splTokenMap.get(event.userPoolTokenAccount)!.mint, 84 | token0Mint: token0Mint, 85 | token1Mint: token1Mint, 86 | token0Amount: convertToUiAmount(event.baseAmountIn, token0Decimals), 87 | token0AmountRaw: event.baseAmountIn.toString(), 88 | token1Amount: convertToUiAmount(event.quoteAmountIn, token1Decimals), 89 | token1AmountRaw: event.quoteAmountIn.toString(), 90 | token0Decimals: token0Decimals, 91 | token1Decimals: token1Decimals, 92 | }; 93 | } 94 | 95 | private parseWithdrawEvent(data: PumpswapEvent): PoolEvent { 96 | const event = data.data as PumpswapWithdrawEvent; 97 | const token0Mint = this.adapter.splTokenMap.get(event.userBaseTokenAccount)!.mint; 98 | const token0Decimals = this.adapter.getTokenDecimals(token0Mint); 99 | const token1Mint = this.adapter.splTokenMap.get(event.userQuoteTokenAccount)!.mint; 100 | const token1Decimals = this.adapter.getTokenDecimals(token1Mint); 101 | return { 102 | ...this.adapter.getPoolEventBase('REMOVE', DEX_PROGRAMS.PUMP_SWAP.id), 103 | idx: data.idx, 104 | poolId: event.pool, 105 | poolLpMint: this.adapter.splTokenMap.get(event.userPoolTokenAccount)!.mint, 106 | token0Mint: token0Mint, 107 | token1Mint: token1Mint, 108 | token0Amount: convertToUiAmount(event.baseAmountOut, token0Decimals), 109 | token0AmountRaw: event.baseAmountOut.toString(), 110 | token1Amount: convertToUiAmount(event.quoteAmountOut, token1Decimals), 111 | token1AmountRaw: event.quoteAmountOut.toString(), 112 | token0Decimals: token0Decimals, 113 | token1Decimals: token1Decimals, 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/parser-pumpswap.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../../transaction-adapter'; 2 | import { 3 | ClassifiedInstruction, 4 | DexInfo, 5 | PumpswapBuyEvent, 6 | PumpswapEvent, 7 | PumpswapSellEvent, 8 | TradeInfo, 9 | TransferData, 10 | } from '../../types'; 11 | import { BaseParser } from '../base-parser'; 12 | import { PumpswapEventParser } from './parser-pumpswap-event'; 13 | import { getPumpswapBuyInfo, getPumpswapSellInfo } from './util'; 14 | 15 | export class PumpswapParser extends BaseParser { 16 | private eventParser: PumpswapEventParser; 17 | 18 | constructor( 19 | adapter: TransactionAdapter, 20 | dexInfo: DexInfo, 21 | transferActions: Record, 22 | classifiedInstructions: ClassifiedInstruction[] 23 | ) { 24 | super(adapter, dexInfo, transferActions, classifiedInstructions); 25 | this.eventParser = new PumpswapEventParser(adapter); 26 | } 27 | 28 | public processTrades(): TradeInfo[] { 29 | const events = this.eventParser 30 | .parseInstructions(this.classifiedInstructions) 31 | .filter((event) => ['BUY', 'SELL'].includes(event.type)); 32 | 33 | return events.map((event) => (event.type === 'BUY' ? this.createBuyInfo(event) : this.createSellInfo(event))); 34 | } 35 | 36 | private createBuyInfo(data: PumpswapEvent): TradeInfo { 37 | const event = data.data as PumpswapBuyEvent; 38 | 39 | const inputMint = this.adapter.splTokenMap.get(event.userQuoteTokenAccount)?.mint; 40 | if (!inputMint) throw new Error('inputMint not found'); 41 | const outputMint = this.adapter.splTokenMap.get(event.userBaseTokenAccount)?.mint; 42 | if (!outputMint) throw new Error('outputMint not found'); 43 | const feeMint = this.adapter.splTokenMap.get(event.protocolFeeRecipientTokenAccount)?.mint; 44 | if (!feeMint) throw new Error('feeMint not found'); 45 | 46 | const inputDecimal = this.adapter.getTokenDecimals(inputMint); 47 | const ouptDecimal = this.adapter.getTokenDecimals(outputMint); 48 | const feeDecimal = this.adapter.getTokenDecimals(feeMint); 49 | 50 | const trade = getPumpswapBuyInfo( 51 | event, 52 | { mint: inputMint, decimals: inputDecimal }, 53 | { mint: outputMint, decimals: ouptDecimal }, 54 | { mint: feeMint, decimals: feeDecimal }, 55 | { 56 | slot: data.slot, 57 | signature: data.signature, 58 | timestamp: data.timestamp, 59 | idx: data.idx, 60 | dexInfo: this.dexInfo, 61 | } 62 | ); 63 | 64 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 65 | } 66 | 67 | private createSellInfo(data: PumpswapEvent): TradeInfo { 68 | const event = data.data as PumpswapSellEvent; 69 | 70 | const inputMint = this.adapter.splTokenMap.get(event.userBaseTokenAccount)?.mint; 71 | if (!inputMint) throw new Error('inputMint not found'); 72 | const outputMint = this.adapter.splTokenMap.get(event.userQuoteTokenAccount)?.mint; 73 | if (!outputMint) throw new Error('outputMint not found'); 74 | const feeMint = this.adapter.splTokenMap.get(event.protocolFeeRecipientTokenAccount)?.mint; 75 | if (!feeMint) throw new Error('feeMint not found'); 76 | 77 | const inputDecimal = this.adapter.getTokenDecimals(inputMint); 78 | const ouptDecimal = this.adapter.getTokenDecimals(outputMint); 79 | const feeDecimal = this.adapter.getTokenDecimals(feeMint); 80 | 81 | const trade = getPumpswapSellInfo( 82 | event, 83 | { mint: inputMint, decimals: inputDecimal }, 84 | { mint: outputMint, decimals: ouptDecimal }, 85 | { mint: feeMint, decimals: feeDecimal }, 86 | { 87 | slot: data.slot, 88 | signature: data.signature, 89 | timestamp: data.timestamp, 90 | idx: data.idx, 91 | dexInfo: this.dexInfo, 92 | } 93 | ); 94 | 95 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/parsers/pumpfun/util.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS, TOKENS } from '../../constants'; 2 | import { 3 | DexInfo, 4 | PumpfunTradeEvent, 5 | PumpswapBuyEvent, 6 | PumpswapSellEvent, 7 | TradeInfo, 8 | TradeType, 9 | convertToUiAmount, 10 | } from '../../types'; 11 | import { getTradeType } from '../../utils'; 12 | 13 | export const getPumpfunTradeInfo = ( 14 | event: PumpfunTradeEvent, 15 | info: { 16 | slot: number; 17 | signature: string; 18 | timestamp: number; 19 | idx?: string; 20 | dexInfo?: DexInfo; 21 | } 22 | ): TradeInfo => { 23 | const tradeType: TradeType = event.isBuy ? 'BUY' : 'SELL'; 24 | const isBuy = tradeType === 'BUY'; 25 | return { 26 | type: tradeType, 27 | inputToken: { 28 | mint: isBuy ? TOKENS.SOL : event.mint, 29 | amount: isBuy ? convertToUiAmount(event.solAmount) : convertToUiAmount(event.tokenAmount, 6), 30 | amountRaw: isBuy ? event.solAmount.toString() : event.tokenAmount.toString(), 31 | decimals: isBuy ? 9 : 6, 32 | }, 33 | outputToken: { 34 | mint: isBuy ? event.mint : TOKENS.SOL, 35 | amount: isBuy ? convertToUiAmount(event.tokenAmount, 6) : convertToUiAmount(event.solAmount), 36 | amountRaw: isBuy ? event.tokenAmount.toString() : event.solAmount.toString(), 37 | decimals: isBuy ? 6 : 9, 38 | }, 39 | user: event.user, 40 | programId: DEX_PROGRAMS.PUMP_FUN.id, 41 | amm: info.dexInfo?.amm || DEX_PROGRAMS.PUMP_FUN.name, 42 | route: info.dexInfo?.route || '', 43 | slot: info.slot, 44 | timestamp: info.timestamp, 45 | signature: info.signature, 46 | idx: info.idx || '', 47 | }; 48 | }; 49 | 50 | export const getPumpswapBuyInfo = ( 51 | event: PumpswapBuyEvent, 52 | inputToken: { 53 | mint: string; 54 | decimals: number; 55 | }, 56 | outputToken: { 57 | mint: string; 58 | decimals: number; 59 | }, 60 | feeToken: { 61 | mint: string; 62 | decimals: number; 63 | }, 64 | info: { 65 | slot: number; 66 | signature: string; 67 | timestamp: number; 68 | idx?: string; 69 | dexInfo?: DexInfo; 70 | } 71 | ): TradeInfo => { 72 | const { mint: inputMint, decimals: inputDecimal } = inputToken; 73 | const { mint: outputMint, decimals: ouptDecimal } = outputToken; 74 | const { mint: feeMint, decimals: feeDecimal } = feeToken; 75 | const feeAmt = BigInt(event.protocolFee) + BigInt(event.coinCreatorFee); 76 | 77 | const trade = { 78 | type: getTradeType(inputMint, outputMint), 79 | inputToken: { 80 | mint: inputMint, 81 | amount: convertToUiAmount(event.quoteAmountInWithLpFee, inputDecimal), 82 | amountRaw: event.quoteAmountInWithLpFee.toString(), 83 | decimals: inputDecimal, 84 | }, 85 | outputToken: { 86 | mint: outputMint, 87 | amount: convertToUiAmount(event.baseAmountOut, ouptDecimal), 88 | amountRaw: event.baseAmountOut.toString(), 89 | decimals: ouptDecimal, 90 | }, 91 | fee: { 92 | mint: feeMint, 93 | amount: convertToUiAmount(feeAmt, feeDecimal), 94 | amountRaw: feeAmt.toString(), 95 | decimals: feeDecimal, 96 | }, 97 | fees: [ 98 | { 99 | mint: feeMint, 100 | amount: convertToUiAmount(event.protocolFee, feeDecimal), 101 | amountRaw: event.protocolFee.toString(), 102 | decimals: feeDecimal, 103 | dex: DEX_PROGRAMS.PUMP_SWAP.name, 104 | type: 'protocol', 105 | recipient: event.protocolFeeRecipient, 106 | }, 107 | ], 108 | user: event.user, 109 | programId: info.dexInfo?.programId || DEX_PROGRAMS.PUMP_SWAP.id, 110 | amm: DEX_PROGRAMS.PUMP_SWAP.name, 111 | route: info.dexInfo?.route || '', 112 | slot: info.slot, 113 | timestamp: info.timestamp, 114 | signature: info.signature, 115 | idx: info.idx || '', 116 | } as TradeInfo; 117 | 118 | if (trade.fees && BigInt(event.coinCreatorFee) > 0) { 119 | trade.fees.push({ 120 | mint: feeMint, 121 | amount: convertToUiAmount(event.coinCreatorFee, feeDecimal), 122 | amountRaw: event.coinCreatorFee.toString(), 123 | decimals: feeDecimal, 124 | dex: DEX_PROGRAMS.PUMP_SWAP.name, 125 | type: 'coinCreator', 126 | recipient: event.coinCreator, 127 | }); 128 | } 129 | return trade; 130 | }; 131 | 132 | export const getPumpswapSellInfo = ( 133 | event: PumpswapSellEvent, 134 | inputToken: { 135 | mint: string; 136 | decimals: number; 137 | }, 138 | outputToken: { 139 | mint: string; 140 | decimals: number; 141 | }, 142 | feeToken: { 143 | mint: string; 144 | decimals: number; 145 | }, 146 | info: { 147 | slot: number; 148 | signature: string; 149 | timestamp: number; 150 | idx?: string; 151 | dexInfo?: DexInfo; 152 | } 153 | ): TradeInfo => { 154 | const { mint: inputMint, decimals: inputDecimal } = inputToken; 155 | const { mint: outputMint, decimals: ouptDecimal } = outputToken; 156 | const { mint: feeMint, decimals: feeDecimal } = feeToken; 157 | const feeAmt = BigInt(event.protocolFee) + BigInt(event.coinCreatorFee); 158 | 159 | const trade = { 160 | type: getTradeType(inputMint, outputMint), 161 | inputToken: { 162 | mint: inputMint, 163 | amount: convertToUiAmount(event.baseAmountIn, inputDecimal), 164 | amountRaw: event.baseAmountIn.toString(), 165 | decimals: inputDecimal, 166 | }, 167 | outputToken: { 168 | mint: outputMint, 169 | amount: convertToUiAmount(event.userQuoteAmountOut, ouptDecimal), 170 | amountRaw: event.userQuoteAmountOut.toString(), 171 | decimals: ouptDecimal, 172 | }, 173 | fee: { 174 | mint: feeMint, 175 | amount: convertToUiAmount(feeAmt, feeDecimal), 176 | amountRaw: event.protocolFee.toString(), 177 | decimals: feeDecimal, 178 | dex: DEX_PROGRAMS.PUMP_SWAP.name, 179 | }, 180 | fees: [ 181 | { 182 | mint: feeMint, 183 | amount: convertToUiAmount(event.protocolFee, feeDecimal), 184 | amountRaw: event.protocolFee.toString(), 185 | decimals: feeDecimal, 186 | dex: DEX_PROGRAMS.PUMP_SWAP.name, 187 | type: 'protocol', 188 | recipient: event.protocolFeeRecipient, 189 | }, 190 | ], 191 | user: event.user, 192 | programId: info.dexInfo?.programId || DEX_PROGRAMS.PUMP_SWAP.id, 193 | amm: DEX_PROGRAMS.PUMP_SWAP.name, 194 | route: info.dexInfo?.route || '', 195 | slot: info.slot, 196 | timestamp: info.timestamp, 197 | signature: info.signature, 198 | idx: info.idx || '', 199 | } as TradeInfo; 200 | if (trade.fees && BigInt(event.coinCreatorFee) > 0) { 201 | trade.fees.push({ 202 | mint: feeMint, 203 | amount: convertToUiAmount(event.coinCreatorFee, feeDecimal), 204 | amountRaw: event.coinCreatorFee.toString(), 205 | decimals: feeDecimal, 206 | dex: DEX_PROGRAMS.PUMP_SWAP.name, 207 | type: 'coinCreator', 208 | recipient: event.coinCreator, 209 | }); 210 | } 211 | return trade; 212 | }; 213 | -------------------------------------------------------------------------------- /src/parsers/raydium/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser-raydium'; 2 | export * from './parser-raydium-liquidity-base'; 3 | export * from './liquidity-raydium-v4'; 4 | export * from './liquidity-raydium-cl'; 5 | export * from './liquidity-raydium-cpmm'; 6 | export * from './parser-raydium-launchpad-event'; 7 | export * from './parser-raydium-launchpad'; 8 | export * from './parser-raydium-logs'; 9 | export * from './util'; 10 | -------------------------------------------------------------------------------- /src/parsers/raydium/layouts/raydium-lcp-create.layout.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | import { 3 | ConstantCurve, 4 | CurveParams, 5 | CurveType, 6 | FixedCurve, 7 | LinearCurve, 8 | MintParams, 9 | RaydiumLCPCreateEvent, 10 | VestingParams, 11 | } from '../../../types'; 12 | import { BinaryReader } from '../../binary-reader'; 13 | 14 | // PoolCreateEventLayout 15 | export class PoolCreateEventLayout { 16 | poolState: Uint8Array; 17 | creator: Uint8Array; 18 | config: Uint8Array; 19 | baseMintParam: MintParams; 20 | curveParam: CurveParams; 21 | vestingParam: VestingParams; 22 | 23 | constructor(fields: { 24 | poolState: Uint8Array; 25 | creator: Uint8Array; 26 | config: Uint8Array; 27 | baseMintParam: MintParams; 28 | curveParam: CurveParams; 29 | vestingParam: VestingParams; 30 | }) { 31 | this.poolState = fields.poolState; 32 | this.creator = fields.creator; 33 | this.config = fields.config; 34 | this.baseMintParam = fields.baseMintParam; 35 | this.curveParam = fields.curveParam; 36 | this.vestingParam = fields.vestingParam; 37 | } 38 | 39 | // Deserialize with BinaryReader 40 | static deserialize(data: Buffer): PoolCreateEventLayout { 41 | const reader = new BinaryReader(data); 42 | 43 | // Read fields 44 | const poolState = reader.readFixedArray(32); 45 | const creator = reader.readFixedArray(32); 46 | const config = reader.readFixedArray(32); 47 | 48 | // Read baseMintParam 49 | const baseMintParam: MintParams = { 50 | decimals: reader.readU8(), 51 | name: reader.readString(), 52 | symbol: reader.readString(), 53 | uri: reader.readString(), 54 | }; 55 | 56 | // Read curveParam 57 | const variant = reader.readU8(); 58 | let curveParam: CurveParams; 59 | try { 60 | if (variant === CurveType.Constant) { 61 | const data: ConstantCurve = { 62 | supply: reader.readU64(), 63 | totalBaseSell: reader.readU64(), 64 | totalQuoteFundRaising: reader.readU64(), 65 | migrateType: reader.readU8(), 66 | }; 67 | curveParam = { variant: 'Constant', data }; 68 | } else if (variant === CurveType.Fixed) { 69 | const data: FixedCurve = { 70 | supply: reader.readU64(), 71 | totalQuoteFundRaising: reader.readU64(), 72 | migrateType: reader.readU8(), 73 | }; 74 | curveParam = { variant: 'Fixed', data }; 75 | } else if (variant === CurveType.Linear) { 76 | const data: LinearCurve = { 77 | supply: reader.readU64(), 78 | totalQuoteFundRaising: reader.readU64(), 79 | migrateType: reader.readU8(), 80 | }; 81 | curveParam = { variant: 'Linear', data }; 82 | } else { 83 | throw new Error(`Unknown CurveParams variant: ${variant}`); 84 | } 85 | } catch (error) { 86 | console.error(`Failed to decode CurveParams at offset ${reader.getOffset()}:`, error); 87 | throw error; 88 | } 89 | 90 | // Read vestingParam 91 | const vestingParam: VestingParams = { 92 | totalLockedAmount: reader.readU64(), 93 | cliffPeriod: reader.readU64(), 94 | unlockPeriod: reader.readU64(), 95 | }; 96 | 97 | return new PoolCreateEventLayout({ 98 | poolState, 99 | creator, 100 | config, 101 | baseMintParam, 102 | curveParam, 103 | vestingParam, 104 | }); 105 | } 106 | 107 | toObject(): RaydiumLCPCreateEvent { 108 | return { 109 | poolState: base58.encode(this.poolState), 110 | creator: base58.encode(this.creator), 111 | config: base58.encode(this.config), 112 | baseMintParam: { 113 | decimals: this.baseMintParam.decimals, 114 | name: this.baseMintParam.name, 115 | symbol: this.baseMintParam.symbol, 116 | uri: this.baseMintParam.uri, 117 | }, 118 | curveParam: { 119 | variant: this.curveParam.variant, 120 | data: { 121 | supply: BigInt(this.curveParam.data.supply), 122 | totalBaseSell: 123 | 'totalBaseSell' in this.curveParam.data ? BigInt(this.curveParam.data.totalBaseSell) : undefined, 124 | totalQuoteFundRaising: BigInt(this.curveParam.data.totalQuoteFundRaising), 125 | migrateType: this.curveParam.data.migrateType, 126 | }, 127 | }, 128 | vestingParam: { 129 | totalLockedAmount: BigInt(this.vestingParam.totalLockedAmount), 130 | cliffPeriod: BigInt(this.vestingParam.cliffPeriod), 131 | unlockPeriod: BigInt(this.vestingParam.unlockPeriod), 132 | }, 133 | baseMint: '', // Initialize baseMint to an empty string 134 | quoteMint: '', // Initialize quoteMint to an empty string 135 | }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/parsers/raydium/layouts/raydium-lcp-trade.layout.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | import { PoolStatus, RaydiumLCPTradeEvent, TradeDirection } from '../../../types/raydium'; 3 | 4 | export class RaydiumLCPTradeLayout { 5 | poolState: Uint8Array; 6 | totalBaseSell: bigint; 7 | virtualBase: bigint; 8 | virtualQuote: bigint; 9 | realBaseBefore: bigint; 10 | realQuoteBefore: bigint; 11 | realBaseAfter: bigint; 12 | realQuoteAfter: bigint; 13 | amountIn: bigint; 14 | amountOut: bigint; 15 | protocolFee: bigint; 16 | platformFee: bigint; 17 | shareFee: bigint; 18 | tradeDirection: TradeDirection; 19 | poolStatus: PoolStatus; 20 | 21 | constructor(fields: { 22 | poolState: Uint8Array; 23 | totalBaseSell: bigint; 24 | virtualBase: bigint; 25 | virtualQuote: bigint; 26 | realBaseBefore: bigint; 27 | realQuoteBefore: bigint; 28 | realBaseAfter: bigint; 29 | realQuoteAfter: bigint; 30 | amountIn: bigint; 31 | amountOut: bigint; 32 | protocolFee: bigint; 33 | platformFee: bigint; 34 | shareFee: bigint; 35 | tradeDirection: TradeDirection; 36 | poolStatus: PoolStatus; 37 | }) { 38 | this.poolState = fields.poolState; 39 | this.totalBaseSell = fields.totalBaseSell; 40 | this.virtualBase = fields.virtualBase; 41 | this.virtualQuote = fields.virtualQuote; 42 | this.realBaseBefore = fields.realBaseBefore; 43 | this.realQuoteBefore = fields.realQuoteBefore; 44 | this.realBaseAfter = fields.realBaseAfter; 45 | this.realQuoteAfter = fields.realQuoteAfter; 46 | this.amountIn = fields.amountIn; 47 | this.amountOut = fields.amountOut; 48 | this.protocolFee = fields.protocolFee; 49 | this.platformFee = fields.platformFee; 50 | this.shareFee = fields.shareFee; 51 | this.tradeDirection = fields.tradeDirection; 52 | this.poolStatus = fields.poolStatus; 53 | } 54 | 55 | static schema = new Map([ 56 | [ 57 | RaydiumLCPTradeLayout, 58 | { 59 | kind: 'struct', 60 | fields: [ 61 | ['poolState', [32]], 62 | ['totalBaseSell', 'u64'], 63 | ['virtualBase', 'u64'], 64 | ['virtualQuote', 'u64'], 65 | ['realBaseBefore', 'u64'], 66 | ['realQuoteBefore', 'u64'], 67 | ['realBaseAfter', 'u64'], 68 | ['realQuoteAfter', 'u64'], 69 | ['amountIn', 'u64'], 70 | ['amountOut', 'u64'], 71 | ['protocolFee', 'u64'], 72 | ['platformFee', 'u64'], 73 | ['shareFee', 'u64'], 74 | ['tradeDirection', 'u8'], 75 | ['poolStatus', 'u8'], 76 | ], 77 | }, 78 | ], 79 | ]); 80 | 81 | toObject(): RaydiumLCPTradeEvent { 82 | return { 83 | poolState: base58.encode(this.poolState), 84 | totalBaseSell: this.totalBaseSell, 85 | virtualBase: this.virtualBase, 86 | virtualQuote: this.virtualQuote, 87 | realBaseBefore: this.realBaseBefore, 88 | realQuoteBefore: this.realQuoteBefore, 89 | realBaseAfter: this.realBaseAfter, 90 | amountIn: this.amountIn, 91 | amountOut: this.amountOut, 92 | protocolFee: this.protocolFee, 93 | platformFee: this.platformFee, 94 | shareFee: this.shareFee, 95 | tradeDirection: this.tradeDirection, 96 | poolStatus: this.poolStatus, 97 | baseMint: '', 98 | quoteMint: '', 99 | user: '', 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/parsers/raydium/liquidity-raydium-cl.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS } from '../../constants'; 2 | import { PoolEventType } from '../../types'; 3 | import { RaydiumLiquidityParserBase, ParseEventConfig } from './parser-raydium-liquidity-base'; 4 | 5 | export class RaydiumCLPoolParser extends RaydiumLiquidityParserBase { 6 | public getPoolAction(data: Buffer): { name: string; type: PoolEventType } | null { 7 | const instructionType = data.slice(0, 8); 8 | 9 | for (const [name, discriminator] of Object.entries(DISCRIMINATORS.RAYDIUM_CL.CREATE)) { 10 | if (instructionType.equals(discriminator)) return { name, type: 'CREATE' }; 11 | } 12 | 13 | for (const [name, discriminator] of Object.entries(DISCRIMINATORS.RAYDIUM_CL.ADD_LIQUIDITY)) { 14 | if (instructionType.equals(discriminator)) return { name, type: 'ADD' }; 15 | } 16 | 17 | for (const [name, discriminator] of Object.entries(DISCRIMINATORS.RAYDIUM_CL.REMOVE_LIQUIDITY)) { 18 | if (instructionType.equals(discriminator)) return { name, type: 'REMOVE' }; 19 | } 20 | 21 | return null; 22 | } 23 | 24 | public getEventConfig(type: PoolEventType, instructionType: { name: string; type: PoolEventType }): ParseEventConfig { 25 | const configs = { 26 | CREATE: { 27 | eventType: 'CREATE' as const, 28 | poolIdIndex: ['openPosition', 'openPositionV2'].includes(instructionType.name) ? 5 : 4, 29 | lpMintIndex: ['openPosition', 'openPositionV2'].includes(instructionType.name) ? 5 : 4, 30 | }, 31 | ADD: { 32 | eventType: 'ADD' as const, 33 | poolIdIndex: 2, 34 | lpMintIndex: 2, 35 | tokenAmountOffsets: { token0: 32, token1: 24, lp: 8 }, 36 | }, 37 | REMOVE: { 38 | eventType: 'REMOVE' as const, 39 | poolIdIndex: 3, 40 | lpMintIndex: 3, 41 | tokenAmountOffsets: { token0: 32, token1: 24, lp: 8 }, 42 | }, 43 | }; 44 | return configs[type]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/parsers/raydium/liquidity-raydium-cpmm.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS } from '../../constants'; 2 | import { PoolEventType } from '../../types'; 3 | import { RaydiumLiquidityParserBase, ParseEventConfig } from './parser-raydium-liquidity-base'; 4 | 5 | export class RaydiumCPMMPoolParser extends RaydiumLiquidityParserBase { 6 | public getPoolAction(data: Buffer): PoolEventType | null { 7 | const instructionType = data.slice(0, 8); 8 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM_CPMM.CREATE)) return 'CREATE'; 9 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM_CPMM.ADD_LIQUIDITY)) return 'ADD'; 10 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM_CPMM.REMOVE_LIQUIDITY)) return 'REMOVE'; 11 | return null; 12 | } 13 | 14 | public getEventConfig(type: PoolEventType): ParseEventConfig { 15 | const configs = { 16 | CREATE: { 17 | eventType: 'CREATE' as const, 18 | poolIdIndex: 3, 19 | lpMintIndex: 6, 20 | tokenAmountOffsets: { token0: 8, token1: 16, lp: 0 }, 21 | }, 22 | ADD: { 23 | eventType: 'ADD' as const, 24 | poolIdIndex: 2, 25 | lpMintIndex: 12, 26 | tokenAmountOffsets: { token0: 16, token1: 24, lp: 8 }, 27 | }, 28 | REMOVE: { 29 | eventType: 'REMOVE' as const, 30 | poolIdIndex: 2, 31 | lpMintIndex: 12, 32 | tokenAmountOffsets: { token0: 16, token1: 24, lp: 8 }, 33 | }, 34 | }; 35 | return configs[type]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/parsers/raydium/liquidity-raydium-v4.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS } from '../../constants'; 2 | import { PoolEventType } from '../../types'; 3 | import { RaydiumLiquidityParserBase, ParseEventConfig } from './parser-raydium-liquidity-base'; 4 | 5 | export class RaydiumV4PoolParser extends RaydiumLiquidityParserBase { 6 | public getPoolAction(data: Buffer): PoolEventType | null { 7 | const instructionType = data.slice(0, 1); 8 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM.CREATE)) return 'CREATE'; 9 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM.ADD_LIQUIDITY)) return 'ADD'; 10 | if (instructionType.equals(DISCRIMINATORS.RAYDIUM.REMOVE_LIQUIDITY)) return 'REMOVE'; 11 | return null; 12 | } 13 | 14 | public getEventConfig(type: PoolEventType): ParseEventConfig { 15 | const configs = { 16 | CREATE: { eventType: 'CREATE' as const, poolIdIndex: 4, lpMintIndex: 7 }, 17 | ADD: { eventType: 'ADD' as const, poolIdIndex: 1, lpMintIndex: 5 }, 18 | REMOVE: { eventType: 'REMOVE' as const, poolIdIndex: 1, lpMintIndex: 5 }, 19 | }; 20 | return configs[type]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/parsers/raydium/parser-raydium-launchpad-event.ts: -------------------------------------------------------------------------------- 1 | import { deserializeUnchecked } from 'borsh'; 2 | import { Buffer } from 'buffer'; 3 | import { DEX_PROGRAMS, DISCRIMINATORS } from '../../constants'; 4 | import { InstructionClassifier } from '../../instruction-classifier'; 5 | import { TransactionAdapter } from '../../transaction-adapter'; 6 | import { 7 | ClassifiedInstruction, 8 | EventsParser, 9 | RaydiumLCPCompleteEvent, 10 | RaydiumLCPCreateEvent, 11 | RaydiumLCPEvent, 12 | RaydiumLCPTradeEvent, 13 | } from '../../types'; 14 | import { getInstructionData, sortByIdx } from '../../utils'; 15 | import { PoolCreateEventLayout } from './layouts/raydium-lcp-create.layout'; 16 | import { RaydiumLCPTradeLayout } from './layouts/raydium-lcp-trade.layout'; 17 | 18 | export class RaydiumLaunchpadEventParser { 19 | constructor(private readonly adapter: TransactionAdapter) {} 20 | 21 | private readonly EventsParsers: Record> = { 22 | CREATE: { 23 | discriminators: [DISCRIMINATORS.RAYDIUM_LCP.CREATE_EVENT], 24 | slice: 16, 25 | decode: this.decodeCreateEvent.bind(this), 26 | }, 27 | TRADE: { 28 | discriminators: [ 29 | DISCRIMINATORS.RAYDIUM_LCP.BUY_EXACT_IN, 30 | DISCRIMINATORS.RAYDIUM_LCP.BUY_EXACT_OUT, 31 | DISCRIMINATORS.RAYDIUM_LCP.SELL_EXACT_IN, 32 | DISCRIMINATORS.RAYDIUM_LCP.SELL_EXACT_OUT, 33 | ], 34 | slice: 8, 35 | decode: this.decodeTradeInstruction.bind(this), 36 | }, 37 | COMPLETE: { 38 | discriminators: [DISCRIMINATORS.RAYDIUM_LCP.MIGRATE_TO_AMM, DISCRIMINATORS.RAYDIUM_LCP.MIGRATE_TO_CPSWAP], 39 | slice: 8, 40 | decode: this.decodeCompleteInstruction.bind(this), 41 | }, 42 | }; 43 | 44 | public processEvents(): RaydiumLCPEvent[] { 45 | const instructions = new InstructionClassifier(this.adapter).getInstructions(DEX_PROGRAMS.RAYDIUM_LCP.id); 46 | return this.parseInstructions(instructions); 47 | } 48 | 49 | public parseInstructions(instructions: ClassifiedInstruction[]): RaydiumLCPEvent[] { 50 | return sortByIdx( 51 | instructions 52 | .map(({ instruction, outerIndex, innerIndex }) => { 53 | try { 54 | const data = getInstructionData(instruction); 55 | 56 | for (const [type, parser] of Object.entries(this.EventsParsers)) { 57 | const discriminator = Buffer.from(data.slice(0, parser.slice)); 58 | if (parser.discriminators.some((it) => discriminator.equals(it))) { 59 | const options = { 60 | instruction, 61 | outerIndex, 62 | innerIndex, 63 | }; 64 | const eventData = parser.decode(data, options); 65 | if (!eventData) return null; 66 | 67 | return { 68 | type: type as 'TRADE' | 'CREATE' | 'COMPLETE', 69 | data: eventData, 70 | slot: this.adapter.slot, 71 | timestamp: this.adapter.blockTime || 0, 72 | signature: this.adapter.signature, 73 | idx: `${outerIndex}-${innerIndex ?? 0}`, 74 | }; 75 | } 76 | } 77 | } catch (error) { 78 | console.error('Failed to parse RaydiumLCP event:', error); 79 | throw error; 80 | } 81 | return null; 82 | }) 83 | .filter((event): event is RaydiumLCPEvent => event !== null) 84 | ); 85 | } 86 | 87 | private decodeTradeInstruction(data: Buffer, options: any): RaydiumLCPTradeEvent { 88 | const eventInstruction = this.adapter.getInnerInstruction( 89 | options.outerIndex, 90 | options.innerIndex == undefined ? 0 : options.innerIndex + 1 91 | ); // find inner instruction 92 | if (!eventInstruction) { 93 | throw new Error('Event instruction not found'); 94 | } 95 | 96 | // get event data from inner instruction 97 | const eventData = getInstructionData(eventInstruction).slice(16); 98 | const layout = deserializeUnchecked(RaydiumLCPTradeLayout.schema, RaydiumLCPTradeLayout, Buffer.from(eventData)); 99 | const event = layout.toObject(); 100 | // get instruction accounts 101 | const accounts = this.adapter.getInstructionAccounts(options.instruction); 102 | event.user = accounts[0]; 103 | event.baseMint = accounts[9]; 104 | event.quoteMint = accounts[10]; 105 | return event as RaydiumLCPTradeEvent; 106 | } 107 | 108 | private decodeCreateEvent(data: Buffer, options: any): RaydiumLCPCreateEvent { 109 | const eventInstruction = this.adapter.instructions[options.outerIndex]; // find outer instruction 110 | if (!eventInstruction) { 111 | throw new Error('Event instruction not found'); 112 | } 113 | // parse event data 114 | const eventData = data.slice(16); 115 | const event = PoolCreateEventLayout.deserialize(eventData).toObject(); 116 | 117 | // get instruction accounts 118 | const accounts = this.adapter.getInstructionAccounts(eventInstruction); 119 | event.baseMint = accounts[6]; 120 | event.quoteMint = accounts[7]; 121 | 122 | return event as RaydiumLCPCreateEvent; 123 | } 124 | 125 | private decodeCompleteInstruction(data: Buffer, options: any): RaydiumLCPCompleteEvent { 126 | const discriminator = Buffer.from(data.slice(0, 8)); 127 | const accounts = this.adapter.getInstructionAccounts(options.instruction); 128 | const [baseMint, quoteMint, poolMint, lpMint] = discriminator.equals(DISCRIMINATORS.RAYDIUM_LCP.MIGRATE_TO_AMM) 129 | ? [accounts[1], accounts[2], accounts[13], accounts[16]] 130 | : [accounts[1], accounts[2], accounts[5], accounts[7]]; 131 | const amm = discriminator.equals(DISCRIMINATORS.RAYDIUM_LCP.MIGRATE_TO_AMM) 132 | ? DEX_PROGRAMS.RAYDIUM_V4.name 133 | : DEX_PROGRAMS.RAYDIUM_CPMM.name; 134 | 135 | return { 136 | baseMint, 137 | quoteMint, 138 | poolMint, 139 | lpMint, 140 | amm, 141 | }; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/parsers/raydium/parser-raydium-launchpad.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAdapter } from '../../transaction-adapter'; 2 | import { 3 | ClassifiedInstruction, 4 | DexInfo, 5 | RaydiumLCPEvent, 6 | RaydiumLCPTradeEvent, 7 | TradeDirection, 8 | TradeInfo, 9 | TransferData, 10 | } from '../../types'; 11 | import { BaseParser } from '../base-parser'; 12 | import { RaydiumLaunchpadEventParser } from './parser-raydium-launchpad-event'; 13 | import { getRaydiumTradeInfo } from './util'; 14 | 15 | export class RaydiumLaunchpadParser extends BaseParser { 16 | private eventParser: RaydiumLaunchpadEventParser; 17 | 18 | constructor( 19 | adapter: TransactionAdapter, 20 | dexInfo: DexInfo, 21 | transferActions: Record, 22 | classifiedInstructions: ClassifiedInstruction[] 23 | ) { 24 | super(adapter, dexInfo, transferActions, classifiedInstructions); 25 | this.eventParser = new RaydiumLaunchpadEventParser(adapter); 26 | } 27 | 28 | public processTrades(): TradeInfo[] { 29 | const events = this.eventParser 30 | .parseInstructions(this.classifiedInstructions) 31 | .filter((event) => event.type === 'TRADE'); 32 | 33 | return events.map((event) => this.createTradeInfo(event)); 34 | } 35 | 36 | private createTradeInfo(data: RaydiumLCPEvent): TradeInfo { 37 | const event = data.data as RaydiumLCPTradeEvent; 38 | const isBuy = event.tradeDirection == TradeDirection.Buy; 39 | const [inputToken, inputDecimal, outputToken, outputDecimal] = isBuy 40 | ? [ 41 | event.quoteMint, 42 | this.adapter.splDecimalsMap.get(event.quoteMint), 43 | event.baseMint, 44 | this.adapter.splDecimalsMap.get(event.baseMint), 45 | ] 46 | : [ 47 | event.baseMint, 48 | this.adapter.splDecimalsMap.get(event.baseMint), 49 | event.quoteMint, 50 | this.adapter.splDecimalsMap.get(event.quoteMint), 51 | ]; 52 | 53 | if (!inputToken || !outputToken) throw new Error('Token not found'); 54 | 55 | const trade = getRaydiumTradeInfo( 56 | event, 57 | { mint: inputToken, decimals: inputDecimal! }, 58 | { mint: outputToken, decimals: outputDecimal! }, 59 | { 60 | slot: data.slot, 61 | signature: data.signature, 62 | timestamp: data.timestamp, 63 | idx: data.idx, 64 | dexInfo: this.dexInfo, 65 | } 66 | ); 67 | 68 | return this.utils.attachTokenTransferInfo(trade, this.transferActions); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/parsers/raydium/parser-raydium-liquidity-base.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from '../../constants'; 2 | import { convertToUiAmount, PoolEvent, PoolEventType, TransferData } from '../../types'; 3 | import { getInstructionData } from '../../utils'; 4 | import { BaseLiquidityParser } from '../base-liquidity-parser'; 5 | 6 | export interface ParseEventConfig { 7 | eventType: PoolEventType; 8 | poolIdIndex: number; 9 | lpMintIndex: number; 10 | tokenAmountOffsets?: { 11 | token0: number; 12 | token1: number; 13 | lp: number; 14 | }; 15 | } 16 | 17 | export abstract class RaydiumLiquidityParserBase extends BaseLiquidityParser { 18 | abstract getPoolAction(data: Buffer): PoolEventType | { name: string; type: PoolEventType } | null; 19 | 20 | abstract getEventConfig( 21 | type: PoolEventType, 22 | instructionType: PoolEventType | { name: string; type: PoolEventType } 23 | ): ParseEventConfig | null; 24 | 25 | public processLiquidity(): PoolEvent[] { 26 | const events: PoolEvent[] = []; 27 | 28 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 29 | const event = this.parseRaydiumInstruction(instruction, programId, outerIndex, innerIndex); 30 | if (event) { 31 | events.push(event); 32 | } 33 | }); 34 | 35 | return events; 36 | } 37 | 38 | protected parseRaydiumInstruction( 39 | instruction: any, 40 | programId: string, 41 | outerIndex: number, 42 | innerIndex?: number 43 | ): PoolEvent | null { 44 | try { 45 | const data = getInstructionData(instruction); 46 | const instructionType = this.getPoolAction(data); 47 | if (!instructionType) return null; 48 | 49 | const accounts = this.adapter.getInstructionAccounts(instruction); 50 | const type = typeof instructionType === 'string' ? instructionType : instructionType.type; 51 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex).filter( 52 | (it) => 53 | !it.info.destination || 54 | (it.info.authority && accounts.includes(it.info.destination) && it.programId != TOKENS.NATIVE) 55 | ); 56 | 57 | const config = this.getEventConfig(type, instructionType); 58 | 59 | if (!config) return null; 60 | return this.parseEvent(instruction, outerIndex, data, transfers, config); 61 | } catch (error) { 62 | console.error('parseRaydiumInstruction error:', error); 63 | throw error; 64 | } 65 | } 66 | 67 | protected parseEvent( 68 | instruction: any, 69 | index: number, 70 | data: Buffer, 71 | transfers: TransferData[], 72 | config: ParseEventConfig 73 | ): PoolEvent | null { 74 | if (config.eventType === 'ADD' && transfers.length < 2) return null; 75 | 76 | const [token0, token1] = this.utils.getLPTransfers(transfers); 77 | const lpToken = transfers.find((it) => it.type === (config.eventType === 'REMOVE' ? 'burn' : 'mintTo')); 78 | const programId = this.adapter.getInstructionProgramId(instruction); 79 | const accounts = this.adapter.getInstructionAccounts(instruction); 80 | 81 | const token0Mint = token0?.info.mint; 82 | const token1Mint = token1?.info.mint; 83 | const [token0Decimals, token1Decimals] = [ 84 | this.adapter.getTokenDecimals(token0Mint), 85 | this.adapter.getTokenDecimals(token1Mint), 86 | ]; 87 | 88 | return { 89 | ...this.adapter.getPoolEventBase(config.eventType, programId), 90 | idx: index.toString(), 91 | poolId: accounts[config.poolIdIndex], 92 | poolLpMint: lpToken?.info.mint || accounts[config.lpMintIndex], 93 | token0Mint, 94 | token1Mint, 95 | token0Amount: 96 | token0?.info.tokenAmount.uiAmount || 97 | (config.tokenAmountOffsets && 98 | convertToUiAmount(data.readBigUInt64LE(config.tokenAmountOffsets.token0), token0Decimals)), 99 | token0AmountRaw: 100 | token0?.info.tokenAmount.amount || 101 | (config.tokenAmountOffsets && data.readBigUInt64LE(config.tokenAmountOffsets.token0).toString()), 102 | token1Amount: 103 | token1?.info.tokenAmount.uiAmount || 104 | (config.tokenAmountOffsets && 105 | convertToUiAmount(data.readBigUInt64LE(config.tokenAmountOffsets.token1), token1Decimals)), 106 | token1AmountRaw: 107 | token1?.info.tokenAmount.amount || 108 | (config.tokenAmountOffsets && data.readBigUInt64LE(config.tokenAmountOffsets.token1).toString()), 109 | token0Decimals, 110 | token1Decimals, 111 | lpAmount: lpToken?.info.tokenAmount.uiAmount, 112 | lpAmountRaw: 113 | lpToken?.info.tokenAmount.amount || 114 | (config.tokenAmountOffsets && data.readBigUInt64LE(config.tokenAmountOffsets.lp).toString()) || 115 | '0', 116 | }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/parsers/raydium/parser-raydium-logs.ts: -------------------------------------------------------------------------------- 1 | // Define log types for different operations 2 | enum LogType { 3 | Init = 0, 4 | Deposit = 1, 5 | Withdraw = 2, 6 | SwapBaseIn = 3, 7 | SwapBaseOut = 4, 8 | } 9 | 10 | // Constants for swap direction 11 | const SWAP_DIRECTION = { 12 | COIN_TO_PC: 0n, // Token A -> Token B (e.g., SOL -> USDC) 13 | PC_TO_COIN: 1n, // Token B -> Token A (e.g., USDC -> SOL) 14 | } as const; 15 | 16 | // Interface for Add Liquidity operation log 17 | interface DepositLog { 18 | logType: LogType; 19 | // Input parameters 20 | maxCoin: bigint; // Maximum amount of token A to add 21 | maxPc: bigint; // Maximum amount of token B to add 22 | base: bigint; // Base value for calculation 23 | // Pool information 24 | poolCoin: bigint; // Current pool token A amount 25 | poolPc: bigint; // Current pool token B amount 26 | poolLp: bigint; // Current pool LP token amount 27 | calcPnlX: bigint; // PnL calculation X 28 | calcPnlY: bigint; // PnL calculation Y 29 | // Operation results 30 | deductCoin: bigint; // Actual token A amount added 31 | deductPc: bigint; // Actual token B amount added 32 | mintLp: bigint; // LP tokens minted 33 | } 34 | 35 | // Interface for Remove Liquidity operation log 36 | interface WithdrawLog { 37 | logType: LogType; 38 | // Input parameters 39 | withdrawLp: bigint; // LP tokens to withdraw 40 | // User information 41 | userLp: bigint; // User's LP token balance 42 | // Pool information 43 | poolCoin: bigint; // Current pool token A amount 44 | poolPc: bigint; // Current pool token B amount 45 | poolLp: bigint; // Current pool LP token amount 46 | calcPnlX: bigint; // PnL calculation X 47 | calcPnlY: bigint; // PnL calculation Y 48 | // Operation results 49 | outCoin: bigint; // Token A amount received 50 | outPc: bigint; // Token B amount received 51 | } 52 | 53 | // Interface for Exact Input Swap operation log 54 | interface SwapBaseInLog { 55 | logType: LogType; 56 | // Input parameters 57 | amountIn: bigint; // Exact amount to swap in 58 | minimumOut: bigint; // Minimum amount to receive 59 | direction: bigint; // Swap direction (0: A->B, 1: B->A) 60 | // User information 61 | userSource: bigint; // User's source token balance 62 | // Pool information 63 | poolCoin: bigint; // Current pool token A amount 64 | poolPc: bigint; // Current pool token B amount 65 | // Operation results 66 | outAmount: bigint; // Actual amount received 67 | } 68 | 69 | // Interface for Exact Output Swap operation log 70 | interface SwapBaseOutLog { 71 | logType: LogType; 72 | // Input parameters 73 | maxIn: bigint; // Maximum amount to swap in 74 | amountOut: bigint; // Exact amount to receive 75 | direction: bigint; // Swap direction (0: A->B, 1: B->A) 76 | // User information 77 | userSource: bigint; // User's source token balance 78 | // Pool information 79 | poolCoin: bigint; // Current pool token A amount 80 | poolPc: bigint; // Current pool token B amount 81 | // Operation results 82 | deductIn: bigint; // Actual amount paid 83 | } 84 | 85 | // Main function to decode Raydium logs 86 | function decodeRaydiumLog(base64Log: string): DepositLog | WithdrawLog | SwapBaseInLog | SwapBaseOutLog | null { 87 | // Remove "ray_log:" prefix and clean the string 88 | const cleanLog = base64Log.replace('ray_log:', '').trim(); 89 | 90 | // Decode base64 string to buffer 91 | const data = Buffer.from(cleanLog, 'base64'); 92 | 93 | // Read log type from first byte 94 | const logType = data[0]; 95 | let offset = 1; 96 | 97 | // Helper function to read uint64 values 98 | function readU64(): bigint { 99 | const value = data.readBigUInt64LE(offset); 100 | offset += 8; 101 | return value; 102 | } 103 | 104 | // Helper function to read uint128 values 105 | function readU128(): bigint { 106 | const value = data.readBigUInt64LE(offset); 107 | const valueHigh = data.readBigUInt64LE(offset + 8); 108 | offset += 16; 109 | return valueHigh * BigInt(2 ** 64) + value; 110 | } 111 | 112 | // Parse log based on its type 113 | switch (logType) { 114 | case LogType.Deposit: 115 | return { 116 | logType: LogType.Deposit, 117 | maxCoin: readU64(), 118 | maxPc: readU64(), 119 | base: readU64(), 120 | poolCoin: readU64(), 121 | poolPc: readU64(), 122 | poolLp: readU64(), 123 | calcPnlX: readU128(), 124 | calcPnlY: readU128(), 125 | deductCoin: readU64(), 126 | deductPc: readU64(), 127 | mintLp: readU64(), 128 | }; 129 | 130 | case LogType.Withdraw: 131 | return { 132 | logType: LogType.Withdraw, 133 | withdrawLp: readU64(), 134 | userLp: readU64(), 135 | poolCoin: readU64(), 136 | poolPc: readU64(), 137 | poolLp: readU64(), 138 | calcPnlX: readU128(), 139 | calcPnlY: readU128(), 140 | outCoin: readU64(), 141 | outPc: readU64(), 142 | }; 143 | 144 | case LogType.SwapBaseIn: 145 | return { 146 | logType: LogType.SwapBaseIn, 147 | amountIn: readU64(), 148 | minimumOut: readU64(), 149 | direction: readU64(), 150 | userSource: readU64(), 151 | poolCoin: readU64(), 152 | poolPc: readU64(), 153 | outAmount: readU64(), 154 | }; 155 | 156 | case LogType.SwapBaseOut: 157 | return { 158 | logType: LogType.SwapBaseOut, 159 | maxIn: readU64(), 160 | amountOut: readU64(), 161 | direction: readU64(), 162 | userSource: readU64(), 163 | poolCoin: readU64(), 164 | poolPc: readU64(), 165 | deductIn: readU64(), 166 | }; 167 | 168 | default: 169 | return null; //Unsupported log type 170 | } 171 | } 172 | 173 | // Helper function to parse swap operation details 174 | function parseRaydiumSwapLog(log: SwapBaseInLog | SwapBaseOutLog) { 175 | const isBaseIn = 'amountIn' in log; 176 | const isBuy = log.direction === SWAP_DIRECTION.PC_TO_COIN; 177 | 178 | const operation = { 179 | type: isBuy ? 'Buy' : 'Sell', 180 | mode: isBaseIn ? 'Exact Input' : 'Exact Output', 181 | inputAmount: isBaseIn ? log.amountIn : log.deductIn, 182 | outputAmount: isBaseIn ? log.outAmount : log.amountOut, 183 | slippageProtection: isBaseIn ? log.minimumOut : log.maxIn, 184 | }; 185 | 186 | return operation; 187 | } 188 | 189 | export { 190 | LogType, 191 | SWAP_DIRECTION, 192 | decodeRaydiumLog, 193 | parseRaydiumSwapLog, 194 | type DepositLog, 195 | type WithdrawLog, 196 | type SwapBaseInLog, 197 | type SwapBaseOutLog, 198 | }; 199 | -------------------------------------------------------------------------------- /src/parsers/raydium/parser-raydium.ts: -------------------------------------------------------------------------------- 1 | import { DISCRIMINATORS } from '../../constants'; 2 | import { TradeInfo } from '../../types'; 3 | import { getProgramName, getInstructionData } from '../../utils'; 4 | import { BaseParser } from '../base-parser'; 5 | 6 | export class RaydiumParser extends BaseParser { 7 | public processTrades(): TradeInfo[] { 8 | const trades: TradeInfo[] = []; 9 | 10 | this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => { 11 | if (this.notLiquidityEvent(instruction)) { 12 | const transfers = this.getTransfersForInstruction(programId, outerIndex, innerIndex); 13 | 14 | if (transfers.length >= 2) { 15 | const trade = this.utils.processSwapData(transfers.slice(0, 2), { 16 | ...this.dexInfo, 17 | amm: this.dexInfo.amm || getProgramName(programId), 18 | }); 19 | 20 | if (trade) { 21 | if (transfers.length > 2) { 22 | trade.fee = this.utils.getTransferTokenInfo(transfers[2]) ?? undefined; 23 | } 24 | trades.push(this.utils.attachTokenTransferInfo(trade, this.transferActions)); 25 | } 26 | } 27 | } 28 | }); 29 | 30 | return trades; 31 | } 32 | 33 | private notLiquidityEvent(instruction: any): boolean { 34 | if (instruction.data) { 35 | const data = getInstructionData(instruction); 36 | const a = Object.values(DISCRIMINATORS.RAYDIUM).some((it) => data.slice(0, 1).equals(it)); 37 | const b = Object.values(DISCRIMINATORS.RAYDIUM_CL) 38 | .flatMap((it) => Object.values(it)) 39 | .some((it) => data.slice(0, 8).equals(it)); 40 | const c = Object.values(DISCRIMINATORS.RAYDIUM_CPMM).some((it) => data.slice(0, 8).equals(it)); 41 | return !a && !b && !c; 42 | } 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/parsers/raydium/util.ts: -------------------------------------------------------------------------------- 1 | import { DEX_PROGRAMS } from '../../constants'; 2 | import { convertToUiAmount, DexInfo, RaydiumLCPTradeEvent, TradeDirection, TradeInfo } from '../../types'; 3 | 4 | export const getRaydiumTradeInfo = ( 5 | event: RaydiumLCPTradeEvent, 6 | inputToken: { 7 | mint: string; 8 | decimals: number; 9 | }, 10 | outputToken: { 11 | mint: string; 12 | decimals: number; 13 | }, 14 | info: { 15 | slot: number; 16 | signature: string; 17 | timestamp: number; 18 | idx?: string; 19 | dexInfo?: DexInfo; 20 | } 21 | ): TradeInfo => { 22 | const { mint: inputMint, decimals: inputDecimal } = inputToken; 23 | const { mint: outputMint, decimals: ouptDecimal } = outputToken; 24 | const isBuy = event.tradeDirection === TradeDirection.Buy; 25 | const fee = BigInt(event.protocolFee) + BigInt(event.platformFee); 26 | return { 27 | type: isBuy ? 'BUY' : 'SELL', 28 | inputToken: { 29 | mint: inputMint, 30 | amount: convertToUiAmount(event.amountIn, inputDecimal), 31 | amountRaw: event.amountIn.toString(), 32 | decimals: inputDecimal, 33 | }, 34 | outputToken: { 35 | mint: outputMint, 36 | amount: convertToUiAmount(event.amountOut, ouptDecimal), 37 | amountRaw: event.amountOut.toString(), 38 | decimals: ouptDecimal, 39 | }, 40 | fee: { 41 | mint: isBuy ? inputMint : outputMint, 42 | amount: convertToUiAmount(fee, isBuy ? inputDecimal : ouptDecimal), 43 | amountRaw: fee.toString(), 44 | decimals: isBuy ? inputDecimal : ouptDecimal, 45 | }, 46 | user: event.user, 47 | programId: info.dexInfo?.programId || DEX_PROGRAMS.RAYDIUM_LCP.id, 48 | amm: DEX_PROGRAMS.RAYDIUM_LCP.name, 49 | route: info.dexInfo?.route || '', 50 | slot: info.slot, 51 | timestamp: info.timestamp, 52 | signature: info.signature, 53 | idx: info.idx || '', 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/transfer-utils.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS, TOKEN_PROGRAM_ID } from './constants'; 2 | import { TransactionAdapter } from './transaction-adapter'; 3 | import { TransferData, convertToUiAmount } from './types'; 4 | import { getPubkeyString, getTranferTokenMint } from './utils'; 5 | 6 | export const isTransferCheck = (instruction: any): boolean => { 7 | return ( 8 | (instruction.programId == TOKEN_PROGRAM_ID || instruction.programId == TOKEN_2022_PROGRAM_ID) && 9 | instruction.parsed.type.includes('transferChecked') 10 | ); 11 | }; 12 | 13 | export const isTransfer = (instruction: any): boolean => { 14 | return ( 15 | instruction.program === 'spl-token' && 16 | instruction.programId == TOKEN_PROGRAM_ID && 17 | instruction.parsed.type === 'transfer' 18 | ); 19 | }; 20 | 21 | export const isNativeTransfer = (instruction: any): boolean => { 22 | return ( 23 | instruction.program === 'system' && instruction.programId == TOKENS.NATIVE && instruction.parsed.type === 'transfer' 24 | ); 25 | }; 26 | 27 | export const processTransfer = (instruction: any, idx: string, adapter: TransactionAdapter): TransferData | null => { 28 | const { info } = instruction.parsed; 29 | if (!info) return null; 30 | 31 | const programId = getPubkeyString(instruction.programId); 32 | const [token1, token2] = [ 33 | adapter.splTokenMap.get(info.destination)?.mint, 34 | adapter.splTokenMap.get(info.source)?.mint, 35 | ]; 36 | if (!token1 && !token2) return null; 37 | 38 | let mint = getTranferTokenMint(token1, token2); 39 | 40 | if (!mint && programId == TOKENS.NATIVE) mint = TOKENS.SOL; 41 | if (!mint) return null; 42 | 43 | const decimals = adapter.splDecimalsMap.get(mint); 44 | if (typeof decimals === 'undefined') return null; 45 | 46 | const [sourceBalance, destinationBalance] = adapter.getTokenAccountBalance([info.source, info.destination]); 47 | const [sourcePreBalance, destinationPreBalance] = adapter.getTokenAccountPreBalance([info.source, info.destination]); 48 | 49 | return { 50 | type: 'transfer', 51 | programId: programId, 52 | info: { 53 | authority: info.authority, 54 | destination: info.destination || '', 55 | destinationOwner: adapter.getTokenAccountOwner(info.destination), 56 | mint, 57 | source: info.source || '', 58 | tokenAmount: { 59 | amount: info.amount, 60 | decimals, 61 | uiAmount: convertToUiAmount(info.amount, decimals), 62 | }, 63 | sourceBalance: sourceBalance, 64 | sourcePreBalance: sourcePreBalance, 65 | destinationBalance: destinationBalance, 66 | destinationPreBalance: destinationPreBalance, 67 | }, 68 | idx: idx, 69 | timestamp: adapter.blockTime, 70 | signature: adapter.signature, 71 | }; 72 | }; 73 | 74 | export const processNatvieTransfer = ( 75 | instruction: any, 76 | idx: string, 77 | adapter: TransactionAdapter 78 | ): TransferData | null => { 79 | const { info } = instruction.parsed; 80 | if (!info) return null; 81 | 82 | const programId = getPubkeyString(instruction.programId); 83 | const mint = TOKENS.SOL; 84 | const decimals = TOKEN_DECIMALS.SOL; 85 | 86 | const [sourceBalance, destinationBalance] = adapter.getAccountBalance([info.source, info.destination]); 87 | const [sourcePreBalance, destinationPreBalance] = adapter.getAccountPreBalance([info.source, info.destination]); 88 | return { 89 | type: 'transfer', 90 | programId: programId, 91 | info: { 92 | authority: info.authority, 93 | destination: info.destination || '', 94 | destinationOwner: adapter.getTokenAccountOwner(info.destination), 95 | mint, 96 | source: info.source || '', 97 | tokenAmount: { 98 | amount: info.lamports, 99 | decimals, 100 | uiAmount: convertToUiAmount(info.lamports, decimals), 101 | }, 102 | sourceBalance: sourceBalance, 103 | sourcePreBalance: sourcePreBalance, 104 | destinationBalance: destinationBalance, 105 | destinationPreBalance: destinationPreBalance, 106 | }, 107 | idx: idx, 108 | timestamp: adapter.blockTime, 109 | signature: adapter.signature, 110 | }; 111 | }; 112 | 113 | export const processTransferCheck = ( 114 | instruction: any, 115 | idx: string, 116 | adapter: TransactionAdapter 117 | ): TransferData | null => { 118 | const { info } = instruction.parsed; 119 | if (!info) return null; 120 | 121 | const decimals = adapter.splDecimalsMap.get(info.mint); 122 | if (typeof decimals === 'undefined') return null; 123 | 124 | const [sourceBalance, destinationBalance] = adapter.getTokenAccountBalance([info.source, info.destination]); 125 | const [sourcePreBalance, destinationPreBalance] = adapter.getTokenAccountPreBalance([info.source, info.destination]); 126 | 127 | return { 128 | type: 'transferChecked', 129 | programId: instruction.programId, 130 | info: { 131 | authority: info.authority, 132 | destination: info.destination || '', 133 | destinationOwner: adapter.getTokenAccountOwner(info.destination), 134 | mint: info.mint || '', 135 | source: info.source || '', 136 | tokenAmount: info.tokenAmount || { 137 | amount: info.amount, 138 | decimals, 139 | uiAmount: convertToUiAmount(info.amount, decimals), 140 | }, 141 | sourceBalance: sourceBalance, 142 | sourcePreBalance: sourcePreBalance, 143 | destinationBalance: destinationBalance, 144 | destinationPreBalance: destinationPreBalance, 145 | }, 146 | idx, 147 | timestamp: adapter.blockTime, 148 | signature: adapter.signature, 149 | }; 150 | }; 151 | 152 | export const isExtraAction = (instruction: any, type: string): boolean => { 153 | return ( 154 | instruction.program === 'spl-token' && instruction.programId == TOKEN_PROGRAM_ID && instruction.parsed.type === type 155 | ); 156 | }; 157 | 158 | export const processExtraAction = ( 159 | instruction: any, 160 | idx: string, 161 | adapter: TransactionAdapter, 162 | type: string 163 | ): TransferData | null => { 164 | const { info } = instruction.parsed; 165 | if (!info) return null; 166 | 167 | const mint = info.mint || adapter.splTokenMap.get(info.destination)?.mint; 168 | if (!mint) return null; 169 | 170 | const decimals = adapter.splDecimalsMap.get(mint); 171 | if (typeof decimals === 'undefined') return null; 172 | 173 | const [sourceBalance, destinationBalance] = adapter.getTokenAccountBalance([info.source, info.destination]); 174 | const [sourcePreBalance, destinationPreBalance] = adapter.getTokenAccountPreBalance([info.source, info.destination]); 175 | 176 | return { 177 | type: type, 178 | programId: instruction.programId, 179 | info: { 180 | authority: info.authority || info.mintAuthority || '', 181 | destination: info.destination || '', 182 | destinationOwner: adapter.getTokenAccountOwner(info.destination), 183 | mint, 184 | source: info.source || '', 185 | tokenAmount: { 186 | amount: info.amount, 187 | decimals, 188 | uiAmount: convertToUiAmount(info.amount, decimals), 189 | }, 190 | sourceBalance: sourceBalance, 191 | sourcePreBalance: sourcePreBalance, 192 | destinationBalance: destinationBalance, 193 | destinationPreBalance: destinationPreBalance, 194 | }, 195 | idx: idx, 196 | timestamp: adapter.blockTime, 197 | signature: adapter.signature, 198 | }; 199 | }; 200 | -------------------------------------------------------------------------------- /src/types/boopfun.ts: -------------------------------------------------------------------------------- 1 | export interface BoopfunTradeEvent { 2 | mint: string; 3 | solAmount: bigint; 4 | tokenAmount: bigint; 5 | isBuy: boolean; 6 | user: string; 7 | bondingCurve: string; 8 | } 9 | 10 | export interface BoopfunCreateEvent { 11 | name: string; 12 | symbol: string; 13 | uri: string; 14 | mint: string; 15 | // bondingCurve: string; 16 | user: string; 17 | } 18 | 19 | export interface BoopfunCompleteEvent { 20 | user: string; 21 | mint: string; 22 | bondingCurve: string; 23 | solAmount: bigint; // sol amount to Raydium 24 | feeAmount: bigint; // fee amount to Boopfun 25 | } 26 | 27 | export interface BoopfunEvent { 28 | type: 'BUY' | 'SELL' | 'CREATE' | 'COMPLETE'; 29 | data: BoopfunTradeEvent | BoopfunCreateEvent | BoopfunCompleteEvent; 30 | slot: number; 31 | timestamp: number; 32 | signature: string; 33 | idx: string; // instruction indexes 34 | } 35 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | import { PoolEvent } from './pool'; 2 | import { TokenAmount, TradeInfo, TransferData } from './trade'; 3 | 4 | export interface ClassifiedInstruction { 5 | instruction: any; 6 | programId: string; 7 | outerIndex: number; 8 | innerIndex?: number; 9 | } 10 | 11 | export interface BalanceChange { 12 | pre: TokenAmount; 13 | post: TokenAmount; 14 | change: TokenAmount; 15 | } 16 | 17 | export interface ParseResult { 18 | state: boolean; 19 | fee: TokenAmount; // transaction gas fee 20 | trades: TradeInfo[]; 21 | liquidities: PoolEvent[]; 22 | transfers: TransferData[]; 23 | solBalanceChange?: BalanceChange; // SOL balance change 24 | tokenBalanceChange?: Map; // token balance change, key is token mint address 25 | moreEvents: Record; // other events, key is Amm name 26 | msg?: string; 27 | } 28 | 29 | export type EventParser = { 30 | discriminator: Buffer | Uint8Array; 31 | decode: (data: Buffer) => T; 32 | }; 33 | 34 | export type EventsParser = { 35 | discriminators: (Buffer | Uint8Array)[]; 36 | slice: number; 37 | decode: (data: Buffer, options: any) => T; 38 | }; 39 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trade'; 2 | export * from './pumpfun'; 3 | export * from './pumpswap'; 4 | export * from './pool'; 5 | export * from './common'; 6 | export * from './jupiter'; 7 | export * from './raydium'; 8 | export * from './boopfun'; 9 | -------------------------------------------------------------------------------- /src/types/jupiter.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export interface JupiterSwapEvent { 4 | amm: PublicKey; 5 | inputMint: PublicKey; 6 | inputAmount: bigint; 7 | outputMint: PublicKey; 8 | outputAmount: bigint; 9 | } 10 | 11 | export interface JupiterSwapEventData extends JupiterSwapEvent { 12 | inputMintDecimals: number; 13 | outputMintDecimals: number; 14 | idx: string; 15 | } 16 | 17 | export interface JupiterSwapInfo { 18 | amms: string[]; 19 | tokenIn: Map; 20 | tokenOut: Map; 21 | decimals: Map; 22 | idx: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/types/pool.ts: -------------------------------------------------------------------------------- 1 | export type PoolEventType = 'CREATE' | 'ADD' | 'REMOVE'; 2 | 3 | export interface PoolEventBase { 4 | user: string; 5 | type: PoolEventType; 6 | programId?: string; // DEX program ID 7 | amm?: string; // AMM type (e.g., 'Raydiumv4', 'Jupiter') 8 | slot: number; 9 | timestamp: number; 10 | signature: string; 11 | idx: string; // instruction indexes 12 | signer?: string[]; // Orignal signer 13 | } 14 | 15 | export interface PoolEvent extends PoolEventBase { 16 | /** 17 | * AMM pool address (market) 18 | */ 19 | poolId: string; 20 | 21 | /** 22 | * LP mint address 23 | */ 24 | poolLpMint?: string; 25 | 26 | /** 27 | * Token A mint address (TOKEN) 28 | */ 29 | token0Mint?: string; 30 | 31 | /** 32 | * Token A uiAmount (TOKEN) 33 | */ 34 | token0Amount?: number; 35 | 36 | /** 37 | * Token A amount (TOKEN) 38 | */ 39 | token0AmountRaw?: string; 40 | 41 | /** 42 | * User token0 balance changed amount 43 | */ 44 | token0BalanceChange?: string; 45 | 46 | /** 47 | * Token A amount (TOKEN) 48 | */ 49 | 50 | token0Decimals?: number; 51 | 52 | /** 53 | * Token B mint address (SOL/USDC/USDT) 54 | */ 55 | token1Mint?: string; 56 | 57 | /** 58 | * Token B uiAmount (SOL/USDC/USDT) 59 | */ 60 | token1Amount?: number; 61 | 62 | /** 63 | * Token B amount (SOL/USDC/USDT) 64 | */ 65 | token1AmountRaw?: string; 66 | 67 | /** 68 | * User token1 balance changed amount 69 | */ 70 | token1BalanceChange?: string; 71 | 72 | token1Decimals?: number; 73 | 74 | /** 75 | * Lp amount 76 | */ 77 | lpAmount?: number; 78 | 79 | lpAmountRaw?: string; 80 | } 81 | -------------------------------------------------------------------------------- /src/types/pumpfun.ts: -------------------------------------------------------------------------------- 1 | export interface PumpfunTradeEvent { 2 | mint: string; 3 | solAmount: bigint; 4 | tokenAmount: bigint; 5 | isBuy: boolean; 6 | user: string; 7 | timestamp: bigint; 8 | virtualSolReserves: bigint; 9 | virtualTokenReserves: bigint; 10 | } 11 | 12 | export interface PumpfunCreateEvent { 13 | name: string; 14 | symbol: string; 15 | uri: string; 16 | mint: string; 17 | bondingCurve: string; 18 | user: string; 19 | } 20 | 21 | export interface PumpfunCompleteEvent { 22 | user: string; 23 | mint: string; 24 | bondingCurve: string; 25 | timestamp: bigint; 26 | } 27 | 28 | export interface PumpfunEvent { 29 | type: 'TRADE' | 'CREATE' | 'COMPLETE'; 30 | data: PumpfunTradeEvent | PumpfunCreateEvent | PumpfunCompleteEvent; 31 | slot: number; 32 | timestamp: number; 33 | signature: string; 34 | idx: string; // instruction indexes 35 | } 36 | -------------------------------------------------------------------------------- /src/types/pumpswap.ts: -------------------------------------------------------------------------------- 1 | export interface PumpswapBuyEvent { 2 | timestamp: number; 3 | baseAmountOut: bigint; 4 | maxQuoteAmountIn: bigint; 5 | userBaseTokenReserves: bigint; 6 | userQuoteTokenReserves: bigint; 7 | poolBaseTokenReserves: bigint; 8 | poolQuoteTokenReserves: bigint; 9 | quoteAmountIn: bigint; 10 | lpFeeBasisPoints: bigint; 11 | lpFee: bigint; 12 | protocolFeeBasisPoints: bigint; 13 | protocolFee: bigint; 14 | quoteAmountInWithLpFee: bigint; 15 | userQuoteAmountIn: bigint; 16 | pool: string; 17 | user: string; 18 | userBaseTokenAccount: string; 19 | userQuoteTokenAccount: string; 20 | protocolFeeRecipient: string; 21 | protocolFeeRecipientTokenAccount: string; 22 | coinCreator: string; 23 | coinCreatorFeeBasisPoints: bigint; 24 | coinCreatorFee: bigint; 25 | } 26 | 27 | export interface PumpswapSellEvent { 28 | timestamp: number; 29 | baseAmountIn: bigint; 30 | minQuoteAmountOut: bigint; 31 | userBaseTokenReserves: bigint; 32 | userQuoteTokenReserves: bigint; 33 | poolBaseTokenReserves: bigint; 34 | poolQuoteTokenReserves: bigint; 35 | quoteAmountOut: bigint; 36 | lpFeeBasisPoints: bigint; 37 | lpFee: bigint; 38 | protocolFeeBasisPoints: bigint; 39 | protocolFee: bigint; 40 | quoteAmountOutWithoutLpFee: bigint; 41 | userQuoteAmountOut: bigint; 42 | pool: string; 43 | user: string; 44 | userBaseTokenAccount: string; 45 | userQuoteTokenAccount: string; 46 | protocolFeeRecipient: string; 47 | protocolFeeRecipientTokenAccount: string; 48 | coinCreator: string; 49 | coinCreatorFeeBasisPoints: bigint; 50 | coinCreatorFee: bigint; 51 | } 52 | 53 | export interface PumpswapCreatePoolEvent { 54 | timestamp: number; 55 | index: number; 56 | creator: string; 57 | baseMint: string; 58 | quoteMint: string; 59 | baseMintDecimals: number; 60 | quoteMintDecimals: number; 61 | baseAmountIn: bigint; 62 | quoteAmountIn: bigint; 63 | poolBaseAmount: bigint; 64 | poolQuotAmount: bigint; 65 | minimumLiquidity: bigint; 66 | initialLiquidity: bigint; 67 | lpTokenAmountOut: bigint; 68 | poolBump: number; 69 | pool: string; 70 | lpMint: string; 71 | userBaseTokenAccount: string; 72 | userQuoteTokenAccount: string; 73 | } 74 | 75 | export interface PumpswapDepositEvent { 76 | timestamp: number; 77 | lpTokenAmountOut: bigint; 78 | maxBaseAmountIn: bigint; 79 | maxQuoteAmountIn: bigint; 80 | userBaseTokenReserves: bigint; 81 | userQuoteTokenReserves: bigint; 82 | poolBaseTokenReserves: bigint; 83 | poolQuoteTokenReserves: bigint; 84 | baseAmountIn: bigint; 85 | quoteAmountIn: bigint; 86 | lpMintSupply: bigint; 87 | 88 | pool: string; 89 | user: string; 90 | userBaseTokenAccount: string; 91 | userQuoteTokenAccount: string; 92 | userPoolTokenAccount: string; 93 | } 94 | 95 | export interface PumpswapWithdrawEvent { 96 | timestamp: number; 97 | lpTokenAmountIn: bigint; 98 | minBaseAmountOut: bigint; 99 | minQuoteAmountOut: bigint; 100 | userBaseTokenReserves: bigint; 101 | userQuoteTokenReserves: bigint; 102 | poolBaseTokenReserves: bigint; 103 | poolQuoteTokenReserves: bigint; 104 | baseAmountOut: bigint; 105 | quoteAmountOut: bigint; 106 | lpMintSupply: bigint; 107 | 108 | pool: string; 109 | user: string; 110 | userBaseTokenAccount: string; 111 | userQuoteTokenAccount: string; 112 | userPoolTokenAccount: string; 113 | } 114 | 115 | export interface PumpswapEvent { 116 | type: 'BUY' | 'SELL' | 'CREATE' | 'ADD' | 'REMOVE'; 117 | data: PumpswapBuyEvent | PumpswapSellEvent | PumpswapCreatePoolEvent | PumpswapDepositEvent | PumpswapWithdrawEvent; 118 | slot: number; 119 | timestamp: number; 120 | signature: string; 121 | idx: string; // instruction indexes 122 | } 123 | -------------------------------------------------------------------------------- /src/types/raydium.ts: -------------------------------------------------------------------------------- 1 | export interface MintParams { 2 | decimals: number; 3 | name: string; 4 | symbol: string; 5 | uri: string; 6 | } 7 | 8 | export interface ConstantCurve { 9 | supply: bigint; 10 | totalBaseSell: bigint; 11 | totalQuoteFundRaising: bigint; 12 | migrateType: number; 13 | } 14 | 15 | export interface FixedCurve { 16 | supply: bigint; 17 | totalQuoteFundRaising: bigint; 18 | migrateType: number; 19 | } 20 | 21 | export interface LinearCurve { 22 | supply: bigint; 23 | totalQuoteFundRaising: bigint; 24 | migrateType: number; 25 | } 26 | 27 | export interface CurveParams { 28 | variant: string; // e.g., "Constant", "Fixed", "Linear" 29 | data: ConstantCurve | FixedCurve | LinearCurve; 30 | } 31 | 32 | export interface VestingParams { 33 | totalLockedAmount: bigint; 34 | cliffPeriod: bigint; 35 | unlockPeriod: bigint; 36 | } 37 | 38 | export enum TradeDirection { 39 | Buy = 0, 40 | Sell = 1, 41 | } 42 | 43 | export enum PoolStatus { 44 | Fund = 0, 45 | Migrate = 1, 46 | Trade = 2, 47 | } 48 | 49 | export enum CurveType { 50 | Constant = 0, 51 | Fixed = 1, 52 | Linear = 2, 53 | } 54 | 55 | export interface RaydiumLCPCreateEvent { 56 | poolState: string; 57 | creator: string; 58 | config: string; 59 | baseMintParam: MintParams; 60 | curveParam: CurveParams; 61 | vestingParam: VestingParams; 62 | baseMint: string; 63 | quoteMint: string; 64 | } 65 | 66 | export interface RaydiumLCPTradeEvent { 67 | poolState: string; 68 | totalBaseSell: bigint; 69 | virtualBase: bigint; 70 | virtualQuote: bigint; 71 | realBaseBefore: bigint; 72 | realQuoteBefore: bigint; 73 | realBaseAfter: bigint; 74 | amountIn: bigint; 75 | amountOut: bigint; 76 | protocolFee: bigint; 77 | platformFee: bigint; 78 | shareFee: bigint; 79 | tradeDirection: TradeDirection; 80 | poolStatus: PoolStatus; 81 | user: string; 82 | baseMint: string; 83 | quoteMint: string; 84 | } 85 | 86 | export interface RaydiumLCPCompleteEvent { 87 | baseMint: string; // token mint 88 | quoteMint: string; // token mint 89 | poolMint: string; 90 | lpMint: string; 91 | amm: string; // RaydiumV4 or RaydiumCPMM 92 | } 93 | 94 | export interface RaydiumLCPEvent { 95 | type: 'TRADE' | 'CREATE' | 'COMPLETE'; 96 | data: RaydiumLCPTradeEvent | RaydiumLCPCreateEvent | RaydiumLCPCompleteEvent; 97 | slot: number; 98 | timestamp: number; 99 | signature: string; 100 | idx: string; // instruction indexes 101 | } 102 | -------------------------------------------------------------------------------- /src/types/trade.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTransactionWithMeta, TransactionResponse, VersionedTransactionResponse } from '@solana/web3.js'; 2 | 3 | /** 4 | * Union type for different Solana transaction formats 5 | * Supports both parsed and compiled transaction types 6 | */ 7 | export type SolanaTransaction = 8 | | ParsedTransactionWithMeta 9 | | VersionedTransactionResponse 10 | | (TransactionResponse & VersionedTransactionResponse); 11 | 12 | /** 13 | * Configuration options for transaction parsing 14 | */ 15 | export interface ParseConfig { 16 | /** 17 | * If true, will try to parse unknown DEXes, results may be inaccurate 18 | * @default true 19 | */ 20 | tryUnknowDEX?: boolean; 21 | 22 | /** 23 | * If set, will only parse transactions from these programIds 24 | * @default undefined 25 | */ 26 | programIds?: string[]; 27 | 28 | /** 29 | * If set, will ignore transactions from these programIds 30 | * @default undefined 31 | */ 32 | ignoreProgramIds?: string[]; 33 | 34 | /** 35 | * If true, will throw an error if parsing fails 36 | * @default false 37 | */ 38 | thorwError?: boolean; 39 | } 40 | 41 | /** 42 | * Basic DEX protocol information 43 | */ 44 | export interface DexInfo { 45 | programId?: string; // DEX program ID on Solana 46 | amm?: string; // Automated Market Maker name 47 | route?: string; // Router or aggregator name 48 | } 49 | 50 | /** 51 | * Token information including balances and accounts 52 | */ 53 | export interface TokenInfo { 54 | mint: string; // Token mint address 55 | amount: number; // Token uiAmount 56 | amountRaw: string; // Raw token amount 57 | decimals: number; // Token decimals 58 | authority?: string; // Token authority (if applicable) 59 | destination?: string; // Destination token account 60 | destinationOwner?: string; // Owner of destination account 61 | destinationBalance?: TokenAmount; // Balance after transfer 62 | destinationPreBalance?: TokenAmount; // Balance before transfer 63 | source?: string; // Source token account 64 | sourceBalance?: TokenAmount; // Source balance after transfer 65 | sourcePreBalance?: TokenAmount; // Source balance before transfer 66 | balanceChange?: string; // Raw user balance change amount, may differ from the amountRaw in some cases 67 | } 68 | 69 | /** 70 | * Standard token amount format with both raw and UI amounts 71 | */ 72 | export interface TokenAmount { 73 | amount: string; // Raw token amount 74 | uiAmount: number | null; // Human-readable amount 75 | decimals: number; // Token decimals 76 | } 77 | 78 | /** 79 | * Transfer information for tracking token movements 80 | */ 81 | export interface TransferInfo { 82 | type: 'TRANSFER_IN' | 'TRANSFER_OUT'; // Transfer direction 83 | token: TokenInfo; // Token details 84 | from: string; // Source address 85 | to: string; // Destination address 86 | timestamp: number; // Unix timestamp 87 | signature: string; // Transaction signature 88 | } 89 | 90 | /** 91 | * Detailed transfer data including account information 92 | */ 93 | export interface TransferData { 94 | type: 'transfer' | 'transferChecked' | string; // Transfer instruction type 95 | programId: string; // Token program ID 96 | info: { 97 | authority?: string; // Transfer authority 98 | destination: string; // Destination account 99 | destinationOwner?: string; // Owner of destination account 100 | mint: string; // Token mint address 101 | source: string; // Source account 102 | tokenAmount: { 103 | amount: string; // Raw amount 104 | uiAmount: number; // Human-readable amount 105 | decimals: number; // Token decimals 106 | }; 107 | sourceBalance?: TokenAmount; // Source balance after transfer 108 | sourcePreBalance?: TokenAmount; // Source balance before transfer 109 | destinationBalance?: TokenAmount; // Balance after transfer 110 | destinationPreBalance?: TokenAmount; // Balance before transfer 111 | solBalanceChange?: string; // Raw user balance change amount 112 | }; 113 | idx: string; // Instruction index 114 | timestamp: number; // Unix timestamp 115 | signature: string; // Transaction signature 116 | isFee?: boolean; // Whether it's a fee transfer 117 | } 118 | 119 | /** 120 | * Trade direction type 121 | */ 122 | export type TradeType = 'BUY' | 'SELL'; 123 | 124 | export interface FeeInfo { 125 | mint: string; // Fee token mint address 126 | amount: number; // Fee amount in UI format 127 | amountRaw: string; // Raw fee amount 128 | decimals: number; // Fee token decimals 129 | dex?: string; // DEX name (e.g., 'Raydium', 'Meteora') 130 | type?: string; // Fee type (e.g., 'protocol', 'coinCreator') 131 | recipient?: string; // Fee recipient account 132 | } 133 | 134 | /** 135 | * Comprehensive trade information 136 | */ 137 | export interface TradeInfo { 138 | user: string; // Signer address (trader) 139 | type: TradeType; // Trade direction (BUY/SELL) 140 | inputToken: TokenInfo; // Token being sold 141 | outputToken: TokenInfo; // Token being bought 142 | fee?: FeeInfo; // Fee information (if applicable) 143 | fees?: FeeInfo[]; // List of fees (if multiple) 144 | programId?: string; // DEX program ID 145 | amm?: string; // AMM type (e.g., 'RaydiumV4', 'Meteora') 146 | amms?: string[]; // List of AMMs (if multiple) 147 | route?: string; // Router or Bot (e.g., 'Jupiter','OKX','BananaGun') 148 | slot: number; // Block slot number 149 | timestamp: number; // Unix timestamp 150 | signature: string; // Transaction signature 151 | idx: string; // Instruction indexes 152 | signer?: string[]; // Orignal signer 153 | } 154 | 155 | /** 156 | * Converts raw token amount to human-readable format 157 | * @param amount Raw amount in bigint or string format 158 | * @param decimals Token decimals (defaults to 9) 159 | * @returns Human-readable amount as number 160 | */ 161 | export const convertToUiAmount = (amount: bigint | string, decimals?: number) => { 162 | if (decimals === 0) return Number(amount); 163 | return Number(amount) / Math.pow(10, decimals || 9); 164 | }; 165 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import base58 from 'bs58'; 2 | import { DEX_PROGRAMS, TOKENS } from './constants'; 3 | import { convertToUiAmount, DexInfo, TradeInfo, TradeType } from './types'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | 6 | /** 7 | * Get instruction data 8 | */ 9 | export const getInstructionData = (instruction: any): Buffer => { 10 | if ('data' in instruction) { 11 | if (typeof instruction.data === 'string') return Buffer.from(base58.decode(instruction.data)); // compatible with both bs58 v4.0.1 and v6.0.0 12 | if (instruction.data instanceof Uint8Array) return Buffer.from(instruction.data); 13 | } 14 | return instruction.data; 15 | }; 16 | 17 | /** 18 | * Get the name of a program by its ID 19 | * @param programId - The program ID to look up 20 | * @returns The name of the program or 'Unknown' if not found 21 | */ 22 | export const getProgramName = (programId: string): string => 23 | Object.values(DEX_PROGRAMS).find((dex) => dex.id === programId)?.name || 'Unknown'; 24 | 25 | /** 26 | * Convert a hex string to Uint8Array 27 | * @param hex - Hex string to convert 28 | * @returns Uint8Array representation of the hex string 29 | */ 30 | export const hexToUint8Array = (hex: string): Uint8Array => 31 | new Uint8Array(hex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))); 32 | 33 | export const absBigInt = (value: bigint): bigint => { 34 | return value < 0n ? -value : value; 35 | }; 36 | 37 | export const getTradeType = (inMint: string, outMint: string): TradeType => { 38 | if (inMint == TOKENS.SOL) return 'BUY'; 39 | if (outMint == TOKENS.SOL) return 'SELL'; 40 | if (Object.values(TOKENS).includes(inMint)) return 'BUY'; 41 | return 'SELL'; 42 | }; 43 | 44 | export const getAMMs = (transferActionKeys: string[]) => { 45 | const amms = Object.values(DEX_PROGRAMS).filter((it) => it.tags.includes('amm')); 46 | return transferActionKeys 47 | .map((it) => { 48 | const item = Object.values(amms).find((amm) => it.split(':')[0] == amm.id); 49 | if (item) return item.name; 50 | return null; 51 | }) 52 | .filter((it) => it != null); 53 | }; 54 | 55 | export const getTranferTokenMint = (token1?: string, token2?: string): string | undefined => { 56 | if (token1 == token2) return token1; 57 | if (token1 && token1 != TOKENS.SOL) return token1; 58 | if (token2 && token2 != TOKENS.SOL) return token2; 59 | return token1 || token2; 60 | }; 61 | 62 | export const getPubkeyString = (value: any): string => { 63 | if (typeof value === 'string') return value; 64 | if (value instanceof PublicKey) return value.toBase58(); 65 | if ('type' in value && value.type == 'Buffer') return base58.encode(value.data); 66 | if (value instanceof Buffer) return base58.encode(value); 67 | return value; 68 | }; 69 | 70 | // ... existing code ... 71 | 72 | /** 73 | * Sort an array of TradeInfo objects by their idx field 74 | * The idx format is 'main-sub', such as '1-0', '2-1', etc. 75 | * @param items The TradeInfo array to be sorted 76 | * @returns The sorted TradeInfo array 77 | */ 78 | export const sortByIdx = (items: T[]): T[] => { 79 | return items && items.length > 1 80 | ? [...items].sort((a, b) => { 81 | const [aMain, aSub = '0'] = a.idx.split('-'); 82 | const [bMain, bSub = '0'] = b.idx.split('-'); 83 | const mainDiff = parseInt(aMain) - parseInt(bMain); 84 | if (mainDiff !== 0) return mainDiff; 85 | return parseInt(aSub) - parseInt(bSub); 86 | }) 87 | : items; 88 | }; 89 | 90 | export const getFinalSwap = (trades: TradeInfo[], dexInfo?: DexInfo): TradeInfo | null => { 91 | if (trades.length == 1) return trades[0]; 92 | if (trades.length >= 2) { 93 | // sort by idx 94 | if (trades.length > 2) { 95 | trades = sortByIdx(trades); 96 | } 97 | 98 | const inputTrade = trades[0]; 99 | const outputTrade = trades[trades.length - 1]; 100 | 101 | if (trades.length >= 2) { 102 | // Merge trades 103 | let [inputAmount, outputAmount] = [0n, 0n]; 104 | for (const trade of trades) { 105 | if (trade.inputToken.mint == inputTrade.inputToken.mint) { 106 | inputAmount += BigInt(trade.inputToken.amountRaw); 107 | } 108 | if (trade.outputToken.mint == outputTrade.outputToken.mint) { 109 | outputAmount += BigInt(trade.outputToken.amountRaw); 110 | } 111 | } 112 | 113 | inputTrade.inputToken.amountRaw = inputAmount.toString(); 114 | inputTrade.inputToken.amount = convertToUiAmount(inputAmount, inputTrade.inputToken.decimals); 115 | 116 | outputTrade.outputToken.amountRaw = outputAmount.toString(); 117 | outputTrade.outputToken.amount = convertToUiAmount(outputAmount, outputTrade.outputToken.decimals); 118 | } 119 | 120 | return { 121 | type: getTradeType(inputTrade.inputToken.mint, outputTrade.outputToken.mint), 122 | inputToken: inputTrade.inputToken, 123 | outputToken: outputTrade.outputToken, 124 | user: inputTrade.user, 125 | programId: inputTrade.programId, 126 | amm: dexInfo?.amm || inputTrade.amm, 127 | route: dexInfo?.route || inputTrade.route || '', 128 | slot: inputTrade.slot, 129 | timestamp: inputTrade.timestamp, 130 | signature: inputTrade.signature, 131 | idx: inputTrade.idx, 132 | } as TradeInfo; 133 | } 134 | return null; 135 | }; 136 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "outDir": "./dist" 12 | }, 13 | "include": ["src/**/*", "index.ts"], 14 | "exclude": ["node_modules", "dist"] 15 | } --------------------------------------------------------------------------------