@clonegod/ttd-bsc-common 1.0.90 → 2.0.1
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/config/BscQuoteAppConfig.d.ts +11 -0
- package/dist/config/BscQuoteAppConfig.js +62 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/quote/event/index.d.ts +1 -0
- package/dist/quote/event/index.js +1 -0
- package/dist/quote/event/swap_debouncer.d.ts +22 -0
- package/dist/quote/event/swap_debouncer.js +92 -0
- package/dist/quote/index.d.ts +1 -0
- package/dist/quote/index.js +1 -0
- package/dist/quote/pricing/index.d.ts +2 -0
- package/dist/quote/pricing/index.js +2 -0
- package/dist/quote/pricing/pool_state_initializer.d.ts +8 -0
- package/dist/quote/pricing/pool_state_initializer.js +142 -0
- package/dist/quote/pricing/sdk_token_factory.d.ts +2 -0
- package/dist/quote/pricing/sdk_token_factory.js +21 -0
- package/dist/quote/tick/clmm_tick_cache.d.ts +40 -0
- package/dist/quote/tick/clmm_tick_cache.js +219 -0
- package/dist/quote/tick/index.d.ts +2 -0
- package/dist/{trade/send → quote/tick}/index.js +2 -8
- package/dist/quote/tick/tick_lens_loaders.d.ts +25 -0
- package/dist/quote/tick/tick_lens_loaders.js +170 -0
- package/dist/redis/redis_client.d.ts +1 -0
- package/dist/redis/redis_client.js +6 -0
- package/dist/trade/abstract_dex_trade.d.ts +30 -16
- package/dist/trade/abstract_dex_trade.js +223 -108
- package/dist/trade/caller_manager.d.ts +35 -0
- package/dist/trade/caller_manager.js +178 -0
- package/dist/trade/index.d.ts +1 -2
- package/dist/trade/index.js +1 -2
- package/dist/types/pool_state.d.ts +31 -0
- package/package.json +3 -2
- package/dist/trade/abstract_dex_trade_plus.d.ts +0 -44
- package/dist/trade/abstract_dex_trade_plus.js +0 -449
- package/dist/trade/send/48club.d.ts +0 -17
- package/dist/trade/send/48club.js +0 -123
- package/dist/trade/send/48club_member.d.ts +0 -1
- package/dist/trade/send/48club_member.js +0 -25
- package/dist/trade/send/48club_sp.d.ts +0 -9
- package/dist/trade/send/48club_sp.js +0 -137
- package/dist/trade/send/blockrazor.d.ts +0 -7
- package/dist/trade/send/blockrazor.js +0 -78
- package/dist/trade/send/blxr.d.ts +0 -0
- package/dist/trade/send/blxr.js +0 -0
- package/dist/trade/send/bsc_rpc.d.ts +0 -6
- package/dist/trade/send/bsc_rpc.js +0 -47
- package/dist/trade/send/index.d.ts +0 -6
- package/dist/trade/send/send_bundle_proxy.d.ts +0 -7
- package/dist/trade/send/send_bundle_proxy.js +0 -30
- package/dist/trade/send/send_bundle_ws.d.ts +0 -7
- package/dist/trade/send/send_bundle_ws.js +0 -30
- package/dist/trade/send/send_tx.d.ts +0 -9
- package/dist/trade/send/send_tx.js +0 -119
- package/dist/ws/bsc_stream_ws_client.d.ts +0 -10
- package/dist/ws/bsc_stream_ws_client.js +0 -95
|
@@ -8,146 +8,261 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
15
|
+
exports.AbstractDexTrade = void 0;
|
|
16
|
+
exports.buildTradeConfig = buildTradeConfig;
|
|
13
17
|
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
14
18
|
const ethers_1 = require("ethers");
|
|
15
|
-
const
|
|
16
|
-
|
|
19
|
+
const caller_manager_1 = require("./caller_manager");
|
|
20
|
+
const ttd_bsc_send_tx_1 = require("@clonegod/ttd-bsc-send-tx");
|
|
21
|
+
const redis_1 = require("../redis");
|
|
22
|
+
const decimal_js_1 = __importDefault(require("decimal.js"));
|
|
23
|
+
function buildTradeConfig() {
|
|
24
|
+
const vaultAddress = process.env.VAULT_ADDRESS;
|
|
25
|
+
if (!vaultAddress) {
|
|
26
|
+
throw new Error('环境变量 VAULT_ADDRESS 未配置');
|
|
27
|
+
}
|
|
28
|
+
const pancakeIdName = process.env.PANCAKE_EXECUTOR_ID || 'PANCAKE';
|
|
29
|
+
const uniswapIdName = process.env.UNISWAP_EXECUTOR_ID || 'UNISWAP';
|
|
30
|
+
return {
|
|
31
|
+
vaultAddress,
|
|
32
|
+
executorIds: {
|
|
33
|
+
pancake: ethers_1.ethers.utils.id(pancakeIdName),
|
|
34
|
+
uniswap: ethers_1.ethers.utils.id(uniswapIdName),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const VAULT_ABI = [
|
|
39
|
+
'function delegatecallExecutorToTrade(bytes32 executorId, bytes calldata data, uint256 deadline) external payable returns (bytes memory result)'
|
|
40
|
+
];
|
|
41
|
+
class AbstractDexTrade extends dist_1.AbastrcatTrade {
|
|
17
42
|
constructor(appConfig) {
|
|
18
43
|
super();
|
|
19
44
|
this.appConfig = appConfig;
|
|
20
|
-
this.approvedTokens = new Map();
|
|
21
|
-
this.pairContracts = new Map();
|
|
22
|
-
this.tokenContracts = new Map();
|
|
23
|
-
this.approvedTokens = new Map();
|
|
24
|
-
this.pairContracts = new Map();
|
|
25
|
-
this.tokenContracts = new Map();
|
|
26
45
|
this.initConfigs();
|
|
46
|
+
this.chainNameLower = this.appConfig.env_args.chain_id.toLowerCase();
|
|
47
|
+
this.redisClient = new redis_1.SimpleRedisClient(`${this.chainNameLower}:tx`);
|
|
48
|
+
this.vaultInterface = new ethers_1.ethers.utils.Interface(VAULT_ABI);
|
|
27
49
|
}
|
|
28
50
|
init() {
|
|
29
51
|
return __awaiter(this, void 0, void 0, function* () {
|
|
30
52
|
this.provider = new ethers_1.ethers.providers.JsonRpcProvider(this.chainConfig.rpcEndpoint);
|
|
31
|
-
this.
|
|
32
|
-
(
|
|
33
|
-
this.
|
|
34
|
-
|
|
53
|
+
this.transactionSender = new ttd_bsc_send_tx_1.TransactionSender(this.appConfig);
|
|
54
|
+
const callerGroupIds = (process.env.CALLER_GROUP_IDS || '').trim().split(',').filter(Boolean);
|
|
55
|
+
this.callerManager = new caller_manager_1.CallerManager({
|
|
56
|
+
chainName: this.chainNameLower,
|
|
57
|
+
provider: this.provider,
|
|
58
|
+
callerGroupIds,
|
|
59
|
+
chainId: this.chainConfig.chainId,
|
|
60
|
+
});
|
|
61
|
+
yield this.callerManager.init();
|
|
62
|
+
this.subscribeTradeMonitor();
|
|
63
|
+
(0, dist_1.log_info)(`AbstractDexTrade initialized, vault=${this.tradeConfig.vaultAddress}, callers=${this.callerManager.getCallerCount()}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
execute(context) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
var _a;
|
|
69
|
+
const { price_msg, order_msg, slippage_bps, order_trace_id, pool_info } = context;
|
|
70
|
+
const { pair } = price_msg;
|
|
71
|
+
const { aToB, amount, unique_order_msg_id } = order_msg;
|
|
72
|
+
(0, dist_1.log_info)(`执行 Vault 交易, orderId=${order_trace_id}`, {
|
|
73
|
+
unique_order_msg_id,
|
|
74
|
+
pair,
|
|
75
|
+
aToB,
|
|
76
|
+
amount,
|
|
77
|
+
slippage_bps
|
|
78
|
+
});
|
|
79
|
+
let maxAttempts = Math.min(Math.max(parseInt(process.env.NONCE_LOCK_MAX_RETRIES || '3'), 1), 3);
|
|
80
|
+
const callerHandle = yield this.callerManager.acquireCaller();
|
|
81
|
+
const { wallet: caller, nonce: initialNonce } = callerHandle;
|
|
82
|
+
try {
|
|
83
|
+
let nonce = initialNonce;
|
|
84
|
+
let nonce_from_error = 0;
|
|
85
|
+
let txid = '';
|
|
86
|
+
let i = 1;
|
|
87
|
+
do {
|
|
88
|
+
try {
|
|
89
|
+
if (i > 1) {
|
|
90
|
+
if (nonce_from_error > 0) {
|
|
91
|
+
nonce = nonce_from_error;
|
|
92
|
+
nonce_from_error = 0;
|
|
93
|
+
(0, dist_1.log_info)(`Attempt ${i}/${maxAttempts}, using nonce from error msg: ${nonce}`, {}, order_trace_id);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
(0, dist_1.log_warn)(`Attempt ${i}/${maxAttempts}, no nonce in error msg, abort`, {}, order_trace_id);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const { executorId, data } = this.encodeTradeData(context);
|
|
101
|
+
const deadline = Math.floor(Date.now() / 1000) + 60;
|
|
102
|
+
const vaultCalldata = this.vaultInterface.encodeFunctionData('delegatecallExecutorToTrade', [executorId, data, deadline]);
|
|
103
|
+
const gasPriceGwei = this.getGasPriceGwei(context);
|
|
104
|
+
const realGasPriceGwei = Math.min(Number(gasPriceGwei), this.chainConfig.gasOptions.maxGasPriceGwei).toString();
|
|
105
|
+
const tx = {
|
|
106
|
+
to: this.tradeConfig.vaultAddress,
|
|
107
|
+
data: vaultCalldata,
|
|
108
|
+
gasLimit: this.chainConfig.gasOptions.gasLimit,
|
|
109
|
+
gasPrice: ethers_1.ethers.utils.parseUnits(realGasPriceGwei, 'gwei'),
|
|
110
|
+
nonce,
|
|
111
|
+
chainId: this.chainConfig.chainId,
|
|
112
|
+
value: 0,
|
|
113
|
+
};
|
|
114
|
+
const signedTx = yield caller.signTransaction(tx);
|
|
115
|
+
txid = ethers_1.ethers.utils.keccak256(signedTx);
|
|
116
|
+
(0, dist_1.log_info)(`Vault 交易已签名`, { txid, nonce, gasPriceGwei: realGasPriceGwei }, order_trace_id);
|
|
117
|
+
const tipNonce = nonce + 1;
|
|
118
|
+
const eoa_tip_transaction = (eoa_address) => __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
const transfer_amount_gwei = this.getBuilderTipAmoutGwei(context);
|
|
120
|
+
context.ui_tip_amount = new decimal_js_1.default(transfer_amount_gwei).div(Math.pow(10, 9)).toNumber();
|
|
121
|
+
return yield this.buildTipTransferTx(eoa_address, transfer_amount_gwei, realGasPriceGwei, tipNonce, caller);
|
|
122
|
+
});
|
|
123
|
+
const only_bundle = order_msg.is_dex_maker;
|
|
124
|
+
yield this.transactionSender.sendTransaction(signedTx, eoa_tip_transaction, order_trace_id, pair, only_bundle);
|
|
125
|
+
(0, dist_1.log_info)(`Vault 交易发送成功`, {
|
|
126
|
+
pair, unique_order_msg_id, txid,
|
|
127
|
+
attempt: i, caller: caller.address
|
|
128
|
+
}, order_trace_id);
|
|
129
|
+
return txid;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errorMessage = ((_a = error.message) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
|
|
133
|
+
if (errorMessage.includes('rate limit')) {
|
|
134
|
+
(0, dist_1.log_warn)(`Rate limit error, no retry! i=${i}`, {}, order_trace_id);
|
|
135
|
+
return txid;
|
|
136
|
+
}
|
|
137
|
+
if (errorMessage.includes('replacement transaction underpriced')) {
|
|
138
|
+
(0, dist_1.log_warn)(`Replacement tx underpriced, continue! i=${i}`, {}, order_trace_id);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const isNonceError = errorMessage.includes('nonce');
|
|
142
|
+
if (!isNonceError || i >= maxAttempts) {
|
|
143
|
+
(0, dist_1.log_warn)(`Non-nonce error or max retries, i=${i}, error: ${errorMessage}`, {}, order_trace_id);
|
|
144
|
+
return txid;
|
|
145
|
+
}
|
|
146
|
+
(0, dist_1.log_info)(`Nonce error detected, will retry! i=${i}`, {}, order_trace_id);
|
|
147
|
+
if (errorMessage.includes('nonce too')) {
|
|
148
|
+
const correctNonce = this.extractNonceFromErrorMsg(errorMessage);
|
|
149
|
+
if (correctNonce !== null && correctNonce > 0) {
|
|
150
|
+
nonce_from_error = correctNonce;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} while (++i <= maxAttempts);
|
|
155
|
+
if (!txid) {
|
|
156
|
+
throw new Error(`Vault 交易执行失败,已重试 ${maxAttempts} 次,orderId: ${order_trace_id}`);
|
|
157
|
+
}
|
|
158
|
+
return txid;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
yield callerHandle.release();
|
|
162
|
+
}
|
|
35
163
|
});
|
|
36
164
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
165
|
+
subscribeTradeMonitor() {
|
|
166
|
+
const wsUrl = process.env.TRADE_MONITOR_WS_URL;
|
|
167
|
+
if (!wsUrl) {
|
|
168
|
+
(0, dist_1.log_warn)('TRADE_MONITOR_WS_URL not configured, trade result nonce correction disabled');
|
|
169
|
+
return;
|
|
41
170
|
}
|
|
42
|
-
|
|
171
|
+
const wsClient = new dist_1.WebSocketClient(wsUrl);
|
|
172
|
+
wsClient.onOpen(() => {
|
|
173
|
+
wsClient.send(JSON.stringify({
|
|
174
|
+
vault_address: this.tradeConfig.vaultAddress,
|
|
175
|
+
}));
|
|
176
|
+
(0, dist_1.log_info)(`Subscribed to trade-monitor for vault=${this.tradeConfig.vaultAddress}`);
|
|
177
|
+
});
|
|
178
|
+
wsClient.onMessage((event) => {
|
|
179
|
+
if (event.type === 'TradeResult' && event.data) {
|
|
180
|
+
const { caller, callerNonce } = event.data;
|
|
181
|
+
if (caller) {
|
|
182
|
+
const nextNonce = callerNonce + 2;
|
|
183
|
+
this.callerManager.confirmNonce(caller, nextNonce).catch(err => (0, dist_1.log_warn)(`Failed to confirm nonce from trade-monitor`, err));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
wsClient.connect();
|
|
43
188
|
}
|
|
44
189
|
getGasPriceGwei(context) {
|
|
45
|
-
let {
|
|
46
|
-
if (evm_gas_price_gwei
|
|
190
|
+
let { evm_gas_price_gwei } = context.trade_runtime.settings.strategy;
|
|
191
|
+
if (!evm_gas_price_gwei || evm_gas_price_gwei <= 0) {
|
|
47
192
|
evm_gas_price_gwei = this.chainConfig.gasOptions.defaultGasPriceGwei;
|
|
48
193
|
}
|
|
49
|
-
(0, dist_1.log_info)(`getGasPriceGwei: ${evm_gas_price_gwei} Gwei`);
|
|
50
194
|
return evm_gas_price_gwei.toString();
|
|
51
195
|
}
|
|
52
196
|
getBuilderTipAmoutGwei(context) {
|
|
53
|
-
let {
|
|
54
|
-
if (evm_tip_amount_gwei
|
|
197
|
+
let { evm_tip_amount_gwei } = context.trade_runtime.settings.strategy;
|
|
198
|
+
if (!evm_tip_amount_gwei || evm_tip_amount_gwei <= 0) {
|
|
55
199
|
evm_tip_amount_gwei = this.chainConfig.gasOptions.defaultTipAmountGwei;
|
|
56
200
|
}
|
|
57
|
-
(0, dist_1.log_info)(`getGasTipAmoutGwei: ${evm_tip_amount_gwei} Gwei`);
|
|
58
201
|
return evm_tip_amount_gwei.toString();
|
|
59
202
|
}
|
|
60
|
-
|
|
203
|
+
buildTipTransferTx(to, transfer_amount_gwei, gas_price_gwei, nonce, wallet) {
|
|
61
204
|
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const tokenContract = this.getTokenContract(address);
|
|
67
|
-
yield this.checkTokenApprove(address, symbol, tokenContract, router);
|
|
68
|
-
})));
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
checkTokenApprove(tokenAddress, tokenSymbol, tokenContract, spenderAddress) {
|
|
72
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
-
if (this.approvedTokens.get(tokenAddress)) {
|
|
74
|
-
(0, dist_1.log_info)(`代币 ${tokenSymbol} 已授权,跳过授权检查`);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
const allowance = yield tokenContract.allowance(this.wallet.address, spenderAddress);
|
|
78
|
-
const maxAllowance = ethers_1.ethers.constants.MaxUint256.div(2);
|
|
79
|
-
(0, dist_1.log_info)(`代币授权检查:`, {
|
|
80
|
-
tokenSymbol,
|
|
81
|
-
currentAllowance: allowance.toString(),
|
|
82
|
-
maxAllowance: maxAllowance.toString(),
|
|
83
|
-
isSufficient: allowance.gte(maxAllowance)
|
|
84
|
-
});
|
|
85
|
-
if (allowance.lt(maxAllowance)) {
|
|
86
|
-
const maxApprovalAmount = ethers_1.ethers.utils.formatUnits(ethers_1.ethers.constants.MaxUint256, yield tokenContract.decimals());
|
|
87
|
-
const tokenSymbolDisplay = tokenSymbol || `${tokenAddress.substring(0, 6)}...`;
|
|
88
|
-
(0, dist_1.log_info)(`正在为合约授权代币 ${tokenSymbolDisplay}`, {
|
|
89
|
-
tokenAddress,
|
|
90
|
-
tokenSymbol: tokenSymbolDisplay,
|
|
91
|
-
currentAllowance: ethers_1.ethers.utils.formatUnits(allowance, yield tokenContract.decimals()),
|
|
92
|
-
newAllowance: maxApprovalAmount
|
|
93
|
-
});
|
|
94
|
-
const tx = yield tokenContract.approve(spenderAddress, ethers_1.ethers.constants.MaxUint256, {
|
|
95
|
-
gasLimit: this.chainConfig.gasOptions.gasLimit
|
|
96
|
-
});
|
|
97
|
-
yield tx.wait();
|
|
98
|
-
this.approvedTokens.set(tokenAddress, true);
|
|
99
|
-
(0, dist_1.log_info)(`代币 ${tokenSymbolDisplay} 授权完成,数量: ${maxApprovalAmount} ${tokenSymbolDisplay}, 交易哈希: ${tx.hash}`);
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
this.approvedTokens.set(tokenAddress, true);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
isNonceRelatedError(error) {
|
|
109
|
-
if (!error)
|
|
110
|
-
return false;
|
|
111
|
-
const errorMessage = error.message ? error.message.toLowerCase() : '';
|
|
112
|
-
const nonceErrorKeywords = [
|
|
113
|
-
'nonce',
|
|
114
|
-
'nonce too low',
|
|
115
|
-
'nonce too high',
|
|
116
|
-
'nonce has already been used',
|
|
117
|
-
'already known',
|
|
118
|
-
'replacement transaction underpriced',
|
|
119
|
-
'transaction with same nonce',
|
|
120
|
-
'transaction nonce is too low',
|
|
121
|
-
'invalid transaction nonce',
|
|
122
|
-
'insufficient funds',
|
|
123
|
-
'sign bundle to get failure details',
|
|
124
|
-
];
|
|
125
|
-
return nonceErrorKeywords.some(keyword => errorMessage.includes(keyword));
|
|
126
|
-
}
|
|
127
|
-
isNativeCurrency(symbol) {
|
|
128
|
-
return symbol.toUpperCase() === this.chainConfig.nativeCurrency;
|
|
129
|
-
}
|
|
130
|
-
getWrappedNativeAddress() {
|
|
131
|
-
return this.chainConfig.wrappedNativeCurrencyAddress;
|
|
132
|
-
}
|
|
133
|
-
buildTipTransferTx(to, transfer_amount_gwei, gas_price_gwei, transfer_nonce) {
|
|
134
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
-
let real_transfer_amount_gwei = Math.min(Number(transfer_amount_gwei), this.chainConfig.gasOptions.maxTipAmountGwei).toString();
|
|
136
|
-
let real_gas_price_gwei = Math.min(Number(gas_price_gwei), this.chainConfig.gasOptions.maxGasPriceGwei).toString();
|
|
137
|
-
let tx_data = {
|
|
138
|
-
from: this.wallet.address,
|
|
205
|
+
const real_transfer_amount_gwei = Math.min(Number(transfer_amount_gwei), this.chainConfig.gasOptions.maxTipAmountGwei).toString();
|
|
206
|
+
const real_gas_price_gwei = Math.min(Number(gas_price_gwei), this.chainConfig.gasOptions.maxGasPriceGwei).toString();
|
|
207
|
+
const tx_data = {
|
|
208
|
+
from: wallet.address,
|
|
139
209
|
to,
|
|
140
210
|
value: ethers_1.ethers.utils.parseUnits(real_transfer_amount_gwei, 'gwei'),
|
|
141
|
-
gasLimit:
|
|
211
|
+
gasLimit: 42000,
|
|
142
212
|
gasPrice: ethers_1.ethers.utils.parseUnits(real_gas_price_gwei, 'gwei'),
|
|
143
|
-
nonce
|
|
213
|
+
nonce,
|
|
144
214
|
chainId: this.chainConfig.chainId
|
|
145
215
|
};
|
|
146
|
-
|
|
147
|
-
(0, dist_1.log_info)(
|
|
148
|
-
|
|
216
|
+
const signedTx = yield wallet.signTransaction(tx_data);
|
|
217
|
+
(0, dist_1.log_info)(`构建 tip 转账交易`, {
|
|
218
|
+
to, nonce,
|
|
219
|
+
tipGwei: real_transfer_amount_gwei,
|
|
220
|
+
txhash: ethers_1.ethers.utils.keccak256(signedTx)
|
|
221
|
+
});
|
|
149
222
|
return signedTx;
|
|
150
223
|
});
|
|
151
224
|
}
|
|
225
|
+
determineInputOutputTokens(order_msg, pool_info) {
|
|
226
|
+
const { aToB } = order_msg;
|
|
227
|
+
const { tokenA, tokenB, quote_token } = pool_info;
|
|
228
|
+
const quoteToken = [tokenA, tokenB].find(token => token.symbol === quote_token);
|
|
229
|
+
const nonQuoteToken = [tokenA, tokenB].find(token => token.symbol !== quote_token);
|
|
230
|
+
const inputToken = aToB ? nonQuoteToken : quoteToken;
|
|
231
|
+
const outputToken = aToB ? quoteToken : nonQuoteToken;
|
|
232
|
+
if (!inputToken || !outputToken) {
|
|
233
|
+
throw new Error(`无法确定输入/输出代币: ${JSON.stringify({
|
|
234
|
+
tokenA: tokenA.symbol, tokenB: tokenB.symbol,
|
|
235
|
+
quote: quote_token, aToB
|
|
236
|
+
})}`);
|
|
237
|
+
}
|
|
238
|
+
return { inputToken, outputToken };
|
|
239
|
+
}
|
|
240
|
+
calculateAmountOutMin(context, inputToken, outputToken) {
|
|
241
|
+
const { price_msg, slippage_bps, order_msg } = context;
|
|
242
|
+
const { aToB } = order_msg;
|
|
243
|
+
const slippage = slippage_bps / 10000;
|
|
244
|
+
const inputAmount = new decimal_js_1.default(order_msg.amount.toString());
|
|
245
|
+
let expectedOut;
|
|
246
|
+
if (aToB) {
|
|
247
|
+
const price = new decimal_js_1.default(price_msg.bid.price);
|
|
248
|
+
expectedOut = inputAmount.mul(price);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const price = new decimal_js_1.default(price_msg.ask.price);
|
|
252
|
+
expectedOut = inputAmount.div(price);
|
|
253
|
+
}
|
|
254
|
+
const minOutput = expectedOut.mul(new decimal_js_1.default(1).sub(new decimal_js_1.default(slippage)));
|
|
255
|
+
return ethers_1.ethers.utils.parseUnits(minOutput.toFixed(outputToken.decimals), outputToken.decimals);
|
|
256
|
+
}
|
|
257
|
+
extractNonceFromErrorMsg(errorMessage) {
|
|
258
|
+
try {
|
|
259
|
+
const match = errorMessage.match(/tx:\s*(\d+)\s*state:\s*(\d+)/);
|
|
260
|
+
if (match) {
|
|
261
|
+
return parseInt(match[2]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (_a) { }
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
152
267
|
}
|
|
153
|
-
exports.
|
|
268
|
+
exports.AbstractDexTrade = AbstractDexTrade;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
export interface CallerHandle {
|
|
3
|
+
wallet: ethers.Wallet;
|
|
4
|
+
nonce: number;
|
|
5
|
+
release: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface CallerManagerConfig {
|
|
8
|
+
chainName: string;
|
|
9
|
+
provider: ethers.providers.JsonRpcProvider;
|
|
10
|
+
callerGroupIds: string[];
|
|
11
|
+
chainId: number;
|
|
12
|
+
lockExpireSeconds?: number;
|
|
13
|
+
acquireTimeoutMs?: number;
|
|
14
|
+
acquireRetryIntervalMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class CallerManager {
|
|
17
|
+
private callers;
|
|
18
|
+
private redis;
|
|
19
|
+
private config;
|
|
20
|
+
constructor(config: CallerManagerConfig);
|
|
21
|
+
init(): Promise<void>;
|
|
22
|
+
acquireCaller(): Promise<CallerHandle>;
|
|
23
|
+
confirmNonce(address: string, confirmedNonce: number): Promise<void>;
|
|
24
|
+
forceSetNonce(address: string, nonce: number): Promise<void>;
|
|
25
|
+
getCallerCount(): number;
|
|
26
|
+
getCallerAddresses(): string[];
|
|
27
|
+
private getNonceRedisKey;
|
|
28
|
+
private getLastUsedRedisKey;
|
|
29
|
+
private getLockKey;
|
|
30
|
+
private getNonce;
|
|
31
|
+
private setNonce;
|
|
32
|
+
private advanceNonce;
|
|
33
|
+
private getCallersSortedByLRU;
|
|
34
|
+
private updateLastUsed;
|
|
35
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
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.CallerManager = void 0;
|
|
13
|
+
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
14
|
+
const ethers_1 = require("ethers");
|
|
15
|
+
const redis_1 = require("../redis");
|
|
16
|
+
const CALLER_NONCE_KEY = 'caller:nonce';
|
|
17
|
+
const CALLER_LAST_USED_KEY = 'caller:last_used';
|
|
18
|
+
const VAULT_CALLERS_KEY = 'vault:callers';
|
|
19
|
+
class CallerManager {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.callers = [];
|
|
22
|
+
this.config = Object.assign({ lockExpireSeconds: 3, acquireTimeoutMs: 2000, acquireRetryIntervalMs: 50 }, config);
|
|
23
|
+
this.redis = new redis_1.SimpleRedisClient(`${config.chainName}:caller`);
|
|
24
|
+
}
|
|
25
|
+
init() {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const walletInfos = (0, dist_1.load_wallet_multi)(this.config.callerGroupIds, false);
|
|
28
|
+
const allWallets = walletInfos.map(info => new ethers_1.ethers.Wallet(info.private_key, this.config.provider));
|
|
29
|
+
(0, dist_1.log_info)(`CallerManager: loaded ${allWallets.length} wallets from CALLER_GROUP_IDS`, allWallets.map(w => w.address));
|
|
30
|
+
const vaultCallersKey = `${this.config.chainName}:${VAULT_CALLERS_KEY}`;
|
|
31
|
+
const allowedAddresses = yield this.redis.lrange(vaultCallersKey);
|
|
32
|
+
if (allowedAddresses && allowedAddresses.length > 0) {
|
|
33
|
+
const allowedSet = new Set(allowedAddresses.map(a => a.toLowerCase()));
|
|
34
|
+
this.callers = allWallets.filter(w => allowedSet.has(w.address.toLowerCase()));
|
|
35
|
+
const matched = this.callers.map(w => w.address);
|
|
36
|
+
const skipped = allWallets.filter(w => !allowedSet.has(w.address.toLowerCase())).map(w => w.address);
|
|
37
|
+
(0, dist_1.log_info)(`CallerManager: matched ${this.callers.length} callers with Vault whitelist`, matched);
|
|
38
|
+
if (skipped.length > 0) {
|
|
39
|
+
(0, dist_1.log_warn)(`CallerManager: skipped ${skipped.length} wallets not in Vault whitelist`, skipped);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.callers = allWallets;
|
|
44
|
+
(0, dist_1.log_warn)(`CallerManager: Vault whitelist not found in Redis (${vaultCallersKey}), using all ${this.callers.length} loaded wallets`);
|
|
45
|
+
}
|
|
46
|
+
if (this.callers.length === 0) {
|
|
47
|
+
throw new Error('CallerManager: no valid callers after whitelist matching');
|
|
48
|
+
}
|
|
49
|
+
yield Promise.all(this.callers.map((caller) => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
const address = caller.address.toLowerCase();
|
|
51
|
+
const nonceKey = this.getNonceRedisKey();
|
|
52
|
+
const existing = yield this.redis.hgetvalue(nonceKey, address);
|
|
53
|
+
if (existing !== null && existing !== undefined) {
|
|
54
|
+
(0, dist_1.log_info)(`Caller ${caller.address} nonce from Redis: ${existing}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const nonce = yield this.config.provider.getTransactionCount(caller.address, 'pending');
|
|
58
|
+
yield this.redis.hsetValue(nonceKey, address, String(nonce), 24 * 60 * 60);
|
|
59
|
+
(0, dist_1.log_info)(`Caller ${caller.address} nonce from chain: ${nonce}`);
|
|
60
|
+
})));
|
|
61
|
+
(0, dist_1.log_info)(`CallerManager initialized, ${this.callers.length} active callers`, this.callers.map(w => w.address));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
acquireCaller() {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
const timeout = this.config.acquireTimeoutMs;
|
|
68
|
+
const retryInterval = this.config.acquireRetryIntervalMs;
|
|
69
|
+
while (Date.now() - startTime < timeout) {
|
|
70
|
+
const sortedCallers = yield this.getCallersSortedByLRU();
|
|
71
|
+
for (const caller of sortedCallers) {
|
|
72
|
+
const lockKey = this.getLockKey(caller.address);
|
|
73
|
+
const lockValue = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
74
|
+
const acquired = yield this.redis.acquireLock(lockKey, lockValue, this.config.lockExpireSeconds);
|
|
75
|
+
if (!acquired)
|
|
76
|
+
continue;
|
|
77
|
+
const nonce = yield this.getNonce(caller.address);
|
|
78
|
+
yield this.updateLastUsed(caller.address);
|
|
79
|
+
(0, dist_1.log_info)(`acquireCaller: ${caller.address}, nonce=${nonce}, took ${Date.now() - startTime}ms`);
|
|
80
|
+
const address = caller.address;
|
|
81
|
+
return {
|
|
82
|
+
wallet: caller,
|
|
83
|
+
nonce,
|
|
84
|
+
release: () => __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
yield this.advanceNonce(address, nonce + 2);
|
|
86
|
+
yield this.redis.releaseLock(lockKey, lockValue);
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
yield (0, dist_1.sleep)(retryInterval);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`acquireCaller failed: all ${this.callers.length} callers are busy, waited ${Date.now() - startTime}ms`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
confirmNonce(address, confirmedNonce) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
const current = yield this.getNonce(address);
|
|
98
|
+
if (confirmedNonce > current) {
|
|
99
|
+
yield this.setNonce(address, confirmedNonce);
|
|
100
|
+
(0, dist_1.log_info)(`confirmNonce: ${address}, ${current} → ${confirmedNonce}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
forceSetNonce(address, nonce) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
const current = yield this.getNonce(address);
|
|
107
|
+
if (nonce !== current) {
|
|
108
|
+
yield this.setNonce(address, nonce);
|
|
109
|
+
(0, dist_1.log_info)(`forceSetNonce: ${address}, ${current} → ${nonce}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
getCallerCount() {
|
|
114
|
+
return this.callers.length;
|
|
115
|
+
}
|
|
116
|
+
getCallerAddresses() {
|
|
117
|
+
return this.callers.map(w => w.address);
|
|
118
|
+
}
|
|
119
|
+
getNonceRedisKey() {
|
|
120
|
+
return `${this.config.chainName}:${CALLER_NONCE_KEY}`;
|
|
121
|
+
}
|
|
122
|
+
getLastUsedRedisKey() {
|
|
123
|
+
return `${this.config.chainName}:${CALLER_LAST_USED_KEY}`;
|
|
124
|
+
}
|
|
125
|
+
getLockKey(address) {
|
|
126
|
+
return `${this.config.chainName}:lock:caller:${address.toLowerCase()}`;
|
|
127
|
+
}
|
|
128
|
+
getNonce(address) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const nonceKey = this.getNonceRedisKey();
|
|
131
|
+
const val = yield this.redis.hgetvalue(nonceKey, address.toLowerCase());
|
|
132
|
+
if (val !== null && val !== undefined) {
|
|
133
|
+
return parseInt(val, 10);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Caller ${address} nonce not found in Redis, init() may not have been called`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
setNonce(address, nonce) {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
const nonceKey = this.getNonceRedisKey();
|
|
141
|
+
yield this.redis.hsetValue(nonceKey, address.toLowerCase(), String(nonce), 24 * 60 * 60);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
advanceNonce(address, newNonce) {
|
|
145
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
146
|
+
const current = yield this.getNonce(address);
|
|
147
|
+
if (newNonce > current) {
|
|
148
|
+
yield this.setNonce(address, newNonce);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getCallersSortedByLRU() {
|
|
153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
const lastUsedKey = this.getLastUsedRedisKey();
|
|
155
|
+
const lastUsedData = yield this.redis.hgetall(lastUsedKey);
|
|
156
|
+
const callersWithTime = this.callers.map(caller => {
|
|
157
|
+
let lastUsedAt = 0;
|
|
158
|
+
const data = lastUsedData === null || lastUsedData === void 0 ? void 0 : lastUsedData[caller.address.toLowerCase()];
|
|
159
|
+
if (data) {
|
|
160
|
+
try {
|
|
161
|
+
lastUsedAt = parseInt(data, 10) || 0;
|
|
162
|
+
}
|
|
163
|
+
catch (_a) { }
|
|
164
|
+
}
|
|
165
|
+
return { caller, lastUsedAt };
|
|
166
|
+
});
|
|
167
|
+
callersWithTime.sort((a, b) => a.lastUsedAt - b.lastUsedAt);
|
|
168
|
+
return callersWithTime.map(item => item.caller);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
updateLastUsed(address) {
|
|
172
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
const lastUsedKey = this.getLastUsedRedisKey();
|
|
174
|
+
yield this.redis.hsetValue(lastUsedKey, address.toLowerCase(), String(Date.now()), 24 * 60 * 60);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.CallerManager = CallerManager;
|
package/dist/trade/index.d.ts
CHANGED
package/dist/trade/index.js
CHANGED
|
@@ -14,8 +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("./send"), exports);
|
|
18
17
|
__exportStar(require("./parse"), exports);
|
|
19
18
|
__exportStar(require("./check"), exports);
|
|
19
|
+
__exportStar(require("./caller_manager"), exports);
|
|
20
20
|
__exportStar(require("./abstract_dex_trade"), exports);
|
|
21
|
-
__exportStar(require("./abstract_dex_trade_plus"), exports);
|
|
@@ -10,6 +10,33 @@ export interface OnchainPoolData {
|
|
|
10
10
|
sqrtPriceX96: string;
|
|
11
11
|
liquidity: string;
|
|
12
12
|
}
|
|
13
|
+
export interface AmmPoolState {
|
|
14
|
+
address: string;
|
|
15
|
+
token0: string;
|
|
16
|
+
token1: string;
|
|
17
|
+
fee: number;
|
|
18
|
+
reserve0: string;
|
|
19
|
+
reserve1: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ClmmPoolState {
|
|
22
|
+
address: string;
|
|
23
|
+
token0: string;
|
|
24
|
+
token1: string;
|
|
25
|
+
fee: number;
|
|
26
|
+
tickSpacing: number;
|
|
27
|
+
tick: number;
|
|
28
|
+
sqrtPriceX96: string;
|
|
29
|
+
liquidity: string;
|
|
30
|
+
}
|
|
31
|
+
export interface InfinityPoolState extends ClmmPoolState {
|
|
32
|
+
hooks: string;
|
|
33
|
+
poolManager: string;
|
|
34
|
+
parameters: string;
|
|
35
|
+
currency0: string;
|
|
36
|
+
currency1: string;
|
|
37
|
+
lpFee?: number;
|
|
38
|
+
protocolFee?: number;
|
|
39
|
+
}
|
|
13
40
|
export interface OnchainPoolChangeEvent {
|
|
14
41
|
provider_id?: string;
|
|
15
42
|
pool_address: string;
|
|
@@ -26,5 +53,9 @@ export interface OnchainPoolChangeEvent {
|
|
|
26
53
|
tick: number;
|
|
27
54
|
sqrtPriceX96: BigInt | string;
|
|
28
55
|
liquidity: BigInt | string;
|
|
56
|
+
tickLower?: number;
|
|
57
|
+
tickUpper?: number;
|
|
58
|
+
liquidityDelta?: string;
|
|
59
|
+
amount?: string;
|
|
29
60
|
};
|
|
30
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clonegod/ttd-bsc-common",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "BSC common library",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"push": "npm run build && npm publish"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@clonegod/ttd-core": "2.1.
|
|
17
|
+
"@clonegod/ttd-core": "2.1.37",
|
|
18
|
+
"@clonegod/ttd-bsc-send-tx": "1.0.0",
|
|
18
19
|
"axios": "^1.12.0",
|
|
19
20
|
"dotenv": "^16.4.7",
|
|
20
21
|
"ethers": "^5.8.0"
|