pinpet-sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +674 -0
- package/dist/index.d.ts +342 -0
- package/dist/pinpet-sdk.cjs.js +50648 -0
- package/dist/pinpet-sdk.cjs.js.map +1 -0
- package/dist/pinpet-sdk.esm.js +39265 -0
- package/dist/pinpet-sdk.esm.js.map +1 -0
- package/dist/pinpet-sdk.js +39277 -0
- package/dist/pinpet-sdk.js.map +1 -0
- package/package.json +67 -0
- package/src/idl/pinpet.json +3511 -0
- package/src/index.js +43 -0
- package/src/modules/chain.js +1102 -0
- package/src/modules/close.js +0 -0
- package/src/modules/fast.js +431 -0
- package/src/modules/param.js +171 -0
- package/src/modules/simulator/buy.js_del +711 -0
- package/src/modules/simulator/buy_sell_token.js +300 -0
- package/src/modules/simulator/calcLiq.js +701 -0
- package/src/modules/simulator/long_shrot_stop.js +602 -0
- package/src/modules/simulator/sell.js_del +323 -0
- package/src/modules/simulator/stop_loss_utils.js +223 -0
- package/src/modules/simulator/utils.js +44 -0
- package/src/modules/simulator.js +133 -0
- package/src/modules/token.js +140 -0
- package/src/modules/trading.js +1087 -0
- package/src/sdk.js +370 -0
- package/src/types/index.d.ts +342 -0
- package/src/utils/constants.js +49 -0
- package/src/utils/curve_amm.js +884 -0
- package/src/utils/orderUtils.js +465 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const CurveAMM = require('../../utils/curve_amm');
|
|
4
|
+
const { convertApiOrdersFormat, absoluteValue } = require('./utils');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simulate buy transaction analysis
|
|
8
|
+
* @param {string} mint - Token address
|
|
9
|
+
* @param {bigint|string|number} buySolAmount - SOL amount to buy (u64 format, precision 10^9)
|
|
10
|
+
* @returns {Promise<Object>} Buy analysis result
|
|
11
|
+
*/
|
|
12
|
+
async function simulateBuy(mint, buySolAmount) {
|
|
13
|
+
// Initialize return result
|
|
14
|
+
const result = {
|
|
15
|
+
success: false,
|
|
16
|
+
errorCode: null,
|
|
17
|
+
errorMessage: null,
|
|
18
|
+
data: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Parameter validation
|
|
23
|
+
if (!mint || typeof mint !== 'string') {
|
|
24
|
+
result.errorCode = 'PARAM_ERROR';
|
|
25
|
+
result.errorMessage = 'Invalid mint parameter';
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (buySolAmount === undefined || buySolAmount === null || buySolAmount <= 0) {
|
|
30
|
+
result.errorCode = 'PARAM_ERROR';
|
|
31
|
+
result.errorMessage = 'Invalid buySolAmount parameter';
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Convert buySolAmount to bigint
|
|
36
|
+
const buyingSolAmountU64 = typeof buySolAmount === 'bigint' ? buySolAmount : BigInt(buySolAmount);
|
|
37
|
+
|
|
38
|
+
// Get current price
|
|
39
|
+
let currentPrice;
|
|
40
|
+
try {
|
|
41
|
+
const priceString = await this.sdk.data.price(mint);
|
|
42
|
+
currentPrice = BigInt(priceString);
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
result.errorCode = 'API_ERROR';
|
|
46
|
+
result.errorMessage = `Failed to get token info: ${error.message}`;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get short order list
|
|
51
|
+
let shortOrderList;
|
|
52
|
+
try {
|
|
53
|
+
const ordersData = await this.sdk.data.orders(mint, { type: 'up_orders' });
|
|
54
|
+
if (!ordersData.success || !ordersData.data || !ordersData.data.orders) {
|
|
55
|
+
result.errorCode = 'API_ERROR';
|
|
56
|
+
result.errorMessage = 'Unable to get order info';
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
shortOrderList = convertApiOrdersFormat(ordersData.data.orders);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
result.errorCode = 'API_ERROR';
|
|
62
|
+
result.errorMessage = `Failed to get order info: ${error.message}`;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle empty order list
|
|
67
|
+
if (shortOrderList.length === 0) {
|
|
68
|
+
shortOrderList.push(null);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Calculate ideal token amount without slippage
|
|
72
|
+
const idealTradeResult = CurveAMM.buyFromPriceWithSolInput(currentPrice, buyingSolAmountU64);
|
|
73
|
+
const idealTokenAmount = idealTradeResult ? idealTradeResult[1] : 0n;
|
|
74
|
+
const idealSolAmount = buyingSolAmountU64;
|
|
75
|
+
|
|
76
|
+
// Initialize price range and liquidity variables
|
|
77
|
+
let maxAllowedPrice = 0n;
|
|
78
|
+
let totalPriceSpan = 0n;
|
|
79
|
+
let transactionCompletionRate = 0.0;
|
|
80
|
+
let totalLiquiditySolAmount = 0n;
|
|
81
|
+
let totalLiquidityTokenAmount = 0n;
|
|
82
|
+
let targetReachedAtSegmentIndex = -1;
|
|
83
|
+
|
|
84
|
+
// Build price segment analysis list
|
|
85
|
+
const priceSegmentAnalysisList = new Array(shortOrderList.length);
|
|
86
|
+
|
|
87
|
+
// Iterate through order list and calculate parameters for each price segment
|
|
88
|
+
for (let segmentIndex = 0; segmentIndex < shortOrderList.length; segmentIndex++) {
|
|
89
|
+
let segmentStartPrice, segmentEndPrice;
|
|
90
|
+
|
|
91
|
+
// Determine start and end prices based on segment position
|
|
92
|
+
if (segmentIndex === 0) {
|
|
93
|
+
// First segment: start from current price
|
|
94
|
+
segmentStartPrice = currentPrice;
|
|
95
|
+
|
|
96
|
+
if (shortOrderList[0] === null) {
|
|
97
|
+
// If first order is null, no orders exist
|
|
98
|
+
segmentEndPrice = CurveAMM.MAX_U128_PRICE;
|
|
99
|
+
maxAllowedPrice = CurveAMM.MAX_U128_PRICE;
|
|
100
|
+
} else {
|
|
101
|
+
// To one unit before first order start price
|
|
102
|
+
segmentEndPrice = BigInt(shortOrderList[0].lockLpStartPrice);
|
|
103
|
+
maxAllowedPrice = BigInt(shortOrderList[0].lockLpStartPrice);
|
|
104
|
+
}
|
|
105
|
+
} else if (shortOrderList[segmentIndex] === null) {
|
|
106
|
+
// Current iteration reaches null (end of list)
|
|
107
|
+
segmentStartPrice = BigInt(shortOrderList[segmentIndex - 1].lockLpEndPrice);
|
|
108
|
+
segmentEndPrice = CurveAMM.MAX_U128_PRICE;
|
|
109
|
+
} else {
|
|
110
|
+
// Normal case: gap between two orders
|
|
111
|
+
segmentStartPrice = BigInt(shortOrderList[segmentIndex - 1].lockLpEndPrice);
|
|
112
|
+
segmentEndPrice = BigInt(shortOrderList[segmentIndex].lockLpStartPrice);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate price segment validity
|
|
116
|
+
if (segmentStartPrice > segmentEndPrice) {
|
|
117
|
+
// Invalid price segment, skip
|
|
118
|
+
priceSegmentAnalysisList[segmentIndex] = {
|
|
119
|
+
startPrice: segmentStartPrice,
|
|
120
|
+
endPrice: segmentEndPrice,
|
|
121
|
+
requiredSolAmount: null,
|
|
122
|
+
obtainableTokenAmount: null,
|
|
123
|
+
isValid: false
|
|
124
|
+
};
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (segmentStartPrice == segmentEndPrice) {
|
|
129
|
+
// Price segments are equal
|
|
130
|
+
priceSegmentAnalysisList[segmentIndex] = {
|
|
131
|
+
startPrice: segmentStartPrice,
|
|
132
|
+
endPrice: segmentEndPrice,
|
|
133
|
+
requiredSolAmount: 0n,
|
|
134
|
+
obtainableTokenAmount: 0n,
|
|
135
|
+
isValid: true
|
|
136
|
+
};
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Use AMM to calculate transaction parameters for this segment
|
|
141
|
+
const segmentTradeResult = CurveAMM.buyFromPriceToPrice(segmentStartPrice, segmentEndPrice);
|
|
142
|
+
|
|
143
|
+
if (!segmentTradeResult) {
|
|
144
|
+
// AMM calculation failed
|
|
145
|
+
priceSegmentAnalysisList[segmentIndex] = {
|
|
146
|
+
startPrice: segmentStartPrice,
|
|
147
|
+
endPrice: segmentEndPrice,
|
|
148
|
+
requiredSolAmount: null,
|
|
149
|
+
obtainableTokenAmount: null,
|
|
150
|
+
isValid: false
|
|
151
|
+
};
|
|
152
|
+
} else {
|
|
153
|
+
// Calculation successful, save result
|
|
154
|
+
const [requiredSolAmount, obtainableTokenAmount] = segmentTradeResult;
|
|
155
|
+
priceSegmentAnalysisList[segmentIndex] = {
|
|
156
|
+
startPrice: segmentStartPrice,
|
|
157
|
+
endPrice: segmentEndPrice,
|
|
158
|
+
requiredSolAmount,
|
|
159
|
+
obtainableTokenAmount,
|
|
160
|
+
isValid: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Accumulate total liquidity depth
|
|
166
|
+
for (let i = 0; i < priceSegmentAnalysisList.length; i++) {
|
|
167
|
+
const segment = priceSegmentAnalysisList[i];
|
|
168
|
+
|
|
169
|
+
if (segment.isValid && segment.requiredSolAmount !== null && segment.obtainableTokenAmount !== null) {
|
|
170
|
+
totalLiquiditySolAmount += BigInt(segment.requiredSolAmount);
|
|
171
|
+
totalLiquidityTokenAmount += BigInt(segment.obtainableTokenAmount);
|
|
172
|
+
|
|
173
|
+
// Check if accumulated token amount has reached ideal target
|
|
174
|
+
if (totalLiquidityTokenAmount >= idealTokenAmount && targetReachedAtSegmentIndex === -1) {
|
|
175
|
+
targetReachedAtSegmentIndex = i;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Calculate actual transaction parameters
|
|
181
|
+
let actualRequiredSolAmount = 0n;
|
|
182
|
+
let actualObtainableTokenAmount = 0n;
|
|
183
|
+
|
|
184
|
+
if (targetReachedAtSegmentIndex !== -1) {
|
|
185
|
+
// Can complete 100% of transaction
|
|
186
|
+
transactionCompletionRate = 100.0;
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i <= targetReachedAtSegmentIndex; i++) {
|
|
189
|
+
const currentSegment = priceSegmentAnalysisList[i];
|
|
190
|
+
|
|
191
|
+
if (i === targetReachedAtSegmentIndex) {
|
|
192
|
+
// Last segment: may only need partial transaction
|
|
193
|
+
const remainingTokenNeeded = idealTokenAmount - actualObtainableTokenAmount;
|
|
194
|
+
const partialTradeResult = CurveAMM.buyFromPriceWithTokenOutput(
|
|
195
|
+
currentSegment.startPrice,
|
|
196
|
+
remainingTokenNeeded
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (partialTradeResult) {
|
|
200
|
+
const [finalPrice, requiredSolForPartial] = partialTradeResult;
|
|
201
|
+
actualRequiredSolAmount += requiredSolForPartial;
|
|
202
|
+
actualObtainableTokenAmount += remainingTokenNeeded;
|
|
203
|
+
totalPriceSpan += absoluteValue(currentSegment.startPrice - finalPrice) + 1n;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Use this segment completely
|
|
207
|
+
actualRequiredSolAmount += currentSegment.requiredSolAmount;
|
|
208
|
+
actualObtainableTokenAmount += currentSegment.obtainableTokenAmount;
|
|
209
|
+
totalPriceSpan += absoluteValue(currentSegment.startPrice - currentSegment.endPrice) + 1n;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// Cannot complete transaction fully, use all available liquidity
|
|
214
|
+
for (let i = 0; i < priceSegmentAnalysisList.length; i++) {
|
|
215
|
+
const segment = priceSegmentAnalysisList[i];
|
|
216
|
+
if (segment.isValid) {
|
|
217
|
+
actualRequiredSolAmount += segment.requiredSolAmount;
|
|
218
|
+
actualObtainableTokenAmount += segment.obtainableTokenAmount;
|
|
219
|
+
totalPriceSpan += absoluteValue(segment.startPrice - segment.endPrice) + 1n;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Calculate transaction completion rate
|
|
224
|
+
if (idealTokenAmount > 0n) {
|
|
225
|
+
transactionCompletionRate = parseFloat(
|
|
226
|
+
CurveAMM.u64ToTokenDecimal(actualObtainableTokenAmount)
|
|
227
|
+
.div(CurveAMM.u64ToTokenDecimal(idealTokenAmount))
|
|
228
|
+
.mul(100)
|
|
229
|
+
.toFixed(2)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Recalculate theoretical SOL needed (based on actual obtainable token amount)
|
|
234
|
+
const theoreticalTradeResult = CurveAMM.buyFromPriceWithTokenOutput(currentPrice, actualObtainableTokenAmount);
|
|
235
|
+
if (theoreticalTradeResult) {
|
|
236
|
+
const [, theoreticalSolNeeded] = theoreticalTradeResult;
|
|
237
|
+
buyingSolAmountU64 = theoreticalSolNeeded;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Calculate minimum slippage percentage
|
|
242
|
+
const minimumSlippagePercentage = Math.abs(
|
|
243
|
+
100.0 * (
|
|
244
|
+
CurveAMM.u64ToSolDecimal(buyingSolAmountU64)
|
|
245
|
+
.minus(CurveAMM.u64ToSolDecimal(actualRequiredSolAmount))
|
|
246
|
+
.div(CurveAMM.u64ToSolDecimal(buyingSolAmountU64))
|
|
247
|
+
.toNumber()
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Set successful result
|
|
252
|
+
result.success = true;
|
|
253
|
+
result.data = {
|
|
254
|
+
inputType: 'sol', // Input currency type
|
|
255
|
+
inputAmount: buyingSolAmountU64, // Input amount
|
|
256
|
+
maxAllowedPrice: maxAllowedPrice, // Maximum allowed start price
|
|
257
|
+
totalPriceSpan: totalPriceSpan, // Transaction price range
|
|
258
|
+
transactionCompletionRate: transactionCompletionRate, // Theoretical transaction completion percentage
|
|
259
|
+
idealTokenAmount: idealTokenAmount, // Ideal token amount obtainable
|
|
260
|
+
idealSolAmount: idealSolAmount, // Ideal SOL amount needed
|
|
261
|
+
actualRequiredSolAmount: actualRequiredSolAmount, // Actual SOL amount required
|
|
262
|
+
actualObtainableTokenAmount: actualObtainableTokenAmount, // Actual token amount obtainable
|
|
263
|
+
theoreticalSolAmount: buyingSolAmountU64, // SOL needed under liquidity pool constraints, no slippage
|
|
264
|
+
minimumSlippagePercentage: minimumSlippagePercentage, // Minimum slippage percentage
|
|
265
|
+
totalLiquiditySolAmount: totalLiquiditySolAmount, // Total liquidity depth SOL
|
|
266
|
+
totalLiquidityTokenAmount: totalLiquidityTokenAmount // Total liquidity depth Token
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// Catch unexpected errors
|
|
271
|
+
result.errorCode = 'DATA_ERROR';
|
|
272
|
+
result.errorMessage = `Error occurred during calculation: ${error.message}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Simulate token buy transaction - calculate if target token amount can be purchased
|
|
281
|
+
* 模拟以 Token 数量为目标的买入交易 - 计算是否能买到指定数量的 Token
|
|
282
|
+
* @param {string} mint - Token address 代币地址
|
|
283
|
+
* @param {bigint|string|number} buyTokenAmount - Target token amount to buy 目标购买的 Token 数量
|
|
284
|
+
* @param {string} passOrder - Optional order address to skip (won't be liquidated) 可选的跳过订单地址
|
|
285
|
+
* @returns {Promise<Object>} Token buy simulation result 模拟结果
|
|
286
|
+
*/
|
|
287
|
+
async function simulateTokenBuy(mint, buyTokenAmount, passOrder = null) {
|
|
288
|
+
// 初始化返回结果 Initialize return result
|
|
289
|
+
const result = {
|
|
290
|
+
success: false,
|
|
291
|
+
errorCode: null,
|
|
292
|
+
errorMessage: null,
|
|
293
|
+
data: null
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// 参数验证 Parameter validation
|
|
298
|
+
if (!mint || typeof mint !== 'string') {
|
|
299
|
+
result.errorCode = 'INVALID_MINT';
|
|
300
|
+
result.errorMessage = 'Invalid mint address';
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 转换并验证 token 数量 Convert and validate token amount
|
|
305
|
+
const targetTokenAmount = typeof buyTokenAmount === 'bigint' ?
|
|
306
|
+
buyTokenAmount : BigInt(buyTokenAmount);
|
|
307
|
+
|
|
308
|
+
if (targetTokenAmount <= 0n) {
|
|
309
|
+
result.errorCode = 'INVALID_AMOUNT';
|
|
310
|
+
result.errorMessage = 'Token amount must be positive';
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 验证 passOrder 参数 Validate passOrder parameter
|
|
315
|
+
if (passOrder !== null && typeof passOrder !== 'string') {
|
|
316
|
+
result.errorCode = 'INVALID_PASS_ORDER';
|
|
317
|
+
result.errorMessage = 'Pass order must be a valid address string';
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 获取当前价格 Get current price
|
|
322
|
+
let currentPrice;
|
|
323
|
+
try {
|
|
324
|
+
const priceString = await this.sdk.data.price(mint);
|
|
325
|
+
currentPrice = BigInt(priceString);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
result.errorCode = 'API_ERROR';
|
|
328
|
+
result.errorMessage = `Failed to get price: ${error.message}`;
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 获取做空订单列表(限制为 MAX_ORDERS_COUNT + 1)
|
|
333
|
+
// Get short order list (limited to MAX_ORDERS_COUNT + 1)
|
|
334
|
+
let orders;
|
|
335
|
+
try {
|
|
336
|
+
const ordersData = await this.sdk.data.orders(mint, {
|
|
337
|
+
type: 'up_orders',
|
|
338
|
+
limit: this.sdk.MAX_ORDERS_COUNT + 1
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (!ordersData.success || !ordersData.data || !ordersData.data.orders) {
|
|
342
|
+
result.errorCode = 'API_ERROR';
|
|
343
|
+
result.errorMessage = 'Unable to get order info';
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 转换订单格式 Convert order format
|
|
348
|
+
orders = ordersData.data.orders;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
result.errorCode = 'API_ERROR';
|
|
351
|
+
result.errorMessage = `Failed to get orders: ${error.message}`;
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 场景 A: 无订单,直接计算
|
|
356
|
+
// Scenario A: No orders, direct calculation
|
|
357
|
+
if (orders.length === 0) {
|
|
358
|
+
const calcResult = CurveAMM.buyFromPriceWithTokenOutput(currentPrice, targetTokenAmount);
|
|
359
|
+
if (calcResult) {
|
|
360
|
+
const [finalPrice, requiredSol] = calcResult;
|
|
361
|
+
|
|
362
|
+
result.success = true;
|
|
363
|
+
result.data = {
|
|
364
|
+
inputType: 'token',
|
|
365
|
+
inputAmount: targetTokenAmount,
|
|
366
|
+
currentPrice: currentPrice,
|
|
367
|
+
canComplete: true,
|
|
368
|
+
completionRate: 100.0,
|
|
369
|
+
limitReason: null,
|
|
370
|
+
idealSolRequired: requiredSol,
|
|
371
|
+
idealEndPrice: finalPrice,
|
|
372
|
+
actualObtainableToken: targetTokenAmount,
|
|
373
|
+
actualRequiredSol: requiredSol,
|
|
374
|
+
actualEndPrice: finalPrice,
|
|
375
|
+
ordersToClose: [],
|
|
376
|
+
ordersToCloseCount: 0,
|
|
377
|
+
passOrderIndex: null,
|
|
378
|
+
hasMoreOrders: false,
|
|
379
|
+
totalAvailableToken: CurveAMM.MAX_U64,
|
|
380
|
+
totalAvailableSol: CurveAMM.MAX_U64,
|
|
381
|
+
priceImpact: Number((finalPrice - currentPrice) * 10000n / currentPrice) / 100,
|
|
382
|
+
maxReachablePrice: CurveAMM.MAX_U128_PRICE,
|
|
383
|
+
segments: []
|
|
384
|
+
};
|
|
385
|
+
return result;
|
|
386
|
+
} else {
|
|
387
|
+
result.errorCode = 'CURVE_ERROR';
|
|
388
|
+
result.errorMessage = 'Failed to calculate buy amounts';
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 初始化变量 Initialize variables
|
|
394
|
+
let totalAvailableToken = 0n; // 累积可购买的 token
|
|
395
|
+
let totalTokenValue = 0n; // 包含订单锁定的 token
|
|
396
|
+
let previousAvailable = 0n; // 上一次的可用量
|
|
397
|
+
let ordersToClose = []; // 需要平仓的订单索引
|
|
398
|
+
let passOrderIndex = null; // 跳过的订单索引
|
|
399
|
+
let targetReached = false; // 是否达到目标
|
|
400
|
+
let finalLpPairsIndex = -1; // 最终区间索引
|
|
401
|
+
let segments = []; // 详细的区间信息
|
|
402
|
+
|
|
403
|
+
// 计算订单数量是否超限 Check if orders exceed limit
|
|
404
|
+
const hasMoreOrders = orders.length > this.sdk.MAX_ORDERS_COUNT;
|
|
405
|
+
const processableOrders = Math.min(orders.length, this.sdk.MAX_ORDERS_COUNT);
|
|
406
|
+
|
|
407
|
+
// 第一步:计算从当前价格到第一个订单前的流动性
|
|
408
|
+
// Step 1: Calculate liquidity from current price to first order
|
|
409
|
+
if (orders.length > 0) {
|
|
410
|
+
const firstOrderStartPrice = BigInt(orders[0].lock_lp_start_price);
|
|
411
|
+
const firstSegmentResult = CurveAMM.buyFromPriceToPrice(currentPrice, firstOrderStartPrice);
|
|
412
|
+
|
|
413
|
+
if (firstSegmentResult) {
|
|
414
|
+
const [solAmount, tokenAmount] = firstSegmentResult;
|
|
415
|
+
totalAvailableToken += tokenAmount;
|
|
416
|
+
totalTokenValue += tokenAmount;
|
|
417
|
+
|
|
418
|
+
segments.push({
|
|
419
|
+
type: 'initial',
|
|
420
|
+
startPrice: currentPrice,
|
|
421
|
+
endPrice: firstOrderStartPrice,
|
|
422
|
+
tokenAmount: tokenAmount,
|
|
423
|
+
solAmount: solAmount
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
if (totalAvailableToken >= targetTokenAmount) {
|
|
427
|
+
targetReached = true;
|
|
428
|
+
finalLpPairsIndex = -1;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 第二步:遍历订单,累积计算
|
|
434
|
+
// Step 2: Iterate orders and calculate cumulatively
|
|
435
|
+
for (let i = 0; i < processableOrders && !targetReached; i++) {
|
|
436
|
+
const order = orders[i];
|
|
437
|
+
const isPassOrder = passOrder && order.order_pda === passOrder;
|
|
438
|
+
|
|
439
|
+
// 处理订单 Process order
|
|
440
|
+
if (isPassOrder) {
|
|
441
|
+
// 跳过的订单:流动性可用但不平仓
|
|
442
|
+
// Skipped order: liquidity available but not liquidated
|
|
443
|
+
previousAvailable = totalAvailableToken;
|
|
444
|
+
totalAvailableToken += BigInt(order.lock_lp_token_amount);
|
|
445
|
+
totalTokenValue += BigInt(order.lock_lp_token_amount);
|
|
446
|
+
passOrderIndex = i;
|
|
447
|
+
|
|
448
|
+
segments.push({
|
|
449
|
+
type: 'order',
|
|
450
|
+
startPrice: BigInt(order.lock_lp_start_price),
|
|
451
|
+
endPrice: BigInt(order.lock_lp_end_price),
|
|
452
|
+
tokenAmount: BigInt(order.lock_lp_token_amount),
|
|
453
|
+
solAmount: BigInt(order.lock_lp_sol_amount),
|
|
454
|
+
orderIndex: i,
|
|
455
|
+
isPassOrder: true
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
// 需要平仓的订单 Order to be liquidated
|
|
459
|
+
totalTokenValue += BigInt(order.lock_lp_token_amount);
|
|
460
|
+
ordersToClose.push(i);
|
|
461
|
+
|
|
462
|
+
segments.push({
|
|
463
|
+
type: 'order',
|
|
464
|
+
startPrice: BigInt(order.lock_lp_start_price),
|
|
465
|
+
endPrice: BigInt(order.lock_lp_end_price),
|
|
466
|
+
tokenAmount: BigInt(order.lock_lp_token_amount),
|
|
467
|
+
solAmount: BigInt(order.lock_lp_sol_amount),
|
|
468
|
+
orderIndex: i,
|
|
469
|
+
isPassOrder: false
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 检查是否达到目标 Check if target reached
|
|
474
|
+
if (totalAvailableToken >= targetTokenAmount) {
|
|
475
|
+
targetReached = true;
|
|
476
|
+
finalLpPairsIndex = i;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 检查 next_order Check next_order
|
|
481
|
+
const nextOrderAddress = order.next_order || null;
|
|
482
|
+
if (!nextOrderAddress) {
|
|
483
|
+
// 链表结束,上方有无限空间
|
|
484
|
+
// Chain ends, unlimited space above
|
|
485
|
+
finalLpPairsIndex = i;
|
|
486
|
+
previousAvailable = totalAvailableToken;
|
|
487
|
+
totalAvailableToken = CurveAMM.MAX_U64;
|
|
488
|
+
targetReached = true;
|
|
489
|
+
|
|
490
|
+
segments.push({
|
|
491
|
+
type: 'final',
|
|
492
|
+
startPrice: BigInt(order.lock_lp_end_price),
|
|
493
|
+
endPrice: CurveAMM.MAX_U128_PRICE,
|
|
494
|
+
tokenAmount: CurveAMM.MAX_U64,
|
|
495
|
+
solAmount: CurveAMM.MAX_U64
|
|
496
|
+
});
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 处理订单间隙 Process gap between orders
|
|
501
|
+
if (i < orders.length - 1 && i < this.sdk.MAX_ORDERS_COUNT - 1) {
|
|
502
|
+
const gapStartPrice = BigInt(order.lock_lp_end_price);
|
|
503
|
+
const gapEndPrice = BigInt(orders[i + 1].lock_lp_start_price);
|
|
504
|
+
|
|
505
|
+
// 检查间隙是否存在 Check if gap exists
|
|
506
|
+
if (gapStartPrice < gapEndPrice) {
|
|
507
|
+
const gapResult = CurveAMM.buyFromPriceToPrice(gapStartPrice, gapEndPrice);
|
|
508
|
+
if (gapResult) {
|
|
509
|
+
const [gapSol, gapToken] = gapResult;
|
|
510
|
+
previousAvailable = totalAvailableToken;
|
|
511
|
+
totalAvailableToken += gapToken;
|
|
512
|
+
totalTokenValue += gapToken;
|
|
513
|
+
|
|
514
|
+
segments.push({
|
|
515
|
+
type: 'gap',
|
|
516
|
+
startPrice: gapStartPrice,
|
|
517
|
+
endPrice: gapEndPrice,
|
|
518
|
+
tokenAmount: gapToken,
|
|
519
|
+
solAmount: gapSol
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (totalAvailableToken >= targetTokenAmount) {
|
|
523
|
+
targetReached = true;
|
|
524
|
+
finalLpPairsIndex = i;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else if (i === processableOrders - 1 && hasMoreOrders) {
|
|
530
|
+
// 最后一个可处理订单且还有更多订单
|
|
531
|
+
// Last processable order with more orders beyond
|
|
532
|
+
const lastGapStartPrice = BigInt(order.lock_lp_end_price);
|
|
533
|
+
const lastGapEndPrice = BigInt(orders[i + 1].lock_lp_start_price);
|
|
534
|
+
|
|
535
|
+
if (lastGapStartPrice < lastGapEndPrice) {
|
|
536
|
+
const lastGapResult = CurveAMM.buyFromPriceToPrice(lastGapStartPrice, lastGapEndPrice);
|
|
537
|
+
if (lastGapResult) {
|
|
538
|
+
const [lastGapSol, lastGapToken] = lastGapResult;
|
|
539
|
+
previousAvailable = totalAvailableToken;
|
|
540
|
+
totalAvailableToken += lastGapToken;
|
|
541
|
+
totalTokenValue += lastGapToken;
|
|
542
|
+
|
|
543
|
+
segments.push({
|
|
544
|
+
type: 'gap',
|
|
545
|
+
startPrice: lastGapStartPrice,
|
|
546
|
+
endPrice: lastGapEndPrice,
|
|
547
|
+
tokenAmount: lastGapToken,
|
|
548
|
+
solAmount: lastGapSol
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (totalAvailableToken >= targetTokenAmount) {
|
|
552
|
+
targetReached = true;
|
|
553
|
+
finalLpPairsIndex = i;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 计算理想情况(无订单阻碍)
|
|
561
|
+
// Calculate ideal case (no order obstacles)
|
|
562
|
+
const idealResult = CurveAMM.buyFromPriceWithTokenOutput(currentPrice, targetTokenAmount);
|
|
563
|
+
const [idealEndPrice, idealSolRequired] = idealResult || [0n, 0n];
|
|
564
|
+
|
|
565
|
+
// 计算实际参数 Calculate actual parameters
|
|
566
|
+
let actualRequiredSol = 0n;
|
|
567
|
+
let actualObtainableToken = 0n;
|
|
568
|
+
let actualEndPrice = currentPrice;
|
|
569
|
+
let completionRate = 0.0;
|
|
570
|
+
let limitReason = null;
|
|
571
|
+
|
|
572
|
+
if (targetReached) {
|
|
573
|
+
// 能完成交易,精确计算所需 SOL
|
|
574
|
+
// Can complete trade, calculate exact SOL needed
|
|
575
|
+
completionRate = 100.0;
|
|
576
|
+
actualObtainableToken = targetTokenAmount;
|
|
577
|
+
|
|
578
|
+
// 计算精确的结束价格和所需 SOL
|
|
579
|
+
// Calculate exact end price and required SOL
|
|
580
|
+
if (finalLpPairsIndex === -1) {
|
|
581
|
+
// 在第一个区间就完成 Completed in first segment
|
|
582
|
+
const exactResult = CurveAMM.buyFromPriceWithTokenOutput(currentPrice, targetTokenAmount);
|
|
583
|
+
if (exactResult) {
|
|
584
|
+
actualEndPrice = exactResult[0];
|
|
585
|
+
actualRequiredSol = exactResult[1];
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
// 需要跨越多个区间 Need to cross multiple segments
|
|
589
|
+
// 计算剩余需要的 token Calculate remaining token needed
|
|
590
|
+
const remainingToken = targetTokenAmount - previousAvailable;
|
|
591
|
+
|
|
592
|
+
// 找到最后的起始价格 Find final start price
|
|
593
|
+
let lastStartPrice = currentPrice;
|
|
594
|
+
if (finalLpPairsIndex >= 0 && finalLpPairsIndex < orders.length) {
|
|
595
|
+
lastStartPrice = BigInt(orders[finalLpPairsIndex].lock_lp_end_price);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 计算最后区间的精确结果 Calculate exact result for last segment
|
|
599
|
+
const partialResult = CurveAMM.buyFromPriceWithTokenOutput(lastStartPrice, remainingToken);
|
|
600
|
+
if (partialResult) {
|
|
601
|
+
actualEndPrice = partialResult[0];
|
|
602
|
+
|
|
603
|
+
// 计算总的 SOL 需求 Calculate total SOL requirement
|
|
604
|
+
const totalResult = CurveAMM.buyFromPriceToPrice(currentPrice, actualEndPrice);
|
|
605
|
+
if (totalResult) {
|
|
606
|
+
// 减去订单占用的 SOL Subtract SOL locked in orders
|
|
607
|
+
let lockedSol = 0n;
|
|
608
|
+
for (const idx of ordersToClose) {
|
|
609
|
+
if (idx < orders.length) {
|
|
610
|
+
lockedSol += BigInt(orders[idx].lock_lp_sol_amount);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
actualRequiredSol = totalResult[0] - lockedSol;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
// 无法完成交易 Cannot complete trade
|
|
619
|
+
actualObtainableToken = Math.min(totalAvailableToken, targetTokenAmount);
|
|
620
|
+
|
|
621
|
+
if (targetTokenAmount > 0n && totalAvailableToken > 0n) {
|
|
622
|
+
completionRate = Number(totalAvailableToken * 10000n / targetTokenAmount) / 100;
|
|
623
|
+
completionRate = Math.min(99.99, completionRate);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 判断限制原因 Determine limit reason
|
|
627
|
+
if (hasMoreOrders) {
|
|
628
|
+
limitReason = 'order_count_limit';
|
|
629
|
+
} else if (totalAvailableToken < targetTokenAmount) {
|
|
630
|
+
limitReason = 'insufficient_liquidity';
|
|
631
|
+
} else {
|
|
632
|
+
limitReason = 'unknown';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 计算能买到的部分所需的 SOL Calculate SOL for obtainable amount
|
|
636
|
+
if (actualObtainableToken > 0n) {
|
|
637
|
+
const partialResult = CurveAMM.buyFromPriceWithTokenOutput(currentPrice, actualObtainableToken);
|
|
638
|
+
if (partialResult) {
|
|
639
|
+
actualEndPrice = partialResult[0];
|
|
640
|
+
actualRequiredSol = partialResult[1];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 计算价格影响 Calculate price impact
|
|
646
|
+
let priceImpact = null;
|
|
647
|
+
if (targetReached && actualEndPrice > currentPrice) {
|
|
648
|
+
priceImpact = Number((actualEndPrice - currentPrice) * 10000n / currentPrice) / 100;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 计算最高可达价格 Calculate max reachable price
|
|
652
|
+
let maxReachablePrice = currentPrice;
|
|
653
|
+
if (segments.length > 0) {
|
|
654
|
+
const lastSegment = segments[segments.length - 1];
|
|
655
|
+
maxReachablePrice = lastSegment.endPrice;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 设置成功结果 Set successful result
|
|
659
|
+
result.success = true;
|
|
660
|
+
result.data = {
|
|
661
|
+
// 基本信息 Basic info
|
|
662
|
+
inputType: 'token',
|
|
663
|
+
inputAmount: targetTokenAmount,
|
|
664
|
+
currentPrice: currentPrice,
|
|
665
|
+
|
|
666
|
+
// 可行性分析 Feasibility analysis
|
|
667
|
+
canComplete: targetReached,
|
|
668
|
+
completionRate: completionRate,
|
|
669
|
+
limitReason: limitReason,
|
|
670
|
+
|
|
671
|
+
// 理想情况 Ideal case
|
|
672
|
+
idealSolRequired: idealSolRequired,
|
|
673
|
+
idealEndPrice: idealEndPrice,
|
|
674
|
+
|
|
675
|
+
// 实际情况 Actual case
|
|
676
|
+
actualObtainableToken: actualObtainableToken,
|
|
677
|
+
actualRequiredSol: actualRequiredSol,
|
|
678
|
+
actualEndPrice: actualEndPrice,
|
|
679
|
+
|
|
680
|
+
// 订单处理 Order processing
|
|
681
|
+
ordersToClose: ordersToClose,
|
|
682
|
+
ordersToCloseCount: ordersToClose.length,
|
|
683
|
+
passOrderIndex: passOrderIndex,
|
|
684
|
+
hasMoreOrders: hasMoreOrders,
|
|
685
|
+
|
|
686
|
+
// 流动性分析 Liquidity analysis
|
|
687
|
+
totalAvailableToken: totalAvailableToken,
|
|
688
|
+
totalAvailableSol: actualRequiredSol, // 简化处理
|
|
689
|
+
|
|
690
|
+
// 价格影响 Price impact
|
|
691
|
+
priceImpact: priceImpact,
|
|
692
|
+
maxReachablePrice: maxReachablePrice,
|
|
693
|
+
|
|
694
|
+
// 详细信息 Detailed info
|
|
695
|
+
segments: segments
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
} catch (error) {
|
|
699
|
+
// 捕获意外错误 Catch unexpected errors
|
|
700
|
+
result.errorCode = 'UNEXPECTED_ERROR';
|
|
701
|
+
result.errorMessage = `Unexpected error: ${error.message}`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
module.exports = {
|
|
709
|
+
simulateBuy,
|
|
710
|
+
simulateTokenBuy
|
|
711
|
+
};
|