@clonegod/ttd-sol-common 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ export declare enum COMMITMENT_LEVEL {
2
+ PROCESSED = "processed",
3
+ CONFIRMED = "confirmed",
4
+ _FINALIZED = "finalized"
5
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMMITMENT_LEVEL = void 0;
4
+ var COMMITMENT_LEVEL;
5
+ (function (COMMITMENT_LEVEL) {
6
+ COMMITMENT_LEVEL["PROCESSED"] = "processed";
7
+ COMMITMENT_LEVEL["CONFIRMED"] = "confirmed";
8
+ COMMITMENT_LEVEL["_FINALIZED"] = "finalized";
9
+ })(COMMITMENT_LEVEL || (exports.COMMITMENT_LEVEL = COMMITMENT_LEVEL = {}));
@@ -0,0 +1 @@
1
+ export * from './constants';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./constants"), exports);
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- export * from './quote';
2
1
  export * from './types';
2
+ export * from './common';
3
+ export * from './quote';
4
+ export * from './trade';
package/dist/index.js CHANGED
@@ -14,5 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./quote"), exports);
18
17
  __exportStar(require("./types"), exports);
18
+ __exportStar(require("./common"), exports);
19
+ __exportStar(require("./quote"), exports);
20
+ __exportStar(require("./trade"), exports);
@@ -0,0 +1,2 @@
1
+ export * from './tx_result_check';
2
+ export * from './tx_result_parse';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./tx_result_check"), exports);
18
+ __exportStar(require("./tx_result_parse"), exports);
@@ -0,0 +1,28 @@
1
+ import { OrderMessageType, PriceMessageType, StandardPoolInfoType, StandardSwapDetailType, TradeResultType } from "@clonegod/ttd-core";
2
+ import { AbstractTransactionResultCheck, TradeContext } from "@clonegod/ttd-core/dist";
3
+ import { Connection } from "@solana/web3.js";
4
+ import { EventEmitter } from 'events';
5
+ import { TransactionResultParser } from "./tx_result_parse";
6
+ export declare class TransactionResultChecker extends AbstractTransactionResultCheck {
7
+ env_args: any;
8
+ event_emitter: EventEmitter;
9
+ wallet: string;
10
+ context: TradeContext;
11
+ group_id: string;
12
+ txid: string;
13
+ trace_id: string;
14
+ price_msg: PriceMessageType;
15
+ order_msg: OrderMessageType;
16
+ pool_info: StandardPoolInfoType;
17
+ intervalId: any;
18
+ check_count: number;
19
+ transactionParser: TransactionResultParser;
20
+ trade_result_already_processed: boolean;
21
+ connection: Connection;
22
+ constructor(env_args: any, event_emitter: EventEmitter);
23
+ init_tx_status(): this;
24
+ check(): Promise<void>;
25
+ on_subscibe_transaction(): void;
26
+ check_tx_result_interval(): Promise<void>;
27
+ map_swap_result_to_tx_result(swap_result: StandardSwapDetailType): TradeResultType;
28
+ }
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TransactionResultChecker = void 0;
13
+ const dist_1 = require("@clonegod/ttd-core/dist");
14
+ const web3_js_1 = require("@solana/web3.js");
15
+ const tx_result_parse_1 = require("./tx_result_parse");
16
+ const common_1 = require("../common");
17
+ class TransactionResultChecker extends dist_1.AbstractTransactionResultCheck {
18
+ constructor(env_args, event_emitter) {
19
+ super(env_args, event_emitter);
20
+ this.env_args = env_args;
21
+ this.event_emitter = event_emitter;
22
+ this.connection = new web3_js_1.Connection(env_args.rpc_endpoint, {
23
+ commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
24
+ });
25
+ this.check_count = 0;
26
+ this.transactionParser = new tx_result_parse_1.TransactionResultParser(this.env_args, new web3_js_1.PublicKey(this.wallet), this.event_emitter);
27
+ this.trade_result_already_processed = false;
28
+ }
29
+ init_tx_status() {
30
+ let empty_trade_result = JSON.parse(JSON.stringify({
31
+ group_id: this.context.group_id,
32
+ txid: this.txid
33
+ }));
34
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_PROCESSING, empty_trade_result);
35
+ return this;
36
+ }
37
+ check() {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ this.check_tx_result_interval();
40
+ this.on_subscibe_transaction();
41
+ });
42
+ }
43
+ on_subscibe_transaction() {
44
+ this.event_emitter.once(dist_1.LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (messageStr) => {
45
+ try {
46
+ this.trade_result_already_processed = true;
47
+ (0, dist_1.log_debug)('on events: EVENT_WALLET_TRANSACTION', messageStr);
48
+ let messageObj = messageStr;
49
+ if (typeof messageStr === 'string') {
50
+ messageObj = JSON.parse(messageStr);
51
+ }
52
+ const result = messageObj.params.result;
53
+ const _txid = result.signature;
54
+ const slot = result.slot;
55
+ (0, dist_1.log_info)('check txid:', { _txid, txid: this.txid, slot });
56
+ result.transaction.transaction.signatures = [_txid];
57
+ let tx_result = {
58
+ slot,
59
+ transaction: result.transaction.transaction,
60
+ meta: result.transaction.meta,
61
+ blockTime: Date.now() / 1000,
62
+ version: result.transaction.version
63
+ };
64
+ let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
65
+ let trade_result = this.map_swap_result_to_tx_result(swap_result);
66
+ if (trade_result.success) {
67
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
68
+ }
69
+ else {
70
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
71
+ }
72
+ console.log('<----------------------------------------------------');
73
+ console.dir(trade_result, { depth: 8 });
74
+ console.log('---------------------------------------------------->');
75
+ }
76
+ catch (err) {
77
+ (0, dist_1.log_error)('parse geyser stream message error!', err);
78
+ return;
79
+ }
80
+ });
81
+ }
82
+ check_tx_result_interval() {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const check_start_time = Date.now();
85
+ const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000');
86
+ const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000');
87
+ (0, dist_1.log_info)(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`);
88
+ if (check_interval >= check_timeout) {
89
+ (0, dist_1.log_warn)(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`);
90
+ return;
91
+ }
92
+ const intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
93
+ this.check_count += 1;
94
+ (0, dist_1.log_info)(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`);
95
+ try {
96
+ if (Date.now() - check_start_time < check_timeout) {
97
+ let tx_result = yield this.connection.getParsedTransaction(this.txid, {
98
+ commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
99
+ maxSupportedTransactionVersion: 0,
100
+ });
101
+ if ((0, dist_1.isEmpty)(tx_result)) {
102
+ return;
103
+ }
104
+ clearInterval(intervalId);
105
+ let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
106
+ let trade_result = this.map_swap_result_to_tx_result(swap_result);
107
+ if (this.trade_result_already_processed) {
108
+ (0, dist_1.log_warn)(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`);
109
+ }
110
+ else {
111
+ if (trade_result.success) {
112
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
113
+ }
114
+ else {
115
+ this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
116
+ }
117
+ console.log('<====================================================');
118
+ console.dir(trade_result, { depth: 8 });
119
+ console.log('====================================================>');
120
+ }
121
+ }
122
+ else {
123
+ clearInterval(intervalId);
124
+ }
125
+ }
126
+ catch (err) {
127
+ clearInterval(intervalId);
128
+ (0, dist_1.log_error)('parse transaction error!', err);
129
+ }
130
+ }), check_interval);
131
+ this.intervalId = intervalId;
132
+ });
133
+ }
134
+ map_swap_result_to_tx_result(swap_result) {
135
+ let { success, error_code, wallet, block_number, block_time: order_block_time, txid, tx_price, tokenA, tokenB, gas_fee } = swap_result;
136
+ let { pool_name, is_reverse_token } = this.pool_info;
137
+ let { chain_id, dex_id, unique_orderbook_id, pair, price_id, time: quote_time, ask, bid } = this.price_msg;
138
+ let { group_id, unique_order_msg_id, order_send_time, order_recv_time, order_submit_time, aToB } = this.order_msg;
139
+ let order_price = aToB ? bid.price : ask.price;
140
+ let server_info = (0, dist_1.getServerInfo)();
141
+ let order_end_time = Date.now();
142
+ let start_time = order_recv_time;
143
+ let end_time = order_end_time;
144
+ let total_time = end_time - start_time;
145
+ let total_order_time = total_time;
146
+ let time = {
147
+ block_time: quote_time.block_time,
148
+ stream_time: quote_time.stream_time,
149
+ quote_start_time: quote_time.quote_start_time,
150
+ quote_end_time: quote_time.quote_end_time,
151
+ price_time: quote_time.price_time,
152
+ total_quote_time: quote_time.total_quote_time,
153
+ order_send_time,
154
+ order_recv_time,
155
+ order_submit_time,
156
+ order_block_time,
157
+ order_end_time,
158
+ total_order_time,
159
+ };
160
+ let broadcast = [];
161
+ let trade_strategy = this.context.trade_runtime.settings.strategy;
162
+ let send_type = trade_strategy.broadcast_type;
163
+ if (send_type === 'rpc') {
164
+ send_type = trade_strategy.speed === 'fast' ? 'mainnet' : 'mainnet-premium';
165
+ }
166
+ broadcast.push({
167
+ rpc: {
168
+ read: 'qn',
169
+ write: 'qn'
170
+ },
171
+ type: send_type,
172
+ fee: gas_fee
173
+ });
174
+ let trade_balance_change = {
175
+ tokenA: {
176
+ symbol: tokenA.symbol,
177
+ address: tokenA.address,
178
+ pre_bal: tokenA.pre_bal,
179
+ post_bal: tokenA.post_bal,
180
+ change: Number(tokenA.change),
181
+ decimals: tokenA.decimals,
182
+ },
183
+ tokenB: {
184
+ symbol: tokenB.symbol,
185
+ address: tokenB.address,
186
+ pre_bal: tokenB.pre_bal,
187
+ post_bal: tokenB.post_bal,
188
+ change: Number(tokenB.change),
189
+ decimals: tokenB.decimals,
190
+ },
191
+ };
192
+ if (is_reverse_token) {
193
+ let { tokenA, tokenB } = trade_balance_change;
194
+ trade_balance_change.tokenA = tokenB;
195
+ trade_balance_change.tokenB = tokenA;
196
+ }
197
+ let trade_extra_info = {
198
+ time,
199
+ broadcast,
200
+ server_info,
201
+ };
202
+ const c_id = this.order_msg.c_id || '';
203
+ const trade_result = {
204
+ success,
205
+ error_code,
206
+ start_time,
207
+ end_time,
208
+ total_time,
209
+ group_id,
210
+ wallet,
211
+ chain_id,
212
+ pair,
213
+ dex_id,
214
+ txid,
215
+ unique_orderbook_id,
216
+ unique_order_msg_id,
217
+ order_price,
218
+ aToB,
219
+ tx_price,
220
+ balance: trade_balance_change,
221
+ execution: trade_extra_info,
222
+ c_id
223
+ };
224
+ return trade_result;
225
+ }
226
+ }
227
+ exports.TransactionResultChecker = TransactionResultChecker;
@@ -0,0 +1,12 @@
1
+ import { StandardPoolInfoType, StandardSwapDetailType } from '@clonegod/ttd-core';
2
+ import { ParsedTransactionWithMeta, PublicKey, TokenBalance } from '@solana/web3.js';
3
+ import { EventEmitter } from 'events';
4
+ export declare class TransactionResultParser {
5
+ env_args: any;
6
+ event_emitter: EventEmitter;
7
+ wallet_pubkey: PublicKey;
8
+ constructor(env_args: any, wallet_pubkey: PublicKey, event_emitter: EventEmitter);
9
+ parse_transaction_data(txData: ParsedTransactionWithMeta, pool_info: StandardPoolInfoType): StandardSwapDetailType;
10
+ getTokenBalance(owner: string, mint: string, tokenBalances: TokenBalance[]): number;
11
+ }
12
+ export declare const parse_tx_failed_error_code: (dex_id_list: string[], tx_failed_error_msg: string) => string;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parse_tx_failed_error_code = exports.TransactionResultParser = void 0;
4
+ const dist_1 = require("@clonegod/ttd-core/dist");
5
+ class TransactionResultParser {
6
+ constructor(env_args, wallet_pubkey, event_emitter) {
7
+ this.env_args = env_args;
8
+ this.wallet_pubkey = wallet_pubkey;
9
+ this.event_emitter = event_emitter;
10
+ }
11
+ parse_transaction_data(txData, pool_info) {
12
+ let success = true;
13
+ let error_code = undefined;
14
+ let { blockTime, meta, transaction, slot } = txData;
15
+ if (meta.err) {
16
+ success = false;
17
+ error_code = (0, exports.parse_tx_failed_error_code)(this.env_args.dex_id_list, JSON.stringify({
18
+ txid: transaction.signatures,
19
+ err: meta.err,
20
+ logMessages: meta.logMessages
21
+ }));
22
+ }
23
+ const { pool_address, tokenA, tokenB, is_reverse_token } = pool_info;
24
+ let txid = transaction.signatures[0];
25
+ const owner = this.wallet_pubkey.toBase58();
26
+ const { preTokenBalances, postTokenBalances, preBalances, postBalances } = meta;
27
+ const tokenA_PreBalance = this.getTokenBalance(owner, tokenA.address, preTokenBalances);
28
+ const tokenA_PostBalance = this.getTokenBalance(owner, tokenA.address, postTokenBalances);
29
+ const tokenB_PreBalance = this.getTokenBalance(owner, tokenB.address, preTokenBalances);
30
+ const tokenB_PostBalance = this.getTokenBalance(owner, tokenB.address, postTokenBalances);
31
+ blockTime = blockTime * 1000 || 0;
32
+ const tokenAChange = Number((tokenA_PostBalance - tokenA_PreBalance).toFixed(tokenA.decimals));
33
+ const tokenBChange = Number((tokenB_PostBalance - tokenB_PreBalance).toFixed(tokenB.decimals));
34
+ let price = "";
35
+ if (success) {
36
+ if (is_reverse_token) {
37
+ price = Math.abs(tokenAChange / tokenBChange).toFixed(tokenA.decimals);
38
+ }
39
+ else {
40
+ price = Math.abs(tokenBChange / tokenAChange).toFixed(tokenB.decimals);
41
+ }
42
+ }
43
+ const base_fee = 5000;
44
+ let priority_fee = 0;
45
+ let total_fee = 0;
46
+ total_fee = preBalances[0] - postBalances[0];
47
+ priority_fee = Math.abs(total_fee - base_fee);
48
+ let gas_fee = {
49
+ base_fee,
50
+ priority_fee,
51
+ total_fee
52
+ };
53
+ const tradeResult = {
54
+ success,
55
+ error_code,
56
+ wallet: owner,
57
+ block_time: blockTime,
58
+ block_number: slot,
59
+ txid,
60
+ pool_address,
61
+ tokenA: Object.assign(Object.assign({}, tokenA), { pre_bal: tokenA_PreBalance, post_bal: tokenA_PostBalance, change: tokenAChange }),
62
+ tokenB: Object.assign(Object.assign({}, tokenB), { pre_bal: tokenB_PreBalance, post_bal: tokenB_PostBalance, change: tokenBChange }),
63
+ tx_price: price,
64
+ gas_fee
65
+ };
66
+ return tradeResult;
67
+ }
68
+ getTokenBalance(owner, mint, tokenBalances) {
69
+ var _a, _b;
70
+ const balance = (_b = (_a = tokenBalances === null || tokenBalances === void 0 ? void 0 : tokenBalances.find((e) => e.owner === owner && e.mint == mint)) === null || _a === void 0 ? void 0 : _a.uiTokenAmount) === null || _b === void 0 ? void 0 : _b.uiAmount;
71
+ if (typeof balance !== 'undefined' && balance !== null) {
72
+ return balance;
73
+ }
74
+ return 0;
75
+ }
76
+ }
77
+ exports.TransactionResultParser = TransactionResultParser;
78
+ const parse_tx_failed_error_code = (dex_id_list, tx_failed_error_msg) => {
79
+ (0, dist_1.log_info)(`parse_tx_failed_error_code`, {
80
+ dex_id_list,
81
+ tx_failed_error_msg
82
+ });
83
+ let err_code = "";
84
+ if (dex_id_list.includes(dist_1.DEX_ID.RAYDIUM_AMM)) {
85
+ err_code = parse_raydium_amm_failure(tx_failed_error_msg);
86
+ }
87
+ else if (dex_id_list.includes(dist_1.DEX_ID.RAYDIUM_CLMM)) {
88
+ err_code = parse_raydium_clmm_failure(tx_failed_error_msg);
89
+ }
90
+ else if (dex_id_list.includes(dist_1.DEX_ID.ORCA_CLMM)) {
91
+ err_code = parse_orca_failure(tx_failed_error_msg);
92
+ }
93
+ if (!err_code) {
94
+ if (tx_failed_error_msg.includes('exceeded CUs meter at BPF instruction')) {
95
+ err_code = dist_1.TradeErrorCodeType.ExceededCUsLimit;
96
+ }
97
+ if (tx_failed_error_msg.includes('insufficient funds')) {
98
+ return dist_1.TradeErrorCodeType.InsufficientFunds;
99
+ }
100
+ }
101
+ if (!err_code) {
102
+ err_code = dist_1.TradeErrorCodeType.UNKOWN;
103
+ }
104
+ return err_code;
105
+ };
106
+ exports.parse_tx_failed_error_code = parse_tx_failed_error_code;
107
+ function parse_raydium_amm_failure(error_msg) {
108
+ if (error_msg.includes('0x28')) {
109
+ return dist_1.TradeErrorCodeType.InsufficientFunds;
110
+ }
111
+ if (error_msg.includes('0x1e') || error_msg.includes('exceeds desired slippage limit')) {
112
+ return dist_1.TradeErrorCodeType.AmountOutBelowMinimum;
113
+ }
114
+ return null;
115
+ }
116
+ function parse_raydium_clmm_failure(error_msg) {
117
+ if (error_msg.includes('Computational budget exceeded')) {
118
+ return dist_1.TradeErrorCodeType.ExceededCUsLimit;
119
+ }
120
+ if (error_msg.includes('0x1786') || error_msg.includes('Too little output received')) {
121
+ return dist_1.TradeErrorCodeType.AmountOutBelowMinimum;
122
+ }
123
+ if (error_msg.includes('0x178c') || error_msg.includes('Invaild first tick array account')) {
124
+ return dist_1.TradeErrorCodeType.InvalidTickArraySequence;
125
+ }
126
+ return null;
127
+ }
128
+ function parse_orca_failure(error_msg) {
129
+ if (error_msg.includes('0x1]') || error_msg.includes('insufficient funds')) {
130
+ return dist_1.TradeErrorCodeType.InsufficientFunds;
131
+ }
132
+ if (error_msg.includes('0x1794') || error_msg.includes('below minimum threshold')) {
133
+ return dist_1.TradeErrorCodeType.AmountOutBelowMinimum;
134
+ }
135
+ if (error_msg.includes('0xbc4') || error_msg.includes('expected this account to be already initialized')) {
136
+ return dist_1.TradeErrorCodeType.AccountNotInitialized;
137
+ }
138
+ if (error_msg.includes('0x1787') || error_msg.includes('Invalid tick array sequence')) {
139
+ return dist_1.TradeErrorCodeType.InvalidTickArraySequence;
140
+ }
141
+ return null;
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sol-common",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Solana 常量定义
3
+ * 这些常量后续会移到 ttd-sol-common 包中
4
+ */
5
+
6
+ /**
7
+ * Solana 连接确认级别
8
+ */
9
+ export enum COMMITMENT_LEVEL {
10
+ PROCESSED = "processed",
11
+ CONFIRMED = "confirmed",
12
+ _FINALIZED = "finalized"
13
+ }
14
+
@@ -0,0 +1,2 @@
1
+ export * from './constants'
2
+
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
- export * from './quote'
2
1
  export * from './types'
2
+ export * from './common'
3
+ export * from './quote'
4
+ export * from './trade'
3
5
 
@@ -0,0 +1,2 @@
1
+ export * from './tx_result_check'
2
+ export * from './tx_result_parse'
@@ -0,0 +1,298 @@
1
+ import { OrderMessageType, PriceMessageType, StandardPoolInfoType, StandardSwapDetailType, TradeBroadcastType, TradeResultBalanceChangeType, TradeResultType, TradeTimeFlowType } from "@clonegod/ttd-core"
2
+ import { AbstractTransactionResultCheck, getServerInfo, isEmpty, LOCAL_EVENT_NAME, LOG, log_debug, log_error, log_info, log_warn, TradeContext, TRANSACTION_STATE_FAILED, TRANSACTION_STATE_PROCESSING, TRANSACTION_STATE_SUCCESS } from "@clonegod/ttd-core/dist"
3
+ import { Connection, ParsedTransactionWithMeta, PublicKey } from "@solana/web3.js"
4
+ import { EventEmitter } from 'events'
5
+ import { TransactionResultParser } from "./tx_result_parse"
6
+ import { COMMITMENT_LEVEL } from "../common"
7
+
8
+
9
+
10
+ /**
11
+ * 获取Transaction的交易结果
12
+ */
13
+ export class TransactionResultChecker extends AbstractTransactionResultCheck {
14
+ env_args: any
15
+ event_emitter: EventEmitter
16
+ wallet: string
17
+
18
+ context: TradeContext
19
+ group_id: string
20
+ txid: string
21
+
22
+ trace_id: string
23
+ price_msg: PriceMessageType
24
+ order_msg: OrderMessageType
25
+ pool_info: StandardPoolInfoType
26
+
27
+ intervalId: any
28
+ check_count: number
29
+ transactionParser: TransactionResultParser
30
+
31
+ trade_result_already_processed: boolean
32
+
33
+ connection: Connection
34
+
35
+ constructor(env_args: any, event_emitter: EventEmitter) {
36
+ super(env_args, event_emitter)
37
+ this.env_args = env_args
38
+ this.event_emitter = event_emitter
39
+
40
+
41
+ this.connection = new Connection(env_args.rpc_endpoint, {
42
+ commitment: COMMITMENT_LEVEL.CONFIRMED,
43
+ })
44
+
45
+ this.check_count = 0
46
+ this.transactionParser = new TransactionResultParser(this.env_args, new PublicKey(this.wallet), this.event_emitter)
47
+ this.trade_result_already_processed = false
48
+ }
49
+
50
+ /**
51
+ * set trade initial status: PROCESSING
52
+ */
53
+ init_tx_status() {
54
+ let empty_trade_result = JSON.parse(JSON.stringify({
55
+ group_id: this.context.group_id,
56
+ txid: this.txid
57
+ }))
58
+ this.event_emitter.emit(TRANSACTION_STATE_PROCESSING, empty_trade_result)
59
+ return this
60
+ }
61
+
62
+ async check() {
63
+ this.check_tx_result_interval()
64
+ this.on_subscibe_transaction()
65
+ }
66
+
67
+ /**
68
+ * 从第三方服务(helius geyser ws)订阅Transaction结果
69
+ */
70
+ on_subscibe_transaction() {
71
+ this.event_emitter.once(LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (messageStr) => {
72
+ try {
73
+ this.trade_result_already_processed = true
74
+ log_debug('on events: EVENT_WALLET_TRANSACTION', messageStr)
75
+
76
+ let messageObj = messageStr
77
+ if(typeof messageStr === 'string') {
78
+ messageObj = JSON.parse(messageStr)
79
+ }
80
+
81
+ const result = messageObj.params.result
82
+ const _txid = result.signature // Extract the signature
83
+ const slot = result.slot
84
+ log_info('check txid:', { _txid, txid: this.txid, slot})
85
+
86
+ // set txid for parse
87
+ result.transaction.transaction.signatures = [_txid]
88
+
89
+ let tx_result:ParsedTransactionWithMeta = {
90
+ slot,
91
+ transaction: result.transaction.transaction,
92
+ meta: result.transaction.meta,
93
+ blockTime: Date.now() / 1000,
94
+ version: result.transaction.version
95
+ }
96
+
97
+ let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info)
98
+
99
+ let trade_result = this.map_swap_result_to_tx_result(swap_result)
100
+ if (trade_result.success) {
101
+ this.event_emitter.emit(TRANSACTION_STATE_SUCCESS, trade_result)
102
+ } else {
103
+ this.event_emitter.emit(TRANSACTION_STATE_FAILED, trade_result)
104
+ }
105
+
106
+ console.log('<----------------------------------------------------')
107
+ console.dir(trade_result, { depth: 8 })
108
+ console.log('---------------------------------------------------->')
109
+ } catch (err) {
110
+ log_error('parse geyser stream message error!', err)
111
+ return
112
+ }
113
+ })
114
+ }
115
+
116
+
117
+ /**
118
+ * 主动确认交易结果
119
+ */
120
+ async check_tx_result_interval() {
121
+ const check_start_time = Date.now()
122
+ const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000')
123
+ const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000')
124
+
125
+ log_info(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`)
126
+
127
+ if(check_interval >= check_timeout) {
128
+ log_warn(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`)
129
+ return
130
+ }
131
+
132
+ const intervalId = setInterval(async () => {
133
+ this.check_count += 1
134
+ log_info(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`)
135
+ try {
136
+ if (Date.now() - check_start_time < check_timeout) {
137
+ let tx_result:ParsedTransactionWithMeta = await this.connection.getParsedTransaction(this.txid, {
138
+ commitment: COMMITMENT_LEVEL.CONFIRMED,
139
+ maxSupportedTransactionVersion: 0,
140
+ })
141
+ if (isEmpty(tx_result)) {
142
+ return
143
+ }
144
+ // 查询到结果
145
+ clearInterval(intervalId)
146
+
147
+ let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info)
148
+ let trade_result = this.map_swap_result_to_tx_result(swap_result)
149
+
150
+ if(this.trade_result_already_processed) {
151
+ log_warn(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`)
152
+ } else {
153
+ if (trade_result.success) {
154
+ this.event_emitter.emit(TRANSACTION_STATE_SUCCESS, trade_result)
155
+ } else {
156
+ this.event_emitter.emit(TRANSACTION_STATE_FAILED, trade_result)
157
+ }
158
+
159
+ console.log('<====================================================')
160
+ console.dir(trade_result, { depth: 8 })
161
+ console.log('====================================================>')
162
+ }
163
+ } else {
164
+ // 超时未查询到结果
165
+ clearInterval(intervalId)
166
+ }
167
+ } catch (err) {
168
+ // 解析结果异常
169
+ clearInterval(intervalId)
170
+ log_error('parse transaction error!', err)
171
+ }
172
+ }, check_interval)
173
+
174
+ this.intervalId = intervalId
175
+ }
176
+
177
+
178
+ map_swap_result_to_tx_result(swap_result: StandardSwapDetailType): TradeResultType {
179
+ let { success, error_code, wallet, block_number, block_time: order_block_time, txid, tx_price, tokenA, tokenB, gas_fee } = swap_result
180
+ let { pool_name, is_reverse_token } = this.pool_info
181
+
182
+ let { chain_id, dex_id, unique_orderbook_id, pair, price_id, time: quote_time, ask, bid } = this.price_msg
183
+ let { group_id, unique_order_msg_id, order_send_time, order_recv_time, order_submit_time, aToB } = this.order_msg
184
+
185
+ let order_price = aToB ? bid.price : ask.price
186
+
187
+ let server_info = getServerInfo()
188
+
189
+ let order_end_time = Date.now()
190
+
191
+ let start_time = order_recv_time
192
+ let end_time = order_end_time
193
+ let total_time = end_time - start_time
194
+
195
+ // block_time 只精确到秒,估算耗时+1000ms
196
+ // let total_order_time = (order_block_time + 1000) - order_recv_time
197
+ let total_order_time = total_time
198
+
199
+ let time: TradeTimeFlowType = {
200
+ // Quote Consumed Time
201
+ block_time: quote_time.block_time, // 触发本次quote询价的交易,链上的出块时间
202
+ stream_time: quote_time.stream_time, // 监听到链上发生新交易的时间(stream/webhook)
203
+ quote_start_time: quote_time.quote_start_time, // 询价开始时间
204
+ quote_end_time: quote_time.quote_end_time, // 询价结束时间
205
+ price_time: quote_time.price_time, // 生成价格的时间
206
+ total_quote_time: quote_time.total_quote_time, // 询价耗时:price_time - stream_time
207
+
208
+ // Trade Consumed Time
209
+ order_send_time, // cex 发送下单消息的时间
210
+ order_recv_time, // dex 接收到下单消息的时间
211
+ order_submit_time, // dex 提交交易时间
212
+ order_block_time, // 交易所属block的出块时间
213
+ order_end_time, // dex 解析得到交易结果的时间
214
+ total_order_time, // 交易总耗时: order_end_time - order_recv_time
215
+ }
216
+
217
+ let broadcast: TradeBroadcastType[] = []
218
+ let trade_strategy = this.context.trade_runtime.settings.strategy
219
+ let send_type = trade_strategy.broadcast_type as string
220
+ if (send_type === 'rpc') {
221
+ send_type = trade_strategy.speed === 'fast' ? 'mainnet' : 'mainnet-premium'
222
+ }
223
+ broadcast.push(
224
+ {
225
+ rpc: {
226
+ read: 'qn',
227
+ write: 'qn'
228
+ }, // quicknode | trongrid ...
229
+ type: send_type, // 主网,私单,自建节点
230
+ fee: gas_fee // gas
231
+ }
232
+ )
233
+
234
+ let trade_balance_change: TradeResultBalanceChangeType = {
235
+ tokenA: {
236
+ symbol: tokenA.symbol,
237
+ address: tokenA.address,
238
+ pre_bal: tokenA.pre_bal,
239
+ post_bal: tokenA.post_bal,
240
+ change: Number(tokenA.change),
241
+ decimals: tokenA.decimals,
242
+ },
243
+ tokenB: {
244
+ symbol: tokenB.symbol,
245
+ address: tokenB.address,
246
+ pre_bal: tokenB.pre_bal,
247
+ post_bal: tokenB.post_bal,
248
+ change: Number(tokenB.change),
249
+ decimals: tokenB.decimals,
250
+ },
251
+ }
252
+
253
+ if (is_reverse_token) {
254
+ let { tokenA, tokenB } = trade_balance_change
255
+ trade_balance_change.tokenA = tokenB
256
+ trade_balance_change.tokenB = tokenA
257
+ }
258
+
259
+ let trade_extra_info = {
260
+ time,
261
+ broadcast,
262
+ server_info,
263
+ }
264
+
265
+ // 返回c_id给cex
266
+ const c_id = this.order_msg.c_id || ''
267
+
268
+ const trade_result: TradeResultType = {
269
+ success,
270
+ error_code,
271
+
272
+ start_time,
273
+ end_time,
274
+ total_time,
275
+
276
+ group_id,
277
+ wallet,
278
+
279
+ chain_id,
280
+ pair,
281
+ dex_id,
282
+ txid,
283
+
284
+ unique_orderbook_id,
285
+ unique_order_msg_id,
286
+ order_price,
287
+ aToB,
288
+ tx_price,
289
+
290
+ balance: trade_balance_change,
291
+ execution: trade_extra_info,
292
+ c_id
293
+ }
294
+
295
+ return trade_result
296
+ }
297
+
298
+ }
@@ -0,0 +1,212 @@
1
+
2
+ import { StandardPoolInfoType, StandardSwapDetailType, TradeGasFeeType } from '@clonegod/ttd-core';
3
+ import { DEX_ID, log_info, TradeErrorCodeType } from '@clonegod/ttd-core/dist';
4
+ import { ParsedTransactionWithMeta, PublicKey, TokenBalance } from '@solana/web3.js';
5
+ import { EventEmitter } from 'events';
6
+
7
+ export class TransactionResultParser {
8
+
9
+ env_args: any
10
+ event_emitter: EventEmitter
11
+ wallet_pubkey: PublicKey
12
+
13
+ constructor(env_args: any, wallet_pubkey: PublicKey, event_emitter: EventEmitter) {
14
+ this.env_args = env_args
15
+ this.wallet_pubkey = wallet_pubkey
16
+ this.event_emitter = event_emitter
17
+ }
18
+
19
+ /**
20
+ * 解析 Transaction Data
21
+ */
22
+ parse_transaction_data(txData: ParsedTransactionWithMeta, pool_info: StandardPoolInfoType): StandardSwapDetailType {
23
+ let success: boolean = true
24
+ let error_code: string = undefined
25
+
26
+ let { blockTime, meta, transaction, slot } = txData
27
+
28
+ if (meta.err) {
29
+ // 交易失败,解析错误码!
30
+ success = false
31
+ error_code = parse_tx_failed_error_code(
32
+ this.env_args.dex_id_list,
33
+ JSON.stringify({
34
+ txid: transaction.signatures,
35
+ err: meta.err,
36
+ logMessages: meta.logMessages
37
+ }))
38
+ }
39
+
40
+ const { pool_address, tokenA, tokenB, is_reverse_token } = pool_info
41
+
42
+ let txid = transaction.signatures[0]
43
+ const owner = this.wallet_pubkey.toBase58()
44
+ const { preTokenBalances, postTokenBalances, preBalances, postBalances } = meta
45
+ const tokenA_PreBalance = this.getTokenBalance(owner, tokenA.address, preTokenBalances)
46
+ const tokenA_PostBalance = this.getTokenBalance(owner, tokenA.address, postTokenBalances)
47
+ const tokenB_PreBalance = this.getTokenBalance(owner, tokenB.address, preTokenBalances)
48
+ const tokenB_PostBalance = this.getTokenBalance(owner, tokenB.address, postTokenBalances)
49
+
50
+
51
+ blockTime = blockTime * 1000 || 0 // blockTime - Unix timestamp
52
+
53
+ const tokenAChange = Number((tokenA_PostBalance - tokenA_PreBalance).toFixed(tokenA.decimals))
54
+ const tokenBChange = Number((tokenB_PostBalance - tokenB_PreBalance).toFixed(tokenB.decimals))
55
+
56
+ let price = ""
57
+ if (success) {
58
+ if (is_reverse_token) {
59
+ // SOL/POPCAT
60
+ price = Math.abs(tokenAChange / tokenBChange).toFixed(tokenA.decimals)
61
+ } else {
62
+ // SOL/USDT
63
+ price = Math.abs(tokenBChange / tokenAChange).toFixed(tokenB.decimals)
64
+ }
65
+ }
66
+
67
+ const base_fee = 5000
68
+ let priority_fee = 0
69
+ let total_fee = 0
70
+
71
+ total_fee = preBalances[0] - postBalances[0]
72
+ priority_fee = Math.abs(total_fee - base_fee)
73
+
74
+
75
+ let gas_fee: TradeGasFeeType = {
76
+ base_fee,
77
+ priority_fee,
78
+ total_fee
79
+ }
80
+
81
+ const tradeResult: StandardSwapDetailType = {
82
+ success,
83
+ error_code,
84
+ wallet: owner,
85
+ block_time: blockTime,
86
+ block_number: slot,
87
+ txid,
88
+ pool_address,
89
+ tokenA: {
90
+ ...tokenA,
91
+ pre_bal: tokenA_PreBalance,
92
+ post_bal: tokenA_PostBalance,
93
+ change: tokenAChange
94
+ },
95
+ tokenB: {
96
+ ...tokenB,
97
+ pre_bal: tokenB_PreBalance,
98
+ post_bal: tokenB_PostBalance,
99
+ change: tokenBChange
100
+ },
101
+ tx_price: price,
102
+ gas_fee
103
+ }
104
+ return tradeResult
105
+ }
106
+
107
+ getTokenBalance(owner: string, mint: string, tokenBalances: TokenBalance[]) {
108
+ const balance = tokenBalances?.find((e) => e.owner === owner && e.mint == mint)?.uiTokenAmount?.uiAmount
109
+ if (typeof balance !== 'undefined' && balance !== null) {
110
+ return balance
111
+ }
112
+ return 0
113
+ }
114
+ }
115
+
116
+
117
+ /**
118
+ * 解析交易失败的原因
119
+ *
120
+ * @param failed_msg 交易失败的相关信息
121
+ * @returns
122
+ */
123
+ export const parse_tx_failed_error_code = (dex_id_list: string[], tx_failed_error_msg: string): string => {
124
+ log_info(`parse_tx_failed_error_code`, {
125
+ dex_id_list,
126
+ tx_failed_error_msg
127
+ })
128
+
129
+ let err_code = ""
130
+
131
+ if (dex_id_list.includes(DEX_ID.RAYDIUM_AMM)) {
132
+ err_code = parse_raydium_amm_failure(tx_failed_error_msg)
133
+ }
134
+ else if (dex_id_list.includes(DEX_ID.RAYDIUM_CLMM)) {
135
+ err_code = parse_raydium_clmm_failure(tx_failed_error_msg)
136
+ }
137
+ else if (dex_id_list.includes(DEX_ID.ORCA_CLMM)) {
138
+ err_code = parse_orca_failure(tx_failed_error_msg)
139
+ }
140
+
141
+ // common errors
142
+ if (!err_code) {
143
+ if (tx_failed_error_msg.includes('exceeded CUs meter at BPF instruction')) {
144
+ err_code = TradeErrorCodeType.ExceededCUsLimit
145
+ }
146
+
147
+ if (tx_failed_error_msg.includes('insufficient funds')) {
148
+ return TradeErrorCodeType.InsufficientFunds
149
+ }
150
+ }
151
+
152
+ if (!err_code) {
153
+ err_code = TradeErrorCodeType.UNKOWN
154
+ }
155
+
156
+ return err_code
157
+
158
+ }
159
+
160
+
161
+ /**
162
+ * Raydium AMM 错误码
163
+ * @param error_msg
164
+ */
165
+ function parse_raydium_amm_failure(error_msg: string) {
166
+ if (error_msg.includes('0x28')) {
167
+ return TradeErrorCodeType.InsufficientFunds
168
+ }
169
+ if (error_msg.includes('0x1e') || error_msg.includes('exceeds desired slippage limit')) {
170
+ return TradeErrorCodeType.AmountOutBelowMinimum
171
+ }
172
+ return null
173
+ }
174
+
175
+ /**
176
+ * Raydium CLMM 错误码
177
+ * @param error_msg
178
+ */
179
+ function parse_raydium_clmm_failure(error_msg: string) {
180
+ if (error_msg.includes('Computational budget exceeded')) {
181
+ return TradeErrorCodeType.ExceededCUsLimit
182
+ }
183
+ if (error_msg.includes('0x1786') || error_msg.includes('Too little output received')) {
184
+ return TradeErrorCodeType.AmountOutBelowMinimum
185
+ }
186
+ if (error_msg.includes('0x178c') || error_msg.includes('Invaild first tick array account')) {
187
+ return TradeErrorCodeType.InvalidTickArraySequence
188
+ }
189
+ return null
190
+ }
191
+
192
+ /**
193
+ * ORCA 错误码
194
+ *
195
+ * https://github.com/coral-xyz/anchor/blob/master/lang/src/error.rs#L185
196
+ */
197
+ function parse_orca_failure(error_msg: string) {
198
+ if (error_msg.includes('0x1]') || error_msg.includes('insufficient funds')) {
199
+ return TradeErrorCodeType.InsufficientFunds
200
+ }
201
+ if (error_msg.includes('0x1794') || error_msg.includes('below minimum threshold')) {
202
+ return TradeErrorCodeType.AmountOutBelowMinimum
203
+ }
204
+ if (error_msg.includes('0xbc4') || error_msg.includes('expected this account to be already initialized')) {
205
+ return TradeErrorCodeType.AccountNotInitialized
206
+ }
207
+ if (error_msg.includes('0x1787') || error_msg.includes('Invalid tick array sequence')) {
208
+ return TradeErrorCodeType.InvalidTickArraySequence
209
+ }
210
+ return null
211
+ }
212
+