@clonegod/ttd-sui-common 2.0.13 → 2.0.15
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/trade/executor/balance_probe.d.ts +21 -0
- package/dist/trade/executor/balance_probe.js +324 -0
- package/dist/trade/executor/central_executor.d.ts +14 -15
- package/dist/trade/executor/central_executor.js +211 -320
- package/dist/trade/executor/coin_maintainer.js +2 -1
- package/dist/trade/executor/core_channel.d.ts +1 -0
- package/dist/trade/executor/core_channel.js +25 -0
- package/dist/trade/executor/index.d.ts +1 -0
- package/dist/trade/executor/index.js +1 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEX_ID } from '@clonegod/ttd-core/dist';
|
|
2
|
+
import { ExecutorCore } from './core_channel';
|
|
3
|
+
export type BalanceProbeMode = 'info' | 'offline' | 'sim' | 'sim-token' | 'concurrency' | 'offline-native' | 'sim-native' | 'swap';
|
|
4
|
+
export interface BalanceProbeOptions {
|
|
5
|
+
mode: BalanceProbeMode;
|
|
6
|
+
walletGroupIds: string[];
|
|
7
|
+
walletAddress?: string;
|
|
8
|
+
coinType?: string;
|
|
9
|
+
amount: bigint;
|
|
10
|
+
gasBudget: bigint;
|
|
11
|
+
n: number;
|
|
12
|
+
live: boolean;
|
|
13
|
+
chainIdentifierFallback?: string;
|
|
14
|
+
dexId?: DEX_ID;
|
|
15
|
+
poolId?: string;
|
|
16
|
+
a2b?: boolean;
|
|
17
|
+
amountIn?: bigint;
|
|
18
|
+
depositFirst?: boolean;
|
|
19
|
+
depositOutput?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function runBalanceProbe(core: ExecutorCore, opts: BalanceProbeOptions): Promise<void>;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runBalanceProbe = runBalanceProbe;
|
|
4
|
+
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
5
|
+
const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
|
|
6
|
+
const transactions_1 = require("@mysten/sui/transactions");
|
|
7
|
+
const constants_1 = require("../../constants");
|
|
8
|
+
const swap_1 = require("../swap");
|
|
9
|
+
const core_channel_1 = require("./core_channel");
|
|
10
|
+
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
11
|
+
async function validDuring(core, fallback = '35834a8a') {
|
|
12
|
+
let chain;
|
|
13
|
+
try {
|
|
14
|
+
chain = (await core.getChainIdentifier()).chainIdentifier;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
chain = fallback;
|
|
18
|
+
}
|
|
19
|
+
let e;
|
|
20
|
+
const url = process.env.SUI_GRAPHQL_URL;
|
|
21
|
+
if (url) {
|
|
22
|
+
try {
|
|
23
|
+
e = await (0, core_channel_1.fetchCurrentEpochViaGraphql)(url);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
const s = await core.getCurrentSystemState();
|
|
27
|
+
e = BigInt(s?.systemState?.epoch ?? s?.epoch);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const s = await core.getCurrentSystemState();
|
|
32
|
+
e = BigInt(s?.systemState?.epoch ?? s?.epoch);
|
|
33
|
+
}
|
|
34
|
+
return { ValidDuring: {
|
|
35
|
+
minEpoch: String(e), maxEpoch: String(e + 1n),
|
|
36
|
+
minTimestamp: null, maxTimestamp: null, chain,
|
|
37
|
+
nonce: Math.floor(Math.random() * 0xFFFFFFFF),
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
function buildRoundTrip(wallet, coinType, amount, exp, gasBudget, gasPrice) {
|
|
41
|
+
const tx = new transactions_1.Transaction();
|
|
42
|
+
tx.setSender(wallet);
|
|
43
|
+
const c = (0, transactions_1.coinWithBalance)({ type: coinType, balance: amount, useGasCoin: false });
|
|
44
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [coinType], arguments: [c, tx.pure.address(wallet)] });
|
|
45
|
+
tx.setGasPayment([]);
|
|
46
|
+
tx.setExpiration(exp);
|
|
47
|
+
tx.setGasBudget(gasBudget);
|
|
48
|
+
tx.setGasPrice(gasPrice);
|
|
49
|
+
return tx;
|
|
50
|
+
}
|
|
51
|
+
function buildRoundTripNative(wallet, coinType, amount, exp, gasBudget, gasPrice) {
|
|
52
|
+
const tx = new transactions_1.Transaction();
|
|
53
|
+
tx.setSender(wallet);
|
|
54
|
+
const wd = tx.withdrawal({ amount, type: coinType });
|
|
55
|
+
const [coin] = tx.moveCall({ target: '0x2::coin::redeem_funds', typeArguments: [coinType], arguments: [wd] });
|
|
56
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [coinType], arguments: [coin, tx.pure.address(wallet)] });
|
|
57
|
+
tx.setGasPayment([]);
|
|
58
|
+
tx.setExpiration(exp);
|
|
59
|
+
tx.setGasBudget(gasBudget);
|
|
60
|
+
tx.setGasPrice(gasPrice);
|
|
61
|
+
return tx;
|
|
62
|
+
}
|
|
63
|
+
function splitTopLevelGenerics(s) {
|
|
64
|
+
const out = [];
|
|
65
|
+
let depth = 0, cur = '';
|
|
66
|
+
for (const ch of s) {
|
|
67
|
+
if (ch === '<')
|
|
68
|
+
depth++;
|
|
69
|
+
else if (ch === '>')
|
|
70
|
+
depth--;
|
|
71
|
+
if (ch === ',' && depth === 0) {
|
|
72
|
+
out.push(cur.trim());
|
|
73
|
+
cur = '';
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
cur += ch;
|
|
77
|
+
}
|
|
78
|
+
if (cur.trim())
|
|
79
|
+
out.push(cur.trim());
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
async function readPoolGenerics(core, poolId) {
|
|
83
|
+
const res = await core.getObjects({ objectIds: [poolId] });
|
|
84
|
+
const obj = res.objects[0];
|
|
85
|
+
if (obj instanceof Error)
|
|
86
|
+
throw new Error(`getObjects(${poolId}) 失败: ${obj.message}`);
|
|
87
|
+
const t = obj.type ?? obj.objectType;
|
|
88
|
+
const m = t?.match(/<(.+)>$/);
|
|
89
|
+
if (!m)
|
|
90
|
+
throw new Error(`pool ${poolId} 非双泛型对象,type=${t}`);
|
|
91
|
+
const parts = splitTopLevelGenerics(m[1]);
|
|
92
|
+
if (parts.length !== 2)
|
|
93
|
+
throw new Error(`pool ${poolId} 泛型数 ${parts.length}≠2`);
|
|
94
|
+
return { typeA: parts[0], typeB: parts[1] };
|
|
95
|
+
}
|
|
96
|
+
async function registerShared(core, tx, objectId, mutable) {
|
|
97
|
+
const res = await core.getObjects({ objectIds: [objectId] });
|
|
98
|
+
const obj = res.objects[0];
|
|
99
|
+
if (obj instanceof Error)
|
|
100
|
+
throw new Error(`getObjects(${objectId}) 失败: ${obj.message}`);
|
|
101
|
+
const owner = obj.owner;
|
|
102
|
+
if (owner?.$kind !== 'Shared')
|
|
103
|
+
throw new Error(`${objectId} 非共享对象($kind=${owner?.$kind})`);
|
|
104
|
+
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId, initialSharedVersion: owner.Shared.initialSharedVersion, mutable }));
|
|
105
|
+
}
|
|
106
|
+
function pickWallet(opts) {
|
|
107
|
+
const infos = (0, dist_1.load_wallet_multi)(opts.walletGroupIds, false);
|
|
108
|
+
for (const info of infos) {
|
|
109
|
+
const kp = ed25519_1.Ed25519Keypair.fromSecretKey(info.private_key);
|
|
110
|
+
const address = kp.getPublicKey().toSuiAddress();
|
|
111
|
+
if (!opts.walletAddress || address.toLowerCase() === opts.walletAddress.toLowerCase())
|
|
112
|
+
return { address, kp };
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`未找到钱包 ${opts.walletAddress || '(第一个)'}`);
|
|
115
|
+
}
|
|
116
|
+
async function runBalanceProbe(core, opts) {
|
|
117
|
+
const { mode, amount, gasBudget } = opts;
|
|
118
|
+
const rawClient = core.rawClient;
|
|
119
|
+
const { address: wallet, kp } = pickWallet(opts);
|
|
120
|
+
(0, dist_1.log_info)(`[probe] mode=${mode} wallet=${wallet} amount=${amount} budget=${gasBudget}`);
|
|
121
|
+
if (mode === 'info') {
|
|
122
|
+
const b = await core.getBalance({ owner: wallet, coinType: '0x2::sui::SUI' });
|
|
123
|
+
const ab = BigInt(b.balance.addressBalance ?? '0');
|
|
124
|
+
const total = BigInt(b.balance.balance ?? '0');
|
|
125
|
+
let n = 0;
|
|
126
|
+
let cursor = undefined;
|
|
127
|
+
do {
|
|
128
|
+
const p = await core.listCoins({ owner: wallet, coinType: SUI, cursor });
|
|
129
|
+
n += p.objects.length;
|
|
130
|
+
cursor = p.hasNextPage ? p.cursor : null;
|
|
131
|
+
} while (cursor);
|
|
132
|
+
(0, dist_1.log_info)(`[probe][info] SUI 总额=${total} 其中 address-balance=${ab} 对象部分=${total - ab}(${n} 个 Coin 对象)`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const exp = await validDuring(core, opts.chainIdentifierFallback);
|
|
136
|
+
const gasPrice = BigInt((await core.getReferenceGasPrice()).referenceGasPrice);
|
|
137
|
+
if (mode === 'offline') {
|
|
138
|
+
const tx = buildRoundTrip(wallet, SUI, amount, exp, gasBudget, gasPrice);
|
|
139
|
+
try {
|
|
140
|
+
const bytes = await tx.build();
|
|
141
|
+
(0, dist_1.log_info)(`[probe][offline] ✅ 离线 build 成功(${bytes.length} bytes)—— balance-withdraw 不需 client resolver,全 balance 不损失离线优势`);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
(0, dist_1.log_warn)(`[probe][offline] ❌ 离线 build 失败:${e.message}`);
|
|
145
|
+
(0, dist_1.log_warn)(`[probe][offline] → coinWithBalance resolver 需在线;全 balance 方案需手工拼 withdraw 命令才能保留离线 build`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (mode === 'sim' || mode === 'sim-token') {
|
|
150
|
+
const coinType = mode === 'sim-token' ? (opts.coinType || '') : SUI;
|
|
151
|
+
if (!coinType)
|
|
152
|
+
throw new Error('sim-token 模式需指定 coinType');
|
|
153
|
+
const tag = mode === 'sim-token'
|
|
154
|
+
? `${coinType.split('::').pop()} 输入(gas 仍走 SUI balance)`
|
|
155
|
+
: 'SUI 输入 + gas 同笔共用 SUI balance【CRUX】';
|
|
156
|
+
const tx = buildRoundTrip(wallet, coinType, amount, exp, gasBudget, gasPrice);
|
|
157
|
+
const bytes = await tx.build({ client: rawClient });
|
|
158
|
+
const res = await core.simulateTransaction({ transaction: bytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
159
|
+
if (!res?.Transaction) {
|
|
160
|
+
(0, dist_1.log_error)(`[probe][${mode}] ❌ 链拒绝(${tag}):${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 500)}`, new Error('rejected'));
|
|
161
|
+
(0, dist_1.log_warn)(`[probe][${mode}] → 印证假设:该组合违规,SUI 池需引入独立 gas 钱包(gas 钱包付 gas,交易钱包 balance 出 SUI 输入)`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const ok = res.Transaction.effects?.status?.success;
|
|
165
|
+
const err = res.Transaction.effects?.status?.error;
|
|
166
|
+
(0, dist_1.log_info)(`[probe][${mode}] ${ok ? '✅ 链接受' : '❌ 执行失败'}(${tag})success=${ok}${err ? ' err=' + JSON.stringify(err) : ''} digest=${res.Transaction.digest}`);
|
|
167
|
+
if (ok && mode === 'sim')
|
|
168
|
+
(0, dist_1.log_info)(`[probe][sim] → SUI 单钱包可同笔 gas+swap 共用 address-balance,无需独立 gas 钱包`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (mode === 'offline-native') {
|
|
172
|
+
const tx = buildRoundTripNative(wallet, SUI, amount, exp, gasBudget, gasPrice);
|
|
173
|
+
try {
|
|
174
|
+
const bytes = await tx.build();
|
|
175
|
+
(0, dist_1.log_info)(`[probe][offline-native] ✅ 离线 build 成功(${bytes.length} bytes)—— tx.withdrawal() 原生输入无需 client,全 balance 可保留离线 build`);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
(0, dist_1.log_warn)(`[probe][offline-native] ❌ 仍需 client:${e.message}`);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (mode === 'sim-native') {
|
|
183
|
+
const tx = buildRoundTripNative(wallet, SUI, amount, exp, gasBudget, gasPrice);
|
|
184
|
+
let bytes;
|
|
185
|
+
let builtOffline = true;
|
|
186
|
+
try {
|
|
187
|
+
bytes = await tx.build();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
builtOffline = false;
|
|
191
|
+
bytes = await tx.build({ client: rawClient });
|
|
192
|
+
}
|
|
193
|
+
const res = await core.simulateTransaction({ transaction: bytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
194
|
+
if (!res?.Transaction) {
|
|
195
|
+
(0, dist_1.log_error)(`[probe][sim-native] ❌ 链拒绝:${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 500)}`, new Error('rejected'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const ok = res.Transaction.effects?.status?.success;
|
|
199
|
+
const err = res.Transaction.effects?.status?.error;
|
|
200
|
+
(0, dist_1.log_info)(`[probe][sim-native] ${ok ? '✅ 链接受' : '❌ 执行失败'}(原生 withdrawal+coin::redeem_funds,离线build=${builtOffline})success=${ok}${err ? ' err=' + JSON.stringify(err) : ''}`);
|
|
201
|
+
if (ok && builtOffline)
|
|
202
|
+
(0, dist_1.log_info)(`[probe][sim-native] → 全 balance + 离线 build 双成立:手工拼 withdraw 命令可取代 coinWithBalance`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (mode === 'swap') {
|
|
206
|
+
if (!opts.dexId || !opts.poolId)
|
|
207
|
+
throw new Error('swap 模式需 dexId + poolId');
|
|
208
|
+
const a2b = opts.a2b ?? true;
|
|
209
|
+
const amountIn = opts.amountIn ?? amount;
|
|
210
|
+
const { typeA, typeB } = await readPoolGenerics(core, opts.poolId);
|
|
211
|
+
const inType = a2b ? typeA : typeB;
|
|
212
|
+
const outType = a2b ? typeB : typeA;
|
|
213
|
+
const sym = (t) => t.split('::').pop();
|
|
214
|
+
(0, dist_1.log_info)(`[probe][swap] dex=${opts.dexId} pool=${opts.poolId.slice(0, 12)}… ${sym(inType)}→${sym(outType)} in=${amountIn}`);
|
|
215
|
+
const b = await core.getBalance({ owner: wallet, coinType: inType });
|
|
216
|
+
const ab = BigInt(b.balance.addressBalance ?? '0');
|
|
217
|
+
if (ab < amountIn) {
|
|
218
|
+
if (!(opts.depositFirst && kp)) {
|
|
219
|
+
(0, dist_1.log_warn)(`[probe][swap] ${sym(inType)} address-balance=${ab} < 需 ${amountIn} —— 非 SUI 币种须先充值到 balance 才能测;加 SUI_PROBE_LIVE=true SUI_PROBE_DEPOSIT_FIRST=true 自动从对象币充值,或手动 coin::send_funds`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const need = amountIn - ab + 1n;
|
|
223
|
+
let cursor = undefined;
|
|
224
|
+
let src = null;
|
|
225
|
+
do {
|
|
226
|
+
const p = await core.listCoins({ owner: wallet, coinType: inType, cursor });
|
|
227
|
+
src = p.objects.find((c) => BigInt(c.balance) >= need) || src;
|
|
228
|
+
cursor = p.hasNextPage ? p.cursor : null;
|
|
229
|
+
} while (cursor && !src);
|
|
230
|
+
if (!src) {
|
|
231
|
+
(0, dist_1.log_warn)(`[probe][swap] 无足额 ${sym(inType)} 对象币可充值(需 ${need})`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const dtx = new transactions_1.Transaction();
|
|
235
|
+
dtx.setSender(wallet);
|
|
236
|
+
const [dep] = dtx.splitCoins(dtx.object(transactions_1.Inputs.ObjectRef({ objectId: src.objectId, version: src.version, digest: src.digest })), [dtx.pure.u64(need)]);
|
|
237
|
+
dtx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [dep, dtx.pure.address(wallet)] });
|
|
238
|
+
dtx.setGasPayment([]);
|
|
239
|
+
dtx.setExpiration(exp);
|
|
240
|
+
dtx.setGasBudget(gasBudget);
|
|
241
|
+
dtx.setGasPrice(gasPrice);
|
|
242
|
+
const dbytes = await dtx.build({ client: rawClient });
|
|
243
|
+
const { signature } = await kp.signTransaction(dbytes);
|
|
244
|
+
const dres = await core.executeTransaction({ transaction: dbytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
245
|
+
(0, dist_1.log_info)(`[probe][swap] 充值 ${need} ${sym(inType)} → balance:${dres.Transaction?.effects?.status?.success ? 'OK' : 'FAIL ' + JSON.stringify(dres.Transaction?.effects?.status?.error)} digest=${dres.Transaction?.digest}`);
|
|
246
|
+
if (!dres.Transaction?.effects?.status?.success)
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const tx = new transactions_1.Transaction();
|
|
250
|
+
tx.setSender(wallet);
|
|
251
|
+
await registerShared(core, tx, opts.poolId, true);
|
|
252
|
+
await registerShared(core, tx, (0, swap_1.configSharedObjectId)(opts.dexId), true);
|
|
253
|
+
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId: swap_1.SUI_CLOCK_ID, initialSharedVersion: '1', mutable: false }));
|
|
254
|
+
const wd = tx.withdrawal({ amount: amountIn, type: inType });
|
|
255
|
+
const [inCoin] = tx.moveCall({ target: '0x2::coin::redeem_funds', typeArguments: [inType], arguments: [wd] });
|
|
256
|
+
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(opts.dexId, tx, {
|
|
257
|
+
coinTypeA: typeA, coinTypeB: typeB, poolId: opts.poolId,
|
|
258
|
+
a2b, byAmountIn: true, amount: amountIn, amountLimit: 0n, sqrtPriceLimit: 0n,
|
|
259
|
+
}, inCoin);
|
|
260
|
+
if (opts.depositOutput) {
|
|
261
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [outType], arguments: [outputCoin, tx.pure.address(wallet)] });
|
|
262
|
+
for (const lo of leftoverCoins)
|
|
263
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [lo, tx.pure.address(wallet)] });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
tx.transferObjects([outputCoin, ...leftoverCoins], tx.pure.address(wallet));
|
|
267
|
+
}
|
|
268
|
+
tx.setGasPayment([]);
|
|
269
|
+
tx.setExpiration(exp);
|
|
270
|
+
tx.setGasBudget(gasBudget);
|
|
271
|
+
tx.setGasPrice(gasPrice);
|
|
272
|
+
let builtOffline = true;
|
|
273
|
+
let bytes;
|
|
274
|
+
try {
|
|
275
|
+
bytes = await tx.build();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
builtOffline = false;
|
|
279
|
+
bytes = await tx.build({ client: rawClient });
|
|
280
|
+
}
|
|
281
|
+
const res = await core.simulateTransaction({ transaction: bytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
282
|
+
if (!res?.Transaction) {
|
|
283
|
+
(0, dist_1.log_error)(`[probe][swap] ❌ 链拒绝(${opts.dexId} ${sym(inType)}→${sym(outType)},balance 输入):${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 500)}`, new Error('rejected'));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const ok = res.Transaction.effects?.status?.success;
|
|
287
|
+
const err = res.Transaction.effects?.status?.error;
|
|
288
|
+
const out = (res.Transaction.balanceChanges || []).find((c) => c.coinType?.includes(sym(outType)) && c.address === wallet);
|
|
289
|
+
(0, dist_1.log_info)(`[probe][swap] ${ok ? '✅ DEX 接受 balance 来源输入' : '❌ 执行失败'}(${opts.dexId} ${sym(inType)}→${sym(outType)},离线build=${builtOffline})success=${ok} out≈${out?.amount ?? '?'}${err ? ' err=' + JSON.stringify(err) : ''}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (mode === 'concurrency') {
|
|
293
|
+
if (!opts.live)
|
|
294
|
+
throw new Error('concurrency 模式会真发交易花 gas,须显式开 live');
|
|
295
|
+
if (!kp)
|
|
296
|
+
throw new Error('concurrency 需私钥(load_wallet_multi 未拿到)');
|
|
297
|
+
const n = opts.n;
|
|
298
|
+
(0, dist_1.log_warn)(`[probe][concurrency] ⚠️ 即将真发 ${n} 笔并发交易(各提 ${amount} MIST SUI→存回,gas 走同一 SUI balance),花真 gas`);
|
|
299
|
+
const tasks = Array.from({ length: n }, async (_, i) => {
|
|
300
|
+
const t0 = Date.now();
|
|
301
|
+
try {
|
|
302
|
+
const tx = buildRoundTrip(wallet, SUI, amount, await validDuring(core, opts.chainIdentifierFallback), gasBudget, gasPrice);
|
|
303
|
+
const bytes = await tx.build({ client: rawClient });
|
|
304
|
+
const { signature } = await kp.signTransaction(bytes);
|
|
305
|
+
const digest = await tx.getDigest();
|
|
306
|
+
const resp = await core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
307
|
+
const ok = resp.Transaction?.effects?.status?.success;
|
|
308
|
+
const err = resp.Transaction?.effects?.status?.error;
|
|
309
|
+
return { i, ok, digest, err, ms: Date.now() - t0 };
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
return { i, ok: false, digest: '-', err: e.message, ms: Date.now() - t0 };
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const results = await Promise.all(tasks);
|
|
316
|
+
const succ = results.filter(r => r.ok).length;
|
|
317
|
+
results.forEach(r => (0, dist_1.log_info)(`[probe][concurrency] #${r.i} ${r.ok ? 'OK' : 'FAIL'} ${r.ms}ms digest=${r.digest}${r.err ? ' err=' + JSON.stringify(r.err).slice(0, 200) : ''}`));
|
|
318
|
+
(0, dist_1.log_info)(`[probe][concurrency] 结果:${succ}/${n} 成功 —— 同一 SUI address-balance 同时支撑 ${succ} 笔并发提现+gas`);
|
|
319
|
+
if (succ < n)
|
|
320
|
+
(0, dist_1.log_warn)(`[probe][concurrency] 有失败:address-balance 同源并发提现存在上限/排序约束,需据失败原因判断是结算上限还是 epoch 级限制`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`未知 probe mode=${mode}`);
|
|
324
|
+
}
|
|
@@ -25,7 +25,6 @@ export interface SwapSubmitResult {
|
|
|
25
25
|
}
|
|
26
26
|
export interface CentralExecutorOptions {
|
|
27
27
|
gasBudget?: bigint;
|
|
28
|
-
reconcileAfterTx?: boolean;
|
|
29
28
|
onBroadcastResult?: (r: {
|
|
30
29
|
digest: string;
|
|
31
30
|
success: boolean;
|
|
@@ -51,7 +50,6 @@ export declare class CentralExecutor {
|
|
|
51
50
|
private readonly core;
|
|
52
51
|
private readonly cache;
|
|
53
52
|
private readonly gasBudget;
|
|
54
|
-
private readonly reconcileAfterTx;
|
|
55
53
|
private readonly onBroadcastResult?;
|
|
56
54
|
private tradeWallets;
|
|
57
55
|
private sharedRefCache;
|
|
@@ -66,40 +64,41 @@ export declare class CentralExecutor {
|
|
|
66
64
|
reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
|
|
67
65
|
rebalanceWalletFunds(): Promise<void>;
|
|
68
66
|
private rebalanceOne;
|
|
69
|
-
private
|
|
67
|
+
private rebalanceBalanceMode;
|
|
68
|
+
private sweepObjectsToBalance;
|
|
70
69
|
private decimalsCache;
|
|
71
70
|
private objectReader?;
|
|
72
71
|
private getCoinDecimalsCached;
|
|
73
72
|
private coinSymbol;
|
|
74
73
|
private pairLabel;
|
|
75
74
|
private fmtAmount;
|
|
76
|
-
private walletBalancesLabel;
|
|
77
|
-
private maintainCoinObjects;
|
|
78
|
-
private execMaintenance;
|
|
79
75
|
private poolGenericsCache;
|
|
80
76
|
private getPoolGenerics;
|
|
81
77
|
private canonicalizeReq;
|
|
82
78
|
private getSharedRefCached;
|
|
83
79
|
private chainIdentifier;
|
|
80
|
+
private chainIdInflight?;
|
|
84
81
|
private epochCache;
|
|
82
|
+
private epochInflight?;
|
|
83
|
+
private cachedChainId;
|
|
84
|
+
private cachedEpoch;
|
|
85
85
|
private getValidDuringExpiration;
|
|
86
86
|
private getGasPrice;
|
|
87
|
+
private fetchEpoch;
|
|
87
88
|
private nextTag;
|
|
88
89
|
private inputCoinType;
|
|
89
90
|
private outputCoinType;
|
|
90
|
-
private chooseTradeWallet;
|
|
91
91
|
private registerShared;
|
|
92
92
|
submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
|
|
93
|
-
private
|
|
94
|
-
private
|
|
95
|
-
private
|
|
93
|
+
private submitSwapBalance;
|
|
94
|
+
private broadcastBalanceAndCommit;
|
|
95
|
+
private isInsufficientBalanceError;
|
|
96
|
+
private rebuildWithObjects;
|
|
96
97
|
private toCheckerReceipt;
|
|
97
|
-
private reconcileAfterFailure;
|
|
98
98
|
simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private
|
|
102
|
-
private onSuccess;
|
|
99
|
+
private swapTxShell;
|
|
100
|
+
private finishSwapTx;
|
|
101
|
+
private buildSwapTxBalance;
|
|
103
102
|
get coinCache(): InProcessCoinCache;
|
|
104
103
|
get tradeWalletAddresses(): string[];
|
|
105
104
|
get coreClient(): ExecutorCore;
|
|
@@ -8,6 +8,7 @@ const format_1 = require("../../utils/format");
|
|
|
8
8
|
const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
|
|
9
9
|
const transactions_1 = require("@mysten/sui/transactions");
|
|
10
10
|
const coin_cache_1 = require("./coin_cache");
|
|
11
|
+
const core_channel_1 = require("./core_channel");
|
|
11
12
|
const effects_1 = require("./effects");
|
|
12
13
|
const swap_1 = require("../swap");
|
|
13
14
|
const constants_1 = require("../../constants");
|
|
@@ -43,8 +44,7 @@ class CentralExecutor {
|
|
|
43
44
|
this.poolGenericsCache = new Map();
|
|
44
45
|
this.chainIdentifier = '';
|
|
45
46
|
this.epochCache = null;
|
|
46
|
-
this.gasBudget = opts.gasBudget ??
|
|
47
|
-
this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
|
|
47
|
+
this.gasBudget = opts.gasBudget ?? (0, format_1.suiToMist)(process.env.SUI_GAS_BUDGET, '0.05');
|
|
48
48
|
this.onBroadcastResult = opts.onBroadcastResult;
|
|
49
49
|
this.tradeCoinTypesProvider = opts.tradeCoinTypes;
|
|
50
50
|
this.tradePoolsProvider = opts.tradePools;
|
|
@@ -142,6 +142,7 @@ class CentralExecutor {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
async rebalanceWalletFunds() {
|
|
145
|
+
void this.getValidDuringExpiration().catch(() => { });
|
|
145
146
|
for (const [addr, kp] of this.tradeWallets) {
|
|
146
147
|
try {
|
|
147
148
|
await this.rebalanceOne(addr, kp);
|
|
@@ -152,86 +153,106 @@ class CentralExecutor {
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
async rebalanceOne(addr, kp) {
|
|
156
|
+
return this.rebalanceBalanceMode(addr, kp);
|
|
157
|
+
}
|
|
158
|
+
async rebalanceBalanceMode(addr, kp) {
|
|
155
159
|
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
156
|
-
const balanceMin = BigInt(process.env.SUI_GAS_BALANCE_MIN || '500000000');
|
|
157
|
-
const balanceTarget = BigInt(process.env.SUI_GAS_BALANCE_TARGET || (balanceMin * 2n).toString());
|
|
158
|
-
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
159
|
-
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
160
|
-
const chunk = BigInt(process.env.SUI_REDEEM_MIN_CHUNK || '100000000');
|
|
161
|
-
const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
|
|
162
|
-
await this.reconcileCoins(addr, SUI, true);
|
|
163
|
-
if (this.cache.hasInflight(addr, SUI))
|
|
164
|
-
return;
|
|
165
|
-
const b = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
|
|
166
|
-
const ab = BigInt(b.balance.addressBalance ?? '0');
|
|
167
|
-
const coins = this.cache.snapshot(addr, SUI).coins.slice()
|
|
168
|
-
.sort((x, y) => (BigInt(y.balance) > BigInt(x.balance) ? 1 : BigInt(y.balance) < BigInt(x.balance) ? -1 : 0));
|
|
169
|
-
if (ab < balanceMin) {
|
|
170
|
-
const need = balanceTarget - ab;
|
|
171
|
-
const src = coins.find(c => BigInt(c.balance) >= need + budget);
|
|
172
|
-
if (!src) {
|
|
173
|
-
(0, dist_1.log_warn)(`[executor] ${addr.slice(0, 10)}… 地址余额 ${ab} < min ${balanceMin} 且无足额 SUI coin 充值 —— 请注资(balance 转账即可)`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const tx = new transactions_1.Transaction();
|
|
177
|
-
tx.setSender(addr);
|
|
178
|
-
const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(need)]);
|
|
179
|
-
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [dep, tx.pure.address(addr)] });
|
|
180
|
-
tx.setGasPayment([{ objectId: src.objectId, version: src.version, digest: src.digest }]);
|
|
181
|
-
await this.execMaintenance(tx, addr, kp, budget, `deposit ${need} → balance`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (ab > balanceTarget + chunk) {
|
|
185
|
-
const excess = ab - balanceTarget;
|
|
186
|
-
const rawClient = this.core.rawClient;
|
|
187
|
-
if (!rawClient)
|
|
188
|
-
return;
|
|
189
|
-
const tx = new transactions_1.Transaction();
|
|
190
|
-
tx.setSender(addr);
|
|
191
|
-
tx.transferObjects([(0, transactions_1.coinWithBalance)({ type: '0x2::sui::SUI', balance: excess, useGasCoin: false })], tx.pure.address(addr));
|
|
192
|
-
tx.setGasPayment([]);
|
|
193
|
-
tx.setExpiration(await this.getValidDuringExpiration());
|
|
194
|
-
await this.execMaintenance(tx, addr, kp, budget, `redeem 超额 ${excess} → coin`, rawClient);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
160
|
let tradeTypes = [];
|
|
198
161
|
if (this.tradeCoinTypesProvider) {
|
|
199
162
|
try {
|
|
200
163
|
tradeTypes = (await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t));
|
|
201
164
|
}
|
|
202
165
|
catch (e) {
|
|
203
|
-
(0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick
|
|
166
|
+
(0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只归集 SUI: ${e.message}`);
|
|
204
167
|
}
|
|
205
168
|
}
|
|
206
169
|
const types = [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))];
|
|
170
|
+
const withObjects = [];
|
|
207
171
|
for (const t of types) {
|
|
208
|
-
if (t !== SUI)
|
|
209
|
-
await this.reconcileCoins(addr, t, true);
|
|
210
|
-
if (this.cache.hasInflight(addr, t))
|
|
211
|
-
continue;
|
|
212
|
-
let minSplitT;
|
|
213
172
|
try {
|
|
214
|
-
|
|
173
|
+
const b = await this.core.getBalance({ owner: addr, coinType: t });
|
|
174
|
+
if (BigInt(b.balance.balance ?? '0') > BigInt(b.balance.addressBalance ?? '0'))
|
|
175
|
+
withObjects.push(t);
|
|
215
176
|
}
|
|
216
177
|
catch (e) {
|
|
217
|
-
|
|
178
|
+
withObjects.push(t);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (withObjects.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
for (const t of withObjects)
|
|
184
|
+
await this.reconcileCoins(addr, t, true);
|
|
185
|
+
await this.sweepObjectsToBalance(addr, kp, withObjects, this.gasBudget);
|
|
186
|
+
}
|
|
187
|
+
async sweepObjectsToBalance(addr, kp, coinTypes, budget) {
|
|
188
|
+
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
189
|
+
const plan = [];
|
|
190
|
+
for (const t of [...new Set(coinTypes)]) {
|
|
191
|
+
if (this.cache.hasInflight(addr, t))
|
|
218
192
|
continue;
|
|
193
|
+
const coins = this.cache.snapshot(addr, t).coins;
|
|
194
|
+
if (coins.length === 0)
|
|
195
|
+
continue;
|
|
196
|
+
const tag = this.nextTag('sweep');
|
|
197
|
+
if (!this.cache.reserve(addr, t, coins.map(c => c.objectId), tag))
|
|
198
|
+
continue;
|
|
199
|
+
plan.push({ coinType: t, coins, tag });
|
|
200
|
+
}
|
|
201
|
+
if (plan.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
try {
|
|
204
|
+
let suiAb = 0n;
|
|
205
|
+
try {
|
|
206
|
+
const sb = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
|
|
207
|
+
suiAb = BigInt(sb.balance.addressBalance ?? '0');
|
|
219
208
|
}
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
catch { }
|
|
210
|
+
const balanceGas = suiAb >= budget;
|
|
211
|
+
const suiPlan = plan.find(p => p.coinType === SUI);
|
|
212
|
+
if (!balanceGas && !suiPlan) {
|
|
213
|
+
(0, dist_1.log_warn)(`[executor] sweep 跳过:SUI balance ${suiAb} < gas ${budget} 且无 SUI 对象付 gas(请注资)`);
|
|
222
214
|
return;
|
|
215
|
+
}
|
|
216
|
+
const tx = new transactions_1.Transaction();
|
|
217
|
+
tx.setSender(addr);
|
|
218
|
+
if (balanceGas) {
|
|
219
|
+
tx.setGasPayment([]);
|
|
220
|
+
tx.setExpiration(await this.getValidDuringExpiration());
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
tx.setGasPayment(suiPlan.coins.map(c => ({ objectId: c.objectId, version: c.version, digest: c.digest })));
|
|
224
|
+
const suiTotal = suiPlan.coins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
225
|
+
if (suiTotal > budget) {
|
|
226
|
+
const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(suiTotal - budget)]);
|
|
227
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [SUI], arguments: [dep, tx.pure.address(addr)] });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const p of plan) {
|
|
231
|
+
if (!balanceGas && p.coinType === SUI)
|
|
232
|
+
continue;
|
|
233
|
+
const primary = tx.object(transactions_1.Inputs.ObjectRef(p.coins[0]));
|
|
234
|
+
if (p.coins.length > 1)
|
|
235
|
+
tx.mergeCoins(primary, p.coins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
236
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [p.coinType], arguments: [primary, tx.pure.address(addr)] });
|
|
237
|
+
}
|
|
238
|
+
tx.setGasBudget(budget);
|
|
239
|
+
tx.setGasPrice(await this.getGasPrice());
|
|
240
|
+
const bytes = await tx.build();
|
|
241
|
+
const { signature } = await kp.signTransaction(bytes);
|
|
242
|
+
const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
243
|
+
const st = resp.Transaction?.effects ? (0, effects_1.coreTxStatus)(resp.Transaction.effects) : { success: false, error: 'no effects' };
|
|
244
|
+
const syms = plan.map(p => `${p.coins.length}×${p.coinType.split('::').pop()}`).join(', ');
|
|
245
|
+
(0, dist_1.log_info)(`[executor] sweep 归集 → balance ${st.success ? 'OK' : 'FAIL'} [${syms}] digest=${resp.Transaction?.digest}${st.error ? ' err=' + st.error : ''}`);
|
|
223
246
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
best = { wallet: w, addressBalance: ab, sum: objs + ab };
|
|
247
|
+
catch (e) {
|
|
248
|
+
(0, dist_1.log_error)(`[executor] sweep 归集失败`, e);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
for (const p of plan) {
|
|
252
|
+
this.cache.abort(p.tag, true);
|
|
253
|
+
await this.reconcileCoins(addr, p.coinType);
|
|
254
|
+
}
|
|
233
255
|
}
|
|
234
|
-
return best ? { wallet: best.wallet, addressBalance: best.addressBalance } : null;
|
|
235
256
|
}
|
|
236
257
|
async getCoinDecimalsCached(coinType) {
|
|
237
258
|
let d = this.decimalsCache.get(coinType);
|
|
@@ -261,73 +282,6 @@ class CentralExecutor {
|
|
|
261
282
|
const frac = (v % base).toString().padStart(d, '0').replace(/0+$/, '');
|
|
262
283
|
return `${neg ? '-' : ''}${intStr}${frac ? '.' + frac : ''} ${sym}`;
|
|
263
284
|
}
|
|
264
|
-
walletBalancesLabel(coinType) {
|
|
265
|
-
return [...this.tradeWallets.keys()].map(w => {
|
|
266
|
-
const { total, count } = this.cache.snapshot(w, coinType);
|
|
267
|
-
return `${w.slice(0, 8)}…=${this.fmtAmount(coinType, total)}(${count}枚)`;
|
|
268
|
-
}).join(', ');
|
|
269
|
-
}
|
|
270
|
-
async maintainCoinObjects(addr, kp, coinType, opts) {
|
|
271
|
-
const short = coinType.split('::').pop();
|
|
272
|
-
const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
|
|
273
|
-
const coins = this.cache.snapshot(addr, coinType).coins;
|
|
274
|
-
const valued = coins.filter(c => BigInt(c.balance) > 0n);
|
|
275
|
-
const zeros = coins.filter(c => BigInt(c.balance) === 0n);
|
|
276
|
-
if (coins.length === 0)
|
|
277
|
-
return false;
|
|
278
|
-
const total = valued.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
279
|
-
const maxPieces = opts.minSplit > 0n ? total / opts.minSplit : 0n;
|
|
280
|
-
const pieces = Math.max(1, Number(maxPieces > BigInt(opts.coinTarget) ? BigInt(opts.coinTarget) : maxPieces));
|
|
281
|
-
const needSplit = valued.length > 0 && valued.length < opts.coinTarget && pieces > valued.length;
|
|
282
|
-
const needZeroPurge = zeros.length >= zeroMergeMin;
|
|
283
|
-
const needDefrag = coins.length > opts.coinMax;
|
|
284
|
-
if (!needSplit && !needZeroPurge && !needDefrag)
|
|
285
|
-
return false;
|
|
286
|
-
const tag = this.nextTag('maint');
|
|
287
|
-
if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
|
|
288
|
-
return false;
|
|
289
|
-
const primaryRef = valued[0] ?? coins[0];
|
|
290
|
-
const tx = new transactions_1.Transaction();
|
|
291
|
-
tx.setSender(addr);
|
|
292
|
-
const primary = tx.object(transactions_1.Inputs.ObjectRef(primaryRef));
|
|
293
|
-
const others = coins.filter(c => c.objectId !== primaryRef.objectId);
|
|
294
|
-
if (others.length)
|
|
295
|
-
tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
296
|
-
const per = pieces > 1 ? total / BigInt(pieces) : 0n;
|
|
297
|
-
if (pieces > 1) {
|
|
298
|
-
const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
|
|
299
|
-
tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
|
|
300
|
-
}
|
|
301
|
-
tx.setGasPayment([]);
|
|
302
|
-
tx.setExpiration(await this.getValidDuringExpiration());
|
|
303
|
-
await this.execMaintenance(tx, addr, kp, opts.budget, `整理 ${short}:${valued.length} 值 + ${zeros.length} 壳 → ${pieces} 份均分(每份≈${(total / BigInt(pieces)).toString()})`, undefined, coinType, tag, { primaryId: primaryRef.objectId, perPiece: per, primaryRemainder: total - per * BigInt(pieces - 1), reservedIds: coins.map(c => c.objectId) });
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag, commitPlan) {
|
|
307
|
-
let committed = false;
|
|
308
|
-
try {
|
|
309
|
-
tx.setGasBudget(budget);
|
|
310
|
-
tx.setGasPrice(await this.getGasPrice());
|
|
311
|
-
const bytes = buildClient ? await tx.build({ client: buildClient }) : await tx.build();
|
|
312
|
-
const { signature } = await kp.signTransaction(bytes);
|
|
313
|
-
const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
314
|
-
const tr = resp.Transaction;
|
|
315
|
-
const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
|
|
316
|
-
(0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
|
|
317
|
-
if (success && commitPlan && reserveTag && tr.effects) {
|
|
318
|
-
const changes = (0, effects_1.extractMaintCoinChanges)(tr.effects, { wallet: addr, coinType, ...commitPlan });
|
|
319
|
-
this.cache.commit(reserveTag, changes);
|
|
320
|
-
committed = true;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
finally {
|
|
324
|
-
if (!committed) {
|
|
325
|
-
if (reserveTag)
|
|
326
|
-
this.cache.abort(reserveTag, true);
|
|
327
|
-
await this.reconcileCoins(addr, coinType);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
285
|
async getPoolGenerics(poolId) {
|
|
332
286
|
const hit = this.poolGenericsCache.get(poolId);
|
|
333
287
|
if (hit !== undefined)
|
|
@@ -379,57 +333,64 @@ class CentralExecutor {
|
|
|
379
333
|
}
|
|
380
334
|
return isv;
|
|
381
335
|
}
|
|
382
|
-
async
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
(0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${
|
|
390
|
-
|
|
391
|
-
|
|
336
|
+
async cachedChainId() {
|
|
337
|
+
if (this.chainIdentifier)
|
|
338
|
+
return this.chainIdentifier;
|
|
339
|
+
if (!this.chainIdInflight) {
|
|
340
|
+
this.chainIdInflight = this.core.getChainIdentifier().then(r => r.chainIdentifier)
|
|
341
|
+
.catch(e => {
|
|
342
|
+
const fb = process.env.SUI_CHAIN_IDENTIFIER || '35834a8a';
|
|
343
|
+
(0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${fb}`);
|
|
344
|
+
return fb;
|
|
345
|
+
})
|
|
346
|
+
.then(id => { this.chainIdentifier = id; return id; })
|
|
347
|
+
.finally(() => { this.chainIdInflight = undefined; });
|
|
348
|
+
}
|
|
349
|
+
return this.chainIdInflight;
|
|
350
|
+
}
|
|
351
|
+
async cachedEpoch() {
|
|
392
352
|
const now = Date.now();
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
353
|
+
if (this.epochCache && now - this.epochCache.ts <= 3_600_000)
|
|
354
|
+
return this.epochCache.epoch;
|
|
355
|
+
if (!this.epochInflight) {
|
|
356
|
+
this.epochInflight = this.fetchEpoch()
|
|
357
|
+
.then(e => { this.epochCache = { epoch: e, ts: Date.now() }; return e; })
|
|
358
|
+
.finally(() => { this.epochInflight = undefined; });
|
|
399
359
|
}
|
|
360
|
+
return this.epochInflight;
|
|
361
|
+
}
|
|
362
|
+
async getValidDuringExpiration() {
|
|
363
|
+
const [chain, epoch] = await Promise.all([this.cachedChainId(), this.cachedEpoch()]);
|
|
400
364
|
return { ValidDuring: {
|
|
401
|
-
minEpoch: String(
|
|
402
|
-
maxEpoch: String(
|
|
365
|
+
minEpoch: String(epoch),
|
|
366
|
+
maxEpoch: String(epoch + 1n),
|
|
403
367
|
minTimestamp: null, maxTimestamp: null,
|
|
404
|
-
chain
|
|
368
|
+
chain,
|
|
405
369
|
nonce: Math.floor(Math.random() * 0xFFFFFFFF),
|
|
406
370
|
} };
|
|
407
371
|
}
|
|
408
372
|
async getGasPrice() {
|
|
409
373
|
return BigInt((await this.core.getReferenceGasPrice()).referenceGasPrice);
|
|
410
374
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
let best = '';
|
|
421
|
-
let bestTotal = -1n;
|
|
422
|
-
for (const w of this.tradeWallets.keys()) {
|
|
423
|
-
const { total } = this.cache.snapshot(w, coinType);
|
|
424
|
-
if (total >= amount && total > bestTotal) {
|
|
425
|
-
best = w;
|
|
426
|
-
bestTotal = total;
|
|
375
|
+
async fetchEpoch() {
|
|
376
|
+
const url = process.env.SUI_GRAPHQL_URL;
|
|
377
|
+
if (url) {
|
|
378
|
+
try {
|
|
379
|
+
return await (0, core_channel_1.fetchCurrentEpochViaGraphql)(url);
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
(0, dist_1.log_warn)(`[executor] GraphQL epoch 查询失败,回退 gRPC getCurrentSystemState: ${e.message}`);
|
|
427
383
|
}
|
|
428
384
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
385
|
+
const s = await this.core.getCurrentSystemState();
|
|
386
|
+
const e = s?.systemState?.epoch ?? s?.epoch;
|
|
387
|
+
if (e == null)
|
|
388
|
+
throw new Error('[executor] getCurrentSystemState 未返回 epoch(ValidDuring 必需)');
|
|
389
|
+
return BigInt(e);
|
|
432
390
|
}
|
|
391
|
+
nextTag(prefix) { return `${prefix}:${Date.now()}:${++this.seq}`; }
|
|
392
|
+
inputCoinType(req) { return req.a2b ? req.coinTypeA : req.coinTypeB; }
|
|
393
|
+
outputCoinType(req) { return req.a2b ? req.coinTypeB : req.coinTypeA; }
|
|
433
394
|
async registerShared(tx, objectId, mutable) {
|
|
434
395
|
const initialSharedVersion = await this.getSharedRefCached(objectId);
|
|
435
396
|
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
|
|
@@ -439,118 +400,99 @@ class CentralExecutor {
|
|
|
439
400
|
req = await this.canonicalizeReq(req);
|
|
440
401
|
const inType = this.inputCoinType(req);
|
|
441
402
|
await this.getCoinDecimalsCached(inType).catch(() => { });
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
|
|
450
|
-
}
|
|
451
|
-
catch (e) {
|
|
452
|
-
(0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 对象不足:需 ${this.fmtAmount(inType, req.amountIn)},当前缓存 [${this.walletBalancesLabel(inType)}] → reconcile 重试`);
|
|
453
|
-
await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
|
|
454
|
-
try {
|
|
455
|
-
wallet = this.chooseTradeWallet(req, inType, req.amountIn);
|
|
456
|
-
inputTag = this.nextTag('in');
|
|
457
|
-
inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
|
|
458
|
-
}
|
|
459
|
-
catch (e2) {
|
|
460
|
-
const picked = await this.selectWalletWithBalance(inType, req.amountIn);
|
|
461
|
-
if (!picked) {
|
|
462
|
-
(0, dist_1.log_error)(`[executor] ${this.pairLabel(req)} 余额不足:需 ${this.fmtAmount(inType, req.amountIn)},对象+address-balance 仍不够 [${this.walletBalancesLabel(inType)}]`, e2);
|
|
463
|
-
throw e2;
|
|
464
|
-
}
|
|
465
|
-
wallet = picked.wallet;
|
|
466
|
-
inputTag = this.nextTag('in');
|
|
467
|
-
inputCoins = this.cache.acquireAvailable(wallet, inType, req.amountIn, inputTag);
|
|
468
|
-
const objSum = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
469
|
-
shortfall = req.amountIn - objSum;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const tAcquire = Date.now();
|
|
473
|
-
const acqTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
474
|
-
const shortStr = shortfall > 0n ? ` + address-balance 补 ${this.fmtAmount(inType, shortfall)}` : '';
|
|
475
|
-
(0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 8)}… ${this.pairLabel(req)} 取 ${inputCoins.length} 枚=${this.fmtAmount(inType, acqTotal)}${shortStr}(需 ${this.fmtAmount(inType, req.amountIn)})选钱包+占币 ${tAcquire - t0}ms`);
|
|
403
|
+
return this.submitSwapBalance(req, t0);
|
|
404
|
+
}
|
|
405
|
+
async submitSwapBalance(req, t0) {
|
|
406
|
+
const inType = this.inputCoinType(req);
|
|
407
|
+
const wallet = req.walletAddress ?? this.tradeWalletAddresses[0];
|
|
408
|
+
if (!wallet || !this.tradeWallets.has(wallet))
|
|
409
|
+
return { digest: '', submitted: false, error: `未知/无交易钱包: ${wallet}` };
|
|
476
410
|
try {
|
|
477
|
-
const { txBytes, tx } = await this.
|
|
411
|
+
const { txBytes, tx } = await this.buildSwapTxBalance(req, wallet);
|
|
478
412
|
const tBuild = Date.now();
|
|
479
413
|
const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
480
414
|
const digest = await tx.getDigest();
|
|
481
415
|
const tSign = Date.now();
|
|
482
|
-
void this.
|
|
483
|
-
(0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${this.pairLabel(req)} in=${this.fmtAmount(inType, req.amountIn)} minOut=${this.fmtAmount(this.outputCoinType(req), req.minOut)} cost={build:${tBuild -
|
|
416
|
+
void this.broadcastBalanceAndCommit(txBytes, [senderSig], digest, wallet, req, 0);
|
|
417
|
+
(0, dist_1.log_info)(`[executor] swap 已签提交(balance) digest=${digest} dex=${req.dexId} ${this.pairLabel(req)} in=${this.fmtAmount(inType, req.amountIn)} minOut=${this.fmtAmount(this.outputCoinType(req), req.minOut)} cost={build:${tBuild - t0}ms, sign+digest:${tSign - tBuild}ms, total:${tSign - t0}ms}`);
|
|
484
418
|
return { digest, submitted: true };
|
|
485
419
|
}
|
|
486
420
|
catch (e) {
|
|
487
|
-
|
|
488
|
-
(0, dist_1.log_error)(`[executor] submitSwap 失败 dex=${req.dexId}`, e);
|
|
421
|
+
(0, dist_1.log_error)(`[executor] submitSwapBalance 失败 dex=${req.dexId}`, e);
|
|
489
422
|
return { digest: '', submitted: false, error: e.message };
|
|
490
423
|
}
|
|
491
424
|
}
|
|
492
|
-
async
|
|
425
|
+
async broadcastBalanceAndCommit(txBytes, signatures, digest, wallet, req, attempt = 0, inputTag) {
|
|
493
426
|
const inType = this.inputCoinType(req);
|
|
494
427
|
const outType = this.outputCoinType(req);
|
|
428
|
+
const releaseInput = () => { if (inputTag) {
|
|
429
|
+
this.cache.abort(inputTag, true);
|
|
430
|
+
void this.reconcileCoins(wallet, inType);
|
|
431
|
+
} };
|
|
495
432
|
const tBroadcast = Date.now();
|
|
496
433
|
try {
|
|
497
434
|
const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
498
435
|
const tr = resp.Transaction;
|
|
499
436
|
if (!tr?.effects) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
(0, dist_1.log_error)(`[executor] 响应缺 effects,无法更新 cache digest=${digest}`, new Error('missing effects'));
|
|
437
|
+
releaseInput();
|
|
438
|
+
(0, dist_1.log_error)(`[executor] 响应缺 effects digest=${digest}`, new Error('missing effects'));
|
|
503
439
|
this.onBroadcastResult?.({ digest, success: false, error: 'missing effects' });
|
|
504
440
|
return;
|
|
505
441
|
}
|
|
506
442
|
const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
|
|
507
443
|
if (!success) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
(0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
|
|
444
|
+
releaseInput();
|
|
445
|
+
(0, dist_1.log_warn)(`[executor] swap 链上失败(balance) digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
|
|
511
446
|
this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
|
|
512
447
|
return;
|
|
513
448
|
}
|
|
514
|
-
|
|
515
|
-
(0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
|
|
449
|
+
(0, dist_1.log_info)(`[executor] swap quorum-executed 确认(balance) digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
|
|
516
450
|
this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
|
|
451
|
+
if (inputTag) {
|
|
452
|
+
this.cache.abort(inputTag, true);
|
|
453
|
+
void this.reconcileCoins(wallet, inType);
|
|
454
|
+
}
|
|
517
455
|
}
|
|
518
456
|
catch (e) {
|
|
519
|
-
this.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await this.reconcileCoins(wallet, inType);
|
|
523
|
-
const retry = await this.rebuildSwap(req, wallet);
|
|
457
|
+
if (!inputTag && this.isInsufficientBalanceError(e) && attempt < this.maxRebuildRetries) {
|
|
458
|
+
(0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} address-balance 不足,改用对象 fallback 重建(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
|
|
459
|
+
const retry = await this.rebuildWithObjects(req, wallet);
|
|
524
460
|
if (retry) {
|
|
525
|
-
void this.
|
|
461
|
+
void this.broadcastBalanceAndCommit(retry.txBytes, retry.signatures, retry.digest, wallet, req, attempt + 1, retry.inputTag);
|
|
526
462
|
return;
|
|
527
463
|
}
|
|
528
|
-
(0, dist_1.log_warn)(`[executor]
|
|
464
|
+
(0, dist_1.log_warn)(`[executor] 对象 fallback 也不足,交结果追踪 digest=${digest}`);
|
|
529
465
|
}
|
|
530
|
-
|
|
531
|
-
(0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
|
|
466
|
+
releaseInput();
|
|
467
|
+
(0, dist_1.log_error)(`[executor] broadcast(balance) 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
|
|
532
468
|
}
|
|
533
469
|
}
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
return msg.includes('unavailable for consumption')
|
|
538
|
-
|| msg.includes('needs to be rebuilt')
|
|
539
|
-
|| (err?.code === 'INVALID_ARGUMENT' && msg.includes('object'));
|
|
470
|
+
isInsufficientBalanceError(e) {
|
|
471
|
+
const msg = (e?.message || String(e)).toLowerCase();
|
|
472
|
+
return msg.includes('insufficient') || msg.includes('withdraw') || (msg.includes('balance') && msg.includes('enough'));
|
|
540
473
|
}
|
|
541
|
-
async
|
|
474
|
+
async rebuildWithObjects(req, wallet) {
|
|
542
475
|
try {
|
|
543
476
|
const inType = this.inputCoinType(req);
|
|
477
|
+
await this.reconcileCoins(wallet, inType);
|
|
478
|
+
const b = await this.core.getBalance({ owner: wallet, coinType: inType });
|
|
479
|
+
const total = BigInt(b.balance.balance ?? '0');
|
|
480
|
+
if (total < req.amountIn) {
|
|
481
|
+
(0, dist_1.log_warn)(`[executor] ${inType.split('::').pop()} 总额 ${total} < 需 ${req.amountIn} → 真不足`);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
544
484
|
const inputTag = this.nextTag('in');
|
|
545
|
-
const
|
|
546
|
-
const
|
|
485
|
+
const inputCoins = this.cache.acquireAvailable(wallet, inType, req.amountIn, inputTag);
|
|
486
|
+
const objSum = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
487
|
+
const shortfall = req.amountIn > objSum ? req.amountIn - objSum : 0n;
|
|
488
|
+
const { txBytes, tx } = await this.buildSwapTxBalance(req, wallet, inputCoins, shortfall);
|
|
547
489
|
const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
548
490
|
const digest = await tx.getDigest();
|
|
549
|
-
(0, dist_1.log_info)(`[executor] swap 重建 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
|
|
550
|
-
return { txBytes, signatures: [signature], digest,
|
|
491
|
+
(0, dist_1.log_info)(`[executor] swap 对象 fallback 重建(balance 管线) digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn} 对象=${inputCoins.length}枚 补=${shortfall}`);
|
|
492
|
+
return { txBytes, signatures: [signature], digest, inputTag };
|
|
551
493
|
}
|
|
552
494
|
catch (e) {
|
|
553
|
-
(0, dist_1.log_warn)(`[executor]
|
|
495
|
+
(0, dist_1.log_warn)(`[executor] 对象 fallback 重建失败 dex=${req.dexId}: ${e.message}`);
|
|
554
496
|
return null;
|
|
555
497
|
}
|
|
556
498
|
}
|
|
@@ -578,113 +520,62 @@ class CentralExecutor {
|
|
|
578
520
|
},
|
|
579
521
|
};
|
|
580
522
|
}
|
|
581
|
-
reconcileAfterFailure(wallet, inType, outType) {
|
|
582
|
-
for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG]))
|
|
583
|
-
void this.reconcileCoins(wallet, t);
|
|
584
|
-
}
|
|
585
523
|
async simulateSwap(req) {
|
|
586
524
|
req = await this.canonicalizeReq(req);
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
const { coins } = this.cache.snapshot(wallet, inType);
|
|
590
|
-
if (!coins.length)
|
|
591
|
-
throw new Error(`[executor] simulate 无 ${inType} 输入币(先 reconcileCoins)`);
|
|
592
|
-
const picked = [];
|
|
593
|
-
let sum = 0n;
|
|
594
|
-
for (const c of coins) {
|
|
595
|
-
picked.push(c);
|
|
596
|
-
sum += BigInt(c.balance);
|
|
597
|
-
if (sum >= req.amountIn)
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
if (sum < req.amountIn)
|
|
601
|
-
throw new Error(`[executor] simulate 输入不足 ${sum} < ${req.amountIn}`);
|
|
602
|
-
const { txBytes } = await this.buildSwapTx(req, wallet, picked);
|
|
525
|
+
const wallet = req.walletAddress ?? this.tradeWalletAddresses[0];
|
|
526
|
+
const { txBytes } = await this.buildSwapTxBalance(req, wallet);
|
|
603
527
|
const res = await this.core.simulateTransaction({ transaction: txBytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
604
528
|
if (!res?.Transaction) {
|
|
605
529
|
throw new Error(`[executor] simulate FailedTransaction: ${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 400)}`);
|
|
606
530
|
}
|
|
607
531
|
return res.Transaction;
|
|
608
532
|
}
|
|
609
|
-
async
|
|
610
|
-
if (coinType === constants_1.SUI_TOKEN_ADDRESS.LONG) {
|
|
611
|
-
return BigInt(process.env.SUI_INPUT_COIN_MIN_SPLIT || '100000000');
|
|
612
|
-
}
|
|
613
|
-
const d = await this.getCoinDecimalsCached(coinType);
|
|
614
|
-
return 10n ** BigInt(d) / 10n;
|
|
615
|
-
}
|
|
616
|
-
async postTradeRebalance(wallet, inType, outType) {
|
|
617
|
-
const kp = this.tradeWallets.get(wallet);
|
|
618
|
-
if (!kp)
|
|
619
|
-
return;
|
|
620
|
-
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
621
|
-
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
622
|
-
const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
|
|
623
|
-
for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG])) {
|
|
624
|
-
try {
|
|
625
|
-
await this.reconcileCoins(wallet, t);
|
|
626
|
-
if (this.cache.hasInflight(wallet, t))
|
|
627
|
-
continue;
|
|
628
|
-
await this.maintainCoinObjects(wallet, kp, t, { coinTarget, coinMax, budget, minSplit: await this.minSplitFor(t) });
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
(0, dist_1.log_warn)(`[executor] postTradeRebalance ${t} 失败(交 30s tick 兜底): ${e.message}`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
async buildSwapTx(req, wallet, inputCoins, shortfall = 0n) {
|
|
533
|
+
async swapTxShell(req, wallet) {
|
|
636
534
|
const tx = new transactions_1.Transaction();
|
|
637
535
|
tx.setSender(wallet);
|
|
638
536
|
await this.registerShared(tx, req.poolId, true);
|
|
639
537
|
await this.registerShared(tx, (0, swap_1.configSharedObjectId)(req.dexId), true);
|
|
640
538
|
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId: swap_1.SUI_CLOCK_ID, initialSharedVersion: '1', mutable: false }));
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (inputCoins.length === 0) {
|
|
645
|
-
inCoin = (0, transactions_1.coinWithBalance)({ type: inType, balance: req.amountIn });
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
const primary = tx.object(transactions_1.Inputs.ObjectRef(inputCoins[0]));
|
|
649
|
-
if (inputCoins.length > 1)
|
|
650
|
-
tx.mergeCoins(primary, inputCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
651
|
-
if (shortfall > 0n)
|
|
652
|
-
tx.mergeCoins(primary, [(0, transactions_1.coinWithBalance)({ type: inType, balance: shortfall })]);
|
|
653
|
-
[inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
|
|
654
|
-
}
|
|
655
|
-
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
|
|
656
|
-
coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
|
|
657
|
-
a2b: req.a2b, byAmountIn: true, amount: req.amountIn,
|
|
658
|
-
amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
|
|
659
|
-
}, inCoin);
|
|
660
|
-
tx.transferObjects([outputCoin, ...leftoverCoins], tx.pure.address(wallet));
|
|
539
|
+
return tx;
|
|
540
|
+
}
|
|
541
|
+
async finishSwapTx(tx, online) {
|
|
661
542
|
tx.setGasPayment([]);
|
|
662
543
|
tx.setExpiration(await this.getValidDuringExpiration());
|
|
663
544
|
tx.setGasBudget(this.gasBudget);
|
|
664
545
|
tx.setGasPrice(await this.getGasPrice());
|
|
665
|
-
const txBytes =
|
|
546
|
+
const txBytes = online ? await tx.build({ client: this.core.rawClient }) : await tx.build();
|
|
666
547
|
return { txBytes, tx };
|
|
667
548
|
}
|
|
668
|
-
async
|
|
549
|
+
async buildSwapTxBalance(req, wallet, objectCoins = [], shortfall = 0n) {
|
|
550
|
+
const tx = await this.swapTxShell(req, wallet);
|
|
669
551
|
const inType = this.inputCoinType(req);
|
|
670
552
|
const outType = this.outputCoinType(req);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
553
|
+
const redeem = (amount) => {
|
|
554
|
+
const wd = tx.withdrawal({ amount, type: inType });
|
|
555
|
+
return tx.moveCall({ target: '0x2::coin::redeem_funds', typeArguments: [inType], arguments: [wd] })[0];
|
|
556
|
+
};
|
|
557
|
+
let inCoin;
|
|
558
|
+
if (objectCoins.length === 0) {
|
|
559
|
+
inCoin = redeem(req.amountIn);
|
|
674
560
|
}
|
|
675
561
|
else {
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
if (this.reconcileAfterTx) {
|
|
686
|
-
void this.postTradeRebalance(wallet, inType, outType);
|
|
562
|
+
const objSum = objectCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
563
|
+
const primary = tx.object(transactions_1.Inputs.ObjectRef(objectCoins[0]));
|
|
564
|
+
if (objectCoins.length > 1)
|
|
565
|
+
tx.mergeCoins(primary, objectCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
566
|
+
if (objSum < req.amountIn)
|
|
567
|
+
tx.mergeCoins(primary, [redeem(req.amountIn - objSum)]);
|
|
568
|
+
[inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
|
|
569
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [primary, tx.pure.address(wallet)] });
|
|
687
570
|
}
|
|
571
|
+
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
|
|
572
|
+
coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
|
|
573
|
+
a2b: req.a2b, byAmountIn: true, amount: req.amountIn, amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
|
|
574
|
+
}, inCoin);
|
|
575
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [outType], arguments: [outputCoin, tx.pure.address(wallet)] });
|
|
576
|
+
for (const lo of leftoverCoins)
|
|
577
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [lo, tx.pure.address(wallet)] });
|
|
578
|
+
return this.finishSwapTx(tx, false);
|
|
688
579
|
}
|
|
689
580
|
get coinCache() { return this.cache; }
|
|
690
581
|
get tradeWalletAddresses() { return [...this.tradeWallets.keys()]; }
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CoinMaintainer = void 0;
|
|
4
4
|
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
5
|
+
const format_1 = require("../../utils/format");
|
|
5
6
|
class CoinMaintainer {
|
|
6
7
|
constructor(executor, opts = {}) {
|
|
7
8
|
this.executor = executor;
|
|
8
9
|
this.timer = null;
|
|
9
10
|
this.running = false;
|
|
10
|
-
this.intervalMs = opts.intervalMs ??
|
|
11
|
+
this.intervalMs = opts.intervalMs ?? (0, format_1.parseDurationMs)(process.env.SUI_COIN_MAINTAIN_INTERVAL_MS, 300000);
|
|
11
12
|
}
|
|
12
13
|
start() {
|
|
13
14
|
if (this.timer)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ResilientCore = void 0;
|
|
4
|
+
exports.fetchCurrentEpochViaGraphql = fetchCurrentEpochViaGraphql;
|
|
4
5
|
exports.buildGrpcCore = buildGrpcCore;
|
|
5
6
|
exports.buildGraphqlCore = buildGraphqlCore;
|
|
6
7
|
exports.buildDefaultCore = buildDefaultCore;
|
|
@@ -25,6 +26,30 @@ const GRPC_CLIENT_OPTIONS = {
|
|
|
25
26
|
'grpc-node.max_session_memory': 64,
|
|
26
27
|
'grpc.default_compression_algorithm': 2,
|
|
27
28
|
};
|
|
29
|
+
const _gqlEpochClients = new Map();
|
|
30
|
+
async function fetchCurrentEpochViaGraphql(url, retries = 2) {
|
|
31
|
+
let client = _gqlEpochClients.get(url);
|
|
32
|
+
if (!client) {
|
|
33
|
+
client = new graphql_1.SuiGraphQLClient({ url, network: 'mainnet' });
|
|
34
|
+
_gqlEpochClients.set(url, client);
|
|
35
|
+
}
|
|
36
|
+
let lastErr;
|
|
37
|
+
for (let i = 0; i <= retries; i++) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await client.query({ query: 'query { epoch { epochId } }', variables: {} });
|
|
40
|
+
const e = res?.data?.epoch?.epochId;
|
|
41
|
+
if (e == null)
|
|
42
|
+
throw new Error(`GraphQL epoch 查询无结果: ${JSON.stringify(res?.errors ?? res).slice(0, 200)}`);
|
|
43
|
+
return BigInt(e);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
lastErr = e;
|
|
47
|
+
if (i < retries)
|
|
48
|
+
await (0, dist_1.sleep)(150 * (i + 1));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastErr;
|
|
52
|
+
}
|
|
28
53
|
function buildGrpcCore(opts = {}) {
|
|
29
54
|
const endpoint = opts.endpoint ?? (0, dist_1.getCoreEnv)().grpc_endpoint;
|
|
30
55
|
const token = opts.token ?? (0, dist_1.getCoreEnv)().grpc_token;
|
|
@@ -21,3 +21,4 @@ __exportStar(require("./central_executor"), exports);
|
|
|
21
21
|
__exportStar(require("./coin_maintainer"), exports);
|
|
22
22
|
__exportStar(require("./executor_protocol"), exports);
|
|
23
23
|
__exportStar(require("./executor_ws_client"), exports);
|
|
24
|
+
__exportStar(require("./balance_probe"), exports);
|
package/dist/utils/format.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
1
|
export declare const normalizeSuiTokenAddress: (address: string, useLongAddress?: boolean) => string;
|
|
2
|
+
export declare function suiToMist(sui: string | undefined, fallbackSui: string): bigint;
|
|
3
|
+
export declare function parseDurationMs(s: string | undefined, fallbackMs: number): number;
|
package/dist/utils/format.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeSuiTokenAddress = void 0;
|
|
4
|
+
exports.suiToMist = suiToMist;
|
|
5
|
+
exports.parseDurationMs = parseDurationMs;
|
|
4
6
|
const constants_1 = require("../constants");
|
|
5
7
|
const normalizeSuiTokenAddress = (address, useLongAddress = true) => {
|
|
6
8
|
if (useLongAddress) {
|
|
@@ -16,3 +18,21 @@ const normalizeSuiTokenAddress = (address, useLongAddress = true) => {
|
|
|
16
18
|
return address;
|
|
17
19
|
};
|
|
18
20
|
exports.normalizeSuiTokenAddress = normalizeSuiTokenAddress;
|
|
21
|
+
function suiToMist(sui, fallbackSui) {
|
|
22
|
+
const s = (sui ?? '').trim() || fallbackSui;
|
|
23
|
+
const neg = s.startsWith('-');
|
|
24
|
+
const [intPart, fracPart = ''] = (neg ? s.slice(1) : s).split('.');
|
|
25
|
+
const frac = (fracPart + '000000000').slice(0, 9);
|
|
26
|
+
const mist = BigInt(intPart || '0') * 1000000000n + BigInt(frac || '0');
|
|
27
|
+
return neg ? -mist : mist;
|
|
28
|
+
}
|
|
29
|
+
function parseDurationMs(s, fallbackMs) {
|
|
30
|
+
const v = (s ?? '').trim();
|
|
31
|
+
if (!v)
|
|
32
|
+
return fallbackMs;
|
|
33
|
+
const m = v.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
34
|
+
if (!m)
|
|
35
|
+
return Number(v) || fallbackMs;
|
|
36
|
+
const unit = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
37
|
+
return Math.round(Number(m[1]) * (unit[m[2] || 'ms']));
|
|
38
|
+
}
|