@clonegod/ttd-sol-common 2.0.68 → 2.0.70
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/depth/amm_depth_calculator.d.ts +19 -0
- package/dist/quote/depth/amm_depth_calculator.js +55 -0
- package/dist/quote/depth/index.d.ts +18 -1
- package/dist/quote/depth/index.js +65 -1
- package/dist/trade/index.d.ts +0 -1
- package/dist/trade/index.js +0 -1
- package/dist/trade/send/send_tx.d.ts +0 -1
- package/dist/trade/send/send_tx.js +0 -13
- package/dist/trade/tx_builder.d.ts +0 -3
- package/dist/trade/tx_builder.js +9 -41
- package/package.json +1 -1
- package/src/quote/depth/amm_depth_calculator.ts +143 -0
- package/src/quote/depth/index.ts +109 -4
- package/src/trade/index.ts +0 -1
- package/src/trade/send/send_tx.ts +0 -25
- package/src/trade/tx_builder.ts +15 -65
- package/src/trade/jito_tip_wallets.ts +0 -143
- package/src/trade/send/jito.ts +0 -132
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AmmDepthInput {
|
|
2
|
+
reserve0: string;
|
|
3
|
+
reserve1: string;
|
|
4
|
+
targetBps: number;
|
|
5
|
+
isBuy: boolean;
|
|
6
|
+
baseIsToken0: boolean;
|
|
7
|
+
token0Decimals: number;
|
|
8
|
+
token1Decimals: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AmmDepthResult {
|
|
11
|
+
amountOutWei: bigint;
|
|
12
|
+
amountOut: number;
|
|
13
|
+
outputDecimals: number;
|
|
14
|
+
amountInWei: bigint;
|
|
15
|
+
amountIn: number;
|
|
16
|
+
inputDecimals: number;
|
|
17
|
+
targetPrice: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function calculateAmmDepth(input: AmmDepthInput): AmmDepthResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateAmmDepth = calculateAmmDepth;
|
|
4
|
+
const clmm_depth_calculator_1 = require("./clmm_depth_calculator");
|
|
5
|
+
function calculateAmmDepth(input) {
|
|
6
|
+
const { targetBps, isBuy, baseIsToken0, token0Decimals, token1Decimals } = input;
|
|
7
|
+
const R0 = BigInt(input.reserve0);
|
|
8
|
+
const R1 = BigInt(input.reserve1);
|
|
9
|
+
if (R0 === 0n || R1 === 0n) {
|
|
10
|
+
return { amountOutWei: 0n, amountOut: 0, outputDecimals: 0, amountInWei: 0n, amountIn: 0, inputDecimals: 0, targetPrice: 0 };
|
|
11
|
+
}
|
|
12
|
+
const k = R0 * R1;
|
|
13
|
+
const bps = BigInt(targetBps);
|
|
14
|
+
const PRECISION = 10n ** 18n;
|
|
15
|
+
let amountOutWei;
|
|
16
|
+
let amountInWei;
|
|
17
|
+
let outputDecimals;
|
|
18
|
+
let inputDecimals;
|
|
19
|
+
if (isBuy) {
|
|
20
|
+
const [baseReserve, quoteReserve] = baseIsToken0 ? [R0, R1] : [R1, R0];
|
|
21
|
+
outputDecimals = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
22
|
+
inputDecimals = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
23
|
+
const baseReserveNew = baseReserve * (0, clmm_depth_calculator_1.bigIntSqrt)(10000n * PRECISION) / (0, clmm_depth_calculator_1.bigIntSqrt)((10000n + bps) * PRECISION);
|
|
24
|
+
const quoteReserveNew = quoteReserve * (0, clmm_depth_calculator_1.bigIntSqrt)((10000n + bps) * PRECISION) / (0, clmm_depth_calculator_1.bigIntSqrt)(10000n * PRECISION);
|
|
25
|
+
amountOutWei = baseReserve - baseReserveNew;
|
|
26
|
+
amountInWei = quoteReserveNew - quoteReserve;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const [baseReserve, quoteReserve] = baseIsToken0 ? [R0, R1] : [R1, R0];
|
|
30
|
+
outputDecimals = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
31
|
+
inputDecimals = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
32
|
+
if (bps >= 10000n) {
|
|
33
|
+
amountOutWei = quoteReserve;
|
|
34
|
+
amountInWei = baseReserve;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const quoteReserveNew = quoteReserve * (0, clmm_depth_calculator_1.bigIntSqrt)((10000n - bps) * PRECISION) / (0, clmm_depth_calculator_1.bigIntSqrt)(10000n * PRECISION);
|
|
38
|
+
const baseReserveNew = baseReserve * (0, clmm_depth_calculator_1.bigIntSqrt)(10000n * PRECISION) / (0, clmm_depth_calculator_1.bigIntSqrt)((10000n - bps) * PRECISION);
|
|
39
|
+
amountOutWei = quoteReserve - quoteReserveNew;
|
|
40
|
+
amountInWei = baseReserveNew - baseReserve;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const amountOut = Number(amountOutWei) / Math.pow(10, outputDecimals);
|
|
44
|
+
const amountIn = Number(amountInWei) / Math.pow(10, inputDecimals);
|
|
45
|
+
const baseDec = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
46
|
+
const quoteDec = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
47
|
+
const baseReserveUi = Number(baseIsToken0 ? R0 : R1) / Math.pow(10, baseDec);
|
|
48
|
+
const quoteReserveUi = Number(baseIsToken0 ? R1 : R0) / Math.pow(10, quoteDec);
|
|
49
|
+
const midPrice = quoteReserveUi / baseReserveUi;
|
|
50
|
+
const bpsNum = Number(targetBps);
|
|
51
|
+
const targetPrice = isBuy
|
|
52
|
+
? midPrice * (10000 + bpsNum) / 10000
|
|
53
|
+
: midPrice * (10000 - bpsNum) / 10000;
|
|
54
|
+
return { amountOutWei, amountOut, outputDecimals, amountInWei, amountIn, inputDecimals, targetPrice };
|
|
55
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { StandardPoolInfoType } from '@clonegod/ttd-core/dist';
|
|
2
|
-
import { QuoteDepthOutput } from '@clonegod/ttd-core/dist';
|
|
2
|
+
import { QuoteTier, QuoteEntry, QuoteDepthOutput } from '@clonegod/ttd-core/dist';
|
|
3
3
|
import { DepthTick } from './clmm_depth_calculator';
|
|
4
4
|
export { calculateClmmDepth, priceFromSqrtX64, computeTargetSqrtX64, bigIntSqrt, swapExactInClmm } from './clmm_depth_calculator';
|
|
5
5
|
export type { DepthTick, ClmmDepthInput, DepthResult, SwapExactInInput, SwapExactInResult } from './clmm_depth_calculator';
|
|
6
|
+
export { calculateAmmDepth } from './amm_depth_calculator';
|
|
7
|
+
export type { AmmDepthInput, AmmDepthResult } from './amm_depth_calculator';
|
|
8
|
+
export declare function grossUpFee(amountInNet: number, feeRateBps: number): {
|
|
9
|
+
amountInGross: number;
|
|
10
|
+
feeAmount: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function assembleEntry(tiers: QuoteTier[]): QuoteEntry;
|
|
6
13
|
export interface BuildClmmDepthInput {
|
|
7
14
|
poolInfo: StandardPoolInfoType;
|
|
8
15
|
poolAddress: string;
|
|
@@ -18,3 +25,13 @@ export interface BuildClmmDepthInput {
|
|
|
18
25
|
feeRateBps: number;
|
|
19
26
|
}
|
|
20
27
|
export declare function buildClmmDepth(input: BuildClmmDepthInput): QuoteDepthOutput | undefined;
|
|
28
|
+
export interface BuildAmmDepthInput {
|
|
29
|
+
poolInfo: StandardPoolInfoType;
|
|
30
|
+
reserve0: string;
|
|
31
|
+
reserve1: string;
|
|
32
|
+
baseIsToken0: boolean;
|
|
33
|
+
basePriceUsd: number;
|
|
34
|
+
quotePriceUsd: number;
|
|
35
|
+
feeRateBps: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function buildAmmDepth(input: BuildAmmDepthInput): QuoteDepthOutput | undefined;
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.swapExactInClmm = exports.bigIntSqrt = exports.computeTargetSqrtX64 = exports.priceFromSqrtX64 = exports.calculateClmmDepth = void 0;
|
|
3
|
+
exports.calculateAmmDepth = exports.swapExactInClmm = exports.bigIntSqrt = exports.computeTargetSqrtX64 = exports.priceFromSqrtX64 = exports.calculateClmmDepth = void 0;
|
|
4
|
+
exports.grossUpFee = grossUpFee;
|
|
5
|
+
exports.assembleEntry = assembleEntry;
|
|
4
6
|
exports.buildClmmDepth = buildClmmDepth;
|
|
7
|
+
exports.buildAmmDepth = buildAmmDepth;
|
|
5
8
|
const dist_1 = require("@clonegod/ttd-core/dist");
|
|
6
9
|
const dist_2 = require("@clonegod/ttd-core/dist");
|
|
7
10
|
const trade_direction_1 = require("../../utils/trade_direction");
|
|
8
11
|
const clmm_depth_calculator_1 = require("./clmm_depth_calculator");
|
|
12
|
+
const amm_depth_calculator_1 = require("./amm_depth_calculator");
|
|
9
13
|
var clmm_depth_calculator_2 = require("./clmm_depth_calculator");
|
|
10
14
|
Object.defineProperty(exports, "calculateClmmDepth", { enumerable: true, get: function () { return clmm_depth_calculator_2.calculateClmmDepth; } });
|
|
11
15
|
Object.defineProperty(exports, "priceFromSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.priceFromSqrtX64; } });
|
|
12
16
|
Object.defineProperty(exports, "computeTargetSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.computeTargetSqrtX64; } });
|
|
13
17
|
Object.defineProperty(exports, "bigIntSqrt", { enumerable: true, get: function () { return clmm_depth_calculator_2.bigIntSqrt; } });
|
|
14
18
|
Object.defineProperty(exports, "swapExactInClmm", { enumerable: true, get: function () { return clmm_depth_calculator_2.swapExactInClmm; } });
|
|
19
|
+
var amm_depth_calculator_2 = require("./amm_depth_calculator");
|
|
20
|
+
Object.defineProperty(exports, "calculateAmmDepth", { enumerable: true, get: function () { return amm_depth_calculator_2.calculateAmmDepth; } });
|
|
15
21
|
let _depthPctLogged = false;
|
|
16
22
|
function logDepthLevelsOnce() {
|
|
17
23
|
if (_depthPctLogged)
|
|
@@ -98,3 +104,61 @@ function buildClmmDepth(input) {
|
|
|
98
104
|
return undefined;
|
|
99
105
|
}
|
|
100
106
|
}
|
|
107
|
+
function buildAmmDepth(input) {
|
|
108
|
+
logDepthLevelsOnce();
|
|
109
|
+
const pctLevels = (0, dist_2.getDepthPricePctLevels)();
|
|
110
|
+
if (pctLevels.length === 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
const { poolInfo, reserve0, reserve1, baseIsToken0, basePriceUsd, quotePriceUsd, feeRateBps } = input;
|
|
113
|
+
if (feeRateBps == null || feeRateBps < 0) {
|
|
114
|
+
(0, dist_1.log_debug)(`[Depth] ${poolInfo.pool_name} AMM: invalid feeRateBps=${feeRateBps}, skip`, '');
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const dir = (0, trade_direction_1.resolveTradeDirection)(poolInfo, true);
|
|
119
|
+
const baseToken = dir.baseToken;
|
|
120
|
+
const quoteToken = dir.quoteToken;
|
|
121
|
+
const baseReserveUi = Number(BigInt(baseIsToken0 ? reserve0 : reserve1)) / Math.pow(10, baseToken.decimals);
|
|
122
|
+
const quoteReserveUi = Number(BigInt(baseIsToken0 ? reserve1 : reserve0)) / Math.pow(10, quoteToken.decimals);
|
|
123
|
+
const midPrice = quoteReserveUi / baseReserveUi;
|
|
124
|
+
const askTiers = [];
|
|
125
|
+
const bidTiers = [];
|
|
126
|
+
for (const pct of pctLevels) {
|
|
127
|
+
const bps = Math.round(pct * 100);
|
|
128
|
+
const askResult = (0, amm_depth_calculator_1.calculateAmmDepth)({
|
|
129
|
+
reserve0, reserve1, targetBps: bps, isBuy: true, baseIsToken0,
|
|
130
|
+
token0Decimals: baseIsToken0 ? baseToken.decimals : quoteToken.decimals,
|
|
131
|
+
token1Decimals: baseIsToken0 ? quoteToken.decimals : baseToken.decimals,
|
|
132
|
+
});
|
|
133
|
+
const bidResult = (0, amm_depth_calculator_1.calculateAmmDepth)({
|
|
134
|
+
reserve0, reserve1, targetBps: bps, isBuy: false, baseIsToken0,
|
|
135
|
+
token0Decimals: baseIsToken0 ? baseToken.decimals : quoteToken.decimals,
|
|
136
|
+
token1Decimals: baseIsToken0 ? quoteToken.decimals : baseToken.decimals,
|
|
137
|
+
});
|
|
138
|
+
const askGross = grossUpFee(askResult.amountIn, feeRateBps);
|
|
139
|
+
const bidGross = grossUpFee(bidResult.amountIn, feeRateBps);
|
|
140
|
+
const askEffPrice = askResult.amountOut > 0 ? askGross.amountInGross / askResult.amountOut : askResult.targetPrice;
|
|
141
|
+
const bidEffPrice = bidGross.amountInGross > 0 ? bidResult.amountOut / bidGross.amountInGross : bidResult.targetPrice;
|
|
142
|
+
askTiers.push({
|
|
143
|
+
pct, price: askEffPrice, amount: askResult.amountOut,
|
|
144
|
+
amount_in: askGross.amountInGross, amount_in_usd: askGross.amountInGross * quotePriceUsd,
|
|
145
|
+
fee: askGross.feeAmount, fee_usd: askGross.feeAmount * quotePriceUsd,
|
|
146
|
+
});
|
|
147
|
+
bidTiers.push({
|
|
148
|
+
pct, price: bidEffPrice, amount: bidResult.amountOut,
|
|
149
|
+
amount_in: bidGross.amountInGross, amount_in_usd: bidGross.amountInGross * basePriceUsd,
|
|
150
|
+
fee: bidGross.feeAmount, fee_usd: bidGross.feeAmount * basePriceUsd,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
mid_price: midPrice,
|
|
155
|
+
fee_rate_bps: feeRateBps,
|
|
156
|
+
ask: assembleEntry(askTiers),
|
|
157
|
+
bid: assembleEntry(bidTiers),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
(0, dist_1.log_debug)(`[Depth] ${poolInfo.pool_name} AMM depth failed: ${error.message}`, '');
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/trade/index.d.ts
CHANGED
package/dist/trade/index.js
CHANGED
|
@@ -18,5 +18,4 @@ __exportStar(require("./tx_result_check"), exports);
|
|
|
18
18
|
__exportStar(require("./tx_result_parse"), exports);
|
|
19
19
|
__exportStar(require("./tx_builder"), exports);
|
|
20
20
|
__exportStar(require("./send"), exports);
|
|
21
|
-
__exportStar(require("./jito_tip_wallets"), exports);
|
|
22
21
|
__exportStar(require("./sol_gas_cache"), exports);
|
|
@@ -3,5 +3,4 @@ import { TradeContext } from '@clonegod/ttd-core/dist';
|
|
|
3
3
|
export declare class TransactionSender {
|
|
4
4
|
constructor();
|
|
5
5
|
sendTransaction(context: TradeContext, mainTx: Transaction | VersionedTransaction, swqos_only?: boolean): Promise<string>;
|
|
6
|
-
sendBundle(context: TradeContext, mainTx: Transaction | VersionedTransaction, tipTx: Transaction | VersionedTransaction, use_multi_ips?: boolean): Promise<string>;
|
|
7
6
|
}
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TransactionSender = void 0;
|
|
4
4
|
const web3_js_1 = require("@solana/web3.js");
|
|
5
5
|
const helius_1 = require("./helius");
|
|
6
|
-
const jito_1 = require("./jito");
|
|
7
6
|
const common_1 = require("../../common");
|
|
8
7
|
const sol_gas_cache_1 = require("../sol_gas_cache");
|
|
9
8
|
class TransactionSender {
|
|
@@ -15,18 +14,6 @@ class TransactionSender {
|
|
|
15
14
|
const singedTxBase64 = serializeTransactionBase64(mainTx);
|
|
16
15
|
return await (0, helius_1.sendTxWithHelius)(singedTxBase64, swqos_only);
|
|
17
16
|
}
|
|
18
|
-
async sendBundle(context, mainTx, tipTx, use_multi_ips = false) {
|
|
19
|
-
const mainTxHash = (0, common_1.getSignature)(mainTx);
|
|
20
|
-
set_transaciton_gas_cost(mainTxHash, context);
|
|
21
|
-
const mainTxBase64 = serializeTransactionBase64(mainTx);
|
|
22
|
-
const tipTxBase64 = serializeTransactionBase64(tipTx);
|
|
23
|
-
if (use_multi_ips) {
|
|
24
|
-
return await (0, jito_1.sendBundleWithJitoMultiIps)(mainTxHash, mainTxBase64, tipTxBase64);
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
return await (0, jito_1.sendBundleWithJito)(mainTxBase64, tipTxBase64);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
17
|
}
|
|
31
18
|
exports.TransactionSender = TransactionSender;
|
|
32
19
|
function serializeTransactionBase64(tx) {
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { TradeContext } from "@clonegod/ttd-core/dist";
|
|
2
2
|
import { Connection, Keypair, Transaction, TransactionInstruction } from "@solana/web3.js";
|
|
3
3
|
import { SolanaTradeAppConfig } from "../appconfig/SolanaTradeAppConfig";
|
|
4
|
-
import { JitoTipWalletManager } from "./jito_tip_wallets";
|
|
5
4
|
export declare class SolTransactionBuilder {
|
|
6
5
|
appConfig: SolanaTradeAppConfig;
|
|
7
6
|
connection: Connection;
|
|
8
7
|
keypair: Keypair;
|
|
9
8
|
recentBlockhash: string;
|
|
10
9
|
recentBlockheight: number;
|
|
11
|
-
jitoTipWalletManager: JitoTipWalletManager;
|
|
12
10
|
constructor(appConfig: SolanaTradeAppConfig);
|
|
13
11
|
init(): Promise<void>;
|
|
14
12
|
private handleBlockUpdateEvent;
|
|
@@ -16,5 +14,4 @@ export declare class SolTransactionBuilder {
|
|
|
16
14
|
private createComputeUnitLimitWithJitoDontFront;
|
|
17
15
|
private getPostInstructions;
|
|
18
16
|
buildTransactionForSwap(context: TradeContext, swapInstructions: TransactionInstruction[]): Transaction;
|
|
19
|
-
buildTransactionForTipJito(context: TradeContext): Transaction;
|
|
20
17
|
}
|
package/dist/trade/tx_builder.js
CHANGED
|
@@ -5,8 +5,6 @@ const dist_1 = require("@clonegod/ttd-core/dist");
|
|
|
5
5
|
const web3_js_1 = require("@solana/web3.js");
|
|
6
6
|
const common_1 = require("../common");
|
|
7
7
|
const helius_1 = require("./send/helius");
|
|
8
|
-
const jito_1 = require("./send/jito");
|
|
9
|
-
const jito_tip_wallets_1 = require("./jito_tip_wallets");
|
|
10
8
|
const jitodontfrontAccounts = [
|
|
11
9
|
'jitodontfront111111111111111111111111111111',
|
|
12
10
|
'jitodontfront111111111111111111111111111123',
|
|
@@ -52,7 +50,6 @@ class SolTransactionBuilder {
|
|
|
52
50
|
this.appConfig = appConfig;
|
|
53
51
|
this.connection = appConfig.connection;
|
|
54
52
|
this.keypair = appConfig.keypair;
|
|
55
|
-
this.jitoTipWalletManager = new jito_tip_wallets_1.JitoTipWalletManager();
|
|
56
53
|
this.init();
|
|
57
54
|
}
|
|
58
55
|
async init() {
|
|
@@ -99,51 +96,22 @@ class SolTransactionBuilder {
|
|
|
99
96
|
swapTx.recentBlockhash = this.recentBlockhash;
|
|
100
97
|
swapTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset;
|
|
101
98
|
swapTx.feePayer = this.keypair.publicKey;
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
let tip_lamports = sol_tip_fee;
|
|
105
|
-
if (tip_lamports < 5000) {
|
|
106
|
-
tip_lamports = 5000;
|
|
107
|
-
}
|
|
108
|
-
if (tip_lamports > 300000) {
|
|
109
|
-
tip_lamports = 300000;
|
|
110
|
-
}
|
|
111
|
-
let tip_instruction = web3_js_1.SystemProgram.transfer({
|
|
112
|
-
fromPubkey: this.keypair.publicKey,
|
|
113
|
-
toPubkey: new web3_js_1.PublicKey(helius_1.HELIUS_TIP_ACCOUNTS[Math.floor(Math.random() * helius_1.HELIUS_TIP_ACCOUNTS.length)]),
|
|
114
|
-
lamports: tip_lamports,
|
|
115
|
-
});
|
|
116
|
-
swapTx.instructions.push(tip_instruction);
|
|
117
|
-
}
|
|
118
|
-
swapTx.sign(this.keypair);
|
|
119
|
-
return swapTx;
|
|
120
|
-
}
|
|
121
|
-
buildTransactionForTipJito(context) {
|
|
122
|
-
const groupId = context.trade_runtime.group.id;
|
|
123
|
-
const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset;
|
|
124
|
-
let tipPayer = this.jitoTipWalletManager.getNextWallet(groupId);
|
|
125
|
-
if (!tipPayer) {
|
|
126
|
-
tipPayer = this.keypair;
|
|
127
|
-
}
|
|
128
|
-
let tipAccount = (0, jito_1.getJitoTipAccount)();
|
|
129
|
-
let tip_lamports = context.trade_runtime.settings.strategy.sol_tip_fee;
|
|
99
|
+
const { sol_tip_fee } = context.trade_runtime.settings.strategy;
|
|
100
|
+
let tip_lamports = sol_tip_fee;
|
|
130
101
|
if (tip_lamports < 5000) {
|
|
131
102
|
tip_lamports = 5000;
|
|
132
103
|
}
|
|
133
104
|
if (tip_lamports > 300000) {
|
|
134
105
|
tip_lamports = 300000;
|
|
135
106
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
toPubkey: new web3_js_1.PublicKey(tipAccount),
|
|
107
|
+
let tip_instruction = web3_js_1.SystemProgram.transfer({
|
|
108
|
+
fromPubkey: this.keypair.publicKey,
|
|
109
|
+
toPubkey: new web3_js_1.PublicKey(helius_1.HELIUS_TIP_ACCOUNTS[Math.floor(Math.random() * helius_1.HELIUS_TIP_ACCOUNTS.length)]),
|
|
140
110
|
lamports: tip_lamports,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
jitoTipTx.sign(tipPayer);
|
|
146
|
-
return jitoTipTx;
|
|
111
|
+
});
|
|
112
|
+
swapTx.instructions.push(tip_instruction);
|
|
113
|
+
swapTx.sign(this.keypair);
|
|
114
|
+
return swapTx;
|
|
147
115
|
}
|
|
148
116
|
}
|
|
149
117
|
exports.SolTransactionBuilder = SolTransactionBuilder;
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AMM 深度计算器
|
|
3
|
+
*
|
|
4
|
+
* 给定 reserves + 价格偏移(bps),用 x*y=k 闭合公式计算可交易量。
|
|
5
|
+
* 一次乘除,零遍历。
|
|
6
|
+
*
|
|
7
|
+
* 与 CLMM 一样,给定价格范围的输出量与 fee 无关(fee 只影响需要多少 input)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { bigIntSqrt } from './clmm_depth_calculator';
|
|
11
|
+
|
|
12
|
+
export interface AmmDepthInput {
|
|
13
|
+
reserve0: string; // token0 reserve(wei string)
|
|
14
|
+
reserve1: string; // token1 reserve(wei string)
|
|
15
|
+
targetBps: number; // 价格偏移(如 15, 30)
|
|
16
|
+
isBuy: boolean; // true=买入 base token(token0), false=卖出 base token
|
|
17
|
+
/** base token 是 pool 的 token0 还是 token1 */
|
|
18
|
+
baseIsToken0: boolean;
|
|
19
|
+
token0Decimals: number;
|
|
20
|
+
token1Decimals: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AmmDepthResult {
|
|
24
|
+
/** 输出 token 数量(wei) */
|
|
25
|
+
amountOutWei: bigint;
|
|
26
|
+
/** 输出 token 数量(UI 单位) */
|
|
27
|
+
amountOut: number;
|
|
28
|
+
/** 输出的 decimals */
|
|
29
|
+
outputDecimals: number;
|
|
30
|
+
/** 输入 token 数量(wei,不含 fee;池子曲线纯输入) */
|
|
31
|
+
amountInWei: bigint;
|
|
32
|
+
/** 输入 token 数量(UI 单位,不含 fee) */
|
|
33
|
+
amountIn: number;
|
|
34
|
+
/** 输入的 decimals */
|
|
35
|
+
inputDecimals: number;
|
|
36
|
+
/** 推到的目标价(quote per base,UI 单位) */
|
|
37
|
+
targetPrice: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 计算 AMM 池子在给定价格偏移范围内的深度
|
|
42
|
+
*
|
|
43
|
+
* 公式推导(以标准 token0/token1 为例):
|
|
44
|
+
* k = R0 * R1
|
|
45
|
+
* P = R1 / R0(token0 相对 token1 的价格)
|
|
46
|
+
*
|
|
47
|
+
* Ask(买入 token0,价格上涨):
|
|
48
|
+
* P_target = P * (1 + bps/10000)
|
|
49
|
+
* R0_new = sqrt(k / P_target)
|
|
50
|
+
* depth_token0 = R0 - R0_new
|
|
51
|
+
*
|
|
52
|
+
* Bid(卖出 token0,价格下跌):
|
|
53
|
+
* P_target = P * (1 - bps/10000)
|
|
54
|
+
* R1_new = sqrt(k * P_target)
|
|
55
|
+
* depth_token1 = R1 - R1_new
|
|
56
|
+
*
|
|
57
|
+
* 注意:pair 的 base/quote 可能与 pool 的 token0/token1 顺序不同,
|
|
58
|
+
* 通过 baseIsToken0 参数处理。
|
|
59
|
+
*/
|
|
60
|
+
export function calculateAmmDepth(input: AmmDepthInput): AmmDepthResult {
|
|
61
|
+
const { targetBps, isBuy, baseIsToken0, token0Decimals, token1Decimals } = input;
|
|
62
|
+
const R0 = BigInt(input.reserve0);
|
|
63
|
+
const R1 = BigInt(input.reserve1);
|
|
64
|
+
|
|
65
|
+
if (R0 === 0n || R1 === 0n) {
|
|
66
|
+
return { amountOutWei: 0n, amountOut: 0, outputDecimals: 0, amountInWei: 0n, amountIn: 0, inputDecimals: 0, targetPrice: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const k = R0 * R1;
|
|
70
|
+
const bps = BigInt(targetBps);
|
|
71
|
+
// 精度放大因子
|
|
72
|
+
const PRECISION = 10n ** 18n;
|
|
73
|
+
|
|
74
|
+
let amountOutWei: bigint;
|
|
75
|
+
let amountInWei: bigint;
|
|
76
|
+
let outputDecimals: number;
|
|
77
|
+
let inputDecimals: number;
|
|
78
|
+
|
|
79
|
+
if (isBuy) {
|
|
80
|
+
// 买入 base token → 价格上涨 → 输出 base token
|
|
81
|
+
// base token 从池子中减少
|
|
82
|
+
|
|
83
|
+
// P = quoteReserve / baseReserve
|
|
84
|
+
// P_target = P * (1 + bps/10000) = P * (10000 + bps) / 10000
|
|
85
|
+
// baseReserve_new = sqrt(k / P_target) = sqrt(k * baseReserve / quoteReserve * 10000 / (10000 + bps))
|
|
86
|
+
//
|
|
87
|
+
// 简化:baseReserve_new = sqrt(k * 10000 / ((10000 + bps) * P))
|
|
88
|
+
// = sqrt(k * baseReserve * 10000 / (quoteReserve * (10000 + bps)))
|
|
89
|
+
|
|
90
|
+
const [baseReserve, quoteReserve] = baseIsToken0 ? [R0, R1] : [R1, R0];
|
|
91
|
+
outputDecimals = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
92
|
+
inputDecimals = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
93
|
+
|
|
94
|
+
// baseReserve_new = baseReserve * sqrt(10000 / (10000+bps))
|
|
95
|
+
// quoteReserve_new = k / baseReserve_new = quoteReserve * sqrt((10000+bps) / 10000)
|
|
96
|
+
// amountOut(base) = baseReserve - baseReserve_new
|
|
97
|
+
// amountIn(quote, not fee-adjusted) = quoteReserve_new - quoteReserve
|
|
98
|
+
const baseReserveNew = baseReserve * bigIntSqrt(10000n * PRECISION) / bigIntSqrt((10000n + bps) * PRECISION);
|
|
99
|
+
const quoteReserveNew = quoteReserve * bigIntSqrt((10000n + bps) * PRECISION) / bigIntSqrt(10000n * PRECISION);
|
|
100
|
+
amountOutWei = baseReserve - baseReserveNew;
|
|
101
|
+
amountInWei = quoteReserveNew - quoteReserve;
|
|
102
|
+
|
|
103
|
+
} else {
|
|
104
|
+
// 卖出 base token → 价格下跌 → 输出 quote token
|
|
105
|
+
// quote token 从池子中减少
|
|
106
|
+
|
|
107
|
+
const [baseReserve, quoteReserve] = baseIsToken0 ? [R0, R1] : [R1, R0];
|
|
108
|
+
outputDecimals = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
109
|
+
inputDecimals = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
110
|
+
|
|
111
|
+
// quoteReserve_new = quoteReserve * sqrt((10000-bps) / 10000)
|
|
112
|
+
// baseReserve_new = k / quoteReserve_new = baseReserve * sqrt(10000 / (10000-bps))
|
|
113
|
+
// amountOut(quote) = quoteReserve - quoteReserve_new
|
|
114
|
+
// amountIn(base, not fee-adjusted) = baseReserve_new - baseReserve
|
|
115
|
+
if (bps >= 10000n) {
|
|
116
|
+
// 100% 偏移 → 所有 quote token;amountIn 取一个有限上界(baseReserve)
|
|
117
|
+
amountOutWei = quoteReserve;
|
|
118
|
+
amountInWei = baseReserve; // 极端情况下池子 base 耗尽
|
|
119
|
+
} else {
|
|
120
|
+
const quoteReserveNew = quoteReserve * bigIntSqrt((10000n - bps) * PRECISION) / bigIntSqrt(10000n * PRECISION);
|
|
121
|
+
const baseReserveNew = baseReserve * bigIntSqrt(10000n * PRECISION) / bigIntSqrt((10000n - bps) * PRECISION);
|
|
122
|
+
amountOutWei = quoteReserve - quoteReserveNew;
|
|
123
|
+
amountInWei = baseReserveNew - baseReserve;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const amountOut = Number(amountOutWei) / Math.pow(10, outputDecimals);
|
|
128
|
+
const amountIn = Number(amountInWei) / Math.pow(10, inputDecimals);
|
|
129
|
+
|
|
130
|
+
// 计算目标价(quote per base,UI 单位)
|
|
131
|
+
// 推到目标价后的池子状态:基于 P_target = P * (1 ± bps/10000)
|
|
132
|
+
const baseDec = baseIsToken0 ? token0Decimals : token1Decimals;
|
|
133
|
+
const quoteDec = baseIsToken0 ? token1Decimals : token0Decimals;
|
|
134
|
+
const baseReserveUi = Number(baseIsToken0 ? R0 : R1) / Math.pow(10, baseDec);
|
|
135
|
+
const quoteReserveUi = Number(baseIsToken0 ? R1 : R0) / Math.pow(10, quoteDec);
|
|
136
|
+
const midPrice = quoteReserveUi / baseReserveUi;
|
|
137
|
+
const bpsNum = Number(targetBps);
|
|
138
|
+
const targetPrice = isBuy
|
|
139
|
+
? midPrice * (10000 + bpsNum) / 10000
|
|
140
|
+
: midPrice * (10000 - bpsNum) / 10000;
|
|
141
|
+
|
|
142
|
+
return { amountOutWei, amountOut, outputDecimals, amountInWei, amountIn, inputDecimals, targetPrice };
|
|
143
|
+
}
|
package/src/quote/depth/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* 深度计算模块(Solana,tier 版)
|
|
3
3
|
*
|
|
4
4
|
* port 自 SUI `ttd-sui-common/src/quote/depth/index.ts`(同源 BSC),差异(有理由的偏离):
|
|
5
|
-
* ·
|
|
6
|
-
* AMM/
|
|
5
|
+
* · **CLMM + AMM**:三个 CLMM(Raydium/Orca/ByReal)全是 Uniswap V3 风格集中流动性 X64 → buildClmmDepth;
|
|
6
|
+
* 两个 AMM(Raydium AMM/CPMM)是 x·y=k 恒定乘积 → buildAmmDepth(reserve 上闭式);
|
|
7
|
+
* DAMMv2(CP-AMM 单 sqrtPrice 区间)复用 buildClmmDepth(ticks=[]);DLMM bin 深度待做。
|
|
7
8
|
* · **X64**:调用 X64 版 clmm_depth_calculator(见同目录,逐字照搬 SUI)。
|
|
8
9
|
* · **tick + sqrtPriceX64 由调用方物化**:各 DEX 把本地 tickArray cache 的已初始化 tick 配
|
|
9
10
|
* **共享 TickMath**(`../tick/clmm_tick_math` getSqrtPriceX64FromTick)算的 sqrtPriceX64,
|
|
@@ -24,9 +25,12 @@ import {
|
|
|
24
25
|
} from '@clonegod/ttd-core/dist';
|
|
25
26
|
import { resolveTradeDirection } from '../../utils/trade_direction';
|
|
26
27
|
import { DepthTick, calculateClmmDepth, priceFromSqrtX64 } from './clmm_depth_calculator';
|
|
28
|
+
import { calculateAmmDepth } from './amm_depth_calculator';
|
|
27
29
|
|
|
28
30
|
export { calculateClmmDepth, priceFromSqrtX64, computeTargetSqrtX64, bigIntSqrt, swapExactInClmm } from './clmm_depth_calculator';
|
|
29
31
|
export type { DepthTick, ClmmDepthInput, DepthResult, SwapExactInInput, SwapExactInResult } from './clmm_depth_calculator';
|
|
32
|
+
export { calculateAmmDepth } from './amm_depth_calculator';
|
|
33
|
+
export type { AmmDepthInput, AmmDepthResult } from './amm_depth_calculator';
|
|
30
34
|
|
|
31
35
|
// ========== 启动日志一次性输出 ==========
|
|
32
36
|
let _depthPctLogged = false;
|
|
@@ -40,8 +44,10 @@ function logDepthLevelsOnce(): void {
|
|
|
40
44
|
/**
|
|
41
45
|
* 池子曲线纯输入 → 用户实付(含费):
|
|
42
46
|
* amountInGross = amountInNet / (1 - feeRate);feeAmount = amountInGross - amountInNet
|
|
47
|
+
*
|
|
48
|
+
* 导出供 DEX 专有 depth 构建复用(如 DLMM 离散 bin 深度,在各自包内但 tier 语义统一)。
|
|
43
49
|
*/
|
|
44
|
-
function grossUpFee(amountInNet: number, feeRateBps: number): { amountInGross: number; feeAmount: number } {
|
|
50
|
+
export function grossUpFee(amountInNet: number, feeRateBps: number): { amountInGross: number; feeAmount: number } {
|
|
45
51
|
if (feeRateBps <= 0 || amountInNet <= 0) return { amountInGross: amountInNet, feeAmount: 0 };
|
|
46
52
|
const feeRatio = feeRateBps / 10000;
|
|
47
53
|
const amountInGross = amountInNet / (1 - feeRatio);
|
|
@@ -49,7 +55,8 @@ function grossUpFee(amountInNet: number, feeRateBps: number): { amountInGross: n
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// ========== 把多档 tier 装配成 QuoteEntry(主档镜像顶层)==========
|
|
52
|
-
|
|
58
|
+
/** 导出供 DEX 专有 depth 构建复用(tier 语义统一)。 */
|
|
59
|
+
export function assembleEntry(tiers: QuoteTier[]): QuoteEntry {
|
|
53
60
|
const def = tiers.find(t => t.pct === DEFAULT_TIER_PCT) ?? tiers[0];
|
|
54
61
|
return {
|
|
55
62
|
price: def.price, amount: def.amount, amount_in: def.amount_in, amount_in_usd: def.amount_in_usd,
|
|
@@ -165,3 +172,101 @@ export function buildClmmDepth(input: BuildClmmDepthInput): QuoteDepthOutput | u
|
|
|
165
172
|
return undefined;
|
|
166
173
|
}
|
|
167
174
|
}
|
|
175
|
+
|
|
176
|
+
// ========== AMM 通用深度构建 ==========
|
|
177
|
+
//
|
|
178
|
+
// port 自 BSC `arb-chain-bsc/.../quote/depth/index.ts` buildAmmDepth;差异(有理由的偏离):
|
|
179
|
+
// · **baseIsToken0 由调用方传**(用链上 mintA/mintB 判定),不在此用地址 lowercase 比较——
|
|
180
|
+
// Solana 地址 base58 大小写敏感,不可 lowercase(与 buildClmmDepth 一致)。
|
|
181
|
+
// · reserve0/reserve1 = 链上 token0(mintA)/token1(mintB) 的 reserve(wei string)。
|
|
182
|
+
|
|
183
|
+
export interface BuildAmmDepthInput {
|
|
184
|
+
poolInfo: StandardPoolInfoType;
|
|
185
|
+
/** token0(=链上 mintA) 的 reserve(wei string) */
|
|
186
|
+
reserve0: string;
|
|
187
|
+
/** token1(=链上 mintB) 的 reserve(wei string) */
|
|
188
|
+
reserve1: string;
|
|
189
|
+
/** base token 是否为链上 token0(=mintA);调用方用链上 mint 判定(base58 大小写敏感) */
|
|
190
|
+
baseIsToken0: boolean;
|
|
191
|
+
basePriceUsd: number;
|
|
192
|
+
quotePriceUsd: number;
|
|
193
|
+
/**
|
|
194
|
+
* 池子 effective fee rate (bps),必填。各 DEX 从配置 fee_rate(market-data 强制整数 ≥1)传入。
|
|
195
|
+
* 错则 verify 跨源 diff 系统性偏移会暴露。
|
|
196
|
+
*/
|
|
197
|
+
feeRateBps: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 通用 AMM 深度构建:多档(默认 5 档)ask/bid 深度,从内存 reserve 零额外 RPC 计算(x·y=k 闭式)。
|
|
202
|
+
*/
|
|
203
|
+
export function buildAmmDepth(input: BuildAmmDepthInput): QuoteDepthOutput | undefined {
|
|
204
|
+
logDepthLevelsOnce();
|
|
205
|
+
const pctLevels = getDepthPricePctLevels();
|
|
206
|
+
if (pctLevels.length === 0) return undefined;
|
|
207
|
+
|
|
208
|
+
const { poolInfo, reserve0, reserve1, baseIsToken0, basePriceUsd, quotePriceUsd, feeRateBps } = input;
|
|
209
|
+
if (feeRateBps == null || feeRateBps < 0) {
|
|
210
|
+
log_debug(`[Depth] ${poolInfo.pool_name} AMM: invalid feeRateBps=${feeRateBps}, skip`, '');
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const dir = resolveTradeDirection(poolInfo, true);
|
|
216
|
+
const baseToken = dir.baseToken;
|
|
217
|
+
const quoteToken = dir.quoteToken;
|
|
218
|
+
|
|
219
|
+
// 中间价:从 reserve 比值算(UI 单位)
|
|
220
|
+
const baseReserveUi = Number(BigInt(baseIsToken0 ? reserve0 : reserve1)) / Math.pow(10, baseToken.decimals);
|
|
221
|
+
const quoteReserveUi = Number(BigInt(baseIsToken0 ? reserve1 : reserve0)) / Math.pow(10, quoteToken.decimals);
|
|
222
|
+
const midPrice = quoteReserveUi / baseReserveUi;
|
|
223
|
+
|
|
224
|
+
const askTiers: QuoteTier[] = [];
|
|
225
|
+
const bidTiers: QuoteTier[] = [];
|
|
226
|
+
|
|
227
|
+
for (const pct of pctLevels) {
|
|
228
|
+
const bps = Math.round(pct * 100);
|
|
229
|
+
|
|
230
|
+
const askResult = calculateAmmDepth({
|
|
231
|
+
reserve0, reserve1, targetBps: bps, isBuy: true, baseIsToken0,
|
|
232
|
+
token0Decimals: baseIsToken0 ? baseToken.decimals : quoteToken.decimals,
|
|
233
|
+
token1Decimals: baseIsToken0 ? quoteToken.decimals : baseToken.decimals,
|
|
234
|
+
});
|
|
235
|
+
const bidResult = calculateAmmDepth({
|
|
236
|
+
reserve0, reserve1, targetBps: bps, isBuy: false, baseIsToken0,
|
|
237
|
+
token0Decimals: baseIsToken0 ? baseToken.decimals : quoteToken.decimals,
|
|
238
|
+
token1Decimals: baseIsToken0 ? quoteToken.decimals : baseToken.decimals,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Ask: amount_in 是 quote token;Bid: amount_in 是 base token
|
|
242
|
+
const askGross = grossUpFee(askResult.amountIn, feeRateBps);
|
|
243
|
+
const bidGross = grossUpFee(bidResult.amountIn, feeRateBps);
|
|
244
|
+
|
|
245
|
+
// 【强制·原则2】tier.price = effective avg price including fee = amount_in_gross / amount_out
|
|
246
|
+
// 跟 chain swap exec_price 严格同语义;禁止改回 marginal / 不含 fee 形态(G4 事故根因)。
|
|
247
|
+
const askEffPrice = askResult.amountOut > 0 ? askGross.amountInGross / askResult.amountOut : askResult.targetPrice;
|
|
248
|
+
const bidEffPrice = bidGross.amountInGross > 0 ? bidResult.amountOut / bidGross.amountInGross : bidResult.targetPrice;
|
|
249
|
+
|
|
250
|
+
askTiers.push({
|
|
251
|
+
pct, price: askEffPrice, amount: askResult.amountOut,
|
|
252
|
+
amount_in: askGross.amountInGross, amount_in_usd: askGross.amountInGross * quotePriceUsd,
|
|
253
|
+
fee: askGross.feeAmount, fee_usd: askGross.feeAmount * quotePriceUsd,
|
|
254
|
+
});
|
|
255
|
+
bidTiers.push({
|
|
256
|
+
pct, price: bidEffPrice, amount: bidResult.amountOut,
|
|
257
|
+
amount_in: bidGross.amountInGross, amount_in_usd: bidGross.amountInGross * basePriceUsd,
|
|
258
|
+
fee: bidGross.feeAmount, fee_usd: bidGross.feeAmount * basePriceUsd,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
mid_price: midPrice,
|
|
264
|
+
fee_rate_bps: feeRateBps,
|
|
265
|
+
ask: assembleEntry(askTiers),
|
|
266
|
+
bid: assembleEntry(bidTiers),
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
log_debug(`[Depth] ${poolInfo.pool_name} AMM depth failed: ${(error as Error).message}`, '');
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/trade/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
2
2
|
import { sendTxWithHelius } from './helius';
|
|
3
|
-
import { sendBundleWithJito, sendBundleWithJitoMultiIps } from './jito';
|
|
4
3
|
import { getSignature } from '../../common';
|
|
5
4
|
import { TradeContext } from '@clonegod/ttd-core/dist';
|
|
6
5
|
import { set_sol_gas_fee } from '../sol_gas_cache';
|
|
@@ -31,30 +30,6 @@ export class TransactionSender {
|
|
|
31
30
|
return await sendTxWithHelius(singedTxBase64, swqos_only)
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
/**
|
|
35
|
-
* 使用Jito Bundle 发送交易
|
|
36
|
-
* - use_multi_ips=true, 使用多台服务器向jito提交交易(缓解jito的限流)
|
|
37
|
-
* - use_multi_ips=false, 本地直接向jito提交交易
|
|
38
|
-
*
|
|
39
|
-
* @param mainTx 主交易
|
|
40
|
-
* @param tipTx 小费交易
|
|
41
|
-
* @param use_multi_ips true:多节点;false: 单节点
|
|
42
|
-
* @returns bundleId
|
|
43
|
-
*/
|
|
44
|
-
async sendBundle(context:TradeContext, mainTx: Transaction | VersionedTransaction, tipTx: Transaction | VersionedTransaction, use_multi_ips: boolean=false): Promise<string> {
|
|
45
|
-
const mainTxHash = getSignature(mainTx)
|
|
46
|
-
set_transaciton_gas_cost(mainTxHash, context)
|
|
47
|
-
|
|
48
|
-
const mainTxBase64 = serializeTransactionBase64(mainTx)
|
|
49
|
-
const tipTxBase64 = serializeTransactionBase64(tipTx)
|
|
50
|
-
|
|
51
|
-
if(use_multi_ips) {
|
|
52
|
-
return await sendBundleWithJitoMultiIps(mainTxHash, mainTxBase64, tipTxBase64)
|
|
53
|
-
} else {
|
|
54
|
-
return await sendBundleWithJito(mainTxBase64, tipTxBase64)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
33
|
}
|
|
59
34
|
|
|
60
35
|
|
package/src/trade/tx_builder.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { LAMPORTS_PER_MICRO_LAMPORTS } from "../common";
|
|
|
4
4
|
import { SolanaBlockMetaUpdateEvent } from "../types";
|
|
5
5
|
import { SolanaTradeAppConfig } from "../appconfig/SolanaTradeAppConfig";
|
|
6
6
|
import { HELIUS_TIP_ACCOUNTS } from "./send/helius";
|
|
7
|
-
import { getJitoTipAccount } from "./send/jito";
|
|
8
|
-
import { JitoTipWalletManager } from "./jito_tip_wallets";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* Jito Sandwich Mitigation: jitodontfront 账户
|
|
@@ -71,13 +69,10 @@ export class SolTransactionBuilder {
|
|
|
71
69
|
recentBlockhash: string
|
|
72
70
|
recentBlockheight: number
|
|
73
71
|
|
|
74
|
-
jitoTipWalletManager: JitoTipWalletManager
|
|
75
|
-
|
|
76
72
|
constructor(appConfig: SolanaTradeAppConfig) {
|
|
77
73
|
this.appConfig = appConfig
|
|
78
74
|
this.connection = appConfig.connection
|
|
79
75
|
this.keypair = appConfig.keypair
|
|
80
|
-
this.jitoTipWalletManager = new JitoTipWalletManager()
|
|
81
76
|
this.init()
|
|
82
77
|
}
|
|
83
78
|
|
|
@@ -175,74 +170,29 @@ export class SolTransactionBuilder {
|
|
|
175
170
|
swapTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset
|
|
176
171
|
swapTx.feePayer = this.keypair.publicKey
|
|
177
172
|
|
|
178
|
-
const {
|
|
179
|
-
|
|
180
|
-
// sol_bundle_only = false, 使用 Helius Sender
|
|
181
|
-
// sol_bundle_only = true, 使用 Jito Bundle - 不需要给Helius小费
|
|
182
|
-
if (!sol_bundle_only) {
|
|
183
|
-
// 使用 Helius Sender:
|
|
184
|
-
// 1、swqos_only: 至少 5000 lamports 小费
|
|
185
|
-
// -> transaction must send a tip of at least 5000 lamports to one of the following Helius wallets: [xxx, ...]
|
|
186
|
-
// 2、swqos + jito: 至少 0.0002 SOL 小费 -> lamports = 0.0002 * LAMPORTS_PER_SOL
|
|
187
|
-
// -> Requires minimum 0.0002 SOL tip.
|
|
188
|
-
let tip_lamports = sol_tip_fee
|
|
189
|
-
if (tip_lamports < 5000) {
|
|
190
|
-
tip_lamports = 5000
|
|
191
|
-
}
|
|
192
|
-
if (tip_lamports > 300000) {
|
|
193
|
-
tip_lamports = 300000
|
|
194
|
-
}
|
|
195
|
-
let tip_instruction = SystemProgram.transfer({
|
|
196
|
-
fromPubkey: this.keypair.publicKey,
|
|
197
|
-
toPubkey: new PublicKey(HELIUS_TIP_ACCOUNTS[Math.floor(Math.random() * HELIUS_TIP_ACCOUNTS.length)]),
|
|
198
|
-
lamports: tip_lamports,
|
|
199
|
-
})
|
|
200
|
-
swapTx.instructions.push(tip_instruction)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
swapTx.sign(this.keypair)
|
|
204
|
-
|
|
205
|
-
return swapTx
|
|
206
|
-
}
|
|
207
|
-
|
|
173
|
+
const { sol_tip_fee } = context.trade_runtime.settings.strategy
|
|
208
174
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const groupId = context.trade_runtime.group.id
|
|
215
|
-
const max_block_offset = context.trade_runtime.settings.strategy.max_block_offset
|
|
216
|
-
|
|
217
|
-
let tipPayer = this.jitoTipWalletManager.getNextWallet(groupId)
|
|
218
|
-
if (!tipPayer) {
|
|
219
|
-
tipPayer = this.keypair
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let tipAccount = getJitoTipAccount()
|
|
223
|
-
let tip_lamports = context.trade_runtime.settings.strategy.sol_tip_fee
|
|
175
|
+
// 发送只走 Helius Sender,必须附带最低小费:
|
|
176
|
+
// 1、swqos_only: 至少 5000 lamports 小费
|
|
177
|
+
// -> transaction must send a tip of at least 5000 lamports to one of the Helius tip wallets
|
|
178
|
+
// 2、swqos + jito: 至少 0.0002 SOL 小费 -> lamports = 0.0002 * LAMPORTS_PER_SOL
|
|
179
|
+
let tip_lamports = sol_tip_fee
|
|
224
180
|
if (tip_lamports < 5000) {
|
|
225
181
|
tip_lamports = 5000
|
|
226
182
|
}
|
|
227
183
|
if (tip_lamports > 300000) {
|
|
228
184
|
tip_lamports = 300000
|
|
229
185
|
}
|
|
186
|
+
let tip_instruction = SystemProgram.transfer({
|
|
187
|
+
fromPubkey: this.keypair.publicKey,
|
|
188
|
+
toPubkey: new PublicKey(HELIUS_TIP_ACCOUNTS[Math.floor(Math.random() * HELIUS_TIP_ACCOUNTS.length)]),
|
|
189
|
+
lamports: tip_lamports,
|
|
190
|
+
})
|
|
191
|
+
swapTx.instructions.push(tip_instruction)
|
|
192
|
+
|
|
193
|
+
swapTx.sign(this.keypair)
|
|
230
194
|
|
|
231
|
-
|
|
232
|
-
jitoTipTx.add(
|
|
233
|
-
SystemProgram.transfer({
|
|
234
|
-
fromPubkey: tipPayer.publicKey,
|
|
235
|
-
toPubkey: new PublicKey(tipAccount),
|
|
236
|
-
lamports: tip_lamports,
|
|
237
|
-
}),
|
|
238
|
-
);
|
|
239
|
-
jitoTipTx.recentBlockhash = this.recentBlockhash
|
|
240
|
-
jitoTipTx.lastValidBlockHeight = this.recentBlockheight + max_block_offset
|
|
241
|
-
jitoTipTx.feePayer = tipPayer.publicKey;
|
|
242
|
-
|
|
243
|
-
jitoTipTx.sign(tipPayer)
|
|
244
|
-
|
|
245
|
-
return jitoTipTx
|
|
195
|
+
return swapTx
|
|
246
196
|
}
|
|
247
197
|
|
|
248
198
|
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { Keypair } from '@solana/web3.js';
|
|
2
|
-
import { log_info, log_warn, log_error } from '@clonegod/ttd-core/dist';
|
|
3
|
-
import bs58 from 'bs58';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import os from 'os';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 钱包配置接口
|
|
10
|
-
*/
|
|
11
|
-
export interface WalletConfig {
|
|
12
|
-
public_key: string;
|
|
13
|
-
private_key: string;
|
|
14
|
-
type: string; // main | sub
|
|
15
|
-
keypair: Keypair; // 添加 Keypair 字段
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 分组配置接口
|
|
20
|
-
*/
|
|
21
|
-
export interface GroupConfig {
|
|
22
|
-
group_id: string;
|
|
23
|
-
tip_wallets: WalletConfig[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Jito Tip 钱包管理器
|
|
28
|
-
*
|
|
29
|
-
* 功能:
|
|
30
|
-
* 1. 从文件加载 tip wallets
|
|
31
|
-
* 2. 返回下一个钱包(轮询)
|
|
32
|
-
*/
|
|
33
|
-
export class JitoTipWalletManager {
|
|
34
|
-
private readonly jito_tip_wallet_filename: string = 'jito_tip_wallet.json';
|
|
35
|
-
private walletConfigs: GroupConfig[] = [];
|
|
36
|
-
private walletIndexMap: Map<string, number> = new Map();
|
|
37
|
-
|
|
38
|
-
constructor() {
|
|
39
|
-
this.loadWalletConfigs();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 从私钥创建 Keypair
|
|
44
|
-
*/
|
|
45
|
-
private createKeypair(privateKeyBase58: string): Keypair {
|
|
46
|
-
const privateKeyBytes = bs58.decode(privateKeyBase58);
|
|
47
|
-
return Keypair.fromSecretKey(privateKeyBytes);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 从文件加载钱包配置
|
|
52
|
-
*/
|
|
53
|
-
private loadWalletConfigs(): void {
|
|
54
|
-
try {
|
|
55
|
-
// 从环境变量获取配置文件路径
|
|
56
|
-
let jito_tip_wallet_path = process.env.JITO_TIP_WALLET_PATH || '';
|
|
57
|
-
if (!jito_tip_wallet_path.startsWith('/')) {
|
|
58
|
-
jito_tip_wallet_path = path.join(os.homedir(), 'data', 'keypairs', this.jito_tip_wallet_filename);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(jito_tip_wallet_path)) {
|
|
62
|
-
log_warn(`[JitoTipWalletManager] 配置文件${this.jito_tip_wallet_filename}不存在!路径: ${jito_tip_wallet_path}`);
|
|
63
|
-
this.walletConfigs = [];
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const configData = fs.readFileSync(jito_tip_wallet_path, 'utf-8');
|
|
68
|
-
this.walletConfigs = JSON.parse(configData);
|
|
69
|
-
|
|
70
|
-
// 为每个钱包生成 KeyPair
|
|
71
|
-
this.walletConfigs.forEach(group => {
|
|
72
|
-
group.tip_wallets.forEach(wallet => {
|
|
73
|
-
try {
|
|
74
|
-
wallet.keypair = this.createKeypair(wallet.private_key);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
log_error(`[JitoTipWalletManager] 生成钱包 KeyPair 失败 - public_key: ${wallet.public_key}`, error);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
log_info(`[JitoTipWalletManager] 成功加载配置文件: ${jito_tip_wallet_path}`);
|
|
82
|
-
|
|
83
|
-
// 加载完成后打印钱包信息
|
|
84
|
-
this.printWalletInfo();
|
|
85
|
-
} catch (error) {
|
|
86
|
-
log_error('[JitoTipWalletManager] 加载钱包配置时出错:', error);
|
|
87
|
-
this.walletConfigs = [];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 打印钱包信息(用于调试)
|
|
93
|
-
*/
|
|
94
|
-
private printWalletInfo(): void {
|
|
95
|
-
log_info('\n=== Tip钱包配置信息 ===');
|
|
96
|
-
if (this.walletConfigs.length === 0) {
|
|
97
|
-
log_info('没有找到任何钱包配置');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.walletConfigs.forEach(group => {
|
|
102
|
-
log_info(`\n分组: ${group.group_id}`);
|
|
103
|
-
log_info(`钱包数量: ${group.tip_wallets.length}`);
|
|
104
|
-
group.tip_wallets.forEach((wallet, index) => {
|
|
105
|
-
const keypairStatus = wallet.keypair ? '✓' : '✗';
|
|
106
|
-
log_info(` ${index + 1}. ${wallet.public_key}${wallet.type ? ` (${wallet.type})` : ''} [KeyPair: ${keypairStatus}]`);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
log_info('\n===================\n');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 获取下一个钱包(轮询)
|
|
114
|
-
* @param groupId 分组ID
|
|
115
|
-
* @returns 钱包配置,如果未找到则返回 null
|
|
116
|
-
*/
|
|
117
|
-
public getNextWallet(groupId: string): Keypair | null {
|
|
118
|
-
const group = this.walletConfigs.find(g => g.group_id === groupId);
|
|
119
|
-
if (!group) {
|
|
120
|
-
log_warn(`[JitoTipWalletManager] 未找到group_id对应的钱包组: ${groupId}`);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (group.tip_wallets.length === 0) {
|
|
125
|
-
log_warn(`[JitoTipWalletManager] group_id: ${groupId} 没有配置钱包`);
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 获取当前索引,如果不存在则初始化为0
|
|
130
|
-
let currentIndex = this.walletIndexMap.get(groupId) || 0;
|
|
131
|
-
|
|
132
|
-
// 获取下一个钱包
|
|
133
|
-
const wallet = group.tip_wallets[currentIndex];
|
|
134
|
-
|
|
135
|
-
// 更新索引,如果达到列表末尾则重置为0
|
|
136
|
-
currentIndex = (currentIndex + 1) % group.tip_wallets.length;
|
|
137
|
-
this.walletIndexMap.set(groupId, currentIndex);
|
|
138
|
-
|
|
139
|
-
return wallet.keypair;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
package/src/trade/send/jito.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { log_warn } from "@clonegod/ttd-core/dist";
|
|
2
|
-
import axios, { AxiosError } from "axios";
|
|
3
|
-
import { getHttpClient } from "./http_client";
|
|
4
|
-
|
|
5
|
-
export const JITO_TIP_ACCOUNTS = [
|
|
6
|
-
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
|
|
7
|
-
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
|
|
8
|
-
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
|
|
9
|
-
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
|
|
10
|
-
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
|
|
11
|
-
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
|
|
12
|
-
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
|
|
13
|
-
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export class JitoUtils {
|
|
17
|
-
static jitoTipAccounts: string[] = []
|
|
18
|
-
static jitoTipAccountUrl = 'https://mainnet.block-engine.jito.wtf/api/v1/getTipAccounts'
|
|
19
|
-
|
|
20
|
-
static async init(): Promise<void> {
|
|
21
|
-
return JitoUtils.fetchJitoTipAccounts().then((accounts) => {
|
|
22
|
-
JitoUtils.jitoTipAccounts = accounts;
|
|
23
|
-
console.log(`fetchJitoTipAccounts success: ${accounts.length} accounts`, accounts)
|
|
24
|
-
}).catch((error) => {
|
|
25
|
-
log_warn(`fetchJitoTipAccounts failed!`, {
|
|
26
|
-
url: JitoUtils.jitoTipAccountUrl,
|
|
27
|
-
error: error.message
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
static async fetchJitoTipAccounts(): Promise<string[]> {
|
|
33
|
-
const response = await axios.post<{ result: string[] }>(JitoUtils.jitoTipAccountUrl, {
|
|
34
|
-
jsonrpc: '2.0',
|
|
35
|
-
id: 1,
|
|
36
|
-
method: 'getTipAccounts',
|
|
37
|
-
params: []
|
|
38
|
-
})
|
|
39
|
-
return response.data.result;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export const getJitoTipAccount = (): string => {
|
|
45
|
-
let jito_tip_accounts = JitoUtils.jitoTipAccounts
|
|
46
|
-
if (jito_tip_accounts.length === 0) {
|
|
47
|
-
jito_tip_accounts = JITO_TIP_ACCOUNTS
|
|
48
|
-
}
|
|
49
|
-
return jito_tip_accounts[Math.floor(Math.random() * jito_tip_accounts.length)];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
JitoUtils.init()
|
|
53
|
-
|
|
54
|
-
// -----------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
export const sendBundleWithJito = async (mainTxBase64: string, tipTxBase64: string): Promise<string> => {
|
|
57
|
-
let url = process.env.JITO_SEND_BUNDLE_URL || 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/bundles'
|
|
58
|
-
|
|
59
|
-
// 使用共享的 axios 实例,复用连接池
|
|
60
|
-
const client = getHttpClient(url)
|
|
61
|
-
|
|
62
|
-
// Bundle 顺序: [mainTx, tipTx]
|
|
63
|
-
// 根据 Jito Sandwich Mitigation 规则 (https://docs.jito.wtf/lowlatencytxnsend/#sandwich-mitigation):
|
|
64
|
-
// - 如果主交易包含 jitodontfront 账户,它必须在 bundle 的第一个位置(索引 0)
|
|
65
|
-
// - 允许的模式: [tx_with_dont_front, tip]
|
|
66
|
-
// - 不允许的模式: [tip, tx_with_dont_front]
|
|
67
|
-
const requestData = {
|
|
68
|
-
jsonrpc: '2.0',
|
|
69
|
-
id: 1,
|
|
70
|
-
method: 'sendBundle',
|
|
71
|
-
params: [
|
|
72
|
-
[mainTxBase64, tipTxBase64],
|
|
73
|
-
{
|
|
74
|
-
"encoding": "base64"
|
|
75
|
-
}
|
|
76
|
-
]
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const response = await client.post<{ result: string }>(url, requestData)
|
|
81
|
-
// {
|
|
82
|
-
// jsonrpc: '2.0',
|
|
83
|
-
// result: 'e62062b4af6963bb0f67fea429f97e4f4fee59851a4ef65154b9f49b5995b5be', // bundle id
|
|
84
|
-
// id: 1
|
|
85
|
-
// }
|
|
86
|
-
// console.dir(response.data, { depth: null })
|
|
87
|
-
const bundleId = response.data.result;
|
|
88
|
-
console.log(`[sendBundleWithJito] bundleId: ${bundleId}`);
|
|
89
|
-
|
|
90
|
-
return bundleId;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if(error instanceof AxiosError) {
|
|
93
|
-
// 提取关键错误信息
|
|
94
|
-
const status = error.response?.status;
|
|
95
|
-
const statusText = error.response?.statusText;
|
|
96
|
-
|
|
97
|
-
log_warn(`[sendBundleWithJito] Request failed: ${status} - ${statusText}`);
|
|
98
|
-
} else {
|
|
99
|
-
log_warn(`[sendBundleWithJito] Unexpected error: ${url} - ${error.message}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
export const sendBundleWithJitoMultiIps = async (mainTxHash: string, mainTxBase64: string, tipTxBase64: string): Promise<string> => {
|
|
106
|
-
let ips = (process.env.JITO_SEND_BUNDLE_SERVER_IPS || '127.0.0.1').split(',')
|
|
107
|
-
let urls = ips.map(ip => `http://${ip}:10429/solana/send_tx`)
|
|
108
|
-
|
|
109
|
-
// Bundle 顺序: [mainTx, tipTx]
|
|
110
|
-
// 与 sendBundleWithJito 保持一致,符合 Jito Sandwich Mitigation 规则
|
|
111
|
-
const body = {
|
|
112
|
-
trace_id: '',
|
|
113
|
-
txid: mainTxHash,
|
|
114
|
-
encoding: 'base64', // base64
|
|
115
|
-
encoded_tx: [mainTxBase64, tipTxBase64].join(','), // bundle: mainTx, tipTx
|
|
116
|
-
max_retry: 3
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
Promise.all(urls.map(async (url) => {
|
|
120
|
-
try {
|
|
121
|
-
const client = getHttpClient(url)
|
|
122
|
-
const response = await client.post<{ result: string }>(url, body)
|
|
123
|
-
return response.data.result;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.error(`sendBundleWithJitoByMultiIps error: ${url}`, error)
|
|
126
|
-
}
|
|
127
|
-
}))
|
|
128
|
-
|
|
129
|
-
return 'success';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|