├── .gitignore ├── 00-sub ├── README.md └── index.ts ├── 01-format ├── README.md └── index.ts ├── 02-txn-parser ├── README.md └── index.ts ├── 03-account-parser ├── README.md └── index.ts ├── 04-example-subNewPool ├── README.md └── index.ts ├── 05-example-subWallet ├── README.md └── index.ts ├── 06-example-subPrice ├── README.md └── index.ts ├── 07-example-subBlock ├── README.md └── index.ts ├── 08-example-subPumpSwap ├── README.md └── index.ts ├── LICENSE ├── README.md └── package.json /.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 | -------------------------------------------------------------------------------- /00-sub/README.md: -------------------------------------------------------------------------------- 1 | # 创建订阅 2 | 3 | 本部分将介绍创建grpc订阅的通用流程。若要更改订阅的数据,只需要修改订阅请求即可。 4 | 5 | ## 创建订阅客户端和数据流 6 | 7 | 首先,需要先指定grpc的endpoint,来创建订阅的客户端。 8 | 9 | ```ts 10 | const client = new Client( 11 | // 如遇到TypeError: Client is not a constructor错误 12 | // 请使用以下方式创建 13 | // 见 https://github.com/rpcpool/yellowstone-grpc/issues/428 14 | // @ts-ignore 15 | // const client = new Client.default( 16 | "https://test-grpc.chainbuff.com", 17 | undefined, 18 | { 19 | "grpc.max_receive_message_length": 16 * 1024 * 1024, // 16MB 20 | } 21 | ); 22 | console.log("Subscribing to event stream..."); 23 | ``` 24 | 25 | ## 创建订阅请求 26 | 27 | 订阅请求应严格按照`SubscribeRequest`接口的格式来创建,不需要的字段不可省略。以下是一个订阅slot更新的例子,获取数据的级别为`processed`。 28 | 29 | ```ts 30 | // 创建订阅请求 31 | const request: SubscribeRequest = { 32 | accounts: {}, 33 | slots: { slot: { filterByCommitment: true } }, // 指定只获取processed的slot 34 | transactions: {}, 35 | transactionsStatus: {}, 36 | blocks: {}, 37 | blocksMeta: {}, 38 | entry: {}, 39 | accountsDataSlice: [], 40 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 41 | ping: undefined, 42 | }; 43 | ``` 44 | 45 | ## 发送订阅请求并获取数据流 46 | 47 | 之后,便可将订阅请求发送给服务端,并获取数据流。 48 | 49 | ```ts 50 | // 发送订阅请求 51 | await new Promise((resolve, reject) => { 52 | stream.write(request, (err) => { 53 | if (err === null || err === undefined) { 54 | resolve(); 55 | } else { 56 | reject(err); 57 | } 58 | }); 59 | }).catch((reason) => { 60 | console.error(reason); 61 | throw reason; 62 | }); 63 | 64 | // 获取订阅数据 65 | stream.on("data", async (data) => { 66 | console.log(data); 67 | }); 68 | ``` 69 | 70 | 输出应如下: 71 | 72 | ```bash 73 | { 74 | filters: [ 'slot' ], 75 | account: undefined, 76 | slot: { 77 | slot: '310371084', 78 | parent: '310371083', 79 | status: 0, 80 | deadError: undefined 81 | }, 82 | transaction: undefined, 83 | transactionStatus: undefined, 84 | block: undefined, 85 | ping: undefined, 86 | pong: undefined, 87 | blockMeta: undefined, 88 | entry: undefined 89 | } 90 | ``` 91 | 92 | ## pingpong机制 93 | 94 | 推荐使用pingpong机制来维持连接,否则连接在一段时间后会被断开。5秒发送一次ping请求是经验上合适的间隔。 95 | 96 | ```ts 97 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 98 | const pingRequest: SubscribeRequest = { 99 | accounts: {}, 100 | slots: {}, // 指定只获取processed的slot 101 | transactions: {}, 102 | transactionsStatus: {}, 103 | blocks: {}, 104 | blocksMeta: {}, 105 | entry: {}, 106 | accountsDataSlice: [], 107 | commitment: undefined, // 指定级别为processed 108 | ping: { id: 1 }, 109 | }; 110 | // 每5秒发送一次ping请求 111 | setInterval(async () => { 112 | await new Promise((resolve, reject) => { 113 | stream.write(pingRequest, (err) => { 114 | if (err === null || err === undefined) { 115 | resolve(); 116 | } else { 117 | reject(err); 118 | } 119 | }); 120 | }).catch((reason) => { 121 | console.error(reason); 122 | throw reason; 123 | }); 124 | }, 5000); 125 | 126 | ``` 127 | 128 | # 总结 129 | 130 | 以上就是创建grpc订阅的基本流程。之后,我们需要了解订阅的基本格式以订阅各种不同的数据。 -------------------------------------------------------------------------------- /00-sub/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | 3 | async function main() { 4 | 5 | // 创建订阅客户端 6 | // const client = new Client( 7 | // 如遇到TypeError: Client is not a constructor错误 8 | // 请使用以下方式创建 9 | // 见 https://github.com/rpcpool/yellowstone-grpc/issues/428 10 | // @ts-ignore 11 | const client = new Client.default( 12 | "https://test-grpc.chainbuff.com", 13 | undefined, 14 | { 15 | "grpc.max_receive_message_length": 16 * 1024 * 1024, // 16MB 16 | } 17 | ); 18 | 19 | // 创建订阅数据流 20 | const stream = await client.subscribe(); 21 | 22 | // 创建订阅请求 23 | const request: SubscribeRequest = { 24 | accounts: {}, 25 | slots: { slot: { filterByCommitment: true } }, // 指定只获取processed的slot 26 | transactions: {}, 27 | transactionsStatus: {}, 28 | blocks: {}, 29 | blocksMeta: {}, 30 | entry: {}, 31 | accountsDataSlice: [], 32 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 33 | ping: undefined, 34 | }; 35 | 36 | // 发送订阅请求 37 | await new Promise((resolve, reject) => { 38 | stream.write(request, (err) => { 39 | if (err === null || err === undefined) { 40 | resolve(); 41 | } else { 42 | reject(err); 43 | } 44 | }); 45 | }).catch((reason) => { 46 | console.error(reason); 47 | throw reason; 48 | }); 49 | 50 | // 获取订阅数据 51 | stream.on("data", async (data) => { 52 | console.log(data); 53 | }); 54 | 55 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 56 | const pingRequest: SubscribeRequest = { 57 | accounts: {}, 58 | slots: {}, 59 | transactions: {}, 60 | transactionsStatus: {}, 61 | blocks: {}, 62 | blocksMeta: {}, 63 | entry: {}, 64 | accountsDataSlice: [], 65 | commitment: undefined, 66 | ping: { id: 1 }, 67 | }; 68 | // 每5秒发送一次ping请求 69 | setInterval(async () => { 70 | await new Promise((resolve, reject) => { 71 | stream.write(pingRequest, (err) => { 72 | if (err === null || err === undefined) { 73 | resolve(); 74 | } else { 75 | reject(err); 76 | } 77 | }); 78 | }).catch((reason) => { 79 | console.error(reason); 80 | throw reason; 81 | }); 82 | }, 5000); 83 | } 84 | 85 | main(); -------------------------------------------------------------------------------- /01-format/README.md: -------------------------------------------------------------------------------- 1 | # 订阅格式 2 | 3 | 本部分将了解订阅的基本格式,建议对照`SubscribeRequest`接口进行学习。 4 | 5 | `SubscribeRequest`接口定义如下: 6 | 7 | ```ts 8 | // 本地路径为 node_modules/@triton-one/yellowstone-grpc/dist/grpc/geyser.d.ts 9 | export interface SubscribeRequest { 10 | accounts: { 11 | [key: string]: SubscribeRequestFilterAccounts; 12 | }; 13 | slots: { 14 | [key: string]: SubscribeRequestFilterSlots; 15 | }; 16 | transactions: { 17 | [key: string]: SubscribeRequestFilterTransactions; 18 | }; 19 | transactionsStatus: { 20 | [key: string]: SubscribeRequestFilterTransactions; 21 | }; 22 | blocks: { 23 | [key: string]: SubscribeRequestFilterBlocks; 24 | }; 25 | blocksMeta: { 26 | [key: string]: SubscribeRequestFilterBlocksMeta; 27 | }; 28 | entry: { 29 | [key: string]: SubscribeRequestFilterEntry; 30 | }; 31 | commitment?: CommitmentLevel | undefined; 32 | accountsDataSlice: SubscribeRequestAccountsDataSlice[]; 33 | ping?: SubscribeRequestPing | undefined; 34 | } 35 | ``` 36 | 37 | 所以,一个空白的订阅请求如下所示: 38 | 39 | ```ts 40 | { 41 | accounts: {}, 42 | slots: {}, 43 | transactions: {}, 44 | transactionsStatus: {}, 45 | blocks: {}, 46 | blocksMeta: {}, 47 | entry: {}, 48 | accountsDataSlice: [], 49 | commitment: undefined, 50 | ping: undefined, 51 | }; 52 | ``` 53 | 54 | 接下来,我们将按照是否常用从高到低排序讲解。 55 | 56 | ## 1. commitment 57 | 58 | 用于指定订阅数据的确认级别。 59 | 60 | 在实时监听场景下,`processed`为最低级别,也是最常用的级别,要以最快的方式获取链上数据,应使用此级别。其后依次为`confirmed`、`finalized`等。 61 | 62 | 一个指定确认级别为`processed`的request示例如下: 63 | 64 | ```ts 65 | { 66 | accounts: {}, 67 | slots: {}, 68 | transactions: {}, 69 | transactionsStatus: {}, 70 | blocks: {}, 71 | blocksMeta: {}, 72 | entry: {}, 73 | accountsDataSlice: [], 74 | commitment: CommitmentLevel.PROCESSED, 75 | ping: undefined, 76 | }; 77 | ``` 78 | 79 | ## 2. ping 80 | 81 | 用于维持连接。 82 | 83 | 正如上部分的例子,建议以定时器的方式定期向服务端发送ping请求。从经验来看,5秒钟发送一次较为合适。 84 | 85 | 一个在向服务端发送ping请求的request示例如下: 86 | 87 | ```ts 88 | { 89 | accounts: {}, 90 | slots: {}, 91 | transactions: {}, 92 | transactionsStatus: {}, 93 | blocks: {}, 94 | blocksMeta: {}, 95 | entry: {}, 96 | accountsDataSlice: [], 97 | commitment: undefined, 98 | ping: { id: 1 }, 99 | }; 100 | ``` 101 | 102 | ## 3. transactions & transactionsStatus 103 | 104 | `transactions`用于过滤符合条件的交易,可添加如下过滤条件: 105 | 106 | ```ts 107 | { 108 | vote?: boolean | undefined; 109 | failed?: boolean | undefined; 110 | signature?: string | undefined; 111 | accountInclude: string[]; 112 | accountExclude: string[]; 113 | accountRequired: string[]; 114 | } 115 | ``` 116 | 117 | - `vote`: 是否监听投票交易 118 | - `failed`: 是否监听失败交易 119 | - `signature`: 指定监听交易的签名 120 | - `accountInclude`: 监听此列表中账户的交易 121 | - `accountExclude`: 不监听此列表中的账户的交易 122 | - `accountRequired`: 监听的交易需要包含列表中的所有账户 123 | 124 | > 注意:各个字段之间为`AND`关系,而单个字段列表中的值为`OR`关系。 125 | 126 | 一个只监听`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`账户(pumpfun合约)交易的request示例如下: 127 | 128 | ```ts 129 | const request: SubscribeRequest = { 130 | accounts: {}, 131 | slots: {}, 132 | transactions: { 133 | txn: { 134 | vote: false, 135 | failed: false, 136 | signature: undefined, 137 | accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 138 | accountExclude: [], 139 | accountRequired: [], 140 | } 141 | }, 142 | transactionsStatus: {}, 143 | blocks: {}, 144 | blocksMeta: {}, 145 | entry: {}, 146 | accountsDataSlice: [], 147 | commitment: undefined, 148 | ping: undefined, 149 | }; 150 | ``` 151 | 152 | `transactionsStatus`用于监听交易状态,request格式与`transactions`相同,但返回的数据无交易的详细信息。 153 | 154 | ## 4. accounts & accountsDataSlice 155 | 156 | `accounts`用于订阅账户更新,可添加如下过滤条件: 157 | 158 | ```ts 159 | { 160 | account: string[]; 161 | owner: string[]; 162 | filters: SubscribeRequestFilterAccountsFilter[]; // 类似http rpc的getProgramAccounts方法 163 | nonemptyTxnSignature?: boolean | undefined; 164 | } 165 | ``` 166 | 167 | - `account`: 监听此列表中的账户 168 | - `owner`: 监听账户的owner 169 | - `filters`: 过滤条件, 包含`memcmp`、`datasize`、`tokenAccountState`、`lamports`,可参考[https://solana.com/docs/rpc#filter-criteria](https://solana.com/docs/rpc#filter-criteria)。 170 | - `nonemptyTxnSignature`: 是否监听非空交易 171 | 172 | 一个监听`8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj`账户(Raydium WSOL/USDC CLMM流动池)的request示例如下: 173 | 174 | ```ts 175 | const request: SubscribeRequest = { 176 | accounts: { 177 | txn: { 178 | account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 179 | owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 180 | filters: [], 181 | nonemptyTxnSignature: true, 182 | } 183 | }, 184 | slots: {}, 185 | transactions: {}, 186 | transactionsStatus: {}, 187 | blocks: {}, 188 | blocksMeta: {}, 189 | entry: {}, 190 | accountsDataSlice: [], 191 | commitment: CommitmentLevel.PROCESSED, 192 | ping: undefined, 193 | }; 194 | ``` 195 | 196 | `accountsDataSlice`用于指定获取账户数据的分片,可添加如下过滤条件: 197 | 198 | ```ts 199 | { 200 | offset: string; 201 | length: string; 202 | } 203 | ``` 204 | 205 | - `offset`: 偏移量 206 | - `length`: 长度 207 | 208 | 一个监听Raydium WSOL/USDC CLMM流动池中`sqrtPriceX64`数据的request示例如下: 209 | 210 | ```ts 211 | const request: SubscribeRequest = { 212 | accounts: { 213 | txn: { 214 | account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 215 | owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 216 | filters: [], 217 | nonemptyTxnSignature: true, 218 | } 219 | }, 220 | slots: {}, 221 | transactions: {}, 222 | transactionsStatus: {}, 223 | blocks: {}, 224 | blocksMeta: {}, 225 | entry: {}, 226 | accountsDataSlice: [ { offset: "253", length: "16" } ], 227 | commitment: CommitmentLevel.PROCESSED, 228 | ping: undefined, 229 | }; 230 | ``` 231 | 232 | ## 5. blocks & blocksMeta 233 | 234 | `blocks`用于过滤订阅区块数据,可添加如下过滤条件: 235 | 236 | ```ts 237 | { 238 | accountInclude: string[]; 239 | includeTransactions?: boolean | undefined; 240 | includeAccounts?: boolean | undefined; 241 | includeEntries?: boolean | undefined; 242 | } 243 | ``` 244 | 245 | - `accountInclude`: 监听包含此列表中的账户的区块 246 | - `includeTransactions`: 是否监听区块中的交易信息 247 | - `includeAccounts`: 是否监听区块中的账户信息 248 | - `includeEntries`: 是否监听区块中的entry信息 249 | 250 | 一个获取区块全部交易信息的request示例如下: 251 | 252 | ```ts 253 | const request: SubscribeRequest = { 254 | accounts: {}, 255 | slots: {}, 256 | transactions: {}, 257 | transactionsStatus: {}, 258 | blocks: { 259 | block: { 260 | accountInclude: [], 261 | includeTransactions: true, 262 | includeAccounts: false, 263 | includeEntries: false, 264 | } 265 | }, 266 | blocksMeta: {}, 267 | entry: {}, 268 | accountsDataSlice: [], 269 | commitment: CommitmentLevel.CONFIRMED, 270 | ping: undefined, 271 | }; 272 | ``` 273 | 274 | `blocksMeta`用于过滤订阅区块元数据,目前暂无过滤器支持。 275 | 276 | ## 6. entry 277 | 278 | `entry`用于过滤订阅entry数据,目前暂无过滤器支持。 279 | 280 | ## 7. slots 281 | 282 | `slots`用于过滤订阅slot更新。 283 | 284 | ```ts 285 | { 286 | filterByCommitment?: boolean | undefined; 287 | } 288 | ``` 289 | 290 | # 总结 291 | 292 | 本部分,我们学习了订阅请求的格式,并大致了解了各个字段的作用,之后使用时,可按照需求自定义订阅格式。 293 | 294 | 在获取到数据后,需要对数据进行解析,这是下部分的内容。 -------------------------------------------------------------------------------- /01-format/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import { commitmentLevelFromJSON } from "@triton-one/yellowstone-grpc/dist/grpc/geyser"; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | 22 | // 3. transactions 23 | // const request: SubscribeRequest = { 24 | // accounts: {}, 25 | // slots: {}, 26 | // transactions: { 27 | // txn: { 28 | // vote: false, 29 | // failed: false, 30 | // signature: undefined, 31 | // accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 32 | // accountExclude: [], 33 | // accountRequired: [], 34 | // } 35 | // }, 36 | // transactionsStatus: {}, 37 | // blocks: {}, 38 | // blocksMeta: {}, 39 | // entry: {}, 40 | // accountsDataSlice: [], 41 | // commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 42 | // ping: undefined, 43 | // }; 44 | 45 | // transactionsStatus 46 | // const request: SubscribeRequest = { 47 | // accounts: {}, 48 | // slots: {}, 49 | // transactions: {}, 50 | // transactionsStatus: { 51 | // txn: { 52 | // vote: false, 53 | // failed: false, 54 | // signature: undefined, 55 | // accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 56 | // accountExclude: [], 57 | // accountRequired: [], 58 | // } 59 | // }, 60 | // blocks: {}, 61 | // blocksMeta: {}, 62 | // entry: {}, 63 | // accountsDataSlice: [], 64 | // // commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 65 | // commitment: undefined, 66 | // ping: undefined, 67 | // }; 68 | 69 | // 4. accounts 70 | // const request: SubscribeRequest = { 71 | // accounts: { 72 | // txn: { 73 | // account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 74 | // owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 75 | // filters: [], 76 | // nonemptyTxnSignature: true, 77 | // } 78 | // }, 79 | // slots: {}, 80 | // transactions: {}, 81 | // transactionsStatus: {}, 82 | // blocks: {}, 83 | // blocksMeta: {}, 84 | // entry: {}, 85 | // accountsDataSlice: [], 86 | // commitment: CommitmentLevel.PROCESSED, 87 | // ping: undefined, 88 | // }; 89 | 90 | // accountsDataSlice 91 | // const request: SubscribeRequest = { 92 | // accounts: { 93 | // txn: { 94 | // account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 95 | // owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 96 | // filters: [], 97 | // nonemptyTxnSignature: true, 98 | // } 99 | // }, 100 | // slots: {}, 101 | // transactions: {}, 102 | // transactionsStatus: {}, 103 | // blocks: {}, 104 | // blocksMeta: {}, 105 | // entry: {}, 106 | // accountsDataSlice: [ { offset: "253", length: "16" } ], 107 | // commitment: CommitmentLevel.PROCESSED, 108 | // ping: undefined, 109 | // }; 110 | 111 | // 5. blocks 112 | const request: SubscribeRequest = { 113 | accounts: {}, 114 | slots: {}, 115 | transactions: {}, 116 | transactionsStatus: {}, 117 | blocks: { 118 | block: { 119 | accountInclude: [], 120 | includeTransactions: true, 121 | includeAccounts: false, 122 | includeEntries: false, 123 | } 124 | }, 125 | blocksMeta: {}, 126 | entry: {}, 127 | accountsDataSlice: [], 128 | commitment: CommitmentLevel.CONFIRMED, 129 | ping: undefined, 130 | }; 131 | 132 | // 发送订阅请求 133 | await new Promise((resolve, reject) => { 134 | stream.write(request, (err) => { 135 | if (err === null || err === undefined) { 136 | resolve(); 137 | } else { 138 | reject(err); 139 | } 140 | }); 141 | }).catch((reason) => { 142 | console.error(reason); 143 | throw reason; 144 | }); 145 | 146 | // 获取订阅数据 147 | stream.on("data", async (data) => { 148 | console.log(data); 149 | }); 150 | 151 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 152 | const pingRequest: SubscribeRequest = { 153 | accounts: {}, 154 | slots: {}, // 指定只获取processed的slot 155 | transactions: {}, 156 | transactionsStatus: {}, 157 | blocks: {}, 158 | blocksMeta: {}, 159 | entry: {}, 160 | accountsDataSlice: [], 161 | commitment: undefined, // 指定级别为processed 162 | ping: { id: 1 }, 163 | }; 164 | // 每5秒发送一次ping请求 165 | setInterval(async () => { 166 | await new Promise((resolve, reject) => { 167 | stream.write(pingRequest, (err) => { 168 | if (err === null || err === undefined) { 169 | resolve(); 170 | } else { 171 | reject(err); 172 | } 173 | }); 174 | }).catch((reason) => { 175 | console.error(reason); 176 | throw reason; 177 | }); 178 | }, 5000); 179 | } 180 | 181 | main(); -------------------------------------------------------------------------------- /02-txn-parser/README.md: -------------------------------------------------------------------------------- 1 | # 交易数据解析 2 | 3 | 在获取到交易数据后,需要解析交易数据,以获取到交易中的具体信息。 4 | 5 | 在本节的示例代码中,我们通过transactions订阅pumpfun相关交易数据,request请求如下: 6 | 7 | ```ts 8 | // 创建订阅请求 9 | const request: SubscribeRequest = { 10 | accounts: {}, 11 | slots: {}, 12 | transactions: { 13 | txn: { 14 | vote: false, 15 | failed: false, 16 | signature: undefined, 17 | accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 18 | accountExclude: [], 19 | accountRequired: [], 20 | } 21 | }, 22 | transactionsStatus: {}, 23 | blocks: {}, 24 | blocksMeta: {}, 25 | entry: {}, 26 | accountsDataSlice: [], 27 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 28 | ping: undefined, 29 | }; 30 | ``` 31 | 32 | 一个示例的订阅交易原始data数据如下: 33 | 34 | ```ts 35 | { 36 | filters: [ 'txn' ], 37 | account: undefined, 38 | slot: undefined, 39 | transaction: { 40 | transaction: { 41 | signature: , 42 | isVote: false, 43 | transaction: [Object], // 交易数据 44 | meta: [Object], // 包含日志信息 45 | index: '909' 46 | }, 47 | slot: '310558888' 48 | }, 49 | transactionStatus: undefined, 50 | block: undefined, 51 | ping: undefined, 52 | pong: undefined, 53 | blockMeta: undefined, 54 | entry: undefined 55 | } 56 | ``` 57 | 58 | 我们主要关心的是`data.transaction.transaction.transaction`中的数据和`data.transaction.transaction.meta`中的数据,其中分别包含交易的具体信息和交易的日志信息,如下: 59 | 60 | ## 交易具体信息 61 | 62 | 一个示例交易的`data.transaction.transaction.transaction`中的数据如下: 63 | 64 | ```ts 65 | { 66 | signatures: [ 67 | 68 | ], 69 | message: { 70 | header: { 71 | numRequiredSignatures: 1, 72 | numReadonlySignedAccounts: 0, 73 | numReadonlyUnsignedAccounts: 8 74 | }, 75 | accountKeys: [ 76 | , 77 | , 78 | , 79 | , 80 | , 81 | , 82 | , 83 | , 84 | , 85 | , 86 | , 87 | , 88 | , 89 | 90 | ], 91 | recentBlockhash: , 92 | instructions: [ [Object], [Object], [Object], [Object] ], 93 | versioned: true, 94 | addressTableLookups: [] 95 | } 96 | } 97 | ``` 98 | 99 | 我们主要需要解析的是: 100 | 101 | - `signatures`:交易签名 102 | - `accountKeys`:交易涉及的账户 103 | - `instructions`:交易指令 104 | 105 | 交易签名和涉及的账户需要转换为base58编码,而交易指令需要按照具体情况进一步解析。 106 | 107 | 修改代码中的获取订阅数据部分如下,可打印出交易签名、涉及的账户和交易指令: 108 | 109 | ```ts 110 | // 获取订阅数据 111 | stream.on("data", async (data) => { 112 | if (data.transaction) { 113 | 114 | // console.log(data.transaction.transaction.transaction); 115 | 116 | // 解析交易 117 | const txnSignature = bs58.encode(data.transaction.transaction.transaction.signatures[0]); 118 | console.log('交易签名:', txnSignature); 119 | // 交易涉及的账户 120 | const accountKeys = data.transaction.transaction.transaction.message.accountKeys.map(ak => bs58.encode(ak)); 121 | console.log('交易涉及的账户:', accountKeys); 122 | // 交易指令 123 | const instructions = data.transaction.transaction.transaction.message.instructions; 124 | console.log('交易指令:', instructions); 125 | } 126 | }); 127 | ``` 128 | 129 | 输入示例如下: 130 | 131 | ```bash 132 | 交易签名: 3dXad5fvPQUedvdjHS1jJkZDHWUegpsSEESeR5Es4dWeJ9Ek4SLvzpBdaCZebymC4sukbsMh8w4NQ7Fm6rG5Ub3z 133 | 交易涉及的账户: [ 134 | 'ErU8SDGTJ6aKHXbN4QiBCAfAxBF5efHzDRn8Lcg9R4XR', 135 | '7jxN82Emp6UrMi7d79BdBLUcoa9jPiQc6HS4NDySsCjN', 136 | '8VvkZvwwm4jmpt5Esa1mcx9uH9SQTTSGEwEJ2gowDCTy', 137 | 'CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM', 138 | 'E8mK46Dk7P4xS1mVZTyih7KTUrWBAU3AwSHFF5RdY4mk', 139 | '11111111111111111111111111111111', 140 | '2b9kESRNxs2t26ZSAovtXqM9cponYcyGwBXMBYLJpump', 141 | '4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf', 142 | '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P', 143 | 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 144 | 'Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1', 145 | 'ComputeBudget111111111111111111111111111111', 146 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' 147 | ] 148 | 交易指令: [ 149 | { 150 | programIdIndex: 11, 151 | accounts: Uint8Array(0) [], 152 | data: 153 | }, 154 | { 155 | programIdIndex: 11, 156 | accounts: Uint8Array(0) [], 157 | data: 158 | }, 159 | { 160 | programIdIndex: 8, 161 | accounts: , 162 | data: 163 | } 164 | ] 165 | ``` 166 | 167 | 可以看到前两条交易指令交互的合约为`ComputeBudget111111111111111111111111111111`,是修改CU更改交易优先费的指令,第三条交易指令交互的合约为`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`,需要参照pumpfun合约或IDL文件进一步精确解析。通过浏览器查看,这实际上是一笔在pumpfun上卖出代币的指令。 168 | 169 | ## 交易日志信息 170 | 171 | 在某些情况下,监听交易日志信息是一个不错的选择。在编写合约时为了确保合约的正确性和规范性,日志中往往包含合约的执行结果等重要信息。 172 | 173 | 同样,可以修改代码中的获取订阅数据部分,打印出交易日志信息: 174 | 175 | ```ts 176 | // 获取订阅数据 177 | stream.on("data", async (data) => { 178 | if (data.transaction) { 179 | 180 | // 日志 181 | console.log('日志:', data.transaction.transaction.meta.logMessages); 182 | } 183 | }); 184 | ``` 185 | 186 | 一个示例交易的`data.transaction.transaction.meta.logMessages`中的数据如下: 187 | 188 | ```ts 189 | 日志: [ 190 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 191 | 'Program ComputeBudget111111111111111111111111111111 success', 192 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 193 | 'Program ComputeBudget111111111111111111111111111111 success', 194 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]', 195 | 'Program log: Instruction: Buy', 196 | 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', 197 | 'Program log: Instruction: Transfer', 198 | 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 76564 compute units', 199 | 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 200 | 'Program 11111111111111111111111111111111 invoke [2]', 201 | 'Program 11111111111111111111111111111111 success', 202 | 'Program 11111111111111111111111111111111 invoke [2]', 203 | 'Program 11111111111111111111111111111111 success', 204 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]', 205 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2003 of 64476 compute units', 206 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success', 207 | 'Program data: vdt/007mYe4I2s9bQJeDK7Vsc0abb1HZtkn00oEApJL31hHDmDkdn4elBH4AAAAANMdMxxcLAAABQFvoWh4tpYvdaQpW4wxu9sWX0f+5ye2VIR53vUXOYLdYSHFnAAAAAJ7V3KMRAAAArWn3u2uCAQCeKbmnCgAAAK3R5G/agwAA', 208 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 38952 of 99700 compute units', 209 | 'Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success', 210 | 'Program 11111111111111111111111111111111 invoke [1]', 211 | 'Program 11111111111111111111111111111111 success', 212 | 'Program 4pP8eDKACuV7T2rbFPE8CHxGKDYAzSdRsdMsGvz2k4oc invoke [1]', 213 | 'Program log: Received timestamp: 1735477356', 214 | 'Program log: Current timestamp: 1735477336', 215 | 'Program log: The provided timestamp is valid.', 216 | 'Program 4pP8eDKACuV7T2rbFPE8CHxGKDYAzSdRsdMsGvz2k4oc consumed 1661 of 60598 compute units', 217 | 'Program 4pP8eDKACuV7T2rbFPE8CHxGKDYAzSdRsdMsGvz2k4oc success', 218 | 'Program 11111111111111111111111111111111 invoke [1]', 219 | 'Program 11111111111111111111111111111111 success', 220 | 'Program HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx invoke [1]', 221 | 'Program log: Powered by bloXroute Trader Api', 222 | 'Program HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx consumed 803 of 58787 compute units', 223 | 'Program HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx success' 224 | ] 225 | ``` 226 | 227 | 可以看到日志中包含`'Program log: Instruction: Buy'`信息,而我们监听的合约地址为`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`,因此可以推断出这是一笔在pumpfun上买入代币的交易。 228 | 229 | # 总结 230 | 231 | 本节,我们简单了解了解析交易数据和交易日志信息的通用方法,下一节我们将介绍如何解析账户数据。 -------------------------------------------------------------------------------- /02-txn-parser/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import bs58 from "bs58"; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | const request: SubscribeRequest = { 22 | accounts: {}, 23 | slots: {}, 24 | transactions: { 25 | txn: { 26 | vote: false, 27 | failed: false, 28 | signature: undefined, 29 | accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 30 | accountExclude: [], 31 | accountRequired: [], 32 | } 33 | }, 34 | transactionsStatus: {}, 35 | blocks: {}, 36 | blocksMeta: {}, 37 | entry: {}, 38 | accountsDataSlice: [], 39 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 40 | ping: undefined, 41 | }; 42 | 43 | // 发送订阅请求 44 | await new Promise((resolve, reject) => { 45 | stream.write(request, (err) => { 46 | if (err === null || err === undefined) { 47 | resolve(); 48 | } else { 49 | reject(err); 50 | } 51 | }); 52 | }).catch((reason) => { 53 | console.error(reason); 54 | throw reason; 55 | }); 56 | 57 | // 获取订阅数据 58 | stream.on("data", async (data) => { 59 | if (data.transaction) { 60 | 61 | // console.log(data.transaction.transaction.transaction); 62 | 63 | // 解析交易 64 | const txnSignature = bs58.encode(data.transaction.transaction.transaction.signatures[0]); 65 | console.log('交易签名:', txnSignature); 66 | // 交易涉及的账户 67 | const accountKeys = data.transaction.transaction.transaction.message.accountKeys.map(ak => bs58.encode(ak)); 68 | console.log('交易涉及的账户:', accountKeys); 69 | // 交易指令 70 | const instructions = data.transaction.transaction.transaction.message.instructions; 71 | console.log('交易指令:', instructions); 72 | 73 | // 日志 74 | console.log('日志:', data.transaction.transaction.meta.logMessages); 75 | } 76 | }); 77 | 78 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 79 | const pingRequest: SubscribeRequest = { 80 | accounts: {}, 81 | slots: {}, 82 | transactions: {}, 83 | transactionsStatus: {}, 84 | blocks: {}, 85 | blocksMeta: {}, 86 | entry: {}, 87 | accountsDataSlice: [], 88 | commitment: undefined, 89 | ping: { id: 1 }, 90 | }; 91 | // 每5秒发送一次ping请求 92 | setInterval(async () => { 93 | await new Promise((resolve, reject) => { 94 | stream.write(pingRequest, (err) => { 95 | if (err === null || err === undefined) { 96 | resolve(); 97 | } else { 98 | reject(err); 99 | } 100 | }); 101 | }).catch((reason) => { 102 | console.error(reason); 103 | throw reason; 104 | }); 105 | }, 5000); 106 | } 107 | 108 | main(); -------------------------------------------------------------------------------- /03-account-parser/README.md: -------------------------------------------------------------------------------- 1 | # 账户数据解析 2 | 3 | 账户数据的解析方法与[之前的教程](https://github.com/ChainBuff/solana-web3js/tree/main/09-buffer)类似,这要求我们需要提前清楚账户数据的结构,这往往是此账户相关的合约里定义的。 4 | 5 | 如Raydium WSOL/USDC CLMM流动池的账户`8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj`,其账户数据保存了此流动池当前的状态,数据结构可以在[合约源码](https://github.com/raydium-io/raydium-clmm/blob/678dc67bc7bdbacb8f81889f8237007fde0a0039/programs/amm/src/states/pool.rs#L58)中找到,包括偏移量,字段长度等信息。 6 | 7 | 通过如下request,我们可以订阅到此账户的状态变化: 8 | 9 | ```ts 10 | // 创建订阅请求 11 | const request: SubscribeRequest = { 12 | accounts: { 13 | txn: { 14 | account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 15 | owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 16 | filters: [], 17 | nonemptyTxnSignature: true, 18 | } 19 | }, 20 | slots: {}, 21 | transactions: {}, 22 | transactionsStatus: {}, 23 | blocks: {}, 24 | blocksMeta: {}, 25 | entry: {}, 26 | accountsDataSlice: [], 27 | commitment: CommitmentLevel.PROCESSED, 28 | ping: undefined, 29 | }; 30 | ``` 31 | 32 | 示例的订阅数据如下: 33 | 34 | ```ts 35 | { 36 | filters: [ 'account' ], 37 | account: { 38 | account: { 39 | pubkey: , 40 | lamports: '1426364460', 41 | owner: , 42 | executable: false, 43 | rentEpoch: '18446744073709551615', 44 | data: , 45 | writeVersion: '1510854150789', 46 | txnSignature: 47 | }, 48 | slot: '310566532', 49 | isStartup: false 50 | }, 51 | slot: undefined, 52 | transaction: undefined, 53 | transactionStatus: undefined, 54 | block: undefined, 55 | ping: undefined, 56 | pong: undefined, 57 | blockMeta: undefined, 58 | entry: undefined 59 | } 60 | ``` 61 | 62 | 在这里,我们主要关心的是`data.account.account.data`部分,这包含了此账户的当前状态。 63 | 64 | 如需要WSOL/USDC流动池价格信息的话,可配合`accountsDataSlice`单独获取此价格字段进行解析,示例如下: 65 | 66 | ```ts 67 | // 获取订阅数据 68 | stream.on("data", async (data) => { 69 | if (data.account) { 70 | 71 | // console.log(data.account.account.data); 72 | 73 | const sqrtPriceX64Value = new BN(data.account.account.data, 'le'); // 使用小端字节序创建BN实例 74 | console.log(`sqrtPriceX64Value`, sqrtPriceX64Value.toString()); 75 | // 计算价格 76 | const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); 77 | const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); 78 | const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; 79 | console.log(`WSOL价格:`, price.toString()) 80 | console.log('---\n') 81 | 82 | } 83 | }); 84 | ``` 85 | 86 | 示例输出如下: 87 | 88 | ```bash 89 | sqrtPriceX64Value 8148105887049610621 90 | WSOL价格: 195.10746368470518 91 | --- 92 | 93 | sqrtPriceX64Value 8149200902555373958 94 | WSOL价格: 195.15990778810752 95 | --- 96 | 97 | sqrtPriceX64Value 8148391978755567616 98 | WSOL价格: 195.1211649320479 99 | --- 100 | 101 | sqrtPriceX64Value 8147974865559671808 102 | WSOL价格: 195.10118908164628 103 | --- 104 | 105 | sqrtPriceX64Value 8148007064812491849 106 | WSOL价格: 195.1027310905575 107 | --- 108 | 109 | sqrtPriceX64Value 8147933772405969711 110 | WSOL价格: 195.09922115634814 111 | --- 112 | 113 | sqrtPriceX64Value 8147701252217193933 114 | WSOL价格: 195.0880860976425 115 | --- 116 | ``` 117 | 118 | # 总结 119 | 120 | 本部分,我们学习如何将账户数据解析为可读的格式,并从中提取出特定的信息。 121 | -------------------------------------------------------------------------------- /03-account-parser/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import BN from 'bn.js'; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | // const request: SubscribeRequest = { 22 | // accounts: { 23 | // account: { 24 | // account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 25 | // owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 26 | // filters: [], 27 | // nonemptyTxnSignature: true, 28 | // } 29 | // }, 30 | // slots: {}, 31 | // transactions: {}, 32 | // transactionsStatus: {}, 33 | // blocks: {}, 34 | // blocksMeta: {}, 35 | // entry: {}, 36 | // accountsDataSlice: [], 37 | // commitment: CommitmentLevel.PROCESSED, 38 | // ping: undefined, 39 | // }; 40 | 41 | const request: SubscribeRequest = { 42 | accounts: { 43 | txn: { 44 | account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 45 | owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 46 | filters: [], 47 | nonemptyTxnSignature: true, 48 | } 49 | }, 50 | slots: {}, 51 | transactions: {}, 52 | transactionsStatus: {}, 53 | blocks: {}, 54 | blocksMeta: {}, 55 | entry: {}, 56 | accountsDataSlice: [ { offset: "253", length: "16" } ], 57 | commitment: CommitmentLevel.PROCESSED, 58 | ping: undefined, 59 | }; 60 | 61 | 62 | // 发送订阅请求 63 | await new Promise((resolve, reject) => { 64 | stream.write(request, (err) => { 65 | if (err === null || err === undefined) { 66 | resolve(); 67 | } else { 68 | reject(err); 69 | } 70 | }); 71 | }).catch((reason) => { 72 | console.error(reason); 73 | throw reason; 74 | }); 75 | 76 | // 获取订阅数据 77 | stream.on("data", async (data) => { 78 | if (data.account) { 79 | 80 | // console.log(data.account.account.data); 81 | 82 | const sqrtPriceX64Value = new BN(data.account.account.data, 'le'); // 使用小端字节序创建BN实例 83 | console.log(`sqrtPriceX64Value`, sqrtPriceX64Value.toString()); 84 | // 计算价格 85 | const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); 86 | const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); 87 | const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; 88 | console.log(`WSOL价格:`, price.toString()) 89 | console.log('---\n') 90 | 91 | } 92 | }); 93 | 94 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 95 | const pingRequest: SubscribeRequest = { 96 | accounts: {}, 97 | slots: {}, 98 | transactions: {}, 99 | transactionsStatus: {}, 100 | blocks: {}, 101 | blocksMeta: {}, 102 | entry: {}, 103 | accountsDataSlice: [], 104 | commitment: undefined, 105 | ping: { id: 1 }, 106 | }; 107 | // 每5秒发送一次ping请求 108 | setInterval(async () => { 109 | await new Promise((resolve, reject) => { 110 | stream.write(pingRequest, (err) => { 111 | if (err === null || err === undefined) { 112 | resolve(); 113 | } else { 114 | reject(err); 115 | } 116 | }); 117 | }).catch((reason) => { 118 | console.error(reason); 119 | throw reason; 120 | }); 121 | }, 5000); 122 | } 123 | 124 | main(); 125 | -------------------------------------------------------------------------------- /04-example-subNewPool/README.md: -------------------------------------------------------------------------------- 1 | # 监听pump新流动池创建 2 | 3 | 本例子基于`transactions`过滤条件和交易解析来监听pumpfun新流动池创建,这是链上大多数狙击手都会用到的方法。 4 | 5 | 通过`npx esrun 04-example-subNewPool/index.ts`运行,输出应如下: 6 | 7 | ``` 8 | slot: 310570418 9 | signature: JXjvgprqYjwJtXRinvEPXas349J2MEBQ91T3gZAUgq1fuWnBYCQzTzt5NjY26LXQKGswWXLZ3mXvSocWHLj5Nik 10 | signer: 3fxhSengmK5vjAWuM2tDUbnb2Y6dgVvPcbaboZCvD9tV 11 | tx: https://solscan.io/tx/JXjvgprqYjwJtXRinvEPXas349J2MEBQ91T3gZAUgq1fuWnBYCQzTzt5NjY26LXQKGswWXLZ3mXvSocWHLj5Nik 12 | --- 13 | 14 | slot: 310570418 15 | signature: 618b5GoPgyYnMt2MnTCpNLMkonM2aVrM6zBEs1LqbHeaamoq75vAr2gfjzV54uCBq3JU5izCpjtmGiWLYrCoPrYg 16 | signer: Eo9k25Go9VHFuQF6zgJ3fRm9SBqP8tG8Ea9sacowzyUv 17 | tx: https://solscan.io/tx/618b5GoPgyYnMt2MnTCpNLMkonM2aVrM6zBEs1LqbHeaamoq75vAr2gfjzV54uCBq3JU5izCpjtmGiWLYrCoPrYg 18 | --- 19 | 20 | slot: 310570424 21 | signature: 3U4bAAXkZG6821hG4X128L6HRjQgBtHxinrUC1JSvzJhuZJ5YwepVpXwTscXn5iWykMyEWGSPQ5QdskLYfR3yhGa 22 | signer: 2p4dPxvaVmwqPotF27DVSmKxMe7nvVvhCk57tBCZEmoW 23 | tx: https://solscan.io/tx/3U4bAAXkZG6821hG4X128L6HRjQgBtHxinrUC1JSvzJhuZJ5YwepVpXwTscXn5iWykMyEWGSPQ5QdskLYfR3yhGa 24 | --- 25 | 26 | slot: 310570424 27 | signature: 2RGnnNfSbAXksJqxS4TvR8tNXJTPAT3gktmErB1VHPTyJbe9LjvEkuGNxqK7wQbt4HNtjXFS4V1cnKaAJ6gkUMKR 28 | signer: EbCLRXdFs3NQENepR2zvPWf3oUHjKTRmV7AePGfZK6DE 29 | tx: https://solscan.io/tx/2RGnnNfSbAXksJqxS4TvR8tNXJTPAT3gktmErB1VHPTyJbe9LjvEkuGNxqK7wQbt4HNtjXFS4V1cnKaAJ6gkUMKR 30 | --- 31 | 32 | slot: 310570442 33 | signature: 5HXpdwUfGkkEefNaYnqJMBsFtn6KZitrMb4Y6sBhnDRh9NkSiMPnX3WBsBu2xmXX8xa5Fd7SjsM9VPxJ13yMxq52 34 | signer: GsCdJcNb1ydmkpfiCLUbx8bVgGbt3yQ3C8sYdPguyAxD 35 | tx: https://solscan.io/tx/5HXpdwUfGkkEefNaYnqJMBsFtn6KZitrMb4Y6sBhnDRh9NkSiMPnX3WBsBu2xmXX8xa5Fd7SjsM9VPxJ13yMxq52 36 | --- 37 | ``` -------------------------------------------------------------------------------- /04-example-subNewPool/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import bs58 from "bs58"; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | const request: SubscribeRequest = { 22 | accounts: {}, 23 | slots: {}, 24 | transactions: { 25 | txn: { 26 | vote: false, 27 | failed: false, 28 | signature: undefined, 29 | accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"], 30 | accountExclude: [], 31 | accountRequired: [], 32 | } 33 | }, 34 | transactionsStatus: {}, 35 | blocks: {}, 36 | blocksMeta: {}, 37 | entry: {}, 38 | accountsDataSlice: [], 39 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 40 | ping: undefined, 41 | }; 42 | 43 | // 发送订阅请求 44 | await new Promise((resolve, reject) => { 45 | stream.write(request, (err) => { 46 | if (err === null || err === undefined) { 47 | resolve(); 48 | } else { 49 | reject(err); 50 | } 51 | }); 52 | }).catch((reason) => { 53 | console.error(reason); 54 | throw reason; 55 | }); 56 | 57 | // 获取订阅数据 58 | stream.on("data", async (data) => { 59 | // 监听池子创建 60 | if (data.transaction && data.transaction.transaction.meta.logMessages && data.transaction.transaction.meta.logMessages.some(log => log.includes("Program log: Instruction: InitializeMint2"))) { 61 | 62 | console.log('slot:', data.transaction.slot); 63 | console.log('signature:', bs58.encode(data.transaction.transaction.signature)); 64 | const accountKeys = data.transaction.transaction.transaction.message.accountKeys.map(ak => bs58.encode(ak)); 65 | console.log('signer:', accountKeys[0]) 66 | console.log(`tx: https://solscan.io/tx/${bs58.encode(data.transaction.transaction.signature)}`); 67 | 68 | console.log('---\n') 69 | 70 | } 71 | }); 72 | 73 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 74 | const pingRequest: SubscribeRequest = { 75 | accounts: {}, 76 | slots: {}, 77 | transactions: {}, 78 | transactionsStatus: {}, 79 | blocks: {}, 80 | blocksMeta: {}, 81 | entry: {}, 82 | accountsDataSlice: [], 83 | commitment: undefined, 84 | ping: { id: 1 }, 85 | }; 86 | // 每5秒发送一次ping请求 87 | setInterval(async () => { 88 | await new Promise((resolve, reject) => { 89 | stream.write(pingRequest, (err) => { 90 | if (err === null || err === undefined) { 91 | resolve(); 92 | } else { 93 | reject(err); 94 | } 95 | }); 96 | }).catch((reason) => { 97 | console.error(reason); 98 | throw reason; 99 | }); 100 | }, 5000); 101 | } 102 | 103 | main(); -------------------------------------------------------------------------------- /05-example-subWallet/README.md: -------------------------------------------------------------------------------- 1 | # 监听钱包 2 | 3 | 本例子基于`transactions`过滤条件和指定的“聪明钱包”来监听这些钱包的交易,这是目前链上大多数跟单策略在使用的方法。 4 | 5 | 通过`npx esrun 05-example-subWallet/index.ts`运行,输出应如下: 6 | 7 | ``` 8 | slot: 310571719 9 | signature: LzjdmrbsxmcGuALW9b3L1NZTLpSH1p8BxguoBhNNiVLx93WaTXs2guyZivJqRbFididQfzuvD4KV61kfbGQeP9E 10 | signer: ZDLFG5UNPzeNsEkacw9TdKHT1fBZCACfAQymjWnpcvg 11 | --- 12 | 13 | slot: 310571719 14 | signature: 38zKoJqQD2zCtXJDh7ne7iytK8UBb7SMsELDL6X7fj8dmxJwYSAhob2Gzc1nkU6qvXBs5JQPxNVjkacF9zzXixYd 15 | signer: ZDLFG5UNPzeNsEkacw9TdKHT1fBZCACfAQymjWnpcvg 16 | --- 17 | ``` -------------------------------------------------------------------------------- /05-example-subWallet/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import bs58 from 'bs58'; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | const request: SubscribeRequest = { 22 | accounts: {}, 23 | slots: {}, 24 | transactions: { 25 | txn: { 26 | vote: false, 27 | failed: false, 28 | signature: undefined, 29 | accountInclude: ["ZDLFG5UNPzeNsEkacw9TdKHT1fBZCACfAQymjWnpcvg"], // 示例聪明钱地址 30 | accountExclude: [], 31 | accountRequired: [], 32 | } 33 | }, 34 | transactionsStatus: {}, 35 | blocks: {}, 36 | blocksMeta: {}, 37 | entry: {}, 38 | accountsDataSlice: [], 39 | commitment: CommitmentLevel.PROCESSED, // 指定级别为processed 40 | ping: undefined, 41 | }; 42 | 43 | // 发送订阅请求 44 | await new Promise((resolve, reject) => { 45 | stream.write(request, (err) => { 46 | if (err === null || err === undefined) { 47 | resolve(); 48 | } else { 49 | reject(err); 50 | } 51 | }); 52 | }).catch((reason) => { 53 | console.error(reason); 54 | throw reason; 55 | }); 56 | 57 | // 获取订阅数据 58 | stream.on("data", async (data) => { 59 | if (data.transaction) { 60 | console.log('slot:', data.transaction.slot); 61 | console.log('signature:', bs58.encode(data.transaction.transaction.signature)); 62 | const accountKeys = data.transaction.transaction.transaction.message.accountKeys.map(ak => bs58.encode(ak)); 63 | console.log('signer:', accountKeys[0]) 64 | 65 | console.log('---\n') 66 | } 67 | }); 68 | 69 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 70 | const pingRequest: SubscribeRequest = { 71 | accounts: {}, 72 | slots: {}, 73 | transactions: {}, 74 | transactionsStatus: {}, 75 | blocks: {}, 76 | blocksMeta: {}, 77 | entry: {}, 78 | accountsDataSlice: [], 79 | commitment: undefined, 80 | ping: { id: 1 }, 81 | }; 82 | // 每5秒发送一次ping请求 83 | setInterval(async () => { 84 | await new Promise((resolve, reject) => { 85 | stream.write(pingRequest, (err) => { 86 | if (err === null || err === undefined) { 87 | resolve(); 88 | } else { 89 | reject(err); 90 | } 91 | }); 92 | }).catch((reason) => { 93 | console.error(reason); 94 | throw reason; 95 | }); 96 | }, 5000); 97 | } 98 | 99 | main(); -------------------------------------------------------------------------------- /06-example-subPrice/README.md: -------------------------------------------------------------------------------- 1 | # 监听代币价格 2 | 3 | 本例子基于基于`accounts`和`accountsDataSlice`过滤条件,监听Raydium USDC/SOL流动池中SOL的相对价格,可根据不同的做市商算法和账户数据结构来获取各个流动池中代币的价格。 4 | 5 | 通过`npx esrun 06-example-subPrice/index.ts`运行,输出应如下: 6 | 7 | ```bash 8 | sqrtPriceX64Value 8177292434225217108 9 | WSOL价格: 196.50771845716352 10 | --- 11 | 12 | sqrtPriceX64Value 8176915664278802990 13 | WSOL价格: 196.48961063052332 14 | --- 15 | 16 | sqrtPriceX64Value 8176922029527744856 17 | WSOL价格: 196.48991654190218 18 | --- 19 | 20 | sqrtPriceX64Value 8177308202420119387 21 | WSOL价格: 196.50847630580762 22 | --- 23 | ``` -------------------------------------------------------------------------------- /06-example-subPrice/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import BN from 'bn.js'; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | // const request: SubscribeRequest = { 22 | // accounts: { 23 | // account: { 24 | // account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 25 | // owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 26 | // filters: [], 27 | // nonemptyTxnSignature: true, 28 | // } 29 | // }, 30 | // slots: {}, 31 | // transactions: {}, 32 | // transactionsStatus: {}, 33 | // blocks: {}, 34 | // blocksMeta: {}, 35 | // entry: {}, 36 | // accountsDataSlice: [], 37 | // commitment: CommitmentLevel.PROCESSED, 38 | // ping: undefined, 39 | // }; 40 | 41 | const request: SubscribeRequest = { 42 | accounts: { 43 | txn: { 44 | account: ["8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"], 45 | owner: ["CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"], 46 | filters: [], 47 | nonemptyTxnSignature: true, 48 | } 49 | }, 50 | slots: {}, 51 | transactions: {}, 52 | transactionsStatus: {}, 53 | blocks: {}, 54 | blocksMeta: {}, 55 | entry: {}, 56 | accountsDataSlice: [ { offset: "253", length: "16" } ], 57 | commitment: CommitmentLevel.PROCESSED, 58 | ping: undefined, 59 | }; 60 | 61 | 62 | // 发送订阅请求 63 | await new Promise((resolve, reject) => { 64 | stream.write(request, (err) => { 65 | if (err === null || err === undefined) { 66 | resolve(); 67 | } else { 68 | reject(err); 69 | } 70 | }); 71 | }).catch((reason) => { 72 | console.error(reason); 73 | throw reason; 74 | }); 75 | 76 | // 获取订阅数据 77 | stream.on("data", async (data) => { 78 | if (data.account) { 79 | 80 | // console.log(data.account.account.data); 81 | 82 | const sqrtPriceX64Value = new BN(data.account.account.data, 'le'); // 使用小端字节序创建BN实例 83 | console.log(`sqrtPriceX64Value`, sqrtPriceX64Value.toString()); 84 | // 计算价格 85 | const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); 86 | const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); 87 | const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; 88 | console.log(`WSOL价格:`, price.toString()) 89 | console.log('---\n') 90 | 91 | } 92 | }); 93 | 94 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 95 | const pingRequest: SubscribeRequest = { 96 | accounts: {}, 97 | slots: {}, 98 | transactions: {}, 99 | transactionsStatus: {}, 100 | blocks: {}, 101 | blocksMeta: {}, 102 | entry: {}, 103 | accountsDataSlice: [], 104 | commitment: undefined, 105 | ping: { id: 1 }, 106 | }; 107 | // 每5秒发送一次ping请求 108 | setInterval(async () => { 109 | await new Promise((resolve, reject) => { 110 | stream.write(pingRequest, (err) => { 111 | if (err === null || err === undefined) { 112 | resolve(); 113 | } else { 114 | reject(err); 115 | } 116 | }); 117 | }).catch((reason) => { 118 | console.error(reason); 119 | throw reason; 120 | }); 121 | }, 5000); 122 | } 123 | 124 | main(); 125 | -------------------------------------------------------------------------------- /07-example-subBlock/README.md: -------------------------------------------------------------------------------- 1 | # 订阅全链数据 2 | 3 | 本例子基于`blocks`过滤条件来订阅区块,希望获取到尽可能全的区块数据,这是目前一些数据服务商和交易平台的后端所需要的。 4 | 5 | 通过`npx esrun 07-example-subBlock/index.ts`运行,输出应如下: 6 | 7 | ```bash 8 | 9 | filters: [ 'block' ], 10 | account: undefined, 11 | slot: undefined, 12 | transaction: undefined, 13 | transactionStatus: undefined, 14 | block: { 15 | slot: '310572801', 16 | blockhash: '7EExTPoZ8JvN83mCkoZvReHqQ2U5UL9EjorA1WX3tSHB', 17 | rewards: { rewards: [Array], numPartitions: undefined }, 18 | blockTime: { timestamp: '1735481206' }, 19 | blockHeight: { blockHeight: '288903946' }, 20 | parentSlot: '310572800', 21 | parentBlockhash: '7WsfigFZk6bnVM7VeKrrPifVYpjiAQ7XcC3HRL5uZW5z', 22 | executedTransactionCount: '1537', 23 | transactions: [ 24 | [Object], [Object], [Object], [Object], [Object], [Object], 25 | [Object], [Object], [Object], [Object], [Object], [Object], 26 | [Object], [Object], [Object], [Object], [Object], [Object], 27 | [Object], [Object], [Object], [Object], [Object], [Object], 28 | [Object], [Object], [Object], [Object], [Object], [Object], 29 | [Object], [Object], [Object], [Object], [Object], [Object], 30 | [Object], [Object], [Object], [Object], [Object], [Object], 31 | [Object], [Object], [Object], [Object], [Object], [Object], 32 | [Object], [Object], [Object], [Object], [Object], [Object], 33 | [Object], [Object], [Object], [Object], [Object], [Object], 34 | [Object], [Object], [Object], [Object], [Object], [Object], 35 | [Object], [Object], [Object], [Object], [Object], [Object], 36 | [Object], [Object], [Object], [Object], [Object], [Object], 37 | [Object], [Object], [Object], [Object], [Object], [Object], 38 | [Object], [Object], [Object], [Object], [Object], [Object], 39 | [Object], [Object], [Object], [Object], [Object], [Object], 40 | [Object], [Object], [Object], [Object], 41 | ... 1437 more items 42 | ], 43 | updatedAccountCount: '5384', 44 | accounts: [ 45 | [Object], [Object], [Object], [Object], [Object], [Object], 46 | [Object], [Object], [Object], [Object], [Object], [Object], 47 | [Object], [Object], [Object], [Object], [Object], [Object], 48 | [Object], [Object], [Object], [Object], [Object], [Object], 49 | [Object], [Object], [Object], [Object], [Object], [Object], 50 | [Object], [Object], [Object], [Object], [Object], [Object], 51 | [Object], [Object], [Object], [Object], [Object], [Object], 52 | [Object], [Object], [Object], [Object], [Object], [Object], 53 | [Object], [Object], [Object], [Object], [Object], [Object], 54 | [Object], [Object], [Object], [Object], [Object], [Object], 55 | [Object], [Object], [Object], [Object], [Object], [Object], 56 | [Object], [Object], [Object], [Object], [Object], [Object], 57 | [Object], [Object], [Object], [Object], [Object], [Object], 58 | [Object], [Object], [Object], [Object], [Object], [Object], 59 | [Object], [Object], [Object], [Object], [Object], [Object], 60 | [Object], [Object], [Object], [Object], [Object], [Object], 61 | [Object], [Object], [Object], [Object], 62 | ... 5284 more items 63 | ], 64 | entriesCount: '482', 65 | entries: [] 66 | }, 67 | ping: undefined, 68 | pong: undefined, 69 | blockMeta: undefined, 70 | entry: undefined 71 | } 72 | ``` -------------------------------------------------------------------------------- /07-example-subBlock/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import { commitmentLevelFromJSON } from "@triton-one/yellowstone-grpc/dist/grpc/geyser"; 3 | 4 | async function main() { 5 | 6 | // 创建client 7 | // @ts-ignore 8 | const client = new Client.default( 9 | "https://test-grpc.chainbuff.com", 10 | undefined, 11 | { 12 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 13 | } 14 | ); 15 | console.log("Subscribing to event stream..."); 16 | 17 | // 创建订阅数据流 18 | const stream = await client.subscribe(); 19 | 20 | // 创建订阅请求 21 | const request: SubscribeRequest = { 22 | accounts: {}, 23 | slots: {}, 24 | transactions: {}, 25 | transactionsStatus: {}, 26 | blocks: { 27 | block: { 28 | accountInclude: [], 29 | includeTransactions: true, 30 | includeAccounts: true, 31 | includeEntries: false, 32 | } 33 | }, 34 | blocksMeta: {}, 35 | entry: {}, 36 | accountsDataSlice: [], 37 | commitment: CommitmentLevel.CONFIRMED, 38 | ping: undefined, 39 | }; 40 | 41 | // 发送订阅请求 42 | await new Promise((resolve, reject) => { 43 | stream.write(request, (err) => { 44 | if (err === null || err === undefined) { 45 | resolve(); 46 | } else { 47 | reject(err); 48 | } 49 | }); 50 | }).catch((reason) => { 51 | console.error(reason); 52 | throw reason; 53 | }); 54 | 55 | // 获取订阅数据 56 | stream.on("data", async (data) => { 57 | console.log(data); 58 | }); 59 | 60 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 61 | const pingRequest: SubscribeRequest = { 62 | accounts: {}, 63 | slots: {}, 64 | transactions: {}, 65 | transactionsStatus: {}, 66 | blocks: {}, 67 | blocksMeta: {}, 68 | entry: {}, 69 | accountsDataSlice: [], 70 | commitment: undefined, 71 | ping: { id: 1 }, 72 | }; 73 | // 每5秒发送一次ping请求 74 | setInterval(async () => { 75 | await new Promise((resolve, reject) => { 76 | stream.write(pingRequest, (err) => { 77 | if (err === null || err === undefined) { 78 | resolve(); 79 | } else { 80 | reject(err); 81 | } 82 | }); 83 | }).catch((reason) => { 84 | console.error(reason); 85 | throw reason; 86 | }); 87 | }, 5000); 88 | } 89 | 90 | main(); -------------------------------------------------------------------------------- /08-example-subPumpSwap/README.md: -------------------------------------------------------------------------------- 1 | # 订阅pump交易 2 | 3 | 本例子基于`transactions`过滤条件来订阅pump交易,通过监听指定 bondingCurve 账户的交易,来获取 Pump 代币的价格变动信息。主要包括买入、卖出和 launch 三种类型的交易。 4 | 5 | 通过`npx esrun 08-example-subPumpSwap/index.ts`运行,输出应如下: 6 | 7 | ```bash 8 | { 9 | signature: '54DnbJ8bdykkittRAo98UsX3XZg8ji4bkV2onJP2Q7XLVEkSYmh1zXRoYrfMZyV32z47Q56dFxm4jdTC2DK3fLj7', 10 | type: 'sell', 11 | bondingCurve: '7USfti8SXfV9k6fxbmsRejqUnHUvKDo8qFprUPaoa5ep', 12 | progress: 0.824495508317647, 13 | price: 3.11158241145335e-7, 14 | swapSolAmount: -0.19179295 15 | } 16 | { 17 | signature: '5ipujYpa6Lzk9YA7RrtrgagKWwToMir6kTBtZadU7D1rEYHcrEVXGstAtfV7qinWxsUrhavrE9VwfUdDKcCvdYh6', 18 | type: 'sell', 19 | bondingCurve: '7USfti8SXfV9k6fxbmsRejqUnHUvKDo8qFprUPaoa5ep', 20 | progress: 0.8221539605764706, 21 | price: 3.099218666246187e-7, 22 | swapSolAmount: -0.199031558 23 | } 24 | ``` 25 | 26 | 输出数据说明: 27 | 28 | - signature: 交易签名 29 | - type: 交易类型,包括 buy(买入)、sell(卖出)、launch(发射) 30 | - bondingCurve: bondingCurve 账户地址 31 | - progress: launch 进度,范围 0-1 32 | - price: 当前价格(SOL/TOKEN),若需与gmgn一致 请乘以当前sol价格 33 | - swapSolAmount: 交易的 SOL 数量,买入为正数,卖出为负数 34 | 35 | 要监听其他 Pump 代币,只需修改 main 函数中的 bondingCurve 地址即可: 36 | ```typescript 37 | new PumpPriceSubscriber( 38 | ['7USfti8SXfV9k6fxbmsRejqUnHUvKDo8qFprUPaoa5ep'], // 在这里修改要监听的 bondingCurve 地址 39 | o => console.log(o) 40 | ).listen(); 41 | ``` -------------------------------------------------------------------------------- /08-example-subPumpSwap/index.ts: -------------------------------------------------------------------------------- 1 | import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc"; 2 | import bs58 from "bs58"; 3 | 4 | const PUMP_FUN_PROGRAM_ID = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P'; 5 | const GRPC_URL = "https://test-grpc.chainbuff.com"; 6 | 7 | type PumpPriceInfo = { 8 | // 交易签名 9 | signature: string; 10 | // 交易类型 11 | type: 'buy' | 'sell' | 'launch'; 12 | // bondingCurve 13 | bondingCurve: string; 14 | // launch进度(0-1) 15 | progress: number; 16 | // 价格 17 | price: number; 18 | // 交易sol金额(买入为正数,卖出为负数) 19 | swapSolAmount: number; 20 | } 21 | 22 | class PumpSwapSubscriber { 23 | 24 | private bondingCurveSet: Set; 25 | private callback: (data: PumpPriceInfo) => void; 26 | 27 | constructor(bondingCurveArr: Array, callback: (data: PumpPriceInfo) => void) { 28 | this.bondingCurveSet = new Set(bondingCurveArr); 29 | this.callback = callback; 30 | } 31 | 32 | async listen() { 33 | const client = new Client.default( 34 | GRPC_URL, 35 | undefined, 36 | { 37 | "grpc.max_receive_message_length": 128 * 1024 * 1024, // 128MB 38 | } 39 | ); 40 | console.log("Subscribing to event stream..."); 41 | 42 | // 创建订阅数据流 43 | const stream = await client.subscribe(); 44 | 45 | const request: SubscribeRequest = { 46 | commitment: CommitmentLevel.PROCESSED, 47 | accountsDataSlice: [], 48 | ping: undefined, 49 | transactions: { 50 | client: { 51 | vote: false, 52 | failed: false, 53 | accountInclude: Array.from(this.bondingCurveSet), 54 | accountRequired: [], 55 | accountExclude: [], 56 | }, 57 | }, 58 | accounts: {}, 59 | slots: {}, 60 | transactionsStatus: {}, 61 | entry: {}, 62 | blocks: {}, 63 | blocksMeta: {}, 64 | }; 65 | 66 | // 发送订阅请求 67 | await new Promise((resolve, reject) => { 68 | stream.write(request, (err) => { 69 | if (err === null || err === undefined) { 70 | resolve(); 71 | } else { 72 | reject(err); 73 | } 74 | }); 75 | }).catch((reason) => { 76 | console.error(reason); 77 | throw reason; 78 | }); 79 | 80 | // 获取订阅数据 81 | stream.on("data", async (data) => { 82 | if (data?.transaction) { 83 | this.checkPrice(data.transaction); 84 | } 85 | }); 86 | 87 | // 为保证连接稳定,需要定期向服务端发送ping请求以维持连接 88 | const pingRequest: SubscribeRequest = { 89 | accounts: {}, 90 | slots: {}, 91 | transactions: {}, 92 | transactionsStatus: {}, 93 | blocks: {}, 94 | blocksMeta: {}, 95 | entry: {}, 96 | accountsDataSlice: [], 97 | commitment: undefined, 98 | ping: { id: 1 }, 99 | }; 100 | // 每5秒发送一次ping请求 101 | setInterval(async () => { 102 | await new Promise((resolve, reject) => { 103 | stream.write(pingRequest, (err) => { 104 | if (err === null || err === undefined) { 105 | resolve(); 106 | } else { 107 | reject(err); 108 | } 109 | }); 110 | }).catch((reason) => { 111 | console.error(reason); 112 | throw reason; 113 | }); 114 | }, 5000); 115 | } 116 | 117 | checkPrice(txn: any) { 118 | const transaction = txn?.transaction; 119 | if (!transaction) { 120 | return; 121 | } 122 | // 将accountKeys编码成base58 123 | const accountKeys = transaction?.transaction?.message?.accountKeys?.map(o=>bs58.encode(o)); 124 | if (!accountKeys) { 125 | return; 126 | } 127 | transaction.transaction.message.accountKeys = accountKeys; 128 | const signature = bs58.encode(txn.transaction.signature); 129 | const pumpProgramIdx = accountKeys.indexOf(PUMP_FUN_PROGRAM_ID); 130 | // 获取该交易的bondingCurve 131 | const txBondingCurves = accountKeys.filter(item=>this.bondingCurveSet.has(item)) 132 | 133 | // 判断是否为launch(Instruction中programId为pump且data的第一个字节等于183) 134 | const isLaunch = txn?.transaction?.transaction?.message?.instructions 135 | .some(item=>item.programIdIndex===pumpProgramIdx && item.data[0] === 183) ?? false; 136 | if (isLaunch) { 137 | this.callback({ 138 | signature: signature, 139 | type: "launch", 140 | bondingCurve: txBondingCurves[0], 141 | progress: 1, 142 | // 由于该交易为launch,bondingCurve中的余额已被提取,因此拿pre的余额作价格 143 | price: this.getPumpSwapInfo(txn, txBondingCurves[0], "pre")?.price ?? 0, 144 | swapSolAmount: 0, 145 | }) 146 | return; 147 | } 148 | // 分析价格等信息 149 | // 一笔交易可能包含多个token的买入或卖出,为避免遗漏遍历所有符合的bondingCurve 150 | for (let bondingCurveItem of txBondingCurves) { 151 | // 获取交易后的价格 152 | const pumpPriceInfo = this.getPumpSwapInfo(txn, bondingCurveItem, "post"); 153 | // 增加健壮性,理论上不会进入这里 154 | if (!pumpPriceInfo) { 155 | console.log('balance not found:', signature); 156 | continue; 157 | } 158 | this.callback({ 159 | signature: signature, 160 | type: pumpPriceInfo.swapSolAmount > 0 ? 'buy' : 'sell', 161 | bondingCurve: bondingCurveItem, 162 | progress: pumpPriceInfo.progress, 163 | price: pumpPriceInfo.price, 164 | swapSolAmount: pumpPriceInfo.swapSolAmount 165 | }) 166 | } 167 | 168 | } 169 | 170 | 171 | getPumpSwapInfo(txn: any, bondingCurve: string, type: "pre" | "post") { 172 | // 获取bondingCurve的token余额 173 | const tokenBalance = txn?.transaction?.meta?.[type + 'TokenBalances']?.find(o=>o.owner===bondingCurve); 174 | if (!tokenBalance) { 175 | return null; 176 | } 177 | // 获取bondingCurve的sol余额 178 | const bondingCurveIdx = txn.transaction.transaction.message.accountKeys.indexOf(bondingCurve); 179 | let preSolBalance = txn.transaction.meta?.preBalances?.[bondingCurveIdx]; 180 | let postSolBalance = txn.transaction.meta?.postBalances?.[bondingCurveIdx]; 181 | const targetSolBalance = txn.transaction.meta?.[type + "Balances"]?.[bondingCurveIdx]; 182 | if (postSolBalance===undefined || preSolBalance === undefined) { 183 | return null; 184 | } 185 | // 通过余额反推虚拟余额,virtualSolReserves(sol余额+30-租金)和virtualTokenReserves(token余额+73000000) 186 | const price = ((Number(targetSolBalance) / (10 ** 9)) + 30 - 0.00123192) / (tokenBalance.uiTokenAmount.uiAmount + 73000000); 187 | const swapSolAmount = (Number(postSolBalance) - Number(preSolBalance)) / (10 ** 9); 188 | const progress = Number(postSolBalance) / (10 ** 9) / 85; 189 | return {price, swapSolAmount, progress}; 190 | } 191 | } 192 | 193 | async function main() { 194 | new PumpSwapSubscriber( 195 | // 订阅bondingCurve 支持订阅多个 196 | ['7USfti8SXfV9k6fxbmsRejqUnHUvKDo8qFprUPaoa5ep'], 197 | o=>console.log(o) 198 | ).listen(); 199 | } 200 | 201 | main(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Buff Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yellowstone gRPC 教程 2 | 3 | Yellowstone gRPC 是获取 Solana 链上数据最快的方式。数据以流的方式推送,客户端需要配置订阅来获取和解析数据。 4 | 5 | 本教程旨在提供一些简单的订阅配置例子,帮助你快速熟悉此工具。 6 | 7 | --- 8 | 9 | 在阅读之前,需要运行如下来安装@triton-one/yellowstone-grpc,本教程使用的版本为1.3.0。 10 | 11 | ```bash 12 | npm install @triton-one/yellowstone-grpc@1.3.0 13 | ``` 14 | 15 | 之后,可通过 npx esrun xxx/index.ts 来运行每节的代码。 16 | 17 | ## 目录 18 | 19 | ### 基础 20 | 21 | 0. [创建订阅](./00-sub/) 22 | 1. [订阅格式](./01-format/) 23 | 24 | ### 进阶 25 | 26 | 2. [交易数据解析](./02-txn-parser/) 27 | 3. [账户数据解析](./03-account-parser/) 28 | 29 | ### 实战 30 | 31 | 4. [监听pump新流动池创建](./04-example-subNewPool/) 32 | 5. [监听钱包](./05-example-subWallet/) 33 | 6. [监听代币价格](./06-example-subPrice/) 34 | 7. [订阅全链数据](./07-example-subBlock/) 35 | 8. [订阅pump交易](./08-example-subPumpSwap/) 36 | 37 | ## 参考 38 | 39 | - https://docs.triton.one/project-yellowstone/dragons-mouth-grpc-subscriptions 40 | - https://docs.helius.dev/data-streaming/geyser-yellowstone 41 | - https://github.com/rpcpool/yellowstone-grpc 42 | 43 | ## 捐赠 44 | 45 | 如果你想支持Buff社区的发展,可通过向 `buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ` 地址捐赠Solana链上资产。 46 | 47 | 社区资金将被用于奖励社区贡献者,贡献包括但不限于 `PR`。 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@triton-one/yellowstone-grpc": "^1.3.0", 4 | "bn.js": "^5.2.1", 5 | "bs58": "^6.0.0" 6 | } 7 | } 8 | --------------------------------------------------------------------------------