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,1102 @@
|
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get complete curve_account (BorrowingBondingCurve) data
|
|
21
|
+
*
|
|
22
|
+
* Read the borrowing liquidity pool account data for a specified token from the blockchain,
|
|
23
|
+
* including addresses and balance information for all related accounts.
|
|
24
|
+
* This function automatically calculates related PDA addresses and concurrently queries all balances,
|
|
25
|
+
* providing complete liquidity pool status.
|
|
26
|
+
*
|
|
27
|
+
* @param {string|PublicKey} mint - Token mint account address
|
|
28
|
+
*
|
|
29
|
+
* @returns {Promise<Object>} Complete BorrowingBondingCurve account data object
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<Object>} Return object contains following complete fields:
|
|
32
|
+
*
|
|
33
|
+
* **Core Reserve Data:**
|
|
34
|
+
* @returns {bigint} returns.lpTokenReserve - LP Token reserves, total reserves of liquidity provider tokens
|
|
35
|
+
* @returns {bigint} returns.lpSolReserve - LP SOL reserves, SOL reserves in the liquidity pool
|
|
36
|
+
* @returns {bigint} returns.price - Current token price, calculated based on AMM algorithm
|
|
37
|
+
* @returns {bigint} returns.borrowTokenReserve - Borrowable Token reserves, borrowable token reserves
|
|
38
|
+
* @returns {bigint} returns.borrowSolReserve - Borrowable SOL reserves, borrowable SOL reserves
|
|
39
|
+
*
|
|
40
|
+
* **Fee and Parameter Configuration:**
|
|
41
|
+
* @returns {number} returns.swapFee - Swap fee rate, expressed in basis points (e.g. 100 = 1%)
|
|
42
|
+
* @returns {number} returns.borrowFee - Borrow fee rate, expressed in basis points
|
|
43
|
+
* @returns {boolean} returns.feeDiscountFlag - Fee discount flag, whether fee discounts are enabled
|
|
44
|
+
* @returns {number} returns.feeSplit - Fee split ratio, determines how fees are distributed among different recipients
|
|
45
|
+
* @returns {number} returns.borrowDuration - Borrow duration, in seconds
|
|
46
|
+
* @returns {number} returns.bump - curve_account PDA bump seed
|
|
47
|
+
*
|
|
48
|
+
* **Account Addresses:**
|
|
49
|
+
* @returns {string} returns.baseFeeRecipient - Base fee recipient address, receives base transaction fees
|
|
50
|
+
* @returns {string} returns.feeRecipient - Fee recipient address, receives additional fee income
|
|
51
|
+
* @returns {string} returns.mint - Token mint account address
|
|
52
|
+
* @returns {string|null} returns.upHead - Up order linked list head account address, null if none
|
|
53
|
+
* @returns {string|null} returns.downHead - Down order linked list head account address, null if none
|
|
54
|
+
* @returns {string} returns.poolTokenAccount - Pool token account address, stores tokens in the liquidity pool
|
|
55
|
+
* @returns {string} returns.poolSolAccount - Pool SOL account address, stores native SOL in the liquidity pool
|
|
56
|
+
*
|
|
57
|
+
* **Balance Information:**
|
|
58
|
+
* @returns {number} returns.baseFeeRecipientBalance - SOL balance of base fee recipient address (lamports)
|
|
59
|
+
* @returns {number} returns.feeRecipientBalance - SOL balance of fee recipient address (lamports)
|
|
60
|
+
* @returns {bigint} returns.poolTokenBalance - Token balance of pool token account
|
|
61
|
+
* @returns {number} returns.poolSolBalance - SOL balance of pool SOL account (lamports)
|
|
62
|
+
*
|
|
63
|
+
* **Metadata:**
|
|
64
|
+
* @returns {Object} returns._metadata - Additional metadata information
|
|
65
|
+
* @returns {string} returns._metadata.accountAddress - Complete address of curve_account
|
|
66
|
+
* @returns {string} returns._metadata.mintAddress - Input token mint address
|
|
67
|
+
*
|
|
68
|
+
* @throws {Error} Throws error when curve_account does not exist
|
|
69
|
+
* @throws {Error} Throws error when unable to decode account data
|
|
70
|
+
* @throws {Error} Throws error when network connection fails
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Basic usage example
|
|
74
|
+
* try {
|
|
75
|
+
* const curveData = await sdk.chain.getCurveAccount('3YggGtxXEGBbjK1WLj2Z79doZC2gkCWXag1ag8BD4cYY');
|
|
76
|
+
*
|
|
77
|
+
* // Display core reserve information
|
|
78
|
+
* console.log('=== Core Reserve Data ===');
|
|
79
|
+
* console.log('LP Token reserves:', curveData.lpTokenReserve.toString());
|
|
80
|
+
* console.log('LP SOL reserves:', curveData.lpSolReserve.toString());
|
|
81
|
+
* console.log('Current price:', curveData.price.toString());
|
|
82
|
+
* console.log('Borrow Token reserves:', curveData.borrowTokenReserve.toString());
|
|
83
|
+
* console.log('Borrow SOL reserves:', curveData.borrowSolReserve.toString());
|
|
84
|
+
*
|
|
85
|
+
* // Display fee configuration
|
|
86
|
+
* console.log('=== Fee Configuration ===');
|
|
87
|
+
* console.log('Swap fee rate:', curveData.swapFee / 100, '%');
|
|
88
|
+
* console.log('Borrow fee rate:', curveData.borrowFee / 100, '%');
|
|
89
|
+
* console.log('Fee discount:', curveData.feeDiscountFlag ? 'Enabled' : 'Disabled');
|
|
90
|
+
* console.log('Borrow duration:', curveData.borrowDuration, 'seconds');
|
|
91
|
+
*
|
|
92
|
+
* // Display account addresses
|
|
93
|
+
* console.log('=== Account Addresses ===');
|
|
94
|
+
* console.log('Base fee recipient address:', curveData.baseFeeRecipient);
|
|
95
|
+
* console.log('Fee recipient address:', curveData.feeRecipient);
|
|
96
|
+
* console.log('Pool token account:', curveData.poolTokenAccount);
|
|
97
|
+
* console.log('Pool SOL account:', curveData.poolSolAccount);
|
|
98
|
+
*
|
|
99
|
+
* // Display balance information
|
|
100
|
+
* console.log('=== Balance Information ===');
|
|
101
|
+
* console.log('Base fee recipient balance:', curveData.baseFeeRecipientBalance / 1e9, 'SOL');
|
|
102
|
+
* console.log('Fee recipient balance:', curveData.feeRecipientBalance / 1e9, 'SOL');
|
|
103
|
+
* console.log('Pool token balance:', curveData.poolTokenBalance.toString());
|
|
104
|
+
* console.log('Pool SOL balance:', curveData.poolSolBalance / 1e9, 'SOL');
|
|
105
|
+
*
|
|
106
|
+
* // Display linked list head information
|
|
107
|
+
* console.log('=== Order Linked Lists ===');
|
|
108
|
+
* console.log('Up order head:', curveData.upHead || 'Empty');
|
|
109
|
+
* console.log('Down order head:', curveData.downHead || 'Empty');
|
|
110
|
+
*
|
|
111
|
+
* } catch (error) {
|
|
112
|
+
* console.error('Failed to get curve account:', error.message);
|
|
113
|
+
* }
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* // Pool monitoring example
|
|
117
|
+
* async function monitorPool(mintAddress) {
|
|
118
|
+
* const data = await sdk.chain.getCurveAccount(mintAddress);
|
|
119
|
+
*
|
|
120
|
+
* // Calculate pool utilization
|
|
121
|
+
* const tokenUtilization = Number(data.lpTokenReserve - data.poolTokenBalance) / Number(data.lpTokenReserve);
|
|
122
|
+
* const solUtilization = Number(data.lpSolReserve - BigInt(data.poolSolBalance)) / Number(data.lpSolReserve);
|
|
123
|
+
*
|
|
124
|
+
* console.log('Token utilization:', (tokenUtilization * 100).toFixed(2), '%');
|
|
125
|
+
* console.log('SOL utilization:', (solUtilization * 100).toFixed(2), '%');
|
|
126
|
+
*
|
|
127
|
+
* // Check fee earnings
|
|
128
|
+
* const totalFeeBalance = data.baseFeeRecipientBalance + data.feeRecipientBalance;
|
|
129
|
+
* console.log('Total fee earnings:', totalFeeBalance / 1e9, 'SOL');
|
|
130
|
+
*
|
|
131
|
+
* return {
|
|
132
|
+
* tokenUtilization,
|
|
133
|
+
* solUtilization,
|
|
134
|
+
* totalFeeBalance,
|
|
135
|
+
* currentPrice: data.price
|
|
136
|
+
* };
|
|
137
|
+
* }
|
|
138
|
+
*
|
|
139
|
+
* @since 1.0.0
|
|
140
|
+
* @version 1.1.0 - Added liquidity pool account balance query functionality
|
|
141
|
+
* @author SpinPet SDK Team
|
|
142
|
+
*/
|
|
143
|
+
async getCurveAccount(mint) {
|
|
144
|
+
try {
|
|
145
|
+
// Parameter validation and conversion
|
|
146
|
+
const mintPubkey = typeof mint === 'string' ? new PublicKey(mint) : mint;
|
|
147
|
+
|
|
148
|
+
// Calculate curve_account PDA address
|
|
149
|
+
// Use the same seeds as in the contract: [b"borrowing_curve", mint_account.key().as_ref()]
|
|
150
|
+
const [curveAccountPDA] = PublicKey.findProgramAddressSync(
|
|
151
|
+
[
|
|
152
|
+
Buffer.from("borrowing_curve"),
|
|
153
|
+
mintPubkey.toBuffer()
|
|
154
|
+
],
|
|
155
|
+
this.sdk.programId
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Use Anchor program to fetch account data directly
|
|
159
|
+
// Method 1: Use program's fetch method
|
|
160
|
+
let decodedData;
|
|
161
|
+
try {
|
|
162
|
+
decodedData = await this.sdk.program.account.borrowingBondingCurve.fetch(curveAccountPDA);
|
|
163
|
+
} catch (fetchError) {
|
|
164
|
+
// Method 2: If fetch fails, use raw method
|
|
165
|
+
const accountInfo = await this.sdk.connection.getAccountInfo(curveAccountPDA);
|
|
166
|
+
if (!accountInfo) {
|
|
167
|
+
throw new Error(`curve_account does not exist`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Manually decode with BorshAccountsCoder
|
|
171
|
+
const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
|
|
172
|
+
|
|
173
|
+
// Try different account names
|
|
174
|
+
try {
|
|
175
|
+
decodedData = accountsCoder.decode('BorrowingBondingCurve', accountInfo.data);
|
|
176
|
+
} catch (decodeError1) {
|
|
177
|
+
try {
|
|
178
|
+
// Try lowercase name
|
|
179
|
+
decodedData = accountsCoder.decode('borrowingBondingCurve', accountInfo.data);
|
|
180
|
+
} catch (decodeError2) {
|
|
181
|
+
// Both failed, throw original error
|
|
182
|
+
throw new Error(`Cannot decode account data: ${decodeError1.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Calculate pool account PDA addresses
|
|
188
|
+
const [poolTokenAccountPDA] = PublicKey.findProgramAddressSync(
|
|
189
|
+
[
|
|
190
|
+
Buffer.from("pool_token"),
|
|
191
|
+
mintPubkey.toBuffer()
|
|
192
|
+
],
|
|
193
|
+
this.sdk.programId
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const [poolSolAccountPDA] = PublicKey.findProgramAddressSync(
|
|
197
|
+
[
|
|
198
|
+
Buffer.from("pool_sol"),
|
|
199
|
+
mintPubkey.toBuffer()
|
|
200
|
+
],
|
|
201
|
+
this.sdk.programId
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Query all balances concurrently
|
|
205
|
+
const [
|
|
206
|
+
baseFeeRecipientBalance,
|
|
207
|
+
feeRecipientBalance,
|
|
208
|
+
poolTokenBalance,
|
|
209
|
+
poolSolBalance
|
|
210
|
+
] = await Promise.all([
|
|
211
|
+
this.sdk.connection.getBalance(decodedData.baseFeeRecipient),
|
|
212
|
+
this.sdk.connection.getBalance(decodedData.feeRecipient),
|
|
213
|
+
this.sdk.connection.getTokenAccountBalance(poolTokenAccountPDA).catch(() => ({ value: { amount: '0' } })),
|
|
214
|
+
this.sdk.connection.getBalance(poolSolAccountPDA)
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
// Convert data format
|
|
218
|
+
const convertedData = {
|
|
219
|
+
// BN types convert to bigint
|
|
220
|
+
lpTokenReserve: BigInt(decodedData.lpTokenReserve.toString()),
|
|
221
|
+
lpSolReserve: BigInt(decodedData.lpSolReserve.toString()),
|
|
222
|
+
price: BigInt(decodedData.price.toString()),
|
|
223
|
+
borrowTokenReserve: BigInt(decodedData.borrowTokenReserve.toString()),
|
|
224
|
+
borrowSolReserve: BigInt(decodedData.borrowSolReserve.toString()),
|
|
225
|
+
|
|
226
|
+
// Numeric types remain unchanged
|
|
227
|
+
swapFee: decodedData.swapFee,
|
|
228
|
+
borrowFee: decodedData.borrowFee,
|
|
229
|
+
feeDiscountFlag: decodedData.feeDiscountFlag,
|
|
230
|
+
feeSplit: decodedData.feeSplit,
|
|
231
|
+
borrowDuration: decodedData.borrowDuration,
|
|
232
|
+
bump: decodedData.bump,
|
|
233
|
+
|
|
234
|
+
// PublicKey types convert to string
|
|
235
|
+
baseFeeRecipient: decodedData.baseFeeRecipient.toString(),
|
|
236
|
+
feeRecipient: decodedData.feeRecipient.toString(),
|
|
237
|
+
mint: decodedData.mint.toString(),
|
|
238
|
+
upHead: decodedData.upHead ? decodedData.upHead.toString() : null,
|
|
239
|
+
downHead: decodedData.downHead ? decodedData.downHead.toString() : null,
|
|
240
|
+
|
|
241
|
+
// SOL balance information
|
|
242
|
+
baseFeeRecipientBalance: baseFeeRecipientBalance, // Unit: lamports
|
|
243
|
+
feeRecipientBalance: feeRecipientBalance, // Unit: lamports
|
|
244
|
+
|
|
245
|
+
// Pool account information
|
|
246
|
+
poolTokenAccount: poolTokenAccountPDA.toString(), // Pool token account address
|
|
247
|
+
poolSolAccount: poolSolAccountPDA.toString(), // Pool SOL account address
|
|
248
|
+
poolTokenBalance: BigInt(poolTokenBalance.value.amount), // Pool token balance
|
|
249
|
+
poolSolBalance: poolSolBalance, // Pool SOL balance (lamports)
|
|
250
|
+
|
|
251
|
+
// Additional metadata
|
|
252
|
+
_metadata: {
|
|
253
|
+
accountAddress: curveAccountPDA.toString(),
|
|
254
|
+
mintAddress: mintPubkey.toString()
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Return converted data
|
|
259
|
+
return convertedData;
|
|
260
|
+
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Provide concise error information
|
|
263
|
+
if (error.message.includes('Account does not exist')) {
|
|
264
|
+
throw new Error(`curve_account does not exist for mint: ${mint}`);
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error(`Failed to get curve_account: ${error.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Batch get multiple tokens' curve_account data
|
|
273
|
+
*
|
|
274
|
+
* @param {Array<string|PublicKey>} mints - Array of token addresses
|
|
275
|
+
* @returns {Promise<Object>} Object containing success and error results
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* const curveDataList = await sdk.chain.getCurveAccountBatch([
|
|
279
|
+
* '3YggGtxXEGBbjK1WLj2Z79doZC2gkCWXag1ag8BD4cYY',
|
|
280
|
+
* 'AnotherTokenMintAddress'
|
|
281
|
+
* ]);
|
|
282
|
+
*/
|
|
283
|
+
async getCurveAccountBatch(mints) {
|
|
284
|
+
if (!Array.isArray(mints)) {
|
|
285
|
+
throw new Error('mints parameter must be an array');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const results = [];
|
|
289
|
+
const errors = [];
|
|
290
|
+
|
|
291
|
+
// Concurrently get all data
|
|
292
|
+
const promises = mints.map(async (mint, index) => {
|
|
293
|
+
try {
|
|
294
|
+
const data = await this.getCurveAccount(mint);
|
|
295
|
+
return { index, success: true, data, mint: mint.toString() };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return { index, success: false, error: error.message, mint: mint.toString() };
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const settled = await Promise.allSettled(promises);
|
|
302
|
+
|
|
303
|
+
// Process results
|
|
304
|
+
for (let i = 0; i < settled.length; i++) {
|
|
305
|
+
const result = settled[i];
|
|
306
|
+
|
|
307
|
+
if (result.status === 'fulfilled') {
|
|
308
|
+
const { success, data, error, mint } = result.value;
|
|
309
|
+
|
|
310
|
+
if (success) {
|
|
311
|
+
results.push(data);
|
|
312
|
+
} else {
|
|
313
|
+
errors.push({ mint, error });
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
errors.push({
|
|
317
|
+
mint: mints[i].toString(),
|
|
318
|
+
error: result.reason?.message || 'Unknown error'
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
success: results,
|
|
325
|
+
errors: errors,
|
|
326
|
+
total: mints.length,
|
|
327
|
+
successCount: results.length,
|
|
328
|
+
errorCount: errors.length
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Calculate curve_account PDA address
|
|
334
|
+
*
|
|
335
|
+
* @param {string|PublicKey} mint - Token mint address
|
|
336
|
+
* @returns {PublicKey} curve_account PDA address
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* const curveAddress = sdk.chain.getCurveAccountAddress('3YggGtxXEGBbjK1WLj2Z79doZC2gkCWXag1ag8BD4cYY');
|
|
340
|
+
* console.log('Curve Account Address:', curveAddress.toString());
|
|
341
|
+
*/
|
|
342
|
+
getCurveAccountAddress(mint) {
|
|
343
|
+
const mintPubkey = typeof mint === 'string' ? new PublicKey(mint) : mint;
|
|
344
|
+
|
|
345
|
+
const [curveAccountPDA] = PublicKey.findProgramAddressSync(
|
|
346
|
+
[
|
|
347
|
+
Buffer.from("borrowing_curve"),
|
|
348
|
+
mintPubkey.toBuffer()
|
|
349
|
+
],
|
|
350
|
+
this.sdk.programId
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return curveAccountPDA;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get price data (read price from chain curveAccountPDA)
|
|
358
|
+
* @param {string} mint - Token address
|
|
359
|
+
* @returns {Promise<string>} Latest price string
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* // Get latest token price
|
|
363
|
+
* const price = await sdk.chain.price('56hfrQYiyRSUZdRKDuUvsqRik8j2UDW9kCisy7BiRxmg');
|
|
364
|
+
* console.log('Latest price:', price); // "13514066072452801812769"
|
|
365
|
+
*/
|
|
366
|
+
async price(mint) {
|
|
367
|
+
// Validate input
|
|
368
|
+
if (!mint || typeof mint !== 'string') {
|
|
369
|
+
throw new Error('price: mint address must be a valid string');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Parameter validation and conversion
|
|
374
|
+
let mintPubkey;
|
|
375
|
+
try {
|
|
376
|
+
mintPubkey = typeof mint === 'string' ? new PublicKey(mint) : mint;
|
|
377
|
+
} catch (pubkeyError) {
|
|
378
|
+
throw new Error(`Invalid mint address: ${mint}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Validate mintPubkey
|
|
382
|
+
if (!mintPubkey || typeof mintPubkey.toBuffer !== 'function') {
|
|
383
|
+
throw new Error(`Invalid mintPubkey`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Calculate curve_account PDA address
|
|
387
|
+
const [curveAccountPDA] = PublicKey.findProgramAddressSync(
|
|
388
|
+
[
|
|
389
|
+
Buffer.from("borrowing_curve"),
|
|
390
|
+
mintPubkey.toBuffer()
|
|
391
|
+
],
|
|
392
|
+
this.sdk.programId
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Use Anchor program to fetch account data directly
|
|
396
|
+
let decodedData;
|
|
397
|
+
try {
|
|
398
|
+
decodedData = await this.sdk.program.account.borrowingBondingCurve.fetch(curveAccountPDA);
|
|
399
|
+
} catch (fetchError) {
|
|
400
|
+
// If fetch fails, use raw method
|
|
401
|
+
const accountInfo = await this.sdk.connection.getAccountInfo(curveAccountPDA);
|
|
402
|
+
if (!accountInfo) {
|
|
403
|
+
throw new Error(`curve_account does not exist`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Manually decode with BorshAccountsCoder
|
|
407
|
+
const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
decodedData = accountsCoder.decode('BorrowingBondingCurve', accountInfo.data);
|
|
411
|
+
} catch (decodeError1) {
|
|
412
|
+
try {
|
|
413
|
+
decodedData = accountsCoder.decode('borrowingBondingCurve', accountInfo.data);
|
|
414
|
+
} catch (decodeError2) {
|
|
415
|
+
throw new Error(`Cannot decode account data: ${decodeError1.message}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check price data and return
|
|
421
|
+
if (decodedData.price && decodedData.price.toString() !== '0') {
|
|
422
|
+
return decodedData.price.toString();
|
|
423
|
+
} else {
|
|
424
|
+
// If no price data, return initial price
|
|
425
|
+
const initialPrice = CurveAMM.getInitialPrice();
|
|
426
|
+
if (initialPrice === null) {
|
|
427
|
+
throw new Error('price: Unable to calculate initial price');
|
|
428
|
+
}
|
|
429
|
+
return initialPrice.toString();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// If getting fails, return initial price
|
|
434
|
+
console.warn(`price: Failed to get chain price, using initial price: ${error.message}`);
|
|
435
|
+
|
|
436
|
+
const initialPrice = CurveAMM.getInitialPrice();
|
|
437
|
+
if (initialPrice === null) {
|
|
438
|
+
throw new Error('price: Unable to calculate initial price');
|
|
439
|
+
}
|
|
440
|
+
return initialPrice.toString();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get Orders Data (Read from Chain)
|
|
446
|
+
* @param {string} mint - Token mint address
|
|
447
|
+
* @param {Object} options - Query parameters
|
|
448
|
+
* @param {string} options.type - Order type: "up_orders" (short) or "down_orders" (long)
|
|
449
|
+
* @param {number} options.page - Page number, default 1
|
|
450
|
+
* @param {number} options.limit - Items per page, default 500, max 1000
|
|
451
|
+
* @returns {Promise<Object>} Order data with raw order list
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* // Get long orders
|
|
455
|
+
* const ordersData = await sdk.chain.orders('6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee', { type: 'down_orders' });
|
|
456
|
+
*
|
|
457
|
+
* // Return value example:
|
|
458
|
+
* // {
|
|
459
|
+
* // "success": true,
|
|
460
|
+
* // "data": {
|
|
461
|
+
* // "orders": [
|
|
462
|
+
* // {
|
|
463
|
+
* // "order_type": "down_orders", // Order type string (converted)
|
|
464
|
+
* // "mint": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee", // Token address
|
|
465
|
+
* // "user": "JD1eNPaJpbtejKfgimbLYLkvpsTHyYzKCCozVLGLS6zu", // User address
|
|
466
|
+
* // "lock_lp_start_price": "46618228118401293964111", // LP start price (string)
|
|
467
|
+
* // "lock_lp_end_price": "45827474968448818396222", // LP end price (string)
|
|
468
|
+
* // "lock_lp_sol_amount": 3299491609, // LP locked SOL amount (lamports)
|
|
469
|
+
* // "lock_lp_token_amount": 713848715669, // LP locked token amount (min unit)
|
|
470
|
+
* // "start_time": 1756352482, // Start time (Unix timestamp)
|
|
471
|
+
* // "end_time": 1756525282, // End time (Unix timestamp)
|
|
472
|
+
* // "margin_sol_amount": 571062973, // Margin SOL amount (lamports)
|
|
473
|
+
* // "borrow_amount": 3860656108, // Borrow amount (lamports)
|
|
474
|
+
* // "position_asset_amount": 713848715669, // Position asset amount (min unit)
|
|
475
|
+
* // "borrow_fee": 300, // Borrow fee (basis points, 300 = 3%)
|
|
476
|
+
* // "order_pda": "5aVwYyzvC5Y2qykDgwG8o7EUwCrL8WgCJpgxoH3mihYb" // Order PDA address
|
|
477
|
+
* // }
|
|
478
|
+
* // ],
|
|
479
|
+
* // "total": 12, // Total order count
|
|
480
|
+
* // "order_type": "down_orders", // Order type (string)
|
|
481
|
+
* // "mint_account": "6ZDJtGFTzrF3FaN5uaqa1h8EexW7BtQd4FwA9Dt7m3ee", // Queried token address
|
|
482
|
+
* // "page": 1, // Current page number
|
|
483
|
+
* // "limit": 50, // Per page limit
|
|
484
|
+
* // "has_next": false, // Whether has next page
|
|
485
|
+
* // "has_prev": false // Whether has previous page
|
|
486
|
+
* // },
|
|
487
|
+
* // "message": "Operation successful" // Operation result message
|
|
488
|
+
* // }
|
|
489
|
+
*
|
|
490
|
+
* // Use utility methods to process data:
|
|
491
|
+
* const lpPairs = sdk.buildLpPairs(ordersData.data.orders); // Build LP pairs array
|
|
492
|
+
* const orderAccounts = sdk.buildOrderAccounts(ordersData.data.orders); // Build order accounts array
|
|
493
|
+
*/
|
|
494
|
+
async orders(mint, options = {}) {
|
|
495
|
+
try {
|
|
496
|
+
// Parameter validation
|
|
497
|
+
if (!mint || typeof mint !== 'string') {
|
|
498
|
+
throw new Error('orders: mint address must be a valid string');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Set default parameters
|
|
502
|
+
const orderType = options.type || 'down_orders';
|
|
503
|
+
const page = options.page || 1;
|
|
504
|
+
const limit = Math.min(options.limit || 500, 1000); // Maximum 1000
|
|
505
|
+
|
|
506
|
+
// Validate order type
|
|
507
|
+
if (!['up_orders', 'down_orders'].includes(orderType)) {
|
|
508
|
+
throw new Error('orders: order type must be "up_orders" or "down_orders"');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Convert API type to linked list direction
|
|
512
|
+
// "up_orders" = short orders = upHead
|
|
513
|
+
// "down_orders" = long orders = downHead
|
|
514
|
+
const direction = orderType === 'up_orders' ? 'upHead' : 'downHead';
|
|
515
|
+
|
|
516
|
+
//console.log(`chain.orders: Get ${orderType} orders, mint=${mint}, limit=${limit}`);
|
|
517
|
+
|
|
518
|
+
// Get curve_account data to get linked list head
|
|
519
|
+
const curveData = await this.getCurveAccount(mint);
|
|
520
|
+
const headAddress = curveData[direction];
|
|
521
|
+
|
|
522
|
+
//console.log(`chain.orders: ${direction} linked list head address:`, headAddress || 'null');
|
|
523
|
+
|
|
524
|
+
// If linked list is empty, return empty result
|
|
525
|
+
if (!headAddress) {
|
|
526
|
+
//console.log(`chain.orders: ${direction} linked list is empty`);
|
|
527
|
+
return {
|
|
528
|
+
success: true,
|
|
529
|
+
data: {
|
|
530
|
+
orders: [],
|
|
531
|
+
total: 0,
|
|
532
|
+
order_type: orderType,
|
|
533
|
+
mint_account: mint,
|
|
534
|
+
page: page,
|
|
535
|
+
limit: limit,
|
|
536
|
+
has_next: false,
|
|
537
|
+
has_prev: false
|
|
538
|
+
},
|
|
539
|
+
message: "Operation successful"
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Traverse linked list to read orders
|
|
544
|
+
const orders = [];
|
|
545
|
+
let currentAddress = new PublicKey(headAddress);
|
|
546
|
+
let count = 0;
|
|
547
|
+
|
|
548
|
+
//console.log(`chain.orders: Start traversing linked list from ${currentAddress.toString()}`);
|
|
549
|
+
|
|
550
|
+
while (currentAddress && count < limit) {
|
|
551
|
+
try {
|
|
552
|
+
//console.log(`chain.orders: 遍历 Traversing [${count}] ${currentAddress.toString()}`);
|
|
553
|
+
|
|
554
|
+
// Get raw account data
|
|
555
|
+
const accountInfo = await this.sdk.connection.getAccountInfo(currentAddress);
|
|
556
|
+
if (!accountInfo) {
|
|
557
|
+
throw new Error(`Order account ${currentAddress.toString()} does not exist`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Manually decode with BorshAccountsCoder
|
|
561
|
+
const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
|
|
562
|
+
let orderData;
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
orderData = accountsCoder.decode('MarginOrder', accountInfo.data);
|
|
566
|
+
} catch (decodeError1) {
|
|
567
|
+
try {
|
|
568
|
+
orderData = accountsCoder.decode('marginOrder', accountInfo.data);
|
|
569
|
+
} catch (decodeError2) {
|
|
570
|
+
console.log("orders 报错时的 accountInfo=", accountInfo);
|
|
571
|
+
console.log("decodeError1=", decodeError1);
|
|
572
|
+
throw new Error(`Cannot decode order account data: ${decodeError2.message}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
// try {
|
|
579
|
+
// // 尝试不同的账户类型名称
|
|
580
|
+
// orderData = accountsCoder.decode('MarginOrder', accountInfo.data);
|
|
581
|
+
// } catch (decodeError1) {
|
|
582
|
+
// try {
|
|
583
|
+
// orderData = accountsCoder.decode('marginOrder', accountInfo.data);
|
|
584
|
+
// } catch (decodeError2) {
|
|
585
|
+
// try {
|
|
586
|
+
// // 使用 program.account 直接解码
|
|
587
|
+
// console.log(`使用 program.account 直接解码 PDA: ${pdaAddress}`);
|
|
588
|
+
// orderData = this.sdk.program.account.marginOrder.fetch(currentAddress);
|
|
589
|
+
// } catch (decodeError3) {
|
|
590
|
+
// error = `解码失败: MarginOrder[${decodeError1.message}] marginOrder[${decodeError2.message}] fetch[${decodeError3.message}]`;
|
|
591
|
+
// throw error
|
|
592
|
+
// }
|
|
593
|
+
// }
|
|
594
|
+
// }
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
// Data transformation
|
|
601
|
+
const convertedOrder = {
|
|
602
|
+
// Convert chain number to API string format
|
|
603
|
+
order_type: orderData.orderType === 1 ? 'down_orders' : 'up_orders', // 1=long=down_orders, 2=short=up_orders
|
|
604
|
+
mint: orderData.mint.toString(),
|
|
605
|
+
user: orderData.user.toString(),
|
|
606
|
+
// Convert BN type to string
|
|
607
|
+
lock_lp_start_price: orderData.lockLpStartPrice.toString(),
|
|
608
|
+
lock_lp_end_price: orderData.lockLpEndPrice.toString(),
|
|
609
|
+
// Keep numeric types unchanged
|
|
610
|
+
lock_lp_sol_amount: orderData.lockLpSolAmount.toNumber(),
|
|
611
|
+
lock_lp_token_amount: orderData.lockLpTokenAmount.toNumber(),
|
|
612
|
+
start_time: orderData.startTime,
|
|
613
|
+
end_time: orderData.endTime,
|
|
614
|
+
margin_init_sol_amount: orderData.marginInitSolAmount.toNumber(),
|
|
615
|
+
margin_sol_amount: orderData.marginSolAmount.toNumber(),
|
|
616
|
+
borrow_amount: orderData.borrowAmount.toNumber(),
|
|
617
|
+
position_asset_amount: orderData.positionAssetAmount.toNumber(),
|
|
618
|
+
borrow_fee: orderData.borrowFee,
|
|
619
|
+
realized_sol_amount: orderData.realizedSolAmount.toNumber(),
|
|
620
|
+
// Add order_pda field
|
|
621
|
+
order_pda: currentAddress.toString()
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
orders.push(convertedOrder);
|
|
625
|
+
count++;
|
|
626
|
+
|
|
627
|
+
//console.log(`chain.orders: Successfully read order ${count}: ${currentAddress.toString()}, type=${convertedOrder.order_type}`);
|
|
628
|
+
|
|
629
|
+
// Move to next node
|
|
630
|
+
if (orderData.nextOrder) {
|
|
631
|
+
currentAddress = orderData.nextOrder;
|
|
632
|
+
//console.log(`chain.orders: 下一个 Next: ${currentAddress.toString()}`);
|
|
633
|
+
} else {
|
|
634
|
+
//console.log(`chain.orders: 链表结束 List ended at node ${count}`);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Add 50ms delay to avoid calling too fast
|
|
639
|
+
//await new Promise(resolve => setTimeout(resolve, 50));
|
|
640
|
+
|
|
641
|
+
} catch (error) {
|
|
642
|
+
// If order account doesn't exist or read fails, throw error
|
|
643
|
+
console.log("A_chain.orders() err:", error);
|
|
644
|
+
throw new Error(`Failed to read order: ${error.message}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Simulate pagination info
|
|
649
|
+
const hasNext = count === limit; // If read limit reached, might have more
|
|
650
|
+
const hasPrev = page > 1;
|
|
651
|
+
|
|
652
|
+
//console.log(`chain.orders: Completed reading, got ${orders.length} orders`);
|
|
653
|
+
|
|
654
|
+
// Return same format as fast.orders
|
|
655
|
+
return {
|
|
656
|
+
success: true,
|
|
657
|
+
data: {
|
|
658
|
+
orders: orders,
|
|
659
|
+
total: orders.length, // Chain can't know total, use current count
|
|
660
|
+
order_type: orderType,
|
|
661
|
+
mint_account: mint,
|
|
662
|
+
page: page,
|
|
663
|
+
limit: limit,
|
|
664
|
+
has_next: hasNext,
|
|
665
|
+
has_prev: hasPrev
|
|
666
|
+
},
|
|
667
|
+
message: "Operation successful"
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
} catch (error) {
|
|
671
|
+
// Error handling
|
|
672
|
+
console.log("chain.orders() err:", error);
|
|
673
|
+
console.error('chain.orders: Failed to get orders', error.message);
|
|
674
|
+
throw new Error(`Failed to get orders: ${error.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get MarginOrder account data by PDA address
|
|
680
|
+
*
|
|
681
|
+
* Read the margin order account data for a specified PDA address from the blockchain,
|
|
682
|
+
* including all order details and status information.
|
|
683
|
+
*
|
|
684
|
+
* @param {string|PublicKey} pda - MarginOrder account PDA address
|
|
685
|
+
*
|
|
686
|
+
* @returns {Promise<Object>} Complete MarginOrder account data object
|
|
687
|
+
*
|
|
688
|
+
* @returns {Promise<Object>} Return object contains following complete fields:
|
|
689
|
+
*
|
|
690
|
+
* **Core Order Data:**
|
|
691
|
+
* @returns {number} returns.order_type - Order type: 1=long(做多), 2=short(做空)
|
|
692
|
+
* @returns {string} returns.mint - Token mint account address
|
|
693
|
+
* @returns {string} returns.user - User account address who created the order
|
|
694
|
+
* @returns {string|null} returns.next_order - Next order PDA address in linked list, null if none
|
|
695
|
+
* @returns {string|null} returns.prev_order - Previous order PDA address in linked list, null if none
|
|
696
|
+
*
|
|
697
|
+
* **Price and Amount Data:**
|
|
698
|
+
* @returns {string} returns.lock_lp_start_price - LP start price when order was created (u128 as string)
|
|
699
|
+
* @returns {string} returns.lock_lp_end_price - LP end price when order was created (u128 as string)
|
|
700
|
+
* @returns {number} returns.lock_lp_sol_amount - LP locked SOL amount (lamports)
|
|
701
|
+
* @returns {number} returns.lock_lp_token_amount - LP locked token amount (min unit)
|
|
702
|
+
* @returns {number} returns.margin_sol_amount - Margin SOL amount (lamports)
|
|
703
|
+
* @returns {number} returns.borrow_amount - Borrowed amount (lamports or min unit)
|
|
704
|
+
* @returns {number} returns.position_asset_amount - Position asset amount (min unit)
|
|
705
|
+
*
|
|
706
|
+
* **Time and Fee Data:**
|
|
707
|
+
* @returns {number} returns.start_time - Order start time (Unix timestamp in seconds)
|
|
708
|
+
* @returns {number} returns.end_time - Order expiry time (Unix timestamp in seconds)
|
|
709
|
+
* @returns {number} returns.borrow_fee - Borrow fee rate (basis points, e.g. 300 = 3%)
|
|
710
|
+
* @returns {string} returns.open_price - Open price when order was created (u128 as string)
|
|
711
|
+
*
|
|
712
|
+
* **Technical Data:**
|
|
713
|
+
* @returns {number} returns.bump - PDA bump seed
|
|
714
|
+
*
|
|
715
|
+
* **Metadata:**
|
|
716
|
+
* @returns {Object} returns._metadata - Additional metadata information
|
|
717
|
+
* @returns {string} returns._metadata.accountAddress - Complete PDA address
|
|
718
|
+
*
|
|
719
|
+
* @throws {Error} Throws error when MarginOrder account does not exist
|
|
720
|
+
* @throws {Error} Throws error when unable to decode account data
|
|
721
|
+
* @throws {Error} Throws error when network connection fails
|
|
722
|
+
* @throws {Error} Throws error when invalid PDA address provided
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* // Basic usage example
|
|
726
|
+
* try {
|
|
727
|
+
* const orderData = await sdk.chain.getOrderAccount('FUYU1mKcV5XsJuK4SfcoD4pfXJvQ18pstg8fGqVLYGDG');
|
|
728
|
+
*
|
|
729
|
+
* // Display core order information
|
|
730
|
+
* console.log('=== Order Information ===');
|
|
731
|
+
* console.log('Order type:', orderData.order_type === 1 ? 'Long' : 'Short');
|
|
732
|
+
* console.log('Token mint:', orderData.mint);
|
|
733
|
+
* console.log('User:', orderData.user);
|
|
734
|
+
* console.log('Margin amount:', orderData.margin_sol_amount / 1e9, 'SOL');
|
|
735
|
+
* console.log('Position size:', orderData.position_asset_amount);
|
|
736
|
+
*
|
|
737
|
+
* // Display price information
|
|
738
|
+
* console.log('=== Price Information ===');
|
|
739
|
+
* console.log('Open price:', orderData.open_price);
|
|
740
|
+
* console.log('LP start price:', orderData.lock_lp_start_price);
|
|
741
|
+
* console.log('LP end price:', orderData.lock_lp_end_price);
|
|
742
|
+
*
|
|
743
|
+
* // Display time information
|
|
744
|
+
* console.log('=== Time Information ===');
|
|
745
|
+
* console.log('Start time:', new Date(orderData.start_time * 1000).toLocaleString());
|
|
746
|
+
* console.log('End time:', new Date(orderData.end_time * 1000).toLocaleString());
|
|
747
|
+
* console.log('Borrow fee:', orderData.borrow_fee / 100, '%');
|
|
748
|
+
*
|
|
749
|
+
* // Display linked list connections
|
|
750
|
+
* console.log('=== Linked List Connections ===');
|
|
751
|
+
* console.log('Previous order:', orderData.prev_order || 'None');
|
|
752
|
+
* console.log('Next order:', orderData.next_order || 'None');
|
|
753
|
+
*
|
|
754
|
+
* } catch (error) {
|
|
755
|
+
* console.error('Failed to get order account:', error.message);
|
|
756
|
+
* }
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* // Order validation example
|
|
760
|
+
* async function validateOrder(orderPda) {
|
|
761
|
+
* try {
|
|
762
|
+
* const orderData = await sdk.chain.getOrderAccount(orderPda);
|
|
763
|
+
*
|
|
764
|
+
* // Check if order is expired
|
|
765
|
+
* const currentTime = Math.floor(Date.now() / 1000);
|
|
766
|
+
* const isExpired = currentTime > orderData.end_time;
|
|
767
|
+
*
|
|
768
|
+
* // Calculate remaining time
|
|
769
|
+
* const remainingTime = orderData.end_time - currentTime;
|
|
770
|
+
*
|
|
771
|
+
* console.log('Order status:', isExpired ? 'Expired' : 'Active');
|
|
772
|
+
* if (!isExpired) {
|
|
773
|
+
* console.log('Remaining time:', Math.floor(remainingTime / 3600), 'hours');
|
|
774
|
+
* }
|
|
775
|
+
*
|
|
776
|
+
* return {
|
|
777
|
+
* isExpired,
|
|
778
|
+
* remainingTime,
|
|
779
|
+
* orderType: orderData.order_type === 1 ? 'long' : 'short',
|
|
780
|
+
* user: orderData.user,
|
|
781
|
+
* marginAmount: orderData.margin_sol_amount
|
|
782
|
+
* };
|
|
783
|
+
* } catch (error) {
|
|
784
|
+
* console.error('Order validation failed:', error.message);
|
|
785
|
+
* return null;
|
|
786
|
+
* }
|
|
787
|
+
* }
|
|
788
|
+
*
|
|
789
|
+
* @since 1.0.0
|
|
790
|
+
* @author SpinPet SDK Team
|
|
791
|
+
*/
|
|
792
|
+
async getOrderAccount(pda) {
|
|
793
|
+
try {
|
|
794
|
+
// Parameter validation and conversion
|
|
795
|
+
let pdaPubkey;
|
|
796
|
+
try {
|
|
797
|
+
pdaPubkey = typeof pda === 'string' ? new PublicKey(pda) : pda;
|
|
798
|
+
} catch (pubkeyError) {
|
|
799
|
+
throw new Error(`Invalid PDA address: ${pda}`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Validate pdaPubkey
|
|
803
|
+
if (!pdaPubkey || typeof pdaPubkey.toBuffer !== 'function') {
|
|
804
|
+
throw new Error(`Invalid PDA public key`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Use Anchor program to fetch account data directly
|
|
808
|
+
// Method 1: Use program's fetch method
|
|
809
|
+
let decodedData;
|
|
810
|
+
try {
|
|
811
|
+
decodedData = await this.sdk.program.account.marginOrder.fetch(pdaPubkey);
|
|
812
|
+
} catch (fetchError) {
|
|
813
|
+
// Method 2: If fetch fails, use raw method
|
|
814
|
+
const accountInfo = await this.sdk.connection.getAccountInfo(pdaPubkey);
|
|
815
|
+
if (!accountInfo) {
|
|
816
|
+
throw new Error(`MarginOrder account does not exist`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Manually decode with BorshAccountsCoder
|
|
820
|
+
const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
|
|
821
|
+
|
|
822
|
+
// Try different account names
|
|
823
|
+
try {
|
|
824
|
+
decodedData = accountsCoder.decode('marginOrder', accountInfo.data);
|
|
825
|
+
} catch (decodeError1) {
|
|
826
|
+
try {
|
|
827
|
+
// Try uppercase name
|
|
828
|
+
decodedData = accountsCoder.decode('MarginOrder', accountInfo.data);
|
|
829
|
+
} catch (decodeError2) {
|
|
830
|
+
// Both failed, throw original error
|
|
831
|
+
throw new Error(`Cannot decode MarginOrder account data: ${decodeError1.message}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Convert data format (following the same pattern as getCurveAccount)
|
|
837
|
+
const convertedData = {
|
|
838
|
+
// Numeric types remain unchanged
|
|
839
|
+
order_type: decodedData.orderType,
|
|
840
|
+
start_time: decodedData.startTime,
|
|
841
|
+
end_time: decodedData.endTime,
|
|
842
|
+
lock_lp_sol_amount: decodedData.lockLpSolAmount.toNumber(),
|
|
843
|
+
lock_lp_token_amount: decodedData.lockLpTokenAmount.toNumber(),
|
|
844
|
+
margin_init_sol_amount: decodedData.marginInitSolAmount.toNumber(),
|
|
845
|
+
margin_sol_amount: decodedData.marginSolAmount.toNumber(),
|
|
846
|
+
borrow_amount: decodedData.borrowAmount.toNumber(),
|
|
847
|
+
position_asset_amount: decodedData.positionAssetAmount.toNumber(),
|
|
848
|
+
borrow_fee: decodedData.borrowFee,
|
|
849
|
+
realized_sol_amount: decodedData.realizedSolAmount.toNumber(),
|
|
850
|
+
bump: decodedData.bump,
|
|
851
|
+
|
|
852
|
+
// BN types convert to string
|
|
853
|
+
lock_lp_start_price: decodedData.lockLpStartPrice.toString(),
|
|
854
|
+
lock_lp_end_price: decodedData.lockLpEndPrice.toString(),
|
|
855
|
+
open_price: decodedData.openPrice.toString(),
|
|
856
|
+
|
|
857
|
+
// PublicKey types convert to string
|
|
858
|
+
mint: decodedData.mint.toString(),
|
|
859
|
+
user: decodedData.user.toString(),
|
|
860
|
+
next_order: decodedData.nextOrder ? decodedData.nextOrder.toString() : null,
|
|
861
|
+
prev_order: decodedData.prevOrder ? decodedData.prevOrder.toString() : null,
|
|
862
|
+
|
|
863
|
+
// Additional metadata
|
|
864
|
+
_metadata: {
|
|
865
|
+
accountAddress: pdaPubkey.toString()
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// Return converted data
|
|
870
|
+
return convertedData;
|
|
871
|
+
|
|
872
|
+
} catch (error) {
|
|
873
|
+
// Provide concise error information
|
|
874
|
+
if (error.message.includes('Account does not exist')) {
|
|
875
|
+
throw new Error(`MarginOrder account does not exist for PDA: ${pda}`);
|
|
876
|
+
} else {
|
|
877
|
+
throw new Error(`Failed to get MarginOrder account: ${error.message}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* 获取用户订单 Get User Orders (Read from Chain)
|
|
884
|
+
* @param {string} user - 用户地址
|
|
885
|
+
* @param {string} mint - 代币地址
|
|
886
|
+
* @param {Object} options - 查询参数
|
|
887
|
+
* @param {number} options.page - 页码,默认1
|
|
888
|
+
* @param {number} options.limit - 每页数量,默认200
|
|
889
|
+
* @param {string} options.order_by - 排序方式,默认'start_time_desc'
|
|
890
|
+
* @returns {Promise<Object>} 用户订单数据
|
|
891
|
+
*
|
|
892
|
+
* @example
|
|
893
|
+
* const userOrders = await sdk.chain.user_orders(
|
|
894
|
+
* '8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb',
|
|
895
|
+
* '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X',
|
|
896
|
+
* { page: 1, limit: 200, order_by: 'start_time_desc' }
|
|
897
|
+
* );
|
|
898
|
+
* // 返回格式:
|
|
899
|
+
* // {
|
|
900
|
+
* // "success": true,
|
|
901
|
+
* // "data": {
|
|
902
|
+
* // "orders": [
|
|
903
|
+
* // {
|
|
904
|
+
* // "order_type": 2,
|
|
905
|
+
* // "mint": "4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X",
|
|
906
|
+
* // "user": "8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb",
|
|
907
|
+
* // "lock_lp_start_price": "753522984132656210522",
|
|
908
|
+
* // "lock_lp_end_price": "833102733432007194898",
|
|
909
|
+
* // "lock_lp_sol_amount": 2535405978,
|
|
910
|
+
* // "lock_lp_token_amount": 32000000000000,
|
|
911
|
+
* // "start_time": 1755964862,
|
|
912
|
+
* // "end_time": 1756137662,
|
|
913
|
+
* // "margin_sol_amount": 1909140052,
|
|
914
|
+
* // "borrow_amount": 32000000000000,
|
|
915
|
+
* // "position_asset_amount": 656690798,
|
|
916
|
+
* // "borrow_fee": 1200,
|
|
917
|
+
* // "order_pda": "59yP5tpDP6DBcyy4mge9wKKKdLmk45Th4sbd6Un9LxVN"
|
|
918
|
+
* // }
|
|
919
|
+
* // ],
|
|
920
|
+
* // "total": 11,
|
|
921
|
+
* // "user": "8iGFeUkRpyRx8w5uoUMbfZepUr6BfTdPuJmqGoNBntdb",
|
|
922
|
+
* // "mint_account": "4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X",
|
|
923
|
+
* // "page": 1,
|
|
924
|
+
* // "limit": 200,
|
|
925
|
+
* // "has_next": false,
|
|
926
|
+
* // "has_prev": false
|
|
927
|
+
* // },
|
|
928
|
+
* // "message": "Operation successful"
|
|
929
|
+
* // }
|
|
930
|
+
*
|
|
931
|
+
* // 使用订单数据:
|
|
932
|
+
* const orders = userOrders.data.orders; // 订单数组
|
|
933
|
+
* const totalCount = userOrders.data.total; // 总数量
|
|
934
|
+
*/
|
|
935
|
+
async user_orders(user, mint, options = {}) {
|
|
936
|
+
try {
|
|
937
|
+
// Parameter validation
|
|
938
|
+
if (!user || typeof user !== 'string') {
|
|
939
|
+
throw new Error('user_orders: user address must be a valid string');
|
|
940
|
+
}
|
|
941
|
+
if (!mint || typeof mint !== 'string') {
|
|
942
|
+
throw new Error('user_orders: mint address must be a valid string');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Set default parameters
|
|
946
|
+
const page = options.page || 1;
|
|
947
|
+
const limit = Math.min(options.limit || 200, 1000); // Maximum 1000
|
|
948
|
+
const orderBy = options.order_by || 'start_time_desc';
|
|
949
|
+
|
|
950
|
+
//console.log(`chain.user_orders: Get user orders, user=${user}, mint=${mint}, page=${page}, limit=${limit}`);
|
|
951
|
+
|
|
952
|
+
// Get curve_account data to get both linked list heads
|
|
953
|
+
const curveData = await this.getCurveAccount(mint);
|
|
954
|
+
const upHeadAddress = curveData.upHead; // Short orders (up_orders)
|
|
955
|
+
const downHeadAddress = curveData.downHead; // Long orders (down_orders)
|
|
956
|
+
|
|
957
|
+
//console.log(`chain.user_orders: upHead=${upHeadAddress || 'null'}, downHead=${downHeadAddress || 'null'}`);
|
|
958
|
+
|
|
959
|
+
// Collect all user orders from both linked lists
|
|
960
|
+
const allUserOrders = [];
|
|
961
|
+
|
|
962
|
+
// Helper function to traverse a linked list and collect user orders
|
|
963
|
+
const traverseLinkedList = async (headAddress) => {
|
|
964
|
+
if (!headAddress) return [];
|
|
965
|
+
|
|
966
|
+
const orders = [];
|
|
967
|
+
let currentAddress = new PublicKey(headAddress);
|
|
968
|
+
let count = 0;
|
|
969
|
+
const maxTraverse = 1000; // Limit traversal to avoid infinite loops
|
|
970
|
+
|
|
971
|
+
while (currentAddress && count < maxTraverse) {
|
|
972
|
+
try {
|
|
973
|
+
// Get raw account data
|
|
974
|
+
const accountInfo = await this.sdk.connection.getAccountInfo(currentAddress);
|
|
975
|
+
if (!accountInfo) {
|
|
976
|
+
console.warn(`Order account ${currentAddress.toString()} does not exist, breaking traversal`);
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Manually decode with BorshAccountsCoder
|
|
981
|
+
const accountsCoder = new anchor.BorshAccountsCoder(this.sdk.program.idl);
|
|
982
|
+
let orderData;
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
orderData = accountsCoder.decode('MarginOrder', accountInfo.data);
|
|
986
|
+
} catch (decodeError1) {
|
|
987
|
+
try {
|
|
988
|
+
orderData = accountsCoder.decode('marginOrder', accountInfo.data);
|
|
989
|
+
} catch (decodeError2) {
|
|
990
|
+
console.warn(`Cannot decode order account ${currentAddress.toString()}: ${decodeError2.message}`);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Check if this order belongs to the target user
|
|
996
|
+
if (orderData.user.toString() === user) {
|
|
997
|
+
// Data transformation - convert to same format as fast.user_orders
|
|
998
|
+
const convertedOrder = {
|
|
999
|
+
// Convert chain number to API number format (keep as number for compatibility)
|
|
1000
|
+
order_type: orderData.orderType, // 1=long, 2=short
|
|
1001
|
+
mint: orderData.mint.toString(),
|
|
1002
|
+
user: orderData.user.toString(),
|
|
1003
|
+
// Convert BN type to string
|
|
1004
|
+
lock_lp_start_price: orderData.lockLpStartPrice.toString(),
|
|
1005
|
+
lock_lp_end_price: orderData.lockLpEndPrice.toString(),
|
|
1006
|
+
// Keep numeric types unchanged
|
|
1007
|
+
lock_lp_sol_amount: orderData.lockLpSolAmount.toNumber(),
|
|
1008
|
+
lock_lp_token_amount: orderData.lockLpTokenAmount.toNumber(),
|
|
1009
|
+
start_time: orderData.startTime,
|
|
1010
|
+
end_time: orderData.endTime,
|
|
1011
|
+
margin_init_sol_amount: orderData.marginInitSolAmount.toNumber(),
|
|
1012
|
+
margin_sol_amount: orderData.marginSolAmount.toNumber(),
|
|
1013
|
+
borrow_amount: orderData.borrowAmount.toNumber(),
|
|
1014
|
+
position_asset_amount: orderData.positionAssetAmount.toNumber(),
|
|
1015
|
+
borrow_fee: orderData.borrowFee,
|
|
1016
|
+
realized_sol_amount: orderData.realizedSolAmount.toNumber(),
|
|
1017
|
+
// Add order_pda field
|
|
1018
|
+
order_pda: currentAddress.toString()
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
orders.push(convertedOrder);
|
|
1022
|
+
//console.log(`chain.user_orders: Found user order: ${currentAddress.toString()}, type=${convertedOrder.order_type}`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
count++;
|
|
1026
|
+
|
|
1027
|
+
// Move to next node
|
|
1028
|
+
if (orderData.nextOrder) {
|
|
1029
|
+
currentAddress = orderData.nextOrder;
|
|
1030
|
+
} else {
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Add small delay to avoid overloading
|
|
1035
|
+
if (count % 50 === 0) {
|
|
1036
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
console.warn(`Error reading order ${currentAddress.toString()}: ${error.message}`);
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return orders;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// Traverse both linked lists in parallel to find user orders
|
|
1049
|
+
const [upOrders, downOrders] = await Promise.all([
|
|
1050
|
+
upHeadAddress ? traverseLinkedList(upHeadAddress) : [], // Short orders
|
|
1051
|
+
downHeadAddress ? traverseLinkedList(downHeadAddress) : [] // Long orders
|
|
1052
|
+
]);
|
|
1053
|
+
|
|
1054
|
+
// Combine all orders
|
|
1055
|
+
allUserOrders.push(...upOrders, ...downOrders);
|
|
1056
|
+
|
|
1057
|
+
//console.log(`chain.user_orders: Found ${allUserOrders.length} total user orders`);
|
|
1058
|
+
|
|
1059
|
+
// Sort orders by start_time
|
|
1060
|
+
if (orderBy === 'start_time_desc') {
|
|
1061
|
+
allUserOrders.sort((a, b) => b.start_time - a.start_time);
|
|
1062
|
+
} else if (orderBy === 'start_time_asc') {
|
|
1063
|
+
allUserOrders.sort((a, b) => a.start_time - b.start_time);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Implement pagination
|
|
1067
|
+
const totalOrders = allUserOrders.length;
|
|
1068
|
+
const startIndex = (page - 1) * limit;
|
|
1069
|
+
const endIndex = startIndex + limit;
|
|
1070
|
+
const paginatedOrders = allUserOrders.slice(startIndex, endIndex);
|
|
1071
|
+
|
|
1072
|
+
// Calculate pagination info
|
|
1073
|
+
const hasNext = endIndex < totalOrders;
|
|
1074
|
+
const hasPrev = page > 1;
|
|
1075
|
+
|
|
1076
|
+
//console.log(`chain.user_orders: Returning ${paginatedOrders.length} orders (page ${page}/${Math.ceil(totalOrders/limit)})`);
|
|
1077
|
+
|
|
1078
|
+
// Return same format as fast.user_orders
|
|
1079
|
+
return {
|
|
1080
|
+
success: true,
|
|
1081
|
+
data: {
|
|
1082
|
+
orders: paginatedOrders,
|
|
1083
|
+
total: totalOrders,
|
|
1084
|
+
user: user,
|
|
1085
|
+
mint_account: mint,
|
|
1086
|
+
page: page,
|
|
1087
|
+
limit: limit,
|
|
1088
|
+
has_next: hasNext,
|
|
1089
|
+
has_prev: hasPrev
|
|
1090
|
+
},
|
|
1091
|
+
message: "Operation successful"
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
// Error handling
|
|
1096
|
+
console.error('chain.user_orders: Failed to get user orders', error.message);
|
|
1097
|
+
throw new Error(`Failed to get user orders: ${error.message}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
module.exports = ChainModule;
|