@clonegod/ttd-sui-common 2.0.6 → 2.0.7
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/quote/abstract_dex_quote.js +3 -0
- package/dist/quote/depth/clmm_depth_calculator.d.ts +28 -0
- package/dist/quote/depth/clmm_depth_calculator.js +114 -0
- package/dist/quote/depth/index.d.ts +24 -0
- package/dist/quote/depth/index.js +99 -0
- package/dist/quote/index.d.ts +1 -0
- package/dist/quote/index.js +1 -0
- package/dist/trade/executor/central_executor.d.ts +12 -1
- package/dist/trade/executor/central_executor.js +216 -53
- package/dist/trade/executor/coin_cache.d.ts +7 -1
- package/dist/trade/executor/coin_cache.js +44 -2
- package/dist/trade/parse/sui_tx_parser.js +9 -1
- package/package.json +1 -1
|
@@ -173,6 +173,9 @@ class AbstractDexQuote {
|
|
|
173
173
|
streamTime: result.streamTimestamp, quoteStartTime: result.quoteStartTime, blockNumber: result.blockNumber,
|
|
174
174
|
quotes: [result.askQuote, result.bidQuote], txid: result.txid, source: result.source, depth: result.depth,
|
|
175
175
|
});
|
|
176
|
+
if (isV2) {
|
|
177
|
+
(0, dist_1.log_info)(`[QUOTE OK v2] ${result.poolInfo.pool_name} ask=${result.askQuote.price} bid=${result.bidQuote.price} blk=${result.blockNumber} tx=${result.txIndex} ${result.txid}`);
|
|
178
|
+
}
|
|
176
179
|
}
|
|
177
180
|
result.trace?.set('published', shouldPush);
|
|
178
181
|
result.trace?.mark('publish');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DepthTick {
|
|
2
|
+
index: number;
|
|
3
|
+
liquidityNet: bigint;
|
|
4
|
+
sqrtPriceX64: bigint;
|
|
5
|
+
}
|
|
6
|
+
export interface ClmmDepthInput {
|
|
7
|
+
sqrtPriceX64: bigint;
|
|
8
|
+
currentTick: number;
|
|
9
|
+
liquidity: bigint;
|
|
10
|
+
ticks: DepthTick[];
|
|
11
|
+
zeroForOne: boolean;
|
|
12
|
+
targetBps: number;
|
|
13
|
+
inputDecimals: number;
|
|
14
|
+
outputDecimals: number;
|
|
15
|
+
}
|
|
16
|
+
export interface DepthResult {
|
|
17
|
+
amountInWei: bigint;
|
|
18
|
+
amountIn: number;
|
|
19
|
+
amountOutWei: bigint;
|
|
20
|
+
amountOut: number;
|
|
21
|
+
currentTick: number;
|
|
22
|
+
targetTick: number;
|
|
23
|
+
targetSqrtPriceX64: bigint;
|
|
24
|
+
}
|
|
25
|
+
export declare function calculateClmmDepth(input: ClmmDepthInput): DepthResult;
|
|
26
|
+
export declare function priceFromSqrtX64(sqrtPriceX64: bigint, baseDecimals: number, quoteDecimals: number, baseIsToken0: boolean): number;
|
|
27
|
+
export declare function computeTargetSqrtX64(currentSqrtPriceX64: bigint, bps: number, zeroForOne: boolean): bigint;
|
|
28
|
+
export declare function bigIntSqrt(n: bigint): bigint;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateClmmDepth = calculateClmmDepth;
|
|
4
|
+
exports.priceFromSqrtX64 = priceFromSqrtX64;
|
|
5
|
+
exports.computeTargetSqrtX64 = computeTargetSqrtX64;
|
|
6
|
+
exports.bigIntSqrt = bigIntSqrt;
|
|
7
|
+
const Q64 = 1n << 64n;
|
|
8
|
+
function calculateClmmDepth(input) {
|
|
9
|
+
const { currentTick, zeroForOne, targetBps, inputDecimals, outputDecimals } = input;
|
|
10
|
+
const currentSqrtPriceX64 = input.sqrtPriceX64;
|
|
11
|
+
let liquidity = input.liquidity;
|
|
12
|
+
if (liquidity <= 0n) {
|
|
13
|
+
return { amountInWei: 0n, amountIn: 0, amountOutWei: 0n, amountOut: 0, currentTick, targetTick: currentTick, targetSqrtPriceX64: currentSqrtPriceX64 };
|
|
14
|
+
}
|
|
15
|
+
const targetSqrtPriceX64 = computeTargetSqrtX64(currentSqrtPriceX64, targetBps, zeroForOne);
|
|
16
|
+
const ticksToTraverse = getTicksBetween(input.ticks, currentTick, zeroForOne);
|
|
17
|
+
let totalInput = 0n;
|
|
18
|
+
let totalOutput = 0n;
|
|
19
|
+
let sqrtPriceCursor = currentSqrtPriceX64;
|
|
20
|
+
for (const tick of ticksToTraverse) {
|
|
21
|
+
const sqrtPriceAtTick = tick.sqrtPriceX64;
|
|
22
|
+
if (zeroForOne && sqrtPriceAtTick <= targetSqrtPriceX64)
|
|
23
|
+
break;
|
|
24
|
+
if (!zeroForOne && sqrtPriceAtTick >= targetSqrtPriceX64)
|
|
25
|
+
break;
|
|
26
|
+
if (liquidity > 0n) {
|
|
27
|
+
totalInput += calcInputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
|
|
28
|
+
totalOutput += calcOutputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
|
|
29
|
+
}
|
|
30
|
+
let liquidityNet = tick.liquidityNet;
|
|
31
|
+
if (zeroForOne)
|
|
32
|
+
liquidityNet = -liquidityNet;
|
|
33
|
+
liquidity = liquidity + liquidityNet;
|
|
34
|
+
if (liquidity < 0n)
|
|
35
|
+
liquidity = 0n;
|
|
36
|
+
sqrtPriceCursor = sqrtPriceAtTick;
|
|
37
|
+
}
|
|
38
|
+
if (liquidity > 0n && sqrtPriceCursor !== targetSqrtPriceX64) {
|
|
39
|
+
totalInput += calcInputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
|
|
40
|
+
totalOutput += calcOutputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
|
|
41
|
+
}
|
|
42
|
+
const amountIn = Number(totalInput) / Math.pow(10, inputDecimals);
|
|
43
|
+
const amountOut = Number(totalOutput) / Math.pow(10, outputDecimals);
|
|
44
|
+
const sqrtPriceFloat = Number(targetSqrtPriceX64) / Number(Q64);
|
|
45
|
+
const targetTick = Math.floor(Math.log(sqrtPriceFloat * sqrtPriceFloat) / Math.log(1.0001));
|
|
46
|
+
return { amountInWei: totalInput, amountIn, amountOutWei: totalOutput, amountOut, currentTick, targetTick, targetSqrtPriceX64 };
|
|
47
|
+
}
|
|
48
|
+
function priceFromSqrtX64(sqrtPriceX64, baseDecimals, quoteDecimals, baseIsToken0) {
|
|
49
|
+
const sp = Number(sqrtPriceX64) / Number(Q64);
|
|
50
|
+
const priceToken1PerToken0 = sp * sp;
|
|
51
|
+
if (baseIsToken0) {
|
|
52
|
+
return priceToken1PerToken0 * Math.pow(10, baseDecimals - quoteDecimals);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return (1 / priceToken1PerToken0) * Math.pow(10, baseDecimals - quoteDecimals);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function computeTargetSqrtX64(currentSqrtPriceX64, bps, zeroForOne) {
|
|
59
|
+
const PRECISION = 10n ** 18n;
|
|
60
|
+
const bpsBigInt = BigInt(bps);
|
|
61
|
+
const factor = zeroForOne ? 10000n - bpsBigInt : 10000n + bpsBigInt;
|
|
62
|
+
const sqrtFactor = bigIntSqrt(factor * PRECISION);
|
|
63
|
+
const sqrtBase = bigIntSqrt(10000n * PRECISION);
|
|
64
|
+
return currentSqrtPriceX64 * sqrtFactor / sqrtBase;
|
|
65
|
+
}
|
|
66
|
+
function bigIntSqrt(n) {
|
|
67
|
+
if (n < 0n)
|
|
68
|
+
throw new Error('sqrt of negative');
|
|
69
|
+
if (n === 0n)
|
|
70
|
+
return 0n;
|
|
71
|
+
if (n <= 3n)
|
|
72
|
+
return 1n;
|
|
73
|
+
let x = n;
|
|
74
|
+
let y = (x + 1n) / 2n;
|
|
75
|
+
while (y < x) {
|
|
76
|
+
x = y;
|
|
77
|
+
y = (x + n / x) / 2n;
|
|
78
|
+
}
|
|
79
|
+
return x;
|
|
80
|
+
}
|
|
81
|
+
function getTicksBetween(ticks, currentTick, zeroForOne) {
|
|
82
|
+
if (zeroForOne) {
|
|
83
|
+
return ticks.filter(t => t.index <= currentTick).sort((a, b) => b.index - a.index);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return ticks.filter(t => t.index > currentTick).sort((a, b) => a.index - b.index);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function calcInputForRange(sqrtPriceA, sqrtPriceB, liquidity, zeroForOne) {
|
|
90
|
+
const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
|
|
91
|
+
const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
|
|
92
|
+
const diff = upper - lower;
|
|
93
|
+
if (diff === 0n || liquidity === 0n)
|
|
94
|
+
return 0n;
|
|
95
|
+
if (zeroForOne) {
|
|
96
|
+
return liquidity * diff * Q64 / (lower * upper);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
return liquidity * diff / Q64;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function calcOutputForRange(sqrtPriceA, sqrtPriceB, liquidity, zeroForOne) {
|
|
103
|
+
const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
|
|
104
|
+
const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
|
|
105
|
+
const diff = upper - lower;
|
|
106
|
+
if (diff === 0n || liquidity === 0n)
|
|
107
|
+
return 0n;
|
|
108
|
+
if (zeroForOne) {
|
|
109
|
+
return liquidity * diff / Q64;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
return liquidity * diff * Q64 / (lower * upper);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QuoteDepthOutput } from '@clonegod/ttd-core';
|
|
2
|
+
import { DepthTick } from './clmm_depth_calculator';
|
|
3
|
+
export { calculateClmmDepth, priceFromSqrtX64, computeTargetSqrtX64, bigIntSqrt } from './clmm_depth_calculator';
|
|
4
|
+
export type { DepthTick, ClmmDepthInput, DepthResult } from './clmm_depth_calculator';
|
|
5
|
+
export interface BuildClmmDepthInput {
|
|
6
|
+
poolInfo: {
|
|
7
|
+
pool_name: string;
|
|
8
|
+
tokenA: any;
|
|
9
|
+
tokenB: any;
|
|
10
|
+
quote_token: string;
|
|
11
|
+
};
|
|
12
|
+
poolAddress: string;
|
|
13
|
+
poolState: {
|
|
14
|
+
currentSqrtPriceX64: bigint;
|
|
15
|
+
currentTick: number;
|
|
16
|
+
liquidity: bigint;
|
|
17
|
+
baseIsToken0: boolean;
|
|
18
|
+
};
|
|
19
|
+
ticks: DepthTick[];
|
|
20
|
+
basePriceUsd: number;
|
|
21
|
+
quotePriceUsd: number;
|
|
22
|
+
feeRateBps: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildClmmDepth(input: BuildClmmDepthInput): QuoteDepthOutput | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bigIntSqrt = exports.computeTargetSqrtX64 = exports.priceFromSqrtX64 = exports.calculateClmmDepth = void 0;
|
|
4
|
+
exports.buildClmmDepth = buildClmmDepth;
|
|
5
|
+
const ttd_core_1 = require("@clonegod/ttd-core");
|
|
6
|
+
const ttd_core_2 = require("@clonegod/ttd-core");
|
|
7
|
+
const trade_direction_1 = require("../../utils/trade_direction");
|
|
8
|
+
const clmm_depth_calculator_1 = require("./clmm_depth_calculator");
|
|
9
|
+
var clmm_depth_calculator_2 = require("./clmm_depth_calculator");
|
|
10
|
+
Object.defineProperty(exports, "calculateClmmDepth", { enumerable: true, get: function () { return clmm_depth_calculator_2.calculateClmmDepth; } });
|
|
11
|
+
Object.defineProperty(exports, "priceFromSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.priceFromSqrtX64; } });
|
|
12
|
+
Object.defineProperty(exports, "computeTargetSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.computeTargetSqrtX64; } });
|
|
13
|
+
Object.defineProperty(exports, "bigIntSqrt", { enumerable: true, get: function () { return clmm_depth_calculator_2.bigIntSqrt; } });
|
|
14
|
+
let _depthPctLogged = false;
|
|
15
|
+
function logDepthLevelsOnce() {
|
|
16
|
+
if (_depthPctLogged)
|
|
17
|
+
return;
|
|
18
|
+
_depthPctLogged = true;
|
|
19
|
+
(0, ttd_core_1.log_info)(`[Depth] pctLevels=${JSON.stringify((0, ttd_core_2.getDepthPricePctLevels)())}, default=${ttd_core_2.DEFAULT_TIER_PCT}`);
|
|
20
|
+
}
|
|
21
|
+
function grossUpFee(amountInNet, feeRateBps) {
|
|
22
|
+
if (feeRateBps <= 0 || amountInNet <= 0)
|
|
23
|
+
return { amountInGross: amountInNet, feeAmount: 0 };
|
|
24
|
+
const feeRatio = feeRateBps / 10000;
|
|
25
|
+
const amountInGross = amountInNet / (1 - feeRatio);
|
|
26
|
+
return { amountInGross, feeAmount: amountInGross - amountInNet };
|
|
27
|
+
}
|
|
28
|
+
function assembleEntry(tiers) {
|
|
29
|
+
const def = tiers.find(t => t.pct === ttd_core_2.DEFAULT_TIER_PCT) ?? tiers[0];
|
|
30
|
+
return {
|
|
31
|
+
price: def.price, amount: def.amount, amount_in: def.amount_in, amount_in_usd: def.amount_in_usd,
|
|
32
|
+
fee: def.fee, fee_usd: def.fee_usd, tick_move: def.tick_move, tiers,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function buildClmmDepth(input) {
|
|
36
|
+
logDepthLevelsOnce();
|
|
37
|
+
const pctLevels = (0, ttd_core_2.getDepthPricePctLevels)();
|
|
38
|
+
if (pctLevels.length === 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
const { poolInfo, poolState, ticks, basePriceUsd, quotePriceUsd, feeRateBps } = input;
|
|
41
|
+
if (poolState.liquidity <= 0n)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (feeRateBps == null || feeRateBps < 0) {
|
|
44
|
+
(0, ttd_core_1.log_debug)(`[Depth] ${poolInfo.pool_name} CLMM: invalid feeRateBps=${feeRateBps}, skip`, '');
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const dir = (0, trade_direction_1.resolveTradeDirection)(poolInfo, true);
|
|
49
|
+
const baseToken = dir.baseToken;
|
|
50
|
+
const quoteToken = dir.quoteToken;
|
|
51
|
+
const baseIsToken0 = poolState.baseIsToken0;
|
|
52
|
+
const askZeroForOne = !baseIsToken0;
|
|
53
|
+
const bidZeroForOne = baseIsToken0;
|
|
54
|
+
const midPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(poolState.currentSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
55
|
+
const askTiers = [];
|
|
56
|
+
const bidTiers = [];
|
|
57
|
+
for (const pct of pctLevels) {
|
|
58
|
+
const bps = Math.round(pct * 100);
|
|
59
|
+
const askResult = (0, clmm_depth_calculator_1.calculateClmmDepth)({
|
|
60
|
+
sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
|
|
61
|
+
liquidity: poolState.liquidity, ticks, zeroForOne: askZeroForOne, targetBps: bps,
|
|
62
|
+
inputDecimals: quoteToken.decimals, outputDecimals: baseToken.decimals,
|
|
63
|
+
});
|
|
64
|
+
const bidResult = (0, clmm_depth_calculator_1.calculateClmmDepth)({
|
|
65
|
+
sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
|
|
66
|
+
liquidity: poolState.liquidity, ticks, zeroForOne: bidZeroForOne, targetBps: bps,
|
|
67
|
+
inputDecimals: baseToken.decimals, outputDecimals: quoteToken.decimals,
|
|
68
|
+
});
|
|
69
|
+
const askTargetPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(askResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
70
|
+
const bidTargetPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(bidResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
71
|
+
const askGross = grossUpFee(askResult.amountIn, feeRateBps);
|
|
72
|
+
const bidGross = grossUpFee(bidResult.amountIn, feeRateBps);
|
|
73
|
+
const askTickMove = `${askResult.targetTick} <- ${askResult.currentTick}`;
|
|
74
|
+
const bidTickMove = `${bidResult.currentTick} -> ${bidResult.targetTick}`;
|
|
75
|
+
const askEffPrice = askResult.amountOut > 0 ? askGross.amountInGross / askResult.amountOut : askTargetPrice;
|
|
76
|
+
const bidEffPrice = bidGross.amountInGross > 0 ? bidResult.amountOut / bidGross.amountInGross : bidTargetPrice;
|
|
77
|
+
askTiers.push({
|
|
78
|
+
pct, price: askEffPrice, amount: askResult.amountOut,
|
|
79
|
+
amount_in: askGross.amountInGross, amount_in_usd: askGross.amountInGross * quotePriceUsd,
|
|
80
|
+
fee: askGross.feeAmount, fee_usd: askGross.feeAmount * quotePriceUsd, tick_move: askTickMove,
|
|
81
|
+
});
|
|
82
|
+
bidTiers.push({
|
|
83
|
+
pct, price: bidEffPrice, amount: bidResult.amountOut,
|
|
84
|
+
amount_in: bidGross.amountInGross, amount_in_usd: bidGross.amountInGross * basePriceUsd,
|
|
85
|
+
fee: bidGross.feeAmount, fee_usd: bidGross.feeAmount * basePriceUsd, tick_move: bidTickMove,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
mid_price: midPrice,
|
|
90
|
+
fee_rate_bps: feeRateBps,
|
|
91
|
+
ask: assembleEntry(askTiers),
|
|
92
|
+
bid: assembleEntry(bidTiers),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
(0, ttd_core_1.log_debug)(`[Depth] ${poolInfo.pool_name} CLMM depth failed: ${error.message}`, '');
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/quote/index.d.ts
CHANGED
package/dist/quote/index.js
CHANGED
|
@@ -21,4 +21,5 @@ __exportStar(require("./quote_trace"), exports);
|
|
|
21
21
|
__exportStar(require("./quote_amount"), exports);
|
|
22
22
|
__exportStar(require("./verify"), exports);
|
|
23
23
|
__exportStar(require("./tick"), exports);
|
|
24
|
+
__exportStar(require("./depth"), exports);
|
|
24
25
|
__exportStar(require("./abstract_dex_quote"), exports);
|
|
@@ -34,6 +34,7 @@ export interface CentralExecutorOptions {
|
|
|
34
34
|
transaction: any;
|
|
35
35
|
};
|
|
36
36
|
}) => void;
|
|
37
|
+
tradeCoinTypes?: () => Promise<string[]>;
|
|
37
38
|
}
|
|
38
39
|
type TxResponse = SuiClientTypes.Transaction<{
|
|
39
40
|
effects: true;
|
|
@@ -49,12 +50,20 @@ export declare class CentralExecutor {
|
|
|
49
50
|
private tradeWallets;
|
|
50
51
|
private sharedRefCache;
|
|
51
52
|
private seq;
|
|
53
|
+
private readonly tradeCoinTypesProvider?;
|
|
52
54
|
constructor(core: ExecutorCore, opts?: CentralExecutorOptions);
|
|
53
55
|
init(): Promise<void>;
|
|
54
|
-
reconcileCoins(wallet: string, coinType: string): Promise<void>;
|
|
56
|
+
reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
|
|
55
57
|
rebalanceWalletFunds(): Promise<void>;
|
|
56
58
|
private rebalanceOne;
|
|
59
|
+
private decimalsCache;
|
|
60
|
+
private objectReader?;
|
|
61
|
+
private getCoinDecimalsCached;
|
|
62
|
+
private maintainCoinObjects;
|
|
57
63
|
private execMaintenance;
|
|
64
|
+
private poolGenericsCache;
|
|
65
|
+
private getPoolGenerics;
|
|
66
|
+
private canonicalizeReq;
|
|
58
67
|
private getSharedRefCached;
|
|
59
68
|
private chainIdentifier;
|
|
60
69
|
private epochCache;
|
|
@@ -70,6 +79,8 @@ export declare class CentralExecutor {
|
|
|
70
79
|
private toCheckerReceipt;
|
|
71
80
|
private reconcileAfterFailure;
|
|
72
81
|
simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
|
|
82
|
+
private minSplitFor;
|
|
83
|
+
private postTradeRebalance;
|
|
73
84
|
private buildSwapTx;
|
|
74
85
|
private onSuccess;
|
|
75
86
|
get coinCache(): InProcessCoinCache;
|
|
@@ -2,12 +2,35 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CentralExecutor = void 0;
|
|
4
4
|
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
5
|
+
const grpc_connection_1 = require("../../grpc/grpc-connection");
|
|
6
|
+
const sui_object_reader_1 = require("../../grpc/sui_object_reader");
|
|
7
|
+
const format_1 = require("../../utils/format");
|
|
5
8
|
const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
|
|
6
9
|
const transactions_1 = require("@mysten/sui/transactions");
|
|
7
10
|
const coin_cache_1 = require("./coin_cache");
|
|
8
11
|
const effects_1 = require("./effects");
|
|
9
12
|
const swap_1 = require("../swap");
|
|
10
13
|
const constants_1 = require("../../constants");
|
|
14
|
+
function splitTopLevelGenerics(s) {
|
|
15
|
+
const out = [];
|
|
16
|
+
let depth = 0, cur = '';
|
|
17
|
+
for (const ch of s) {
|
|
18
|
+
if (ch === '<')
|
|
19
|
+
depth++;
|
|
20
|
+
else if (ch === '>')
|
|
21
|
+
depth--;
|
|
22
|
+
if (ch === ',' && depth === 0) {
|
|
23
|
+
out.push(cur.trim());
|
|
24
|
+
cur = '';
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cur += ch;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (cur.trim())
|
|
31
|
+
out.push(cur.trim());
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
11
34
|
class CentralExecutor {
|
|
12
35
|
constructor(core, opts = {}) {
|
|
13
36
|
this.core = core;
|
|
@@ -15,11 +38,14 @@ class CentralExecutor {
|
|
|
15
38
|
this.tradeWallets = new Map();
|
|
16
39
|
this.sharedRefCache = new Map();
|
|
17
40
|
this.seq = 0;
|
|
41
|
+
this.decimalsCache = new Map();
|
|
42
|
+
this.poolGenericsCache = new Map();
|
|
18
43
|
this.chainIdentifier = '';
|
|
19
44
|
this.epochCache = null;
|
|
20
45
|
this.gasBudget = opts.gasBudget ?? BigInt(process.env.SUI_GAS_BUDGET || '50000000');
|
|
21
46
|
this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
|
|
22
47
|
this.onBroadcastResult = opts.onBroadcastResult;
|
|
48
|
+
this.tradeCoinTypesProvider = opts.tradeCoinTypes;
|
|
23
49
|
}
|
|
24
50
|
async init() {
|
|
25
51
|
const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
@@ -34,18 +60,22 @@ class CentralExecutor {
|
|
|
34
60
|
}
|
|
35
61
|
(0, dist_1.log_info)(`[executor] init: trade=${[...this.tradeWallets.keys()].join(',')} (单钱包模式: gas=各自地址余额), gasBudget=${this.gasBudget}`);
|
|
36
62
|
}
|
|
37
|
-
async reconcileCoins(wallet, coinType) {
|
|
63
|
+
async reconcileCoins(wallet, coinType, quietIfUnchanged = false) {
|
|
38
64
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
66
|
+
const epoch = this.cache.mutationEpoch(wallet, coinType);
|
|
67
|
+
const refs = [];
|
|
68
|
+
let cursor = undefined;
|
|
69
|
+
do {
|
|
70
|
+
const page = await this.core.listCoins({ owner: wallet, coinType, cursor });
|
|
71
|
+
for (const c of page.objects) {
|
|
72
|
+
refs.push({ objectId: c.objectId, version: c.version, digest: c.digest, balance: c.balance });
|
|
73
|
+
}
|
|
74
|
+
cursor = page.hasNextPage ? page.cursor : null;
|
|
75
|
+
} while (cursor);
|
|
76
|
+
if (this.cache.reconcile(wallet, coinType, refs, epoch, quietIfUnchanged))
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
49
79
|
}
|
|
50
80
|
catch (e) {
|
|
51
81
|
(0, dist_1.log_error)(`[executor] reconcileCoins 失败 ${wallet} ${coinType}`, e);
|
|
@@ -66,11 +96,10 @@ class CentralExecutor {
|
|
|
66
96
|
const balanceMin = BigInt(process.env.SUI_GAS_BALANCE_MIN || '500000000');
|
|
67
97
|
const balanceTarget = BigInt(process.env.SUI_GAS_BALANCE_TARGET || (balanceMin * 2n).toString());
|
|
68
98
|
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
69
|
-
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX ||
|
|
70
|
-
const minSplit = BigInt(process.env.SUI_INPUT_COIN_MIN_SPLIT || '100000000');
|
|
99
|
+
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
71
100
|
const chunk = BigInt(process.env.SUI_REDEEM_MIN_CHUNK || '100000000');
|
|
72
101
|
const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
|
|
73
|
-
await this.reconcileCoins(addr, SUI);
|
|
102
|
+
await this.reconcileCoins(addr, SUI, true);
|
|
74
103
|
if (this.cache.hasInflight(addr, SUI))
|
|
75
104
|
return;
|
|
76
105
|
const b = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
|
|
@@ -87,8 +116,7 @@ class CentralExecutor {
|
|
|
87
116
|
const tx = new transactions_1.Transaction();
|
|
88
117
|
tx.setSender(addr);
|
|
89
118
|
const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(need)]);
|
|
90
|
-
|
|
91
|
-
tx.moveCall({ target: '0x2::balance::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [bal, tx.pure.address(addr)] });
|
|
119
|
+
tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [dep, tx.pure.address(addr)] });
|
|
92
120
|
tx.setGasPayment([{ objectId: src.objectId, version: src.version, digest: src.digest }]);
|
|
93
121
|
await this.execMaintenance(tx, addr, kp, budget, `deposit ${need} → balance`);
|
|
94
122
|
return;
|
|
@@ -106,42 +134,136 @@ class CentralExecutor {
|
|
|
106
134
|
await this.execMaintenance(tx, addr, kp, budget, `redeem 超额 ${excess} → coin`, rawClient);
|
|
107
135
|
return;
|
|
108
136
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
tx.setGasPayment([]);
|
|
118
|
-
tx.setExpiration(await this.getValidDuringExpiration());
|
|
119
|
-
await this.execMaintenance(tx, addr, kp, budget, `split ${big.objectId.slice(0, 10)}… 对半(${coins.length}→${coins.length + 1} 个)`);
|
|
120
|
-
return;
|
|
137
|
+
let tradeTypes = [];
|
|
138
|
+
if (this.tradeCoinTypesProvider) {
|
|
139
|
+
try {
|
|
140
|
+
tradeTypes = (await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t));
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
(0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只维护 SUI: ${e.message}`);
|
|
144
|
+
}
|
|
121
145
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
const types = [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))];
|
|
147
|
+
for (const t of types) {
|
|
148
|
+
if (t !== SUI)
|
|
149
|
+
await this.reconcileCoins(addr, t, true);
|
|
150
|
+
if (this.cache.hasInflight(addr, t))
|
|
151
|
+
continue;
|
|
152
|
+
let minSplitT;
|
|
153
|
+
try {
|
|
154
|
+
minSplitT = await this.minSplitFor(t);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
(0, dist_1.log_warn)(`[executor] 读 ${t} decimals 失败,本 tick 跳过该币种维护: ${e.message}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const acted = await this.maintainCoinObjects(addr, kp, t, { coinTarget, coinMax, budget, minSplit: minSplitT });
|
|
161
|
+
if (acted)
|
|
162
|
+
return;
|
|
133
163
|
}
|
|
134
164
|
}
|
|
135
|
-
async
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
async getCoinDecimalsCached(coinType) {
|
|
166
|
+
let d = this.decimalsCache.get(coinType);
|
|
167
|
+
if (d == null) {
|
|
168
|
+
if (!this.objectReader) {
|
|
169
|
+
const { grpc_endpoint, grpc_token } = (0, dist_1.getCoreEnv)();
|
|
170
|
+
if (!grpc_endpoint)
|
|
171
|
+
throw new Error('GRPC_ENDPOINT 未配置(读 CoinMetadata decimals 需要)');
|
|
172
|
+
this.objectReader = new sui_object_reader_1.SuiObjectReader(grpc_connection_1.GrpcConnection.getInstance(grpc_endpoint, grpc_token));
|
|
173
|
+
}
|
|
174
|
+
d = await this.objectReader.getCoinDecimals(coinType);
|
|
175
|
+
this.decimalsCache.set(coinType, d);
|
|
176
|
+
}
|
|
177
|
+
return d;
|
|
178
|
+
}
|
|
179
|
+
async maintainCoinObjects(addr, kp, coinType, opts) {
|
|
180
|
+
const short = coinType.split('::').pop();
|
|
181
|
+
const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
|
|
182
|
+
const coins = this.cache.snapshot(addr, coinType).coins;
|
|
183
|
+
const valued = coins.filter(c => BigInt(c.balance) > 0n);
|
|
184
|
+
const zeros = coins.filter(c => BigInt(c.balance) === 0n);
|
|
185
|
+
if (coins.length === 0)
|
|
186
|
+
return false;
|
|
187
|
+
const total = valued.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
188
|
+
const maxPieces = opts.minSplit > 0n ? total / opts.minSplit : 0n;
|
|
189
|
+
const pieces = Math.max(1, Number(maxPieces > BigInt(opts.coinTarget) ? BigInt(opts.coinTarget) : maxPieces));
|
|
190
|
+
const needSplit = valued.length > 0 && valued.length < opts.coinTarget && pieces > valued.length;
|
|
191
|
+
const needZeroPurge = zeros.length >= zeroMergeMin;
|
|
192
|
+
const needDefrag = coins.length > opts.coinMax;
|
|
193
|
+
if (!needSplit && !needZeroPurge && !needDefrag)
|
|
194
|
+
return false;
|
|
195
|
+
const tag = this.nextTag('maint');
|
|
196
|
+
if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
|
|
197
|
+
return false;
|
|
198
|
+
const primaryRef = valued[0] ?? coins[0];
|
|
199
|
+
const tx = new transactions_1.Transaction();
|
|
200
|
+
tx.setSender(addr);
|
|
201
|
+
const primary = tx.object(transactions_1.Inputs.ObjectRef(primaryRef));
|
|
202
|
+
const others = coins.filter(c => c.objectId !== primaryRef.objectId);
|
|
203
|
+
if (others.length)
|
|
204
|
+
tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
|
|
205
|
+
if (pieces > 1) {
|
|
206
|
+
const per = total / BigInt(pieces);
|
|
207
|
+
const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
|
|
208
|
+
tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
|
|
209
|
+
}
|
|
210
|
+
tx.setGasPayment([]);
|
|
211
|
+
tx.setExpiration(await this.getValidDuringExpiration());
|
|
212
|
+
await this.execMaintenance(tx, addr, kp, opts.budget, `整理 ${short}:${valued.length} 值 + ${zeros.length} 壳 → ${pieces} 份均分(每份≈${(total / BigInt(pieces)).toString()})`, undefined, coinType, tag);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag) {
|
|
216
|
+
try {
|
|
217
|
+
tx.setGasBudget(budget);
|
|
218
|
+
tx.setGasPrice(await this.getGasPrice());
|
|
219
|
+
const bytes = buildClient ? await tx.build({ client: buildClient }) : await tx.build();
|
|
220
|
+
const { signature } = await kp.signTransaction(bytes);
|
|
221
|
+
const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
|
|
222
|
+
const tr = resp.Transaction;
|
|
223
|
+
const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
|
|
224
|
+
(0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (reserveTag)
|
|
228
|
+
this.cache.abort(reserveTag, true);
|
|
229
|
+
await this.reconcileCoins(addr, coinType);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async getPoolGenerics(poolId) {
|
|
233
|
+
const hit = this.poolGenericsCache.get(poolId);
|
|
234
|
+
if (hit !== undefined)
|
|
235
|
+
return hit;
|
|
236
|
+
const res = await this.core.getObjects({ objectIds: [poolId] });
|
|
237
|
+
const obj = res.objects[0];
|
|
238
|
+
if (obj instanceof Error)
|
|
239
|
+
throw new Error(`[executor] getObjects(${poolId}) 失败: ${obj.message}`);
|
|
240
|
+
const t = obj.type ?? obj.objectType;
|
|
241
|
+
let out = null;
|
|
242
|
+
const m = t?.match(/<(.+)>$/);
|
|
243
|
+
if (m) {
|
|
244
|
+
const parts = splitTopLevelGenerics(m[1]);
|
|
245
|
+
if (parts.length === 2) {
|
|
246
|
+
out = { typeA: (0, format_1.normalizeSuiTokenAddress)(parts[0]), typeB: (0, format_1.normalizeSuiTokenAddress)(parts[1]) };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.poolGenericsCache.set(poolId, out);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
async canonicalizeReq(req) {
|
|
253
|
+
const g = await this.getPoolGenerics(req.poolId);
|
|
254
|
+
if (!g)
|
|
255
|
+
return req;
|
|
256
|
+
const a = (0, format_1.normalizeSuiTokenAddress)(req.coinTypeA);
|
|
257
|
+
const b = (0, format_1.normalizeSuiTokenAddress)(req.coinTypeB);
|
|
258
|
+
if (a === g.typeA && b === g.typeB)
|
|
259
|
+
return req;
|
|
260
|
+
if (a === g.typeB && b === g.typeA) {
|
|
261
|
+
if (req.sqrtPriceLimit !== 0n)
|
|
262
|
+
throw new Error(`[executor] pool ${req.poolId} token 序翻转时不支持显式 sqrtPriceLimit`);
|
|
263
|
+
(0, dist_1.log_info)(`[executor] pool ${req.poolId.slice(0, 10)}… 配置 token 序与链上相反,已翻转(a2b ${req.a2b}→${!req.a2b})`);
|
|
264
|
+
return { ...req, coinTypeA: b, coinTypeB: a, a2b: !req.a2b };
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`[executor] pool token 与链上不符: 链上 <${g.typeA}, ${g.typeB}> vs 请求 <${a}, ${b}>(配置可能指错池子)`);
|
|
145
267
|
}
|
|
146
268
|
async getSharedRefCached(objectId) {
|
|
147
269
|
let isv = this.sharedRefCache.get(objectId);
|
|
@@ -160,7 +282,13 @@ class CentralExecutor {
|
|
|
160
282
|
}
|
|
161
283
|
async getValidDuringExpiration() {
|
|
162
284
|
if (!this.chainIdentifier) {
|
|
163
|
-
|
|
285
|
+
try {
|
|
286
|
+
this.chainIdentifier = (await this.core.getChainIdentifier()).chainIdentifier;
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
this.chainIdentifier = process.env.SUI_CHAIN_IDENTIFIER || '35834a8a';
|
|
290
|
+
(0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${this.chainIdentifier}`);
|
|
291
|
+
}
|
|
164
292
|
}
|
|
165
293
|
const now = Date.now();
|
|
166
294
|
if (!this.epochCache || now - this.epochCache.ts > 300_000) {
|
|
@@ -209,8 +337,17 @@ class CentralExecutor {
|
|
|
209
337
|
}
|
|
210
338
|
async submitSwap(req) {
|
|
211
339
|
const t0 = Date.now();
|
|
340
|
+
req = await this.canonicalizeReq(req);
|
|
212
341
|
const inType = this.inputCoinType(req);
|
|
213
|
-
|
|
342
|
+
let wallet;
|
|
343
|
+
try {
|
|
344
|
+
wallet = this.chooseTradeWallet(req, inType, req.amountIn);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
(0, dist_1.log_warn)(`[executor] ${inType} 选钱包失败(${e.message}),按需 reconcile 后重试`);
|
|
348
|
+
await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
|
|
349
|
+
wallet = this.chooseTradeWallet(req, inType, req.amountIn);
|
|
350
|
+
}
|
|
214
351
|
const inputTag = this.nextTag('in');
|
|
215
352
|
const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
|
|
216
353
|
const tAcquire = Date.now();
|
|
@@ -290,6 +427,7 @@ class CentralExecutor {
|
|
|
290
427
|
void this.reconcileCoins(wallet, t);
|
|
291
428
|
}
|
|
292
429
|
async simulateSwap(req) {
|
|
430
|
+
req = await this.canonicalizeReq(req);
|
|
293
431
|
const inType = this.inputCoinType(req);
|
|
294
432
|
const wallet = req.walletAddress ?? this.chooseTradeWallet(req, inType, req.amountIn);
|
|
295
433
|
const { coins } = this.cache.snapshot(wallet, inType);
|
|
@@ -312,6 +450,32 @@ class CentralExecutor {
|
|
|
312
450
|
}
|
|
313
451
|
return res.Transaction;
|
|
314
452
|
}
|
|
453
|
+
async minSplitFor(coinType) {
|
|
454
|
+
if (coinType === constants_1.SUI_TOKEN_ADDRESS.LONG) {
|
|
455
|
+
return BigInt(process.env.SUI_INPUT_COIN_MIN_SPLIT || '100000000');
|
|
456
|
+
}
|
|
457
|
+
const d = await this.getCoinDecimalsCached(coinType);
|
|
458
|
+
return 10n ** BigInt(d) / 10n;
|
|
459
|
+
}
|
|
460
|
+
async postTradeRebalance(wallet, inType, outType) {
|
|
461
|
+
const kp = this.tradeWallets.get(wallet);
|
|
462
|
+
if (!kp)
|
|
463
|
+
return;
|
|
464
|
+
const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
|
|
465
|
+
const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
|
|
466
|
+
const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
|
|
467
|
+
for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG])) {
|
|
468
|
+
try {
|
|
469
|
+
await this.reconcileCoins(wallet, t);
|
|
470
|
+
if (this.cache.hasInflight(wallet, t))
|
|
471
|
+
continue;
|
|
472
|
+
await this.maintainCoinObjects(wallet, kp, t, { coinTarget, coinMax, budget, minSplit: await this.minSplitFor(t) });
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
(0, dist_1.log_warn)(`[executor] postTradeRebalance ${t} 失败(交 30s tick 兜底): ${e.message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
315
479
|
async buildSwapTx(req, wallet, inputCoins) {
|
|
316
480
|
const tx = new transactions_1.Transaction();
|
|
317
481
|
tx.setSender(wallet);
|
|
@@ -344,8 +508,7 @@ class CentralExecutor {
|
|
|
344
508
|
const tradeChanges = (0, effects_1.extractCoinChangesFromCore)(tr.effects, objectTypes, { owners: new Set([wallet]), coinTypeById });
|
|
345
509
|
this.cache.commit(inputTag, tradeChanges);
|
|
346
510
|
if (this.reconcileAfterTx) {
|
|
347
|
-
|
|
348
|
-
void this.reconcileCoins(wallet, t);
|
|
511
|
+
void this.postTradeRebalance(wallet, inType, outType);
|
|
349
512
|
}
|
|
350
513
|
}
|
|
351
514
|
get coinCache() { return this.cache; }
|
|
@@ -2,16 +2,22 @@ import { CoinRef, CoinReservation, CoinObjectChange } from '../coin/types';
|
|
|
2
2
|
export declare class InProcessCoinCache {
|
|
3
3
|
private available;
|
|
4
4
|
private inflight;
|
|
5
|
+
private epochs;
|
|
6
|
+
private epochKey;
|
|
7
|
+
private bumpEpoch;
|
|
8
|
+
mutationEpoch(wallet: string, coinType: string): number;
|
|
5
9
|
private walletMap;
|
|
6
10
|
private list;
|
|
7
11
|
private sortDesc;
|
|
8
12
|
seed(wallet: string, coinType: string, coins: CoinRef[]): void;
|
|
9
|
-
reconcile(wallet: string, coinType: string, freshCoins: CoinRef[]):
|
|
13
|
+
reconcile(wallet: string, coinType: string, freshCoins: CoinRef[], epochAtStart?: number, quietIfUnchanged?: boolean): boolean;
|
|
10
14
|
acquire(wallet: string, coinType: string, amount: bigint, txTag: string, maxCoins?: number): CoinReservation;
|
|
15
|
+
reserve(wallet: string, coinType: string, objectIds: string[], txTag: string): CoinRef[] | null;
|
|
11
16
|
hasInflight(wallet: string, coinType: string): boolean;
|
|
12
17
|
commit(txTag: string, changes: CoinObjectChange[]): void;
|
|
13
18
|
abort(txTag: string, consumed: boolean): void;
|
|
14
19
|
private removeEverywhere;
|
|
20
|
+
coinTypes(wallet: string): string[];
|
|
15
21
|
snapshot(wallet: string, coinType: string): {
|
|
16
22
|
count: number;
|
|
17
23
|
total: bigint;
|
|
@@ -6,6 +6,15 @@ class InProcessCoinCache {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
this.available = new Map();
|
|
8
8
|
this.inflight = new Map();
|
|
9
|
+
this.epochs = new Map();
|
|
10
|
+
}
|
|
11
|
+
epochKey(wallet, coinType) { return `${wallet}|${coinType}`; }
|
|
12
|
+
bumpEpoch(wallet, coinType) {
|
|
13
|
+
const k = this.epochKey(wallet, coinType);
|
|
14
|
+
this.epochs.set(k, (this.epochs.get(k) || 0) + 1);
|
|
15
|
+
}
|
|
16
|
+
mutationEpoch(wallet, coinType) {
|
|
17
|
+
return this.epochs.get(this.epochKey(wallet, coinType)) || 0;
|
|
9
18
|
}
|
|
10
19
|
walletMap(wallet) {
|
|
11
20
|
let m = this.available.get(wallet);
|
|
@@ -31,9 +40,14 @@ class InProcessCoinCache {
|
|
|
31
40
|
const l = coins.slice();
|
|
32
41
|
this.sortDesc(l);
|
|
33
42
|
this.walletMap(wallet).set(coinType, l);
|
|
43
|
+
this.bumpEpoch(wallet, coinType);
|
|
34
44
|
(0, dist_1.log_info)(`[coin-cache] seed ${wallet} ${coinType}: ${l.length} coins`);
|
|
35
45
|
}
|
|
36
|
-
reconcile(wallet, coinType, freshCoins) {
|
|
46
|
+
reconcile(wallet, coinType, freshCoins, epochAtStart, quietIfUnchanged = false) {
|
|
47
|
+
if (epochAtStart != null && this.mutationEpoch(wallet, coinType) !== epochAtStart) {
|
|
48
|
+
(0, dist_1.log_info)(`[coin-cache] reconcile ${wallet} ${coinType}: 快照在途期间缓存已变更,丢弃(防旧覆盖新)`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
37
51
|
const inflightIds = new Set();
|
|
38
52
|
for (const r of this.inflight.values()) {
|
|
39
53
|
if (r.wallet === wallet && r.coinType === coinType)
|
|
@@ -41,8 +55,15 @@ class InProcessCoinCache {
|
|
|
41
55
|
}
|
|
42
56
|
const kept = freshCoins.filter(c => !inflightIds.has(c.objectId));
|
|
43
57
|
this.sortDesc(kept);
|
|
58
|
+
const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
|
|
59
|
+
const unchanged = sig(kept) === sig(this.list(wallet, coinType));
|
|
44
60
|
this.walletMap(wallet).set(coinType, kept);
|
|
45
|
-
|
|
61
|
+
const msg = `[coin-cache] reconcile ${wallet} ${coinType}: ${kept.length} avail (${inflightIds.size} inflight kept)`;
|
|
62
|
+
if (unchanged && quietIfUnchanged)
|
|
63
|
+
(0, dist_1.log_debug)(msg);
|
|
64
|
+
else
|
|
65
|
+
(0, dist_1.log_info)(msg);
|
|
66
|
+
return true;
|
|
46
67
|
}
|
|
47
68
|
acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
|
|
48
69
|
if (this.inflight.has(txTag))
|
|
@@ -65,8 +86,22 @@ class InProcessCoinCache {
|
|
|
65
86
|
const pickedIds = new Set(picked.map(c => c.objectId));
|
|
66
87
|
this.walletMap(wallet).set(coinType, l.filter(c => !pickedIds.has(c.objectId)));
|
|
67
88
|
this.inflight.set(txTag, { wallet, coinType, coins: picked, gas: false });
|
|
89
|
+
this.bumpEpoch(wallet, coinType);
|
|
68
90
|
return { txTag, coinType, coins: picked };
|
|
69
91
|
}
|
|
92
|
+
reserve(wallet, coinType, objectIds, txTag) {
|
|
93
|
+
if (this.inflight.has(txTag))
|
|
94
|
+
throw new Error(`[coin-cache] txTag 重复: ${txTag}`);
|
|
95
|
+
const l = this.list(wallet, coinType);
|
|
96
|
+
const ids = new Set(objectIds);
|
|
97
|
+
const picked = l.filter(c => ids.has(c.objectId));
|
|
98
|
+
if (picked.length !== ids.size)
|
|
99
|
+
return null;
|
|
100
|
+
this.walletMap(wallet).set(coinType, l.filter(c => !ids.has(c.objectId)));
|
|
101
|
+
this.inflight.set(txTag, { wallet, coinType, coins: picked, gas: false });
|
|
102
|
+
this.bumpEpoch(wallet, coinType);
|
|
103
|
+
return picked;
|
|
104
|
+
}
|
|
70
105
|
hasInflight(wallet, coinType) {
|
|
71
106
|
for (const r of this.inflight.values()) {
|
|
72
107
|
if (r.wallet === wallet && r.coinType === coinType)
|
|
@@ -81,6 +116,9 @@ class InProcessCoinCache {
|
|
|
81
116
|
return;
|
|
82
117
|
}
|
|
83
118
|
this.inflight.delete(txTag);
|
|
119
|
+
this.bumpEpoch(res.wallet, res.coinType);
|
|
120
|
+
for (const ch of changes)
|
|
121
|
+
this.bumpEpoch(res.wallet, ch.coinType);
|
|
84
122
|
for (const ch of changes) {
|
|
85
123
|
const wallet = res.wallet;
|
|
86
124
|
if (ch.kind === 'deleted') {
|
|
@@ -107,6 +145,7 @@ class InProcessCoinCache {
|
|
|
107
145
|
return;
|
|
108
146
|
}
|
|
109
147
|
this.inflight.delete(txTag);
|
|
148
|
+
this.bumpEpoch(res.wallet, res.coinType);
|
|
110
149
|
if (consumed) {
|
|
111
150
|
(0, dist_1.log_warn)(`[coin-cache] abort consumed txTag=${txTag}(${res.coins.length} coin 交 reconcile 兜底)`);
|
|
112
151
|
return;
|
|
@@ -123,6 +162,9 @@ class InProcessCoinCache {
|
|
|
123
162
|
if (idx >= 0)
|
|
124
163
|
l.splice(idx, 1);
|
|
125
164
|
}
|
|
165
|
+
coinTypes(wallet) {
|
|
166
|
+
return [...this.walletMap(wallet).keys()];
|
|
167
|
+
}
|
|
126
168
|
snapshot(wallet, coinType) {
|
|
127
169
|
const l = this.list(wallet, coinType);
|
|
128
170
|
const total = l.reduce((s, c) => s + BigInt(c.balance), 0n);
|
|
@@ -39,10 +39,18 @@ class SuiTransactionParser {
|
|
|
39
39
|
let tokenBChangeAmount = new decimal_js_1.default(0);
|
|
40
40
|
const balanceChanges = tx_receipt.transaction?.balance_changes || [];
|
|
41
41
|
(0, dist_1.log_info)('balance_changes', { txid, balance_changes: balanceChanges });
|
|
42
|
+
const g = tx_receipt.transaction?.effects?.gas_used;
|
|
43
|
+
const gasNetMist = g
|
|
44
|
+
? new decimal_js_1.default(g.computation_cost || 0).plus(g.storage_cost || 0).minus(g.storage_rebate || 0)
|
|
45
|
+
: new decimal_js_1.default(0);
|
|
46
|
+
const SUI_LONG = (0, index_1.normalizeSuiTokenAddress)('0x2::sui::SUI');
|
|
42
47
|
for (const change of balanceChanges) {
|
|
43
|
-
|
|
48
|
+
let amount = new decimal_js_1.default(change.amount);
|
|
44
49
|
const coinType = change.coin_type;
|
|
45
50
|
const normalizedCoinType = (0, index_1.normalizeSuiTokenAddress)(coinType);
|
|
51
|
+
if (normalizedCoinType === SUI_LONG) {
|
|
52
|
+
amount = amount.plus(gasNetMist);
|
|
53
|
+
}
|
|
46
54
|
const poolTokenAAddress = (0, index_1.normalizeSuiTokenAddress)(pool_info.tokenA.address);
|
|
47
55
|
const poolTokenBAddress = (0, index_1.normalizeSuiTokenAddress)(pool_info.tokenB.address);
|
|
48
56
|
if (normalizedCoinType === poolTokenAAddress) {
|