@clonegod/ttd-sui-common 2.0.12 → 2.0.14
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 +13 -1
- package/dist/trade/executor/central_executor.js +281 -48
- package/dist/trade/executor/coin_cache.d.ts +2 -0
- package/dist/trade/executor/coin_cache.js +9 -3
- 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 +9 -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
|
+
}
|
|
@@ -66,6 +66,9 @@ export declare class CentralExecutor {
|
|
|
66
66
|
reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
|
|
67
67
|
rebalanceWalletFunds(): Promise<void>;
|
|
68
68
|
private rebalanceOne;
|
|
69
|
+
private rebalanceBalanceMode;
|
|
70
|
+
private rebalanceObjectMode;
|
|
71
|
+
private sweepObjectsToBalance;
|
|
69
72
|
private selectWalletWithBalance;
|
|
70
73
|
private decimalsCache;
|
|
71
74
|
private objectReader?;
|
|
@@ -84,12 +87,18 @@ export declare class CentralExecutor {
|
|
|
84
87
|
private epochCache;
|
|
85
88
|
private getValidDuringExpiration;
|
|
86
89
|
private getGasPrice;
|
|
90
|
+
private fetchEpoch;
|
|
91
|
+
private fundModeFor;
|
|
87
92
|
private nextTag;
|
|
88
93
|
private inputCoinType;
|
|
89
94
|
private outputCoinType;
|
|
90
95
|
private chooseTradeWallet;
|
|
91
96
|
private registerShared;
|
|
92
97
|
submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
|
|
98
|
+
private submitSwapBalance;
|
|
99
|
+
private broadcastBalanceAndCommit;
|
|
100
|
+
private isInsufficientBalanceError;
|
|
101
|
+
private rebuildWithObjects;
|
|
93
102
|
private broadcastAndCommit;
|
|
94
103
|
private isRebuildableObjectError;
|
|
95
104
|
private rebuildSwap;
|
|
@@ -98,7 +107,10 @@ export declare class CentralExecutor {
|
|
|
98
107
|
simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
|
|
99
108
|
private minSplitFor;
|
|
100
109
|
private postTradeRebalance;
|
|
101
|
-
private
|
|
110
|
+
private swapTxShell;
|
|
111
|
+
private finishSwapTx;
|
|
112
|
+
private buildSwapTxObject;
|
|
113
|
+
private buildSwapTxBalance;
|
|
102
114
|
private onSuccess;
|
|
103
115
|
get coinCache(): InProcessCoinCache;
|
|
104
116
|
get tradeWalletAddresses(): string[];
|
|
@@ -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,12 +44,13 @@ class CentralExecutor {
|
|
|
43
44
|
this.poolGenericsCache = new Map();
|
|
44
45
|
this.chainIdentifier = '';
|
|
45
46
|
this.epochCache = null;
|
|
46
|
-
this.gasBudget = opts.gasBudget ??
|
|
47
|
+
this.gasBudget = opts.gasBudget ?? (0, format_1.suiToMist)(process.env.SUI_GAS_BUDGET, '0.05');
|
|
47
48
|
this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
|
|
48
49
|
this.onBroadcastResult = opts.onBroadcastResult;
|
|
49
50
|
this.tradeCoinTypesProvider = opts.tradeCoinTypes;
|
|
50
51
|
this.tradePoolsProvider = opts.tradePools;
|
|
51
52
|
this.cache.setAmountFormatter((ct, raw) => this.fmtAmount(ct, raw));
|
|
53
|
+
this.decimalsCache.set(constants_1.SUI_TOKEN_ADDRESS.LONG, 9);
|
|
52
54
|
}
|
|
53
55
|
async init() {
|
|
54
56
|
const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -117,6 +119,9 @@ class CentralExecutor {
|
|
|
117
119
|
}
|
|
118
120
|
async reconcileCoins(wallet, coinType, quietIfUnchanged = false) {
|
|
119
121
|
try {
|
|
122
|
+
const abP = this.core.getBalance({ owner: wallet, coinType })
|
|
123
|
+
.then(b => this.cache.setAddressBalance(wallet, coinType, BigInt(b.balance.addressBalance ?? '0')))
|
|
124
|
+
.catch(() => { });
|
|
120
125
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
121
126
|
const epoch = this.cache.mutationEpoch(wallet, coinType);
|
|
122
127
|
const refs = [];
|
|
@@ -128,6 +133,7 @@ class CentralExecutor {
|
|
|
128
133
|
}
|
|
129
134
|
cursor = page.hasNextPage ? page.cursor : null;
|
|
130
135
|
} while (cursor);
|
|
136
|
+
await abP;
|
|
131
137
|
if (this.cache.reconcile(wallet, coinType, refs, epoch, quietIfUnchanged))
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
@@ -147,13 +153,40 @@ class CentralExecutor {
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
async rebalanceOne(addr, kp) {
|
|
156
|
+
if (this.fundModeFor() === 'balance')
|
|
157
|
+
return this.rebalanceBalanceMode(addr, kp);
|
|
158
|
+
return this.rebalanceObjectMode(addr, kp);
|
|
159
|
+
}
|
|
160
|
+
async rebalanceBalanceMode(addr, kp) {
|
|
161
|
+
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
162
|
+
const budget = this.gasBudget;
|
|
163
|
+
let tradeTypes = [];
|
|
164
|
+
if (this.tradeCoinTypesProvider) {
|
|
165
|
+
try {
|
|
166
|
+
tradeTypes = (await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t));
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
(0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只归集 SUI: ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const t of [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))]) {
|
|
173
|
+
await this.reconcileCoins(addr, t, true);
|
|
174
|
+
if (this.cache.hasInflight(addr, t))
|
|
175
|
+
continue;
|
|
176
|
+
if (this.cache.snapshot(addr, t).coins.length === 0)
|
|
177
|
+
continue;
|
|
178
|
+
if (await this.sweepObjectsToBalance(addr, kp, t, budget))
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async rebalanceObjectMode(addr, kp) {
|
|
150
183
|
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
151
|
-
const balanceMin =
|
|
152
|
-
const balanceTarget =
|
|
184
|
+
const balanceMin = (0, format_1.suiToMist)(process.env.SUI_GAS_BALANCE_MIN, '0.5');
|
|
185
|
+
const balanceTarget = (0, format_1.suiToMist)(process.env.SUI_GAS_BALANCE_TARGET, '1');
|
|
186
|
+
const chunk = (0, format_1.suiToMist)(process.env.SUI_REDEEM_MIN_CHUNK, '0.1');
|
|
187
|
+
const budget = this.gasBudget;
|
|
153
188
|
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
154
189
|
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
155
|
-
const chunk = BigInt(process.env.SUI_REDEEM_MIN_CHUNK || '100000000');
|
|
156
|
-
const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
|
|
157
190
|
await this.reconcileCoins(addr, SUI, true);
|
|
158
191
|
if (this.cache.hasInflight(addr, SUI))
|
|
159
192
|
return;
|
|
@@ -165,7 +198,7 @@ class CentralExecutor {
|
|
|
165
198
|
const need = balanceTarget - ab;
|
|
166
199
|
const src = coins.find(c => BigInt(c.balance) >= need + budget);
|
|
167
200
|
if (!src) {
|
|
168
|
-
(0, dist_1.log_warn)(`[executor] ${addr.slice(0, 10)}…
|
|
201
|
+
(0, dist_1.log_warn)(`[executor] ${addr.slice(0, 10)}… SUI address-balance ${ab} < gasMin ${balanceMin} 且无足额 SUI 对象充值 —— 请注资`);
|
|
169
202
|
return;
|
|
170
203
|
}
|
|
171
204
|
const tx = new transactions_1.Transaction();
|
|
@@ -173,7 +206,7 @@ class CentralExecutor {
|
|
|
173
206
|
const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(need)]);
|
|
174
207
|
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [dep, tx.pure.address(addr)] });
|
|
175
208
|
tx.setGasPayment([{ objectId: src.objectId, version: src.version, digest: src.digest }]);
|
|
176
|
-
await this.execMaintenance(tx, addr, kp, budget, `
|
|
209
|
+
await this.execMaintenance(tx, addr, kp, budget, `gas 储备充值 ${need} → balance`);
|
|
177
210
|
return;
|
|
178
211
|
}
|
|
179
212
|
if (ab > balanceTarget + chunk) {
|
|
@@ -198,8 +231,7 @@ class CentralExecutor {
|
|
|
198
231
|
(0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只维护 SUI: ${e.message}`);
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
|
-
const
|
|
202
|
-
for (const t of types) {
|
|
234
|
+
for (const t of [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))]) {
|
|
203
235
|
if (t !== SUI)
|
|
204
236
|
await this.reconcileCoins(addr, t, true);
|
|
205
237
|
if (this.cache.hasInflight(addr, t))
|
|
@@ -212,11 +244,47 @@ class CentralExecutor {
|
|
|
212
244
|
(0, dist_1.log_warn)(`[executor] 读 ${t} decimals 失败,本 tick 跳过该币种维护: ${e.message}`);
|
|
213
245
|
continue;
|
|
214
246
|
}
|
|
215
|
-
|
|
216
|
-
if (acted)
|
|
247
|
+
if (await this.maintainCoinObjects(addr, kp, t, { coinTarget, coinMax, budget, minSplit: minSplitT }))
|
|
217
248
|
return;
|
|
218
249
|
}
|
|
219
250
|
}
|
|
251
|
+
async sweepObjectsToBalance(addr, kp, coinType, budget) {
|
|
252
|
+
const coins = this.cache.snapshot(addr, coinType).coins;
|
|
253
|
+
if (coins.length === 0)
|
|
254
|
+
return false;
|
|
255
|
+
const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
|
|
256
|
+
const tag = this.nextTag('sweep');
|
|
257
|
+
if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
|
|
258
|
+
return false;
|
|
259
|
+
const tx = new transactions_1.Transaction();
|
|
260
|
+
tx.setSender(addr);
|
|
261
|
+
let suiAb = 0n;
|
|
262
|
+
try {
|
|
263
|
+
const sb = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
|
|
264
|
+
suiAb = BigInt(sb.balance.addressBalance ?? '0');
|
|
265
|
+
}
|
|
266
|
+
catch { }
|
|
267
|
+
if (coinType === SUI && suiAb < budget) {
|
|
268
|
+
const total = coins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
269
|
+
if (total <= budget) {
|
|
270
|
+
this.cache.abort(tag, false);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
tx.setGasPayment(coins.map(c => ({ objectId: c.objectId, version: c.version, digest: c.digest })));
|
|
274
|
+
const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(total - budget)]);
|
|
275
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [SUI], arguments: [dep, tx.pure.address(addr)] });
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const primary = tx.object(transactions_1.Inputs.ObjectRef(coins[0]));
|
|
279
|
+
if (coins.length > 1)
|
|
280
|
+
tx.mergeCoins(primary, coins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
281
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [coinType], arguments: [primary, tx.pure.address(addr)] });
|
|
282
|
+
tx.setGasPayment([]);
|
|
283
|
+
tx.setExpiration(await this.getValidDuringExpiration());
|
|
284
|
+
}
|
|
285
|
+
await this.execMaintenance(tx, addr, kp, budget, `归集 ${coins.length} 个 ${coinType.split('::').pop()} 对象 → balance`, undefined, coinType, tag);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
220
288
|
async selectWalletWithBalance(inType, amount) {
|
|
221
289
|
let best = null;
|
|
222
290
|
for (const w of this.tradeWallets.keys()) {
|
|
@@ -386,11 +454,7 @@ class CentralExecutor {
|
|
|
386
454
|
}
|
|
387
455
|
const now = Date.now();
|
|
388
456
|
if (!this.epochCache || now - this.epochCache.ts > 300_000) {
|
|
389
|
-
|
|
390
|
-
const e = s?.systemState?.epoch ?? s?.epoch;
|
|
391
|
-
if (e == null)
|
|
392
|
-
throw new Error('[executor] getCurrentSystemState 未返回 epoch(ValidDuring 必需)');
|
|
393
|
-
this.epochCache = { epoch: BigInt(e), ts: now };
|
|
457
|
+
this.epochCache = { epoch: await this.fetchEpoch(), ts: now };
|
|
394
458
|
}
|
|
395
459
|
return { ValidDuring: {
|
|
396
460
|
minEpoch: String(this.epochCache.epoch),
|
|
@@ -403,6 +467,25 @@ class CentralExecutor {
|
|
|
403
467
|
async getGasPrice() {
|
|
404
468
|
return BigInt((await this.core.getReferenceGasPrice()).referenceGasPrice);
|
|
405
469
|
}
|
|
470
|
+
async fetchEpoch() {
|
|
471
|
+
const url = process.env.SUI_GRAPHQL_URL;
|
|
472
|
+
if (url) {
|
|
473
|
+
try {
|
|
474
|
+
return await (0, core_channel_1.fetchCurrentEpochViaGraphql)(url);
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
(0, dist_1.log_warn)(`[executor] GraphQL epoch 查询失败,回退 gRPC getCurrentSystemState: ${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const s = await this.core.getCurrentSystemState();
|
|
481
|
+
const e = s?.systemState?.epoch ?? s?.epoch;
|
|
482
|
+
if (e == null)
|
|
483
|
+
throw new Error('[executor] getCurrentSystemState 未返回 epoch(ValidDuring 必需)');
|
|
484
|
+
return BigInt(e);
|
|
485
|
+
}
|
|
486
|
+
fundModeFor(_coinType) {
|
|
487
|
+
return (process.env.SUI_FUND_MODE || 'balance').toLowerCase() === 'object' ? 'object' : 'balance';
|
|
488
|
+
}
|
|
406
489
|
nextTag(prefix) { return `${prefix}:${Date.now()}:${++this.seq}`; }
|
|
407
490
|
inputCoinType(req) { return req.a2b ? req.coinTypeA : req.coinTypeB; }
|
|
408
491
|
outputCoinType(req) { return req.a2b ? req.coinTypeB : req.coinTypeA; }
|
|
@@ -434,6 +517,8 @@ class CentralExecutor {
|
|
|
434
517
|
req = await this.canonicalizeReq(req);
|
|
435
518
|
const inType = this.inputCoinType(req);
|
|
436
519
|
await this.getCoinDecimalsCached(inType).catch(() => { });
|
|
520
|
+
if (this.fundModeFor(inType) === 'balance')
|
|
521
|
+
return this.submitSwapBalance(req, t0);
|
|
437
522
|
let wallet = '';
|
|
438
523
|
let inputCoins = [];
|
|
439
524
|
let inputTag = '';
|
|
@@ -469,7 +554,7 @@ class CentralExecutor {
|
|
|
469
554
|
const shortStr = shortfall > 0n ? ` + address-balance 补 ${this.fmtAmount(inType, shortfall)}` : '';
|
|
470
555
|
(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`);
|
|
471
556
|
try {
|
|
472
|
-
const { txBytes, tx } = await this.
|
|
557
|
+
const { txBytes, tx } = await this.buildSwapTxObject(req, wallet, inputCoins, shortfall);
|
|
473
558
|
const tBuild = Date.now();
|
|
474
559
|
const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
475
560
|
const digest = await tx.getDigest();
|
|
@@ -484,6 +569,100 @@ class CentralExecutor {
|
|
|
484
569
|
return { digest: '', submitted: false, error: e.message };
|
|
485
570
|
}
|
|
486
571
|
}
|
|
572
|
+
async submitSwapBalance(req, t0) {
|
|
573
|
+
const inType = this.inputCoinType(req);
|
|
574
|
+
const wallet = req.walletAddress ?? this.tradeWalletAddresses[0];
|
|
575
|
+
if (!wallet || !this.tradeWallets.has(wallet))
|
|
576
|
+
return { digest: '', submitted: false, error: `未知/无交易钱包: ${wallet}` };
|
|
577
|
+
try {
|
|
578
|
+
const { txBytes, tx } = await this.buildSwapTxBalance(req, wallet);
|
|
579
|
+
const tBuild = Date.now();
|
|
580
|
+
const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
581
|
+
const digest = await tx.getDigest();
|
|
582
|
+
const tSign = Date.now();
|
|
583
|
+
void this.broadcastBalanceAndCommit(txBytes, [senderSig], digest, wallet, req, 0);
|
|
584
|
+
(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}`);
|
|
585
|
+
return { digest, submitted: true };
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
(0, dist_1.log_error)(`[executor] submitSwapBalance 失败 dex=${req.dexId}`, e);
|
|
589
|
+
return { digest: '', submitted: false, error: e.message };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async broadcastBalanceAndCommit(txBytes, signatures, digest, wallet, req, attempt = 0, inputTag) {
|
|
593
|
+
const inType = this.inputCoinType(req);
|
|
594
|
+
const outType = this.outputCoinType(req);
|
|
595
|
+
const releaseInput = () => { if (inputTag) {
|
|
596
|
+
this.cache.abort(inputTag, true);
|
|
597
|
+
void this.reconcileCoins(wallet, inType);
|
|
598
|
+
} };
|
|
599
|
+
const tBroadcast = Date.now();
|
|
600
|
+
try {
|
|
601
|
+
const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
602
|
+
const tr = resp.Transaction;
|
|
603
|
+
if (!tr?.effects) {
|
|
604
|
+
releaseInput();
|
|
605
|
+
(0, dist_1.log_error)(`[executor] 响应缺 effects digest=${digest}`, new Error('missing effects'));
|
|
606
|
+
this.onBroadcastResult?.({ digest, success: false, error: 'missing effects' });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
|
|
610
|
+
if (!success) {
|
|
611
|
+
releaseInput();
|
|
612
|
+
(0, dist_1.log_warn)(`[executor] swap 链上失败(balance) digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
|
|
613
|
+
this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (inputTag)
|
|
617
|
+
this.cache.abort(inputTag, true);
|
|
618
|
+
(0, dist_1.log_info)(`[executor] swap quorum-executed 确认(balance) digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
|
|
619
|
+
this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
|
|
620
|
+
if (this.reconcileAfterTx)
|
|
621
|
+
void this.postTradeRebalance(wallet, inType, outType);
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
if (!inputTag && this.isInsufficientBalanceError(e) && attempt < this.maxRebuildRetries) {
|
|
625
|
+
(0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} address-balance 不足,改用对象 fallback 重建(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
|
|
626
|
+
const retry = await this.rebuildWithObjects(req, wallet);
|
|
627
|
+
if (retry) {
|
|
628
|
+
void this.broadcastBalanceAndCommit(retry.txBytes, retry.signatures, retry.digest, wallet, req, attempt + 1, retry.inputTag);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
(0, dist_1.log_warn)(`[executor] 对象 fallback 也不足,交结果追踪 digest=${digest}`);
|
|
632
|
+
}
|
|
633
|
+
releaseInput();
|
|
634
|
+
(0, dist_1.log_error)(`[executor] broadcast(balance) 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
isInsufficientBalanceError(e) {
|
|
638
|
+
const msg = (e?.message || String(e)).toLowerCase();
|
|
639
|
+
return msg.includes('insufficient') || msg.includes('withdraw') || (msg.includes('balance') && msg.includes('enough'));
|
|
640
|
+
}
|
|
641
|
+
async rebuildWithObjects(req, wallet) {
|
|
642
|
+
try {
|
|
643
|
+
const inType = this.inputCoinType(req);
|
|
644
|
+
await this.reconcileCoins(wallet, inType);
|
|
645
|
+
const b = await this.core.getBalance({ owner: wallet, coinType: inType });
|
|
646
|
+
const total = BigInt(b.balance.balance ?? '0');
|
|
647
|
+
if (total < req.amountIn) {
|
|
648
|
+
(0, dist_1.log_warn)(`[executor] ${inType.split('::').pop()} 总额 ${total} < 需 ${req.amountIn} → 真不足`);
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const inputTag = this.nextTag('in');
|
|
652
|
+
const inputCoins = this.cache.acquireAvailable(wallet, inType, req.amountIn, inputTag);
|
|
653
|
+
const objSum = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
654
|
+
const shortfall = req.amountIn > objSum ? req.amountIn - objSum : 0n;
|
|
655
|
+
const { txBytes, tx } = await this.buildSwapTxBalance(req, wallet, inputCoins, shortfall);
|
|
656
|
+
const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
657
|
+
const digest = await tx.getDigest();
|
|
658
|
+
(0, dist_1.log_info)(`[executor] swap 对象 fallback 重建(balance 管线) digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn} 对象=${inputCoins.length}枚 补=${shortfall}`);
|
|
659
|
+
return { txBytes, signatures: [signature], digest, inputTag };
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
(0, dist_1.log_warn)(`[executor] 对象 fallback 重建失败 dex=${req.dexId}: ${e.message}`);
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
487
666
|
async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0, usedAddressBalance = false) {
|
|
488
667
|
const inType = this.inputCoinType(req);
|
|
489
668
|
const outType = this.outputCoinType(req);
|
|
@@ -538,7 +717,7 @@ class CentralExecutor {
|
|
|
538
717
|
const inType = this.inputCoinType(req);
|
|
539
718
|
const inputTag = this.nextTag('in');
|
|
540
719
|
const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
|
|
541
|
-
const { txBytes, tx } = await this.
|
|
720
|
+
const { txBytes, tx } = await this.buildSwapTxObject(req, wallet, inputRes.coins);
|
|
542
721
|
const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
543
722
|
const digest = await tx.getDigest();
|
|
544
723
|
(0, dist_1.log_info)(`[executor] swap 重建 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
|
|
@@ -580,21 +759,28 @@ class CentralExecutor {
|
|
|
580
759
|
async simulateSwap(req) {
|
|
581
760
|
req = await this.canonicalizeReq(req);
|
|
582
761
|
const inType = this.inputCoinType(req);
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
762
|
+
const balanceMode = this.fundModeFor() === 'balance';
|
|
763
|
+
const wallet = req.walletAddress ?? (balanceMode ? this.tradeWalletAddresses[0] : this.chooseTradeWallet(req, inType, req.amountIn));
|
|
764
|
+
let txBytes;
|
|
765
|
+
if (balanceMode) {
|
|
766
|
+
({ txBytes } = await this.buildSwapTxBalance(req, wallet));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
const { coins } = this.cache.snapshot(wallet, inType);
|
|
770
|
+
if (!coins.length)
|
|
771
|
+
throw new Error(`[executor] simulate 无 ${inType} 输入币(先 reconcileCoins)`);
|
|
772
|
+
const picked = [];
|
|
773
|
+
let sum = 0n;
|
|
774
|
+
for (const c of coins) {
|
|
775
|
+
picked.push(c);
|
|
776
|
+
sum += BigInt(c.balance);
|
|
777
|
+
if (sum >= req.amountIn)
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
if (sum < req.amountIn)
|
|
781
|
+
throw new Error(`[executor] simulate 输入不足 ${sum} < ${req.amountIn}`);
|
|
782
|
+
({ txBytes } = await this.buildSwapTxObject(req, wallet, picked));
|
|
783
|
+
}
|
|
598
784
|
const res = await this.core.simulateTransaction({ transaction: txBytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
599
785
|
if (!res?.Transaction) {
|
|
600
786
|
throw new Error(`[executor] simulate FailedTransaction: ${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 400)}`);
|
|
@@ -603,7 +789,7 @@ class CentralExecutor {
|
|
|
603
789
|
}
|
|
604
790
|
async minSplitFor(coinType) {
|
|
605
791
|
if (coinType === constants_1.SUI_TOKEN_ADDRESS.LONG) {
|
|
606
|
-
return
|
|
792
|
+
return (0, format_1.suiToMist)(process.env.SUI_INPUT_COIN_MIN_SPLIT, '0.1');
|
|
607
793
|
}
|
|
608
794
|
const d = await this.getCoinDecimalsCached(coinType);
|
|
609
795
|
return 10n ** BigInt(d) / 10n;
|
|
@@ -614,51 +800,98 @@ class CentralExecutor {
|
|
|
614
800
|
return;
|
|
615
801
|
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
616
802
|
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
617
|
-
const budget =
|
|
803
|
+
const budget = this.gasBudget;
|
|
618
804
|
for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG])) {
|
|
619
805
|
try {
|
|
620
806
|
await this.reconcileCoins(wallet, t);
|
|
621
807
|
if (this.cache.hasInflight(wallet, t))
|
|
622
808
|
continue;
|
|
623
|
-
|
|
809
|
+
if (this.fundModeFor(t) === 'balance') {
|
|
810
|
+
if (this.cache.snapshot(wallet, t).coins.length > 0)
|
|
811
|
+
await this.sweepObjectsToBalance(wallet, kp, t, budget);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
await this.maintainCoinObjects(wallet, kp, t, { coinTarget, coinMax, budget, minSplit: await this.minSplitFor(t) });
|
|
815
|
+
}
|
|
624
816
|
}
|
|
625
817
|
catch (e) {
|
|
626
|
-
(0, dist_1.log_warn)(`[executor] postTradeRebalance ${t}
|
|
818
|
+
(0, dist_1.log_warn)(`[executor] postTradeRebalance ${t} 失败(交定时 tick 兜底): ${e.message}`);
|
|
627
819
|
}
|
|
628
820
|
}
|
|
629
821
|
}
|
|
630
|
-
async
|
|
822
|
+
async swapTxShell(req, wallet) {
|
|
631
823
|
const tx = new transactions_1.Transaction();
|
|
632
824
|
tx.setSender(wallet);
|
|
633
825
|
await this.registerShared(tx, req.poolId, true);
|
|
634
826
|
await this.registerShared(tx, (0, swap_1.configSharedObjectId)(req.dexId), true);
|
|
635
827
|
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId: swap_1.SUI_CLOCK_ID, initialSharedVersion: '1', mutable: false }));
|
|
828
|
+
return tx;
|
|
829
|
+
}
|
|
830
|
+
async finishSwapTx(tx, online) {
|
|
831
|
+
tx.setGasPayment([]);
|
|
832
|
+
tx.setExpiration(await this.getValidDuringExpiration());
|
|
833
|
+
tx.setGasBudget(this.gasBudget);
|
|
834
|
+
tx.setGasPrice(await this.getGasPrice());
|
|
835
|
+
const txBytes = online ? await tx.build({ client: this.core.rawClient }) : await tx.build();
|
|
836
|
+
return { txBytes, tx };
|
|
837
|
+
}
|
|
838
|
+
async buildSwapTxObject(req, wallet, inputCoins, shortfall = 0n) {
|
|
839
|
+
const tx = await this.swapTxShell(req, wallet);
|
|
636
840
|
const inType = this.inputCoinType(req);
|
|
637
|
-
const usedBalance = shortfall > 0n || inputCoins.length === 0;
|
|
638
841
|
let inCoin;
|
|
842
|
+
let usedResolver = false;
|
|
639
843
|
if (inputCoins.length === 0) {
|
|
640
844
|
inCoin = (0, transactions_1.coinWithBalance)({ type: inType, balance: req.amountIn });
|
|
845
|
+
usedResolver = true;
|
|
641
846
|
}
|
|
642
847
|
else {
|
|
643
848
|
const primary = tx.object(transactions_1.Inputs.ObjectRef(inputCoins[0]));
|
|
644
849
|
if (inputCoins.length > 1)
|
|
645
850
|
tx.mergeCoins(primary, inputCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
646
|
-
if (shortfall > 0n)
|
|
851
|
+
if (shortfall > 0n) {
|
|
647
852
|
tx.mergeCoins(primary, [(0, transactions_1.coinWithBalance)({ type: inType, balance: shortfall })]);
|
|
853
|
+
usedResolver = true;
|
|
854
|
+
}
|
|
855
|
+
;
|
|
648
856
|
[inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
|
|
649
857
|
}
|
|
650
858
|
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
|
|
651
859
|
coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
|
|
652
|
-
a2b: req.a2b, byAmountIn: true, amount: req.amountIn,
|
|
653
|
-
amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
|
|
860
|
+
a2b: req.a2b, byAmountIn: true, amount: req.amountIn, amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
|
|
654
861
|
}, inCoin);
|
|
655
862
|
tx.transferObjects([outputCoin, ...leftoverCoins], tx.pure.address(wallet));
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
tx
|
|
660
|
-
const
|
|
661
|
-
|
|
863
|
+
return this.finishSwapTx(tx, usedResolver);
|
|
864
|
+
}
|
|
865
|
+
async buildSwapTxBalance(req, wallet, objectCoins = [], shortfall = 0n) {
|
|
866
|
+
const tx = await this.swapTxShell(req, wallet);
|
|
867
|
+
const inType = this.inputCoinType(req);
|
|
868
|
+
const outType = this.outputCoinType(req);
|
|
869
|
+
const redeem = (amount) => {
|
|
870
|
+
const wd = tx.withdrawal({ amount, type: inType });
|
|
871
|
+
return tx.moveCall({ target: '0x2::coin::redeem_funds', typeArguments: [inType], arguments: [wd] })[0];
|
|
872
|
+
};
|
|
873
|
+
let inCoin;
|
|
874
|
+
if (objectCoins.length === 0) {
|
|
875
|
+
inCoin = redeem(req.amountIn);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
const objSum = objectCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
879
|
+
const primary = tx.object(transactions_1.Inputs.ObjectRef(objectCoins[0]));
|
|
880
|
+
if (objectCoins.length > 1)
|
|
881
|
+
tx.mergeCoins(primary, objectCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
882
|
+
if (objSum < req.amountIn)
|
|
883
|
+
tx.mergeCoins(primary, [redeem(req.amountIn - objSum)]);
|
|
884
|
+
[inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
|
|
885
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [primary, tx.pure.address(wallet)] });
|
|
886
|
+
}
|
|
887
|
+
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
|
|
888
|
+
coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
|
|
889
|
+
a2b: req.a2b, byAmountIn: true, amount: req.amountIn, amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
|
|
890
|
+
}, inCoin);
|
|
891
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [outType], arguments: [outputCoin, tx.pure.address(wallet)] });
|
|
892
|
+
for (const lo of leftoverCoins)
|
|
893
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [lo, tx.pure.address(wallet)] });
|
|
894
|
+
return this.finishSwapTx(tx, false);
|
|
662
895
|
}
|
|
663
896
|
async onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance = false) {
|
|
664
897
|
const inType = this.inputCoinType(req);
|
|
@@ -11,6 +11,8 @@ export declare class InProcessCoinCache {
|
|
|
11
11
|
private amountFmt?;
|
|
12
12
|
setAmountFormatter(fn: (coinType: string, raw: bigint) => string): void;
|
|
13
13
|
private fmt;
|
|
14
|
+
private addressBalances;
|
|
15
|
+
setAddressBalance(wallet: string, coinType: string, ab: bigint): void;
|
|
14
16
|
logCoinBreakdown(wallet: string, coinType: string, reason: string, quiet?: boolean): void;
|
|
15
17
|
private walletMap;
|
|
16
18
|
private list;
|
|
@@ -8,6 +8,7 @@ class InProcessCoinCache {
|
|
|
8
8
|
this.inflight = new Map();
|
|
9
9
|
this.committedAt = new Map();
|
|
10
10
|
this.epochs = new Map();
|
|
11
|
+
this.addressBalances = new Map();
|
|
11
12
|
}
|
|
12
13
|
commitKey(wallet, coinType, objectId) { return `${wallet}|${coinType}|${objectId}`; }
|
|
13
14
|
epochKey(wallet, coinType) { return `${wallet}|${coinType}`; }
|
|
@@ -20,6 +21,7 @@ class InProcessCoinCache {
|
|
|
20
21
|
}
|
|
21
22
|
setAmountFormatter(fn) { this.amountFmt = fn; }
|
|
22
23
|
fmt(coinType, raw) { return this.amountFmt ? this.amountFmt(coinType, raw) : `${raw.toString()}(raw)`; }
|
|
24
|
+
setAddressBalance(wallet, coinType, ab) { this.addressBalances.set(`${wallet}|${coinType}`, ab); }
|
|
23
25
|
logCoinBreakdown(wallet, coinType, reason, quiet = false) {
|
|
24
26
|
const avail = this.list(wallet, coinType);
|
|
25
27
|
const inUse = [];
|
|
@@ -29,7 +31,9 @@ class InProcessCoinCache {
|
|
|
29
31
|
}
|
|
30
32
|
const symbol = coinType.split('::').pop();
|
|
31
33
|
const sum = (cs) => cs.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
32
|
-
const availT = sum(avail), useT = sum(inUse),
|
|
34
|
+
const availT = sum(avail), useT = sum(inUse), objT = availT + useT;
|
|
35
|
+
const ab = this.addressBalances.get(`${wallet}|${coinType}`) ?? 0n;
|
|
36
|
+
const grand = objT + ab;
|
|
33
37
|
const mapObj = (cs) => cs.map(c => ({
|
|
34
38
|
objectId: c.objectId,
|
|
35
39
|
version: c.version,
|
|
@@ -38,11 +42,13 @@ class InProcessCoinCache {
|
|
|
38
42
|
const detail = {
|
|
39
43
|
reason, wallet, symbol, coinType,
|
|
40
44
|
total: this.fmt(coinType, grand),
|
|
45
|
+
address_balance: this.fmt(coinType, ab),
|
|
46
|
+
objects_total: this.fmt(coinType, objT),
|
|
41
47
|
available_count: avail.length, available_total: this.fmt(coinType, availT), available: mapObj(avail),
|
|
42
48
|
in_use_count: inUse.length, in_use_total: this.fmt(coinType, useT), in_use: mapObj(inUse),
|
|
43
49
|
};
|
|
44
|
-
const useStr = inUse.length ?
|
|
45
|
-
const header = `[coin-cache] ${reason} ${symbol}: 共 ${this.fmt(coinType, grand)}
|
|
50
|
+
const useStr = inUse.length ? ` +${inUse.length}个交易中` : '';
|
|
51
|
+
const header = `[coin-cache] ${reason} ${symbol}: 共 ${this.fmt(coinType, grand)} = 对象 ${this.fmt(coinType, objT)}(${avail.length}个${useStr}) + address-balance ${this.fmt(coinType, ab)}`;
|
|
46
52
|
if (quiet)
|
|
47
53
|
(0, dist_1.log_debug)(header, detail);
|
|
48
54
|
else
|
|
@@ -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, 60000);
|
|
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,14 @@ const GRPC_CLIENT_OPTIONS = {
|
|
|
25
26
|
'grpc-node.max_session_memory': 64,
|
|
26
27
|
'grpc.default_compression_algorithm': 2,
|
|
27
28
|
};
|
|
29
|
+
async function fetchCurrentEpochViaGraphql(url) {
|
|
30
|
+
const client = new graphql_1.SuiGraphQLClient({ url, network: 'mainnet' });
|
|
31
|
+
const res = await client.query({ query: 'query { epoch { epochId } }', variables: {} });
|
|
32
|
+
const e = res?.data?.epoch?.epochId;
|
|
33
|
+
if (e == null)
|
|
34
|
+
throw new Error(`GraphQL epoch 查询无结果: ${JSON.stringify(res?.errors ?? res).slice(0, 200)}`);
|
|
35
|
+
return BigInt(e);
|
|
36
|
+
}
|
|
28
37
|
function buildGrpcCore(opts = {}) {
|
|
29
38
|
const endpoint = opts.endpoint ?? (0, dist_1.getCoreEnv)().grpc_endpoint;
|
|
30
39
|
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
|
+
}
|