@clonegod/ttd-sol-common 2.0.7 → 2.0.8
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.
- package/dist/common/constants.d.ts +5 -0
- package/dist/common/constants.js +9 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/trade/index.d.ts +2 -0
- package/dist/trade/index.js +18 -0
- package/dist/trade/tx_result_check.d.ts +28 -0
- package/dist/trade/tx_result_check.js +235 -0
- package/dist/trade/tx_result_parse.d.ts +12 -0
- package/dist/trade/tx_result_parse.js +142 -0
- package/package.json +1 -1
- package/src/common/constants.ts +14 -0
- package/src/common/index.ts +2 -0
- package/src/index.ts +3 -1
- package/src/trade/index.ts +2 -0
- package/src/trade/tx_result_check.ts +305 -0
- package/src/trade/tx_result_parse.ts +212 -0
|
@@ -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
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,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
|
+
connection: Connection;
|
|
10
|
+
wallet: string;
|
|
11
|
+
context: TradeContext;
|
|
12
|
+
group_id: string;
|
|
13
|
+
txid: string;
|
|
14
|
+
trace_id: string;
|
|
15
|
+
price_msg: PriceMessageType;
|
|
16
|
+
order_msg: OrderMessageType;
|
|
17
|
+
pool_info: StandardPoolInfoType;
|
|
18
|
+
intervalId: any;
|
|
19
|
+
check_count: number;
|
|
20
|
+
transactionParser: TransactionResultParser;
|
|
21
|
+
trade_result_already_processed: boolean;
|
|
22
|
+
constructor(env_args: any, event_emitter: EventEmitter, context: TradeContext, txid: string);
|
|
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,235 @@
|
|
|
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, context, txid) {
|
|
19
|
+
super(env_args, event_emitter);
|
|
20
|
+
this.env_args = env_args;
|
|
21
|
+
this.event_emitter = event_emitter;
|
|
22
|
+
this.context = context;
|
|
23
|
+
this.group_id = context.group_id;
|
|
24
|
+
this.connection = new web3_js_1.Connection(env_args.rpc_endpoint, {
|
|
25
|
+
commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
|
|
26
|
+
});
|
|
27
|
+
this.wallet = context.trade_runtime.wallet.public_key;
|
|
28
|
+
this.txid = txid;
|
|
29
|
+
this.price_msg = context.price_msg;
|
|
30
|
+
this.order_msg = context.order_msg;
|
|
31
|
+
this.pool_info = context.pool_info;
|
|
32
|
+
this.trace_id = context.order_msg.order_trace_id;
|
|
33
|
+
this.check_count = 0;
|
|
34
|
+
this.transactionParser = new tx_result_parse_1.TransactionResultParser(this.env_args, new web3_js_1.PublicKey(this.wallet), this.event_emitter);
|
|
35
|
+
this.trade_result_already_processed = false;
|
|
36
|
+
}
|
|
37
|
+
init_tx_status() {
|
|
38
|
+
let empty_trade_result = JSON.parse(JSON.stringify({
|
|
39
|
+
group_id: this.context.group_id,
|
|
40
|
+
txid: this.txid
|
|
41
|
+
}));
|
|
42
|
+
this.event_emitter.emit(dist_1.TRANSACTION_STATE_PROCESSING, empty_trade_result);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
check() {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
this.check_tx_result_interval();
|
|
48
|
+
this.on_subscibe_transaction();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
on_subscibe_transaction() {
|
|
52
|
+
this.event_emitter.once(dist_1.LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (messageStr) => {
|
|
53
|
+
try {
|
|
54
|
+
this.trade_result_already_processed = true;
|
|
55
|
+
(0, dist_1.log_debug)('on events: EVENT_WALLET_TRANSACTION', messageStr);
|
|
56
|
+
let messageObj = messageStr;
|
|
57
|
+
if (typeof messageStr === 'string') {
|
|
58
|
+
messageObj = JSON.parse(messageStr);
|
|
59
|
+
}
|
|
60
|
+
const result = messageObj.params.result;
|
|
61
|
+
const _txid = result.signature;
|
|
62
|
+
const slot = result.slot;
|
|
63
|
+
(0, dist_1.log_info)('check txid:', { _txid, txid: this.txid, slot });
|
|
64
|
+
result.transaction.transaction.signatures = [_txid];
|
|
65
|
+
let tx_result = {
|
|
66
|
+
slot,
|
|
67
|
+
transaction: result.transaction.transaction,
|
|
68
|
+
meta: result.transaction.meta,
|
|
69
|
+
blockTime: Date.now() / 1000,
|
|
70
|
+
version: result.transaction.version
|
|
71
|
+
};
|
|
72
|
+
let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
|
|
73
|
+
let trade_result = this.map_swap_result_to_tx_result(swap_result);
|
|
74
|
+
if (trade_result.success) {
|
|
75
|
+
this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
|
|
79
|
+
}
|
|
80
|
+
console.log('<----------------------------------------------------');
|
|
81
|
+
console.dir(trade_result, { depth: 8 });
|
|
82
|
+
console.log('---------------------------------------------------->');
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
(0, dist_1.log_error)('parse geyser stream message error!', err);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
check_tx_result_interval() {
|
|
91
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
const check_start_time = Date.now();
|
|
93
|
+
const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000');
|
|
94
|
+
const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000');
|
|
95
|
+
(0, dist_1.log_info)(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`);
|
|
96
|
+
if (check_interval >= check_timeout) {
|
|
97
|
+
(0, dist_1.log_warn)(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
this.check_count += 1;
|
|
102
|
+
(0, dist_1.log_info)(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`);
|
|
103
|
+
try {
|
|
104
|
+
if (Date.now() - check_start_time < check_timeout) {
|
|
105
|
+
let tx_result = yield this.connection.getParsedTransaction(this.txid, {
|
|
106
|
+
commitment: common_1.COMMITMENT_LEVEL.CONFIRMED,
|
|
107
|
+
maxSupportedTransactionVersion: 0,
|
|
108
|
+
});
|
|
109
|
+
if ((0, dist_1.isEmpty)(tx_result)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
clearInterval(intervalId);
|
|
113
|
+
let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info);
|
|
114
|
+
let trade_result = this.map_swap_result_to_tx_result(swap_result);
|
|
115
|
+
if (this.trade_result_already_processed) {
|
|
116
|
+
(0, dist_1.log_warn)(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (trade_result.success) {
|
|
120
|
+
this.event_emitter.emit(dist_1.TRANSACTION_STATE_SUCCESS, trade_result);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.event_emitter.emit(dist_1.TRANSACTION_STATE_FAILED, trade_result);
|
|
124
|
+
}
|
|
125
|
+
console.log('<====================================================');
|
|
126
|
+
console.dir(trade_result, { depth: 8 });
|
|
127
|
+
console.log('====================================================>');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
clearInterval(intervalId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
clearInterval(intervalId);
|
|
136
|
+
(0, dist_1.log_error)('parse transaction error!', err);
|
|
137
|
+
}
|
|
138
|
+
}), check_interval);
|
|
139
|
+
this.intervalId = intervalId;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
map_swap_result_to_tx_result(swap_result) {
|
|
143
|
+
let { success, error_code, wallet, block_number, block_time: order_block_time, txid, tx_price, tokenA, tokenB, gas_fee } = swap_result;
|
|
144
|
+
let { pool_name, is_reverse_token } = this.pool_info;
|
|
145
|
+
let { chain_id, dex_id, unique_orderbook_id, pair, price_id, time: quote_time, ask, bid } = this.price_msg;
|
|
146
|
+
let { group_id, unique_order_msg_id, order_send_time, order_recv_time, order_submit_time, aToB } = this.order_msg;
|
|
147
|
+
let order_price = aToB ? bid.price : ask.price;
|
|
148
|
+
let server_info = (0, dist_1.getServerInfo)();
|
|
149
|
+
let order_end_time = Date.now();
|
|
150
|
+
let start_time = order_recv_time;
|
|
151
|
+
let end_time = order_end_time;
|
|
152
|
+
let total_time = end_time - start_time;
|
|
153
|
+
let total_order_time = total_time;
|
|
154
|
+
let time = {
|
|
155
|
+
block_time: quote_time.block_time,
|
|
156
|
+
stream_time: quote_time.stream_time,
|
|
157
|
+
quote_start_time: quote_time.quote_start_time,
|
|
158
|
+
quote_end_time: quote_time.quote_end_time,
|
|
159
|
+
price_time: quote_time.price_time,
|
|
160
|
+
total_quote_time: quote_time.total_quote_time,
|
|
161
|
+
order_send_time,
|
|
162
|
+
order_recv_time,
|
|
163
|
+
order_submit_time,
|
|
164
|
+
order_block_time,
|
|
165
|
+
order_end_time,
|
|
166
|
+
total_order_time,
|
|
167
|
+
};
|
|
168
|
+
let broadcast = [];
|
|
169
|
+
let trade_strategy = this.context.trade_runtime.settings.strategy;
|
|
170
|
+
let send_type = trade_strategy.broadcast_type;
|
|
171
|
+
if (send_type === 'rpc') {
|
|
172
|
+
send_type = trade_strategy.speed === 'fast' ? 'mainnet' : 'mainnet-premium';
|
|
173
|
+
}
|
|
174
|
+
broadcast.push({
|
|
175
|
+
rpc: {
|
|
176
|
+
read: 'qn',
|
|
177
|
+
write: 'qn'
|
|
178
|
+
},
|
|
179
|
+
type: send_type,
|
|
180
|
+
fee: gas_fee
|
|
181
|
+
});
|
|
182
|
+
let trade_balance_change = {
|
|
183
|
+
tokenA: {
|
|
184
|
+
symbol: tokenA.symbol,
|
|
185
|
+
address: tokenA.address,
|
|
186
|
+
pre_bal: tokenA.pre_bal,
|
|
187
|
+
post_bal: tokenA.post_bal,
|
|
188
|
+
change: Number(tokenA.change),
|
|
189
|
+
decimals: tokenA.decimals,
|
|
190
|
+
},
|
|
191
|
+
tokenB: {
|
|
192
|
+
symbol: tokenB.symbol,
|
|
193
|
+
address: tokenB.address,
|
|
194
|
+
pre_bal: tokenB.pre_bal,
|
|
195
|
+
post_bal: tokenB.post_bal,
|
|
196
|
+
change: Number(tokenB.change),
|
|
197
|
+
decimals: tokenB.decimals,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
if (is_reverse_token) {
|
|
201
|
+
let { tokenA, tokenB } = trade_balance_change;
|
|
202
|
+
trade_balance_change.tokenA = tokenB;
|
|
203
|
+
trade_balance_change.tokenB = tokenA;
|
|
204
|
+
}
|
|
205
|
+
let trade_extra_info = {
|
|
206
|
+
time,
|
|
207
|
+
broadcast,
|
|
208
|
+
server_info,
|
|
209
|
+
};
|
|
210
|
+
const c_id = this.order_msg.c_id || '';
|
|
211
|
+
const trade_result = {
|
|
212
|
+
success,
|
|
213
|
+
error_code,
|
|
214
|
+
start_time,
|
|
215
|
+
end_time,
|
|
216
|
+
total_time,
|
|
217
|
+
group_id,
|
|
218
|
+
wallet,
|
|
219
|
+
chain_id,
|
|
220
|
+
pair,
|
|
221
|
+
dex_id,
|
|
222
|
+
txid,
|
|
223
|
+
unique_orderbook_id,
|
|
224
|
+
unique_order_msg_id,
|
|
225
|
+
order_price,
|
|
226
|
+
aToB,
|
|
227
|
+
tx_price,
|
|
228
|
+
balance: trade_balance_change,
|
|
229
|
+
execution: trade_extra_info,
|
|
230
|
+
c_id
|
|
231
|
+
};
|
|
232
|
+
return trade_result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
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
package/src/index.ts
CHANGED
|
@@ -0,0 +1,305 @@
|
|
|
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
|
+
connection: Connection
|
|
17
|
+
wallet: string
|
|
18
|
+
|
|
19
|
+
context: TradeContext
|
|
20
|
+
group_id: string
|
|
21
|
+
txid: string
|
|
22
|
+
|
|
23
|
+
trace_id: string
|
|
24
|
+
price_msg: PriceMessageType
|
|
25
|
+
order_msg: OrderMessageType
|
|
26
|
+
pool_info: StandardPoolInfoType
|
|
27
|
+
|
|
28
|
+
intervalId: any
|
|
29
|
+
check_count: number
|
|
30
|
+
transactionParser: TransactionResultParser
|
|
31
|
+
|
|
32
|
+
trade_result_already_processed: boolean
|
|
33
|
+
|
|
34
|
+
constructor(env_args: any, event_emitter: EventEmitter, context: TradeContext, txid: string) {
|
|
35
|
+
super(env_args, event_emitter)
|
|
36
|
+
this.env_args = env_args
|
|
37
|
+
this.event_emitter = event_emitter
|
|
38
|
+
|
|
39
|
+
this.context = context
|
|
40
|
+
this.group_id = context.group_id
|
|
41
|
+
this.connection = new Connection(env_args.rpc_endpoint, {
|
|
42
|
+
commitment: COMMITMENT_LEVEL.CONFIRMED,
|
|
43
|
+
})
|
|
44
|
+
this.wallet = context.trade_runtime.wallet.public_key
|
|
45
|
+
|
|
46
|
+
this.txid = txid
|
|
47
|
+
this.price_msg = context.price_msg
|
|
48
|
+
this.order_msg = context.order_msg
|
|
49
|
+
this.pool_info = context.pool_info
|
|
50
|
+
this.trace_id = context.order_msg.order_trace_id
|
|
51
|
+
|
|
52
|
+
this.check_count = 0
|
|
53
|
+
this.transactionParser = new TransactionResultParser(this.env_args, new PublicKey(this.wallet), this.event_emitter)
|
|
54
|
+
this.trade_result_already_processed = false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* set trade initial status: PROCESSING
|
|
59
|
+
*/
|
|
60
|
+
init_tx_status() {
|
|
61
|
+
let empty_trade_result = JSON.parse(JSON.stringify({
|
|
62
|
+
group_id: this.context.group_id,
|
|
63
|
+
txid: this.txid
|
|
64
|
+
}))
|
|
65
|
+
this.event_emitter.emit(TRANSACTION_STATE_PROCESSING, empty_trade_result)
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async check() {
|
|
70
|
+
this.check_tx_result_interval()
|
|
71
|
+
this.on_subscibe_transaction()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 从第三方服务(helius geyser ws)订阅Transaction结果
|
|
76
|
+
*/
|
|
77
|
+
on_subscibe_transaction() {
|
|
78
|
+
this.event_emitter.once(LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (messageStr) => {
|
|
79
|
+
try {
|
|
80
|
+
this.trade_result_already_processed = true
|
|
81
|
+
log_debug('on events: EVENT_WALLET_TRANSACTION', messageStr)
|
|
82
|
+
|
|
83
|
+
let messageObj = messageStr
|
|
84
|
+
if(typeof messageStr === 'string') {
|
|
85
|
+
messageObj = JSON.parse(messageStr)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = messageObj.params.result
|
|
89
|
+
const _txid = result.signature // Extract the signature
|
|
90
|
+
const slot = result.slot
|
|
91
|
+
log_info('check txid:', { _txid, txid: this.txid, slot})
|
|
92
|
+
|
|
93
|
+
// set txid for parse
|
|
94
|
+
result.transaction.transaction.signatures = [_txid]
|
|
95
|
+
|
|
96
|
+
let tx_result:ParsedTransactionWithMeta = {
|
|
97
|
+
slot,
|
|
98
|
+
transaction: result.transaction.transaction,
|
|
99
|
+
meta: result.transaction.meta,
|
|
100
|
+
blockTime: Date.now() / 1000,
|
|
101
|
+
version: result.transaction.version
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info)
|
|
105
|
+
|
|
106
|
+
let trade_result = this.map_swap_result_to_tx_result(swap_result)
|
|
107
|
+
if (trade_result.success) {
|
|
108
|
+
this.event_emitter.emit(TRANSACTION_STATE_SUCCESS, trade_result)
|
|
109
|
+
} else {
|
|
110
|
+
this.event_emitter.emit(TRANSACTION_STATE_FAILED, trade_result)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('<----------------------------------------------------')
|
|
114
|
+
console.dir(trade_result, { depth: 8 })
|
|
115
|
+
console.log('---------------------------------------------------->')
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log_error('parse geyser stream message error!', err)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 主动确认交易结果
|
|
126
|
+
*/
|
|
127
|
+
async check_tx_result_interval() {
|
|
128
|
+
const check_start_time = Date.now()
|
|
129
|
+
const check_interval = parseInt(process.env.CHECK_TX_RESULT_INTERVAL_MILLS || '3000')
|
|
130
|
+
const check_timeout = parseInt(process.env.CHECK_TX_RESULT_TIMEOUT_MILLS || '10000')
|
|
131
|
+
|
|
132
|
+
log_info(`check transaction start: check_interval=${check_interval}, check_timeout=${check_timeout}`)
|
|
133
|
+
|
|
134
|
+
if(check_interval >= check_timeout) {
|
|
135
|
+
log_warn(`check_interval=${check_interval} >= check_timeout=${check_timeout}, check_tx_result_interval failed!`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const intervalId = setInterval(async () => {
|
|
140
|
+
this.check_count += 1
|
|
141
|
+
log_info(`check transaction: seq=[${this.check_count}], txhash= ${this.txid}, trace_id=${this.trace_id}`)
|
|
142
|
+
try {
|
|
143
|
+
if (Date.now() - check_start_time < check_timeout) {
|
|
144
|
+
let tx_result:ParsedTransactionWithMeta = await this.connection.getParsedTransaction(this.txid, {
|
|
145
|
+
commitment: COMMITMENT_LEVEL.CONFIRMED,
|
|
146
|
+
maxSupportedTransactionVersion: 0,
|
|
147
|
+
})
|
|
148
|
+
if (isEmpty(tx_result)) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
// 查询到结果
|
|
152
|
+
clearInterval(intervalId)
|
|
153
|
+
|
|
154
|
+
let swap_result = this.transactionParser.parse_transaction_data(tx_result, this.pool_info)
|
|
155
|
+
let trade_result = this.map_swap_result_to_tx_result(swap_result)
|
|
156
|
+
|
|
157
|
+
if(this.trade_result_already_processed) {
|
|
158
|
+
log_warn(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by interval check!`)
|
|
159
|
+
} else {
|
|
160
|
+
if (trade_result.success) {
|
|
161
|
+
this.event_emitter.emit(TRANSACTION_STATE_SUCCESS, trade_result)
|
|
162
|
+
} else {
|
|
163
|
+
this.event_emitter.emit(TRANSACTION_STATE_FAILED, trade_result)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('<====================================================')
|
|
167
|
+
console.dir(trade_result, { depth: 8 })
|
|
168
|
+
console.log('====================================================>')
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// 超时未查询到结果
|
|
172
|
+
clearInterval(intervalId)
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// 解析结果异常
|
|
176
|
+
clearInterval(intervalId)
|
|
177
|
+
log_error('parse transaction error!', err)
|
|
178
|
+
}
|
|
179
|
+
}, check_interval)
|
|
180
|
+
|
|
181
|
+
this.intervalId = intervalId
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
map_swap_result_to_tx_result(swap_result: StandardSwapDetailType): TradeResultType {
|
|
186
|
+
let { success, error_code, wallet, block_number, block_time: order_block_time, txid, tx_price, tokenA, tokenB, gas_fee } = swap_result
|
|
187
|
+
let { pool_name, is_reverse_token } = this.pool_info
|
|
188
|
+
|
|
189
|
+
let { chain_id, dex_id, unique_orderbook_id, pair, price_id, time: quote_time, ask, bid } = this.price_msg
|
|
190
|
+
let { group_id, unique_order_msg_id, order_send_time, order_recv_time, order_submit_time, aToB } = this.order_msg
|
|
191
|
+
|
|
192
|
+
let order_price = aToB ? bid.price : ask.price
|
|
193
|
+
|
|
194
|
+
let server_info = getServerInfo()
|
|
195
|
+
|
|
196
|
+
let order_end_time = Date.now()
|
|
197
|
+
|
|
198
|
+
let start_time = order_recv_time
|
|
199
|
+
let end_time = order_end_time
|
|
200
|
+
let total_time = end_time - start_time
|
|
201
|
+
|
|
202
|
+
// block_time 只精确到秒,估算耗时+1000ms
|
|
203
|
+
// let total_order_time = (order_block_time + 1000) - order_recv_time
|
|
204
|
+
let total_order_time = total_time
|
|
205
|
+
|
|
206
|
+
let time: TradeTimeFlowType = {
|
|
207
|
+
// Quote Consumed Time
|
|
208
|
+
block_time: quote_time.block_time, // 触发本次quote询价的交易,链上的出块时间
|
|
209
|
+
stream_time: quote_time.stream_time, // 监听到链上发生新交易的时间(stream/webhook)
|
|
210
|
+
quote_start_time: quote_time.quote_start_time, // 询价开始时间
|
|
211
|
+
quote_end_time: quote_time.quote_end_time, // 询价结束时间
|
|
212
|
+
price_time: quote_time.price_time, // 生成价格的时间
|
|
213
|
+
total_quote_time: quote_time.total_quote_time, // 询价耗时:price_time - stream_time
|
|
214
|
+
|
|
215
|
+
// Trade Consumed Time
|
|
216
|
+
order_send_time, // cex 发送下单消息的时间
|
|
217
|
+
order_recv_time, // dex 接收到下单消息的时间
|
|
218
|
+
order_submit_time, // dex 提交交易时间
|
|
219
|
+
order_block_time, // 交易所属block的出块时间
|
|
220
|
+
order_end_time, // dex 解析得到交易结果的时间
|
|
221
|
+
total_order_time, // 交易总耗时: order_end_time - order_recv_time
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let broadcast: TradeBroadcastType[] = []
|
|
225
|
+
let trade_strategy = this.context.trade_runtime.settings.strategy
|
|
226
|
+
let send_type = trade_strategy.broadcast_type as string
|
|
227
|
+
if (send_type === 'rpc') {
|
|
228
|
+
send_type = trade_strategy.speed === 'fast' ? 'mainnet' : 'mainnet-premium'
|
|
229
|
+
}
|
|
230
|
+
broadcast.push(
|
|
231
|
+
{
|
|
232
|
+
rpc: {
|
|
233
|
+
read: 'qn',
|
|
234
|
+
write: 'qn'
|
|
235
|
+
}, // quicknode | trongrid ...
|
|
236
|
+
type: send_type, // 主网,私单,自建节点
|
|
237
|
+
fee: gas_fee // gas
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
let trade_balance_change: TradeResultBalanceChangeType = {
|
|
242
|
+
tokenA: {
|
|
243
|
+
symbol: tokenA.symbol,
|
|
244
|
+
address: tokenA.address,
|
|
245
|
+
pre_bal: tokenA.pre_bal,
|
|
246
|
+
post_bal: tokenA.post_bal,
|
|
247
|
+
change: Number(tokenA.change),
|
|
248
|
+
decimals: tokenA.decimals,
|
|
249
|
+
},
|
|
250
|
+
tokenB: {
|
|
251
|
+
symbol: tokenB.symbol,
|
|
252
|
+
address: tokenB.address,
|
|
253
|
+
pre_bal: tokenB.pre_bal,
|
|
254
|
+
post_bal: tokenB.post_bal,
|
|
255
|
+
change: Number(tokenB.change),
|
|
256
|
+
decimals: tokenB.decimals,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (is_reverse_token) {
|
|
261
|
+
let { tokenA, tokenB } = trade_balance_change
|
|
262
|
+
trade_balance_change.tokenA = tokenB
|
|
263
|
+
trade_balance_change.tokenB = tokenA
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let trade_extra_info = {
|
|
267
|
+
time,
|
|
268
|
+
broadcast,
|
|
269
|
+
server_info,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 返回c_id给cex
|
|
273
|
+
const c_id = this.order_msg.c_id || ''
|
|
274
|
+
|
|
275
|
+
const trade_result: TradeResultType = {
|
|
276
|
+
success,
|
|
277
|
+
error_code,
|
|
278
|
+
|
|
279
|
+
start_time,
|
|
280
|
+
end_time,
|
|
281
|
+
total_time,
|
|
282
|
+
|
|
283
|
+
group_id,
|
|
284
|
+
wallet,
|
|
285
|
+
|
|
286
|
+
chain_id,
|
|
287
|
+
pair,
|
|
288
|
+
dex_id,
|
|
289
|
+
txid,
|
|
290
|
+
|
|
291
|
+
unique_orderbook_id,
|
|
292
|
+
unique_order_msg_id,
|
|
293
|
+
order_price,
|
|
294
|
+
aToB,
|
|
295
|
+
tx_price,
|
|
296
|
+
|
|
297
|
+
balance: trade_balance_change,
|
|
298
|
+
execution: trade_extra_info,
|
|
299
|
+
c_id
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return trade_result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
}
|
|
@@ -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
|
+
|