100x-sdk 1.0.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.
@@ -0,0 +1,1310 @@
1
+
2
+ const { PublicKey } = require('@solana/web3.js');
3
+ const anchor = require('@coral-xyz/anchor');
4
+ const CurveAMM = require('../utils/curve_amm');
5
+ // 统一使用 buffer 包,所有平台一致
6
+ const { Buffer } = require('buffer');
7
+
8
+ /**
9
+ * Chain Data Module
10
+ * When no auxiliary server is available, directly call on-chain data to get transaction parameters
11
+ * The downside is that during peak trading periods, on-chain data may have delays, causing transaction failures
12
+ * Provides functionality to read account data from Solana blockchain
13
+ */
14
+ class ChainModule {
15
+ constructor(sdk) {
16
+ this.sdk = sdk;
17
+ // getCurveAccount 缓存:key = mint string, value = { data, timestamp }
18
+ this._curveAccountCache = new Map();
19
+ this._CACHE_TTL = 10000; // 10 秒 TTL
20
+ }
21
+
22
+ /**
23
+ * 清除指定 mint 的 curveAccount 缓存(交易后调用)
24
+ * @param {string} [mint] - 指定 mint 地址,不传则清除全部
25
+ */
26
+ invalidateCurveCache(mint) {
27
+ if (mint) {
28
+ this._curveAccountCache.delete(typeof mint === 'string' ? mint : mint.toString());
29
+ } else {
30
+ this._curveAccountCache.clear();
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get complete curve_account (BorrowingBondingCurve) data
36
+ *
37
+ * Read the borrowing liquidity pool account data for a specified token from the blockchain,
38
+ * including addresses and balance information for all related accounts.
39
+ * This function automatically calculates related PDA addresses and concurrently queries all balances,
40
+ * providing complete liquidity pool status.
41
+ *
42
+ * @param {string|PublicKey} mint - Token mint account address
43
+ *
44
+ * @returns {Promise<Object>} Complete BorrowingBondingCurve account data object
45
+ *
46
+ * @returns {Promise<Object>} Return object contains following complete fields:
47
+ *
48
+ * **Core Reserve Data:**
49
+ * @returns {bigint} returns.lpTokenReserve - LP Token reserves, total reserves of liquidity provider tokens
50
+ * @returns {bigint} returns.lpSolReserve - LP SOL reserves, SOL reserves in the liquidity pool
51
+ * @returns {bigint} returns.price - Current token price, calculated based on AMM algorithm
52
+ * @returns {bigint} returns.borrowTokenReserve - Borrowable Token reserves, borrowable token reserves
53
+ * @returns {bigint} returns.borrowSolReserve - Borrowable SOL reserves, borrowable SOL reserves
54
+ *
55
+ * **Fee and Parameter Configuration:**
56
+ * @returns {number} returns.swapFee - Swap fee rate, expressed in basis points (e.g. 100 = 1%)
57
+ * @returns {number} returns.borrowFee - Borrow fee rate, expressed in basis points
58
+ * @returns {number} returns.feeDiscountFlag - Fee discount flag (0: normal, 1: 50% off, 2: 25% off, 3: 12.5% off)
59
+ * @returns {number} returns.feeSplit - Fee split ratio, determines how fees are distributed among different recipients
60
+ * @returns {number} returns.borrowDuration - Borrow duration, in seconds
61
+ * @returns {number} returns.bump - curve_account PDA bump seed
62
+ *
63
+ * **Account Addresses:**
64
+ * @returns {string} returns.baseFeeRecipient - Base fee recipient address, receives base transaction fees
65
+ * @returns {string} returns.feeRecipient - Fee recipient address, receives additional fee income
66
+ * @returns {string} returns.mint - Token mint account address
67
+ * @returns {string} returns.upOrderbook - Up orderbook (short orders) PDA address
68
+ * @returns {string} returns.downOrderbook - Down orderbook (long orders) PDA address
69
+ * @returns {string} returns.creator - Token creator address, the wallet that created this token
70
+ * @returns {string} returns.poolTokenAccount - Pool token account address, stores tokens in the liquidity pool
71
+ * @returns {string} returns.poolSolAccount - Pool SOL account address, stores native SOL in the liquidity pool
72
+ *
73
+ * **Token Supply Information:**
74
+ * @returns {bigint} returns.totalSupply - Total token supply (smallest unit)
75
+ * @returns {number} returns.decimals - Token decimal places
76
+ * @returns {bigint} returns.initialBorrowPool - Initial borrow pool size = totalSupply - initialVirtualToken
77
+ * @returns {bigint} returns.circulatingSupply - Circulating supply = totalSupply - borrowTokenReserve
78
+ *
79
+ * **Balance Information:**
80
+ * @returns {number} returns.baseFeeRecipientBalance - SOL balance of base fee recipient address (lamports)
81
+ * @returns {number} returns.feeRecipientBalance - SOL balance of fee recipient address (lamports)
82
+ * @returns {bigint} returns.poolTokenBalance - Token balance of pool token account
83
+ * @returns {number} returns.poolSolBalance - SOL balance of pool SOL account (lamports)
84
+ *
85
+ * **Metadata:**
86
+ * @returns {Object} returns._metadata - Additional metadata information
87
+ * @returns {string} returns._metadata.accountAddress - Complete address of curve_account
88
+ * @returns {string} returns._metadata.mintAddress - Input token mint address
89
+ *
90
+ * @throws {Error} Throws error when curve_account does not exist
91
+ * @throws {Error} Throws error when unable to decode account data
92
+ * @throws {Error} Throws error when network connection fails
93
+ *
94
+ * @example
95
+ * // Basic usage example
96
+ * try {
97
+ * const curveData = await sdk.chain.getCurveAccount('3YggGtxXEGBbjK1WLj2Z79doZC2gkCWXag1ag8BD4cYY');
98
+ *
99
+ * // Display core reserve information
100
+ * console.log('=== Core Reserve Data ===');
101
+ * console.log('LP Token reserves:', curveData.lpTokenReserve.toString());
102
+ * console.log('LP SOL reserves:', curveData.lpSolReserve.toString());
103
+ * console.log('Current price:', curveData.price.toString());
104
+ * console.log('Borrow Token reserves:', curveData.borrowTokenReserve.toString());
105
+ * console.log('Borrow SOL reserves:', curveData.borrowSolReserve.toString());
106
+ *
107
+ * // Display fee configuration
108
+ * console.log('=== Fee Configuration ===');
109
+ * console.log('Swap fee rate:', curveData.swapFee / 100, '%');
110
+ * console.log('Borrow fee rate:', curveData.borrowFee / 100, '%');
111
+ * console.log('Fee discount flag:', curveData.feeDiscountFlag);
112
+ * console.log('Borrow duration:', curveData.borrowDuration, 'seconds');
113
+ *
114
+ * // Display account addresses
115
+ * console.log('=== Account Addresses ===');
116
+ * console.log('Token creator:', curveData.creator);
117
+ * console.log('Base fee recipient address:', curveData.baseFeeRecipient);
118
+ * console.log('Fee recipient address:', curveData.feeRecipient);
119
+ * console.log('Pool token account:', curveData.poolTokenAccount);
120
+ * console.log('Pool SOL account:', curveData.poolSolAccount);
121
+ *
122
+ * // Display balance information
123
+ * console.log('=== Balance Information ===');
124
+ * console.log('Base fee recipient balance:', curveData.baseFeeRecipientBalance / 1e9, 'SOL');
125
+ * console.log('Fee recipient balance:', curveData.feeRecipientBalance / 1e9, 'SOL');
126
+ * console.log('Pool token balance:', curveData.poolTokenBalance.toString());
127
+ * console.log('Pool SOL balance:', curveData.poolSolBalance / 1e9, 'SOL');
128
+ *
129
+ * // Display orderbook addresses
130
+ * console.log('=== Order Books ===');
131
+ * console.log('Up orderbook (short):', curveData.upOrderbook);
132
+ * console.log('Down orderbook (long):', curveData.downOrderbook);
133
+ *
134
+ * } catch (error) {
135
+ * console.error('Failed to get curve account:', error.message);
136
+ * }
137
+ *
138
+ * @example
139
+ * // Pool monitoring example
140
+ * async function monitorPool(mintAddress) {
141
+ * const data = await sdk.chain.getCurveAccount(mintAddress);
142
+ *
143
+ * // Calculate pool utilization
144
+ * const tokenUtilization = Number(data.lpTokenReserve - data.poolTokenBalance) / Number(data.lpTokenReserve);
145
+ * const solUtilization = Number(data.lpSolReserve - BigInt(data.poolSolBalance)) / Number(data.lpSolReserve);
146
+ *
147
+ * console.log('Token utilization:', (tokenUtilization * 100).toFixed(2), '%');
148
+ * console.log('SOL utilization:', (solUtilization * 100).toFixed(2), '%');
149
+ *
150
+ * // Check fee earnings
151
+ * const totalFeeBalance = data.baseFeeRecipientBalance + data.feeRecipientBalance;
152
+ * console.log('Total fee earnings:', totalFeeBalance / 1e9, 'SOL');
153
+ *
154
+ * return {
155
+ * tokenUtilization,
156
+ * solUtilization,
157
+ * totalFeeBalance,
158
+ * currentPrice: data.price
159
+ * };
160
+ * }
161
+ *
162
+ * @since 1.0.0
163
+ * @version 2.0.0 - Updated to use new OrderBook structure (up_orderbook/down_orderbook instead of upHead/downHead)
164
+ * @author 100x SDK Team
165
+ */
166
+ async getCurveAccount(mint, options = {}) {
167
+ const { skipBalances = false } = options;
168
+ try {
169
+ // Parameter validation and conversion
170
+ const mintPubkey = typeof mint === 'string' ? new PublicKey(mint) : mint;
171
+ const mintKey = mintPubkey.toString();
172
+
173
+ // 检查缓存
174
+ const cached = this._curveAccountCache.get(mintKey);
175
+ if (cached && (Date.now() - cached.timestamp < this._CACHE_TTL)) {
176
+ // 缓存命中:如果请求包含余额但缓存没有,需要重新获取余额部分
177
+ if (!skipBalances && cached.skipBalances) {
178
+ // 缓存是 skipBalances 的,但现在需要余额,需要补充查询
179
+ } else {
180
+ return cached.data;
181
+ }
182
+ }
183
+
184
+ // Calculate curve_account PDA address
185
+ // Use the same seeds as in the contract: [b"borrowing_curve", mint_account.key().as_ref()]
186
+ const [curveAccountPDA] = PublicKey.findProgramAddressSync(
187
+ [
188
+ Buffer.from("borrowing_curve"),
189
+ mintPubkey.toBuffer()
190
+ ],
191
+ this.sdk.programId
192
+ );
193
+
194
+ // Use Anchor program to fetch account data directly
195
+ // Method 1: Use program's fetch method
196
+ let decodedData;
197
+ try {
198
+ decodedData = await this.sdk.program.account.borrowingBondingCurve.fetch(curveAccountPDA);
199
+ } catch (fetchError) {
200
+ // Method 2: If fetch fails, use raw method
201
+ const accountInfo = await this.sdk.connection.getAccountInfo(curveAccountPDA);
202
+ if (!accountInfo) {
203
+ throw new Error(`curve_account does not exist`);
204
+ }
205
+
206
+ // Manually decode with BorshAccountsCoder
207
+ const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
208
+
209
+ // Try different account names
210
+ try {
211
+ decodedData = accountsCoder.decode('BorrowingBondingCurve', accountInfo.data);
212
+ } catch (decodeError1) {
213
+ try {
214
+ // Try lowercase name
215
+ decodedData = accountsCoder.decode('borrowingBondingCurve', accountInfo.data);
216
+ } catch (decodeError2) {
217
+ // Both failed, throw original error
218
+ throw new Error(`Cannot decode account data: ${decodeError1.message}`);
219
+ }
220
+ }
221
+ }
222
+
223
+ // Calculate pool account PDA addresses
224
+ const [poolTokenAccountPDA] = PublicKey.findProgramAddressSync(
225
+ [
226
+ Buffer.from("pool_token"),
227
+ mintPubkey.toBuffer()
228
+ ],
229
+ this.sdk.programId
230
+ );
231
+
232
+ const [poolSolAccountPDA] = PublicKey.findProgramAddressSync(
233
+ [
234
+ Buffer.from("pool_sol"),
235
+ mintPubkey.toBuffer()
236
+ ],
237
+ this.sdk.programId
238
+ );
239
+
240
+ // Query balances (skip if not needed)
241
+ let baseFeeRecipientBalance = 0;
242
+ let feeRecipientBalance = 0;
243
+ let poolTokenBalance = { value: { amount: '0' } };
244
+ let poolSolBalance = 0;
245
+ let tokenSupply = { value: { amount: '0', decimals: 0 } };
246
+
247
+ if (!skipBalances) {
248
+ [
249
+ baseFeeRecipientBalance,
250
+ feeRecipientBalance,
251
+ poolTokenBalance,
252
+ poolSolBalance,
253
+ tokenSupply
254
+ ] = await Promise.all([
255
+ this.sdk.connection.getBalance(decodedData.baseFeeRecipient),
256
+ this.sdk.connection.getBalance(decodedData.feeRecipient),
257
+ this.sdk.connection.getTokenAccountBalance(poolTokenAccountPDA).catch(() => ({ value: { amount: '0' } })),
258
+ this.sdk.connection.getBalance(poolSolAccountPDA),
259
+ this.sdk.connection.getTokenSupply(mintPubkey)
260
+ ]);
261
+ }
262
+
263
+ // Convert data format
264
+ const convertedData = {
265
+ // BN types convert to bigint
266
+ lpTokenReserve: BigInt(decodedData.lpTokenReserve.toString()),
267
+ lpSolReserve: BigInt(decodedData.lpSolReserve.toString()),
268
+ price: BigInt(decodedData.price.toString()),
269
+ borrowTokenReserve: BigInt(decodedData.borrowTokenReserve.toString()),
270
+ borrowSolReserve: BigInt(decodedData.borrowSolReserve.toString()),
271
+
272
+ // Numeric types remain unchanged
273
+ swapFee: decodedData.swapFee,
274
+ borrowFee: decodedData.borrowFee,
275
+ feeDiscountFlag: decodedData.feeDiscountFlag,
276
+ feeSplit: decodedData.feeSplit,
277
+ borrowDuration: decodedData.borrowDuration,
278
+ bump: decodedData.bump,
279
+
280
+ // PublicKey types convert to string
281
+ baseFeeRecipient: decodedData.baseFeeRecipient.toString(),
282
+ feeRecipient: decodedData.feeRecipient.toString(),
283
+ mint: decodedData.mint.toString(),
284
+
285
+ // New OrderBook structure - always has value (not optional)
286
+ upOrderbook: decodedData.upOrderbook.toString(),
287
+ downOrderbook: decodedData.downOrderbook.toString(),
288
+
289
+ // Creator address
290
+ creator: decodedData.creator.toString(),
291
+
292
+ // === 第一阶段新增字段:动态流动池参数 ===
293
+ initialVirtualSol: BigInt(decodedData.initialVirtualSol.toString()), // u64, lamports
294
+ initialVirtualToken: BigInt(decodedData.initialVirtualToken.toString()), // u64, 最小单位
295
+
296
+ // === 第二阶段新增字段:高级版池子参数 ===
297
+ poolType: decodedData.poolType, // u8, 0=普通版, 1=高级版
298
+ borrowPoolRatio: decodedData.borrowPoolRatio, // u8, 5-30表示5%-30%
299
+
300
+ // Token Supply 信息
301
+ totalSupply: BigInt(tokenSupply.value.amount),
302
+ decimals: tokenSupply.value.decimals,
303
+ initialBorrowPool: BigInt(tokenSupply.value.amount) - BigInt(decodedData.initialVirtualToken.toString()),
304
+ circulatingSupply: BigInt(decodedData.initialVirtualToken.toString())
305
+ + (BigInt(tokenSupply.value.amount) - BigInt(decodedData.initialVirtualToken.toString()) - BigInt(decodedData.borrowTokenReserve.toString())),
306
+
307
+ // SOL balance information
308
+ baseFeeRecipientBalance: baseFeeRecipientBalance, // Unit: lamports
309
+ feeRecipientBalance: feeRecipientBalance, // Unit: lamports
310
+
311
+ // Pool account information
312
+ poolTokenAccount: poolTokenAccountPDA.toString(), // Pool token account address
313
+ poolSolAccount: poolSolAccountPDA.toString(), // Pool SOL account address
314
+ poolTokenBalance: BigInt(poolTokenBalance.value.amount), // Pool token balance
315
+ poolSolBalance: poolSolBalance, // Pool SOL balance (lamports)
316
+
317
+ // Additional metadata
318
+ _metadata: {
319
+ accountAddress: curveAccountPDA.toString(),
320
+ mintAddress: mintPubkey.toString()
321
+ }
322
+ };
323
+
324
+ // 写入缓存
325
+ this._curveAccountCache.set(mintKey, {
326
+ data: convertedData,
327
+ timestamp: Date.now(),
328
+ skipBalances: skipBalances
329
+ });
330
+
331
+ // Return converted data
332
+ return convertedData;
333
+
334
+ } catch (error) {
335
+ // Provide concise error information
336
+ if (error.message.includes('Account does not exist')) {
337
+ throw new Error(`curve_account does not exist for mint: ${mint}`);
338
+ } else {
339
+ throw new Error(`Failed to get curve_account: ${error.message}`);
340
+ }
341
+ }
342
+ }
343
+
344
+
345
+
346
+ /**
347
+ * Calculate curve_account PDA address
348
+ *
349
+ * @param {string|PublicKey} mint - Token mint address
350
+ * @returns {PublicKey} curve_account PDA address
351
+ *
352
+ * @example
353
+ * const curveAddress = sdk.chain.getCurveAccountAddress('3YggGtxXEGBbjK1WLj2Z79doZC2gkCWXag1ag8BD4cYY');
354
+ * console.log('Curve Account Address:', curveAddress.toString());
355
+ */
356
+ getCurveAccountAddress(mint) {
357
+ const mintPubkey = typeof mint === 'string' ? new PublicKey(mint) : mint;
358
+
359
+ const [curveAccountPDA] = PublicKey.findProgramAddressSync(
360
+ [
361
+ Buffer.from("borrowing_curve"),
362
+ mintPubkey.toBuffer()
363
+ ],
364
+ this.sdk.programId
365
+ );
366
+
367
+ return curveAccountPDA;
368
+ }
369
+
370
+ /**
371
+ * Get price data (read price from chain curveAccountPDA)
372
+ * @param {string} mint - Token address
373
+ * @returns {Promise<string>} Latest price string
374
+ *
375
+ * @example
376
+ * // Get latest token price
377
+ * const price = await sdk.chain.price('56hfrQYiyRSUZdRKDuUvsqRik8j2UDW9kCisy7BiRxmg');
378
+ * console.log('Latest price:', price); // "13514066072452801812769"
379
+ */
380
+ async price(mint) {
381
+ // Validate input
382
+ if (!mint || typeof mint !== 'string') {
383
+ throw new Error('price: mint address must be a valid string');
384
+ }
385
+
386
+ try {
387
+ // 复用 getCurveAccount 缓存,避免重复 fetch 同一个账户
388
+ const curveData = await this.getCurveAccount(mint, { skipBalances: true });
389
+ const price = curveData.price;
390
+
391
+ if (price && price !== 0n) {
392
+ return price.toString();
393
+ } else {
394
+ const initialPrice = CurveAMM.getInitialPrice();
395
+ if (initialPrice === null) {
396
+ throw new Error('price: Unable to calculate initial price');
397
+ }
398
+ return initialPrice.toString();
399
+ }
400
+
401
+ } catch (error) {
402
+ // If getting fails, return initial price
403
+ console.warn(`price: Failed to get chain price, using initial price: ${error.message}`);
404
+
405
+ const initialPrice = CurveAMM.getInitialPrice();
406
+ if (initialPrice === null) {
407
+ throw new Error('price: Unable to calculate initial price');
408
+ }
409
+ return initialPrice.toString();
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Get Orders Data (Read from Chain using new OrderBook structure)
415
+ * Returns ALL orders regardless of pagination parameters (for compatibility)
416
+ * @param {string} mint - Token mint address
417
+ * @param {Object} options - Query parameters (page and limit are ignored but kept for compatibility)
418
+ * @param {string} options.type - Order type: "up_orders" (short) or "down_orders" (long)
419
+ * @param {number} options.page - Page number (ignored, always returns all data)
420
+ * @param {number} options.limit - Items per page (ignored, always returns all data)
421
+ * @returns {Promise<Object>} Order data with ALL orders
422
+ *
423
+ * @example
424
+ * // Get long orders (returns ALL orders)
425
+ * const ordersData = await sdk.chain.orders('6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee', { type: 'down_orders' });
426
+ *
427
+ * // Return value example:
428
+ * // {
429
+ * // "success": true,
430
+ * // "data": {
431
+ * // "orders": [
432
+ * // {
433
+ * // "order_type": "down_orders", // Order type string (converted)
434
+ * // "mint": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee", // Token address
435
+ * // "user": "JD1eNPaJpbtejKfgimbLYLkvpsTHyYzKCCozVLGLS6zu", // User address
436
+ * // "lock_lp_start_price": "46618228118401293964111", // LP start price (string)
437
+ * // "lock_lp_end_price": "45827474968448818396222", // LP end price (string)
438
+ * // "open_price": "46222851543425056180166", // Open price (string) - NEW
439
+ * // "order_id": "12345", // Order ID (u64 as string) - NEW
440
+ * // "lock_lp_sol_amount": 3299491609, // LP locked SOL amount (lamports)
441
+ * // "lock_lp_token_amount": 713848715669, // LP locked token amount (min unit)
442
+ * // "next_lp_sol_amount": 3299491609, // Next LP SOL amount (lamports) - NEW
443
+ * // "next_lp_token_amount": 713848715669, // Next LP token amount (min unit) - NEW
444
+ * // "start_time": 1756352482, // Start time (Unix timestamp, i64 as number)
445
+ * // "end_time": 1756525282, // End time (Unix timestamp, i64 as number)
446
+ * // "margin_init_sol_amount": 571062973, // Initial margin SOL amount (lamports) - NEW
447
+ * // "margin_sol_amount": 571062973, // Margin SOL amount (lamports)
448
+ * // "borrow_amount": 3860656108, // Borrow amount (lamports)
449
+ * // "position_asset_amount": 713848715669, // Position asset amount (min unit)
450
+ * // "realized_sol_amount": 0, // Realized SOL amount (lamports) - NEW
451
+ * // "borrow_fee": 300, // Borrow fee (basis points, 300 = 3%)
452
+ * // "index": 0, // Order index in OrderBook (currentIndex)
453
+ * // "next_order": 1, // Next order index in linked list (u16, 65535=none) - NEW
454
+ * // "prev_order": 65535 // Previous order index in linked list (u16, 65535=none) - NEW
455
+ * // }
456
+ * // ],
457
+ * // "total": 12, // Total order count
458
+ * // "order_type": "down_orders", // Order type (string)
459
+ * // "mint_account": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee", // Queried token address
460
+ * // "page": 1, // Always 1 (for compatibility)
461
+ * // "limit": <total>, // Always equals total (for compatibility)
462
+ * // "has_next": false, // Always false (all data returned)
463
+ * // "has_prev": false // Always false (all data returned)
464
+ * // },
465
+ * // "message": "Operation successful" // Operation result message
466
+ * // }
467
+ *
468
+ * // Use utility methods to process data:
469
+ */
470
+ async orders(mint, options = {}) {
471
+ try {
472
+ // Parameter validation
473
+ if (!mint || typeof mint !== 'string') {
474
+ throw new Error('orders: mint address must be a valid string');
475
+ }
476
+
477
+ // Set default parameters (kept for compatibility but not used for pagination)
478
+ const orderType = options.type || 'down_orders';
479
+ const page = 1; // Always return page 1
480
+
481
+ // Validate order type
482
+ if (!['up_orders', 'down_orders'].includes(orderType)) {
483
+ throw new Error('orders: order type must be "up_orders" or "down_orders"');
484
+ }
485
+
486
+ // Convert API type to orderbook direction
487
+ // "up_orders" = short orders = upOrderbook (orderType=2)
488
+ // "down_orders" = long orders = downOrderbook (orderType=1)
489
+ const seed = orderType === 'up_orders' ? 'up_orderbook' : 'down_orderbook';
490
+
491
+ // 本地计算 orderbook PDA,避免调用 getCurveAccount(省 5 个 RPC)
492
+ const mintPubkey = new PublicKey(mint);
493
+ const [orderbookPubkey] = PublicKey.findProgramAddressSync(
494
+ [Buffer.from(seed), mintPubkey.toBuffer()],
495
+ this.sdk.programId
496
+ );
497
+
498
+ // Get OrderBook account data
499
+ const accountInfo = await this.sdk.connection.getAccountInfo(orderbookPubkey);
500
+
501
+ if (!accountInfo) {
502
+ // OrderBook account doesn't exist, return empty result
503
+ return {
504
+ success: true,
505
+ data: {
506
+ orders: [],
507
+ total: 0,
508
+ order_type: orderType,
509
+ mint_account: mint,
510
+ page: page,
511
+ limit: 0,
512
+ has_next: false,
513
+ has_prev: false
514
+ },
515
+ message: "Operation successful"
516
+ };
517
+ }
518
+
519
+ const data = accountInfo.data;
520
+
521
+ // Parse OrderBook Header
522
+ const header = this._parseOrderBookHeader(data);
523
+
524
+ // If no orders in orderbook, return empty result
525
+ if (header.total === 0 || header.head === 65535) {
526
+ return {
527
+ success: true,
528
+ data: {
529
+ orders: [],
530
+ total: 0,
531
+ order_type: orderType,
532
+ mint_account: mint,
533
+ page: page,
534
+ limit: 0,
535
+ has_next: false,
536
+ has_prev: false
537
+ },
538
+ message: "Operation successful"
539
+ };
540
+ }
541
+
542
+ // Traverse linked list to read ALL orders
543
+ const orders = [];
544
+ let currentIndex = header.head;
545
+
546
+ // Read all orders without limit
547
+ while (currentIndex !== 65535) {
548
+ // Parse order at current index
549
+ const order = this._parseMarginOrder(data, currentIndex, header.headerSize);
550
+
551
+ // Data transformation - convert to API format
552
+ const convertedOrder = {
553
+ // Convert chain number to API string format
554
+ order_type: order.orderType === 1 ? 'down_orders' : 'up_orders', // 1=long=down_orders, 2=short=up_orders
555
+ mint: mint, // Use mint from function parameter (not stored in MarginOrder)
556
+ user: order.user.toString(),
557
+
558
+ // Price fields (u128 -> string)
559
+ lock_lp_start_price: order.lockLpStartPrice.toString(),
560
+ lock_lp_end_price: order.lockLpEndPrice.toString(),
561
+ open_price: order.openPrice.toString(),
562
+
563
+ // Order ID field (u64 -> string)
564
+ order_id: order.orderId.toString(),
565
+
566
+ // Amount fields (u64 -> string) - Fix precision issue
567
+ lock_lp_sol_amount: order.lockLpSolAmount.toString(),
568
+ lock_lp_token_amount: order.lockLpTokenAmount.toString(),
569
+ next_lp_sol_amount: order.nextLpSolAmount.toString(),
570
+ next_lp_token_amount: order.nextLpTokenAmount.toString(),
571
+
572
+ // Time fields (u32 -> number)
573
+ start_time: order.startTime,
574
+ end_time: order.endTime,
575
+
576
+ // Margin and position fields (u64 -> string) - Fix precision issue
577
+ margin_init_sol_amount: order.marginInitSolAmount.toString(),
578
+ margin_sol_amount: order.marginSolAmount.toString(),
579
+ borrow_amount: order.borrowAmount.toString(),
580
+ position_asset_amount: order.positionAssetAmount.toString(),
581
+ realized_sol_amount: order.realizedSolAmount.toString(),
582
+
583
+ // Fee field (u16 -> number)
584
+ borrow_fee: order.borrowFee,
585
+
586
+ // Order index in OrderBook (uses currentIndex from linked list)
587
+ index: currentIndex,
588
+
589
+ // Linked list navigation fields (u16 -> number)
590
+ next_order: order.nextOrder,
591
+ prev_order: order.prevOrder
592
+ };
593
+
594
+ orders.push(convertedOrder);
595
+
596
+ // Move to next order
597
+ currentIndex = order.nextOrder;
598
+ }
599
+
600
+ // Return all orders with pagination-like format for compatibility
601
+ const totalOrders = orders.length;
602
+
603
+ return {
604
+ success: true,
605
+ data: {
606
+ orders: orders,
607
+ total: totalOrders,
608
+ order_type: orderType,
609
+ mint_account: mint,
610
+ page: page,
611
+ limit: totalOrders, // limit equals total for compatibility
612
+ has_next: false, // Always false since all data is returned
613
+ has_prev: false // Always false since all data is returned
614
+ },
615
+ message: "Operation successful"
616
+ };
617
+
618
+ } catch (error) {
619
+ // Error handling
620
+ console.error('chain.orders: Failed to get orders', error.message);
621
+ throw new Error(`Failed to get orders: ${error.message}`);
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Parse OrderBook Header from account data
627
+ * @private
628
+ * @param {Buffer} data - OrderBook account data
629
+ * @returns {Object} Parsed header object
630
+ */
631
+ _parseOrderBookHeader(data) {
632
+ // Header structure (112 bytes total):
633
+ // discriminator(8) + version(1) + order_type(1) + bump(1) + padding1(5) +
634
+ // authority(32) + order_id_counter(8) + created_at(8) + last_modified(8) +
635
+ // total_capacity(4) + head(2) + tail(2) + total(2) + padding2(2) + reserved(32)
636
+
637
+ let offset = 8; // Skip discriminator
638
+
639
+ const version = data.readUInt8(offset);
640
+ offset += 1;
641
+
642
+ const orderType = data.readUInt8(offset);
643
+ offset += 1;
644
+
645
+ const bump = data.readUInt8(offset);
646
+ offset += 1;
647
+
648
+ offset += 5; // Skip padding1
649
+
650
+ const authority = new PublicKey(data.slice(offset, offset + 32));
651
+ offset += 32;
652
+
653
+ const orderIdCounter = data.readBigUInt64LE(offset);
654
+ offset += 8;
655
+
656
+ // created_at: i64 (8 bytes) - Unix timestamp in seconds
657
+ const createdAt = Number(data.readBigInt64LE(offset));
658
+ offset += 8;
659
+
660
+ // last_modified: i64 (8 bytes) - Unix timestamp in seconds
661
+ const lastModified = Number(data.readBigInt64LE(offset));
662
+ offset += 8;
663
+
664
+ const totalCapacity = data.readUInt32LE(offset);
665
+ offset += 4;
666
+
667
+ const head = data.readUInt16LE(offset);
668
+ offset += 2;
669
+
670
+ const tail = data.readUInt16LE(offset);
671
+ offset += 2;
672
+
673
+ const total = data.readUInt16LE(offset);
674
+ offset += 2;
675
+
676
+ offset += 2; // Skip padding2
677
+ offset += 32; // Skip reserved
678
+
679
+ return {
680
+ version,
681
+ orderType,
682
+ bump,
683
+ authority,
684
+ orderIdCounter,
685
+ createdAt,
686
+ lastModified,
687
+ totalCapacity,
688
+ head,
689
+ tail,
690
+ total,
691
+ headerSize: 112 // Fixed header size (updated for i64 timestamps)
692
+ };
693
+ }
694
+
695
+ /**
696
+ * Parse MarginOrder from OrderBook account data
697
+ * @private
698
+ * @param {Buffer} data - OrderBook account data
699
+ * @param {number} index - Order slot index
700
+ * @param {number} headerSize - Header size (112 bytes)
701
+ * @returns {Object} Parsed order object
702
+ */
703
+ _parseMarginOrder(data, index, headerSize) {
704
+ // MarginOrder structure (192 bytes per order):
705
+ // user(32) + lock_lp_start_price(16) + lock_lp_end_price(16) + open_price(16) +
706
+ // order_id(8) + lock_lp_sol_amount(8) + lock_lp_token_amount(8) +
707
+ // next_lp_sol_amount(8) + next_lp_token_amount(8) +
708
+ // margin_init_sol_amount(8) + margin_sol_amount(8) + borrow_amount(8) +
709
+ // position_asset_amount(8) + realized_sol_amount(8) +
710
+ // start_time(8) + end_time(8) + version(4) +
711
+ // next_order(2) + prev_order(2) + borrow_fee(2) +
712
+ // order_type(1) + padding(5)
713
+
714
+ const MARGIN_ORDER_SIZE = 192;
715
+ let offset = 8 + headerSize + index * MARGIN_ORDER_SIZE;
716
+
717
+ // Boundary check
718
+ if (offset + MARGIN_ORDER_SIZE > data.length) {
719
+ throw new Error(`Order index ${index} exceeds data boundary`);
720
+ }
721
+
722
+ // user (32 bytes)
723
+ const user = new PublicKey(data.slice(offset, offset + 32));
724
+ offset += 32;
725
+
726
+ // lock_lp_start_price (u128, 16 bytes)
727
+ const lockLpStartPrice = this._readU128LE(data, offset);
728
+ offset += 16;
729
+
730
+ // lock_lp_end_price (u128, 16 bytes)
731
+ const lockLpEndPrice = this._readU128LE(data, offset);
732
+ offset += 16;
733
+
734
+ // open_price (u128, 16 bytes)
735
+ const openPrice = this._readU128LE(data, offset);
736
+ offset += 16;
737
+
738
+ // order_id (u64, 8 bytes)
739
+ const orderId = data.readBigUInt64LE(offset);
740
+ offset += 8;
741
+
742
+ // lock_lp_sol_amount (u64, 8 bytes)
743
+ const lockLpSolAmount = data.readBigUInt64LE(offset);
744
+ offset += 8;
745
+
746
+ // lock_lp_token_amount (u64, 8 bytes)
747
+ const lockLpTokenAmount = data.readBigUInt64LE(offset);
748
+ offset += 8;
749
+
750
+ // next_lp_sol_amount (u64, 8 bytes)
751
+ const nextLpSolAmount = data.readBigUInt64LE(offset);
752
+ offset += 8;
753
+
754
+ // next_lp_token_amount (u64, 8 bytes)
755
+ const nextLpTokenAmount = data.readBigUInt64LE(offset);
756
+ offset += 8;
757
+
758
+ // margin_init_sol_amount (u64, 8 bytes)
759
+ const marginInitSolAmount = data.readBigUInt64LE(offset);
760
+ offset += 8;
761
+
762
+ // margin_sol_amount (u64, 8 bytes)
763
+ const marginSolAmount = data.readBigUInt64LE(offset);
764
+ offset += 8;
765
+
766
+ // borrow_amount (u64, 8 bytes)
767
+ const borrowAmount = data.readBigUInt64LE(offset);
768
+ offset += 8;
769
+
770
+ // position_asset_amount (u64, 8 bytes)
771
+ const positionAssetAmount = data.readBigUInt64LE(offset);
772
+ offset += 8;
773
+
774
+ // realized_sol_amount (u64, 8 bytes)
775
+ const realizedSolAmount = data.readBigUInt64LE(offset);
776
+ offset += 8;
777
+
778
+ // start_time (i64, 8 bytes) - Unix timestamp in seconds
779
+ const startTime = Number(data.readBigInt64LE(offset));
780
+ offset += 8;
781
+
782
+ // end_time (i64, 8 bytes) - Unix timestamp in seconds
783
+ const endTime = Number(data.readBigInt64LE(offset));
784
+ offset += 8;
785
+
786
+ // version (u32, 4 bytes)
787
+ const version = data.readUInt32LE(offset);
788
+ offset += 4;
789
+
790
+ // next_order (u16, 2 bytes)
791
+ const nextOrder = data.readUInt16LE(offset);
792
+ offset += 2;
793
+
794
+ // prev_order (u16, 2 bytes)
795
+ const prevOrder = data.readUInt16LE(offset);
796
+ offset += 2;
797
+
798
+ // borrow_fee (u16, 2 bytes)
799
+ const borrowFee = data.readUInt16LE(offset);
800
+ offset += 2;
801
+
802
+ // order_type (u8, 1 byte)
803
+ const orderType = data.readUInt8(offset);
804
+ offset += 1;
805
+
806
+ // Skip padding (5 bytes)
807
+ offset += 5;
808
+
809
+ // Note: mint is not stored in MarginOrder structure
810
+ // It should be obtained from context (the mint parameter passed to orders() function)
811
+
812
+ return {
813
+ user,
814
+ lockLpStartPrice,
815
+ lockLpEndPrice,
816
+ openPrice,
817
+ orderId,
818
+ lockLpSolAmount,
819
+ lockLpTokenAmount,
820
+ nextLpSolAmount,
821
+ nextLpTokenAmount,
822
+ marginInitSolAmount,
823
+ marginSolAmount,
824
+ borrowAmount,
825
+ positionAssetAmount,
826
+ realizedSolAmount,
827
+ version,
828
+ startTime,
829
+ endTime,
830
+ nextOrder,
831
+ prevOrder,
832
+ borrowFee,
833
+ orderType
834
+ };
835
+ }
836
+
837
+ /**
838
+ * Read u128 value (little-endian) from buffer
839
+ * @private
840
+ * @param {Buffer} buffer - Data buffer
841
+ * @param {number} offset - Read offset
842
+ * @returns {bigint} u128 value as BigInt
843
+ */
844
+ _readU128LE(buffer, offset) {
845
+ // Read low 64 bits
846
+ const low = buffer.readBigUInt64LE(offset);
847
+ // Read high 64 bits
848
+ const high = buffer.readBigUInt64LE(offset + 8);
849
+ // Combine into u128
850
+ return (high << 64n) | low;
851
+ }
852
+
853
+
854
+ /**
855
+ * Debug Orders Data (Read ALL order slots from Chain, ignore linked list structure)
856
+ *
857
+ * This function is designed for debugging corrupted linked list data.
858
+ * It directly reads ALL order slots based on totalCapacity, without following next_order/prev_order.
859
+ * Use this when linked list navigation is broken (corrupted next_order/prev_order values).
860
+ *
861
+ * @param {string} mint - Token mint address
862
+ * @param {Object} options - Query parameters
863
+ * @param {string} options.type - Order type: "up_orders" (short) or "down_orders" (long)
864
+ * @returns {Promise<Object>} Debug order data with ALL order slots (including empty ones)
865
+ *
866
+ * @example
867
+ * // Get all long order slots for debugging (ignores linked list)
868
+ * const debugData = await sdk.chain.debug_orders('6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee', { type: 'down_orders' });
869
+ *
870
+ * // Return format:
871
+ * // {
872
+ * // "success": true,
873
+ * // "data": {
874
+ * // "header": {
875
+ * // "version": 1,
876
+ * // "orderType": 1,
877
+ * // "bump": 253,
878
+ * // "authority": "7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5",
879
+ * // "orderIdCounter": "123",
880
+ * // "createdAt": 1755964862,
881
+ * // "lastModified": 1756137662,
882
+ * // "totalCapacity": 100, // Max order slots
883
+ * // "head": 0, // Head index (may be corrupted)
884
+ * // "tail": 5, // Tail index (may be corrupted)
885
+ * // "total": 6, // Total active orders (may be incorrect)
886
+ * // "headerSize": 104
887
+ * // },
888
+ * // "orders": [
889
+ * // {
890
+ * // "slot_index": 0, // Physical slot index (0 to totalCapacity-1)
891
+ * // "is_empty": false, // Whether this slot is empty (all zeros)
892
+ * // "order_type": "down_orders",
893
+ * // "mint": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee",
894
+ * // "user": "JD1eNPaJpbtejKfgimbLYLkvpsTHyYzKCCozVLGLS6zu",
895
+ * // "lock_lp_start_price": "46618228118401293964111",
896
+ * // "lock_lp_end_price": "45827474968448818396222",
897
+ * // "open_price": "46222851543425056180166",
898
+ * // "order_id": "12345",
899
+ * // "lock_lp_sol_amount": 3299491609,
900
+ * // "lock_lp_token_amount": 713848715669,
901
+ * // "next_lp_sol_amount": 3299491609,
902
+ * // "next_lp_token_amount": 713848715669,
903
+ * // "start_time": 1756352482, // Unix timestamp (i64 as number)
904
+ * // "end_time": 1756525282, // Unix timestamp (i64 as number)
905
+ * // "margin_init_sol_amount": 571062973,
906
+ * // "margin_sol_amount": 571062973,
907
+ * // "borrow_amount": 3860656108,
908
+ * // "position_asset_amount": 713848715669,
909
+ * // "realized_sol_amount": 0,
910
+ * // "borrow_fee": 300,
911
+ * // "next_order": 2, // Next index (may be corrupted)
912
+ * // "prev_order": 65535 // Prev index (may be corrupted)
913
+ * // },
914
+ * // {
915
+ * // "slot_index": 1,
916
+ * // "is_empty": true, // Empty slot (all zeros)
917
+ * // "order_type": 0,
918
+ * // "user": "11111111111111111111111111111111",
919
+ * // ... // All fields will be zeros/default values
920
+ * // },
921
+ * // ... // All slots up to totalCapacity
922
+ * // ],
923
+ * // "total_slots": 100, // Total capacity (all slots)
924
+ * // "non_empty_slots": 6, // Count of non-empty slots
925
+ * // "order_type": "down_orders",
926
+ * // "mint_account": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee"
927
+ * // },
928
+ * // "message": "Debug data retrieved (ignores linked list)"
929
+ * // }
930
+ *
931
+ * @note This function is for debugging only. It may return empty slots and ignores linked list navigation.
932
+ * @note Use regular orders() function for production code.
933
+ */
934
+ async debug_orders(mint, options = {}) {
935
+ try {
936
+ // Parameter validation
937
+ if (!mint || typeof mint !== 'string') {
938
+ throw new Error('debug_orders: mint address must be a valid string');
939
+ }
940
+
941
+ // Set default parameters
942
+ const orderType = options.type || 'down_orders';
943
+
944
+ // Validate order type
945
+ if (!['up_orders', 'down_orders'].includes(orderType)) {
946
+ throw new Error('debug_orders: order type must be "up_orders" or "down_orders"');
947
+ }
948
+
949
+ // 本地计算 orderbook PDA,避免调用 getCurveAccount(省 5 个 RPC)
950
+ const seed = orderType === 'up_orders' ? 'up_orderbook' : 'down_orderbook';
951
+ const mintPubkey = new PublicKey(mint);
952
+ const [orderbookPubkey] = PublicKey.findProgramAddressSync(
953
+ [Buffer.from(seed), mintPubkey.toBuffer()],
954
+ this.sdk.programId
955
+ );
956
+
957
+ // Get OrderBook account data
958
+ const accountInfo = await this.sdk.connection.getAccountInfo(orderbookPubkey);
959
+
960
+ if (!accountInfo) {
961
+ // OrderBook account doesn't exist, return empty result
962
+ return {
963
+ success: true,
964
+ data: {
965
+ header: null,
966
+ orders: [],
967
+ total_slots: 0,
968
+ non_empty_slots: 0,
969
+ order_type: orderType,
970
+ mint_account: mint
971
+ },
972
+ message: "OrderBook account does not exist"
973
+ };
974
+ }
975
+
976
+ const data = accountInfo.data;
977
+
978
+ // Parse OrderBook Header
979
+ const header = this._parseOrderBookHeader(data);
980
+
981
+ // Read ALL order slots based on totalCapacity (ignore linked list)
982
+ const orders = [];
983
+ let nonEmptyCount = 0;
984
+
985
+ // Iterate through ALL slots from 0 to totalCapacity-1
986
+ for (let slotIndex = 0; slotIndex < header.totalCapacity; slotIndex++) {
987
+ try {
988
+ // Parse order at this slot
989
+ const order = this._parseMarginOrder(data, slotIndex, header.headerSize);
990
+
991
+ // Check if slot is empty (user address is all zeros)
992
+ const isEmpty = order.user.toString() === '11111111111111111111111111111111';
993
+
994
+ if (!isEmpty) {
995
+ nonEmptyCount++;
996
+ }
997
+
998
+ // Convert to API format
999
+ const convertedOrder = {
1000
+ slot_index: slotIndex, // Physical slot position
1001
+ is_empty: isEmpty, // Empty slot indicator
1002
+
1003
+ // Order data fields
1004
+ order_type: order.orderType === 1 ? 'down_orders' : 'up_orders',
1005
+ mint: mint,
1006
+ user: order.user.toString(),
1007
+
1008
+ // Price fields (u128 -> string)
1009
+ lock_lp_start_price: order.lockLpStartPrice.toString(),
1010
+ lock_lp_end_price: order.lockLpEndPrice.toString(),
1011
+ open_price: order.openPrice.toString(),
1012
+
1013
+ // Order ID field (u64 -> string)
1014
+ order_id: order.orderId.toString(),
1015
+
1016
+ // Amount fields (u64 -> string) - Fix precision issue
1017
+ lock_lp_sol_amount: order.lockLpSolAmount.toString(),
1018
+ lock_lp_token_amount: order.lockLpTokenAmount.toString(),
1019
+ next_lp_sol_amount: order.nextLpSolAmount.toString(),
1020
+ next_lp_token_amount: order.nextLpTokenAmount.toString(),
1021
+
1022
+ // Time fields (u32 -> number)
1023
+ start_time: order.startTime,
1024
+ end_time: order.endTime,
1025
+
1026
+ // Margin and position fields (u64 -> string) - Fix precision issue
1027
+ margin_init_sol_amount: order.marginInitSolAmount.toString(),
1028
+ margin_sol_amount: order.marginSolAmount.toString(),
1029
+ borrow_amount: order.borrowAmount.toString(),
1030
+ position_asset_amount: order.positionAssetAmount.toString(),
1031
+ realized_sol_amount: order.realizedSolAmount.toString(),
1032
+
1033
+ // Fee field (u16 -> number)
1034
+ borrow_fee: order.borrowFee,
1035
+
1036
+ // Linked list navigation (may be corrupted, for debugging)
1037
+ next_order: order.nextOrder,
1038
+ prev_order: order.prevOrder
1039
+ };
1040
+
1041
+ orders.push(convertedOrder);
1042
+
1043
+ } catch (error) {
1044
+ // If parsing fails (beyond data boundary), stop iteration
1045
+ console.warn(`debug_orders: Failed to parse slot ${slotIndex}: ${error.message}`);
1046
+ break;
1047
+ }
1048
+ }
1049
+
1050
+ // Return debug data
1051
+ return {
1052
+ success: true,
1053
+ data: {
1054
+ header: {
1055
+ version: header.version,
1056
+ orderType: header.orderType,
1057
+ bump: header.bump,
1058
+ authority: header.authority.toString(),
1059
+ orderIdCounter: header.orderIdCounter.toString(),
1060
+ createdAt: header.createdAt,
1061
+ lastModified: header.lastModified,
1062
+ totalCapacity: header.totalCapacity,
1063
+ head: header.head,
1064
+ tail: header.tail,
1065
+ total: header.total,
1066
+ headerSize: header.headerSize
1067
+ },
1068
+ orders: orders,
1069
+ total_slots: orders.length, // Total slots read
1070
+ non_empty_slots: nonEmptyCount, // Non-empty slots count
1071
+ order_type: orderType,
1072
+ mint_account: mint
1073
+ },
1074
+ message: "Debug data retrieved (ignores linked list)"
1075
+ };
1076
+
1077
+ } catch (error) {
1078
+ // Error handling
1079
+ console.error('chain.debug_orders: Failed to get debug orders', error.message);
1080
+ throw new Error(`Failed to get debug orders: ${error.message}`);
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Get User Orders (Read from Chain using new OrderBook structure)
1086
+ * Returns ALL user orders regardless of pagination parameters (for compatibility)
1087
+ * @param {string} user - User wallet address
1088
+ * @param {string} mint - Token mint address
1089
+ * @param {Object} options - Query parameters (page and limit are ignored but kept for compatibility)
1090
+ * @param {number} options.page - Page number (ignored, always returns all data)
1091
+ * @param {number} options.limit - Items per page (ignored, always returns all data)
1092
+ * @param {string} options.order_by - Sort order, default 'start_time_desc'
1093
+ * @returns {Promise<Object>} User orders data with ALL orders
1094
+ *
1095
+ * @example
1096
+ * const userOrders = await sdk.chain.user_orders(
1097
+ * '8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb',
1098
+ * '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X',
1099
+ * { order_by: 'start_time_desc' }
1100
+ * );
1101
+ * // Return format:
1102
+ * // {
1103
+ * // "success": true,
1104
+ * // "data": {
1105
+ * // "orders": [
1106
+ * // {
1107
+ * // "order_type": 2, // Order type: 1=long, 2=short
1108
+ * // "mint": "4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X", // Token address
1109
+ * // "user": "8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb", // User address
1110
+ * // "lock_lp_start_price": "753522984132656210522", // LP start price
1111
+ * // "lock_lp_end_price": "833102733432007194898", // LP end price
1112
+ * // "open_price": "793312858782331702710", // Open price - NEW
1113
+ * // "order_id": "12345", // Order ID (u64 as string) - NEW
1114
+ * // "lock_lp_sol_amount": 2535405978, // LP locked SOL
1115
+ * // "lock_lp_token_amount": 32000000000000, // LP locked token
1116
+ * // "next_lp_sol_amount": 2535405978, // Next LP SOL - NEW
1117
+ * // "next_lp_token_amount": 32000000000000, // Next LP token - NEW
1118
+ * // "start_time": 1755964862, // Start timestamp (i64 as number)
1119
+ * // "end_time": 1756137662, // End timestamp (i64 as number)
1120
+ * // "margin_init_sol_amount": 1909140052, // Initial margin - NEW
1121
+ * // "margin_sol_amount": 1909140052, // Current margin
1122
+ * // "borrow_amount": 32000000000000, // Borrow amount
1123
+ * // "position_asset_amount": 656690798, // Position asset
1124
+ * // "realized_sol_amount": 0, // Realized SOL - NEW
1125
+ * // "borrow_fee": 1200, // Borrow fee (bps)
1126
+ * // "index": 0, // Order index in OrderBook (currentIndex)
1127
+ * // "next_order": 2, // Next order index in linked list (u16, 65535=none) - NEW
1128
+ * // "prev_order": 65535 // Previous order index in linked list (u16, 65535=none) - NEW
1129
+ * // }
1130
+ * // ],
1131
+ * // "total": 11, // Total order count
1132
+ * // "user": "8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb", // User address
1133
+ * // "mint_account": "4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X", // Token address
1134
+ * // "page": 1, // Always 1 (for compatibility)
1135
+ * // "limit": <total>, // Always equals total (for compatibility)
1136
+ * // "has_next": false, // Always false (all data returned)
1137
+ * // "has_prev": false // Always false (all data returned)
1138
+ * // },
1139
+ * // "message": "Operation successful"
1140
+ * // }
1141
+ *
1142
+ * // Use order data:
1143
+ * const orders = userOrders.data.orders; // Order array
1144
+ * const totalCount = userOrders.data.total; // Total count
1145
+ */
1146
+ async user_orders(user, mint, options = {}) {
1147
+ try {
1148
+ // Parameter validation
1149
+ if (!user || typeof user !== 'string') {
1150
+ throw new Error('user_orders: user address must be a valid string');
1151
+ }
1152
+ if (!mint || typeof mint !== 'string') {
1153
+ throw new Error('user_orders: mint address must be a valid string');
1154
+ }
1155
+
1156
+ // Set default parameters (kept for compatibility)
1157
+ const page = 1; // Always return page 1
1158
+ const orderBy = options.order_by || 'start_time_desc';
1159
+
1160
+ // 本地计算 orderbook PDA,避免调用 getCurveAccount(省 5 个 RPC)
1161
+ const mintPubkey = new PublicKey(mint);
1162
+ const [upOrderbookPubkey] = PublicKey.findProgramAddressSync(
1163
+ [Buffer.from('up_orderbook'), mintPubkey.toBuffer()],
1164
+ this.sdk.programId
1165
+ );
1166
+ const [downOrderbookPubkey] = PublicKey.findProgramAddressSync(
1167
+ [Buffer.from('down_orderbook'), mintPubkey.toBuffer()],
1168
+ this.sdk.programId
1169
+ );
1170
+ const upOrderbookAddress = upOrderbookPubkey.toString(); // Short orders (orderType=2)
1171
+ const downOrderbookAddress = downOrderbookPubkey.toString(); // Long orders (orderType=1)
1172
+
1173
+ // Collect all user orders from both OrderBooks
1174
+ const allUserOrders = [];
1175
+
1176
+ // Helper function to traverse an OrderBook and collect user orders
1177
+ const traverseOrderBook = async (orderbookAddress) => {
1178
+ if (!orderbookAddress) return [];
1179
+
1180
+ const orders = [];
1181
+
1182
+ // Get OrderBook account data
1183
+ const orderbookPubkey = new PublicKey(orderbookAddress);
1184
+ const accountInfo = await this.sdk.connection.getAccountInfo(orderbookPubkey);
1185
+
1186
+ if (!accountInfo) {
1187
+ return []; // OrderBook doesn't exist
1188
+ }
1189
+
1190
+ const data = accountInfo.data;
1191
+
1192
+ // Parse OrderBook Header
1193
+ const header = this._parseOrderBookHeader(data);
1194
+
1195
+ // If no orders in orderbook, return empty
1196
+ if (header.total === 0 || header.head === 65535) {
1197
+ return [];
1198
+ }
1199
+
1200
+ // Traverse linked list to find ALL user orders
1201
+ let currentIndex = header.head;
1202
+
1203
+ while (currentIndex !== 65535) {
1204
+ try {
1205
+ // Parse order at current index
1206
+ const order = this._parseMarginOrder(data, currentIndex, header.headerSize);
1207
+
1208
+ // Check if this order belongs to the target user
1209
+ if (order.user.toString() === user) {
1210
+ // Data transformation - convert to API format
1211
+ const convertedOrder = {
1212
+ // Keep as number for compatibility (1=long, 2=short)
1213
+ order_type: order.orderType,
1214
+ mint: mint, // Use mint from function parameter
1215
+ user: order.user.toString(),
1216
+
1217
+ // Price fields (u128 -> string)
1218
+ lock_lp_start_price: order.lockLpStartPrice.toString(),
1219
+ lock_lp_end_price: order.lockLpEndPrice.toString(),
1220
+ open_price: order.openPrice.toString(),
1221
+
1222
+ // Order ID field (u64 -> string)
1223
+ order_id: order.orderId.toString(),
1224
+
1225
+ // Amount fields (u64 -> string) - Fix precision issue
1226
+ lock_lp_sol_amount: order.lockLpSolAmount.toString(),
1227
+ lock_lp_token_amount: order.lockLpTokenAmount.toString(),
1228
+ next_lp_sol_amount: order.nextLpSolAmount.toString(),
1229
+ next_lp_token_amount: order.nextLpTokenAmount.toString(),
1230
+
1231
+ // Time fields (u32 -> number)
1232
+ start_time: order.startTime,
1233
+ end_time: order.endTime,
1234
+
1235
+ // Margin and position fields (u64 -> string) - Fix precision issue
1236
+ margin_init_sol_amount: order.marginInitSolAmount.toString(),
1237
+ margin_sol_amount: order.marginSolAmount.toString(),
1238
+ borrow_amount: order.borrowAmount.toString(),
1239
+ position_asset_amount: order.positionAssetAmount.toString(),
1240
+ realized_sol_amount: order.realizedSolAmount.toString(),
1241
+
1242
+ // Fee field (u16 -> number)
1243
+ borrow_fee: order.borrowFee,
1244
+
1245
+ // Order index in OrderBook (uses currentIndex from linked list)
1246
+ index: currentIndex,
1247
+
1248
+ // Linked list navigation fields (u16 -> number)
1249
+ next_order: order.nextOrder,
1250
+ prev_order: order.prevOrder
1251
+ };
1252
+
1253
+ orders.push(convertedOrder);
1254
+ }
1255
+
1256
+ // Move to next order
1257
+ currentIndex = order.nextOrder;
1258
+
1259
+ } catch (error) {
1260
+ console.warn(`user_orders: Error parsing order at index ${currentIndex}: ${error.message}`);
1261
+ break;
1262
+ }
1263
+ }
1264
+
1265
+ return orders;
1266
+ };
1267
+
1268
+ // Traverse both OrderBooks in parallel to find user orders
1269
+ const [upOrders, downOrders] = await Promise.all([
1270
+ traverseOrderBook(upOrderbookAddress),
1271
+ traverseOrderBook(downOrderbookAddress)
1272
+ ]);
1273
+
1274
+ // Combine all orders
1275
+ allUserOrders.push(...upOrders, ...downOrders);
1276
+
1277
+ // Sort orders by start_time
1278
+ if (orderBy === 'start_time_desc') {
1279
+ allUserOrders.sort((a, b) => b.start_time - a.start_time);
1280
+ } else if (orderBy === 'start_time_asc') {
1281
+ allUserOrders.sort((a, b) => a.start_time - b.start_time);
1282
+ }
1283
+
1284
+ // Return all orders with pagination-like format for compatibility
1285
+ const totalOrders = allUserOrders.length;
1286
+
1287
+ return {
1288
+ success: true,
1289
+ data: {
1290
+ orders: allUserOrders,
1291
+ total: totalOrders,
1292
+ user: user,
1293
+ mint_account: mint,
1294
+ page: page,
1295
+ limit: totalOrders, // limit equals total for compatibility
1296
+ has_next: false, // Always false since all data is returned
1297
+ has_prev: false // Always false since all data is returned
1298
+ },
1299
+ message: "Operation successful"
1300
+ };
1301
+
1302
+ } catch (error) {
1303
+ // Error handling
1304
+ console.error('chain.user_orders: Failed to get user orders', error.message);
1305
+ throw new Error(`Failed to get user orders: ${error.message}`);
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ module.exports = ChainModule;