@n1xyz/nord-ts 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.js +39 -82
- package/dist/bundle.js +79181 -0
- package/dist/client/Nord.d.ts +2 -2
- package/dist/client/Nord.js +46 -78
- package/dist/client/NordAdmin.d.ts +2 -2
- package/dist/client/NordAdmin.js +57 -89
- package/dist/client/NordUser.js +118 -147
- package/dist/const.js +5 -8
- package/dist/error.js +7 -5
- package/dist/gen/nord_pb.js +88 -92
- package/dist/gen/openapi.d.ts +5 -6
- package/dist/gen/openapi.js +1 -2
- package/dist/index.js +10 -49
- package/dist/types.d.ts +1 -0
- package/dist/types.js +21 -60
- package/dist/utils.js +38 -86
- package/dist/websocket/NordWebSocketClient.js +12 -17
- package/dist/websocket/Subscriber.js +6 -7
- package/dist/websocket/events.js +1 -2
- package/dist/websocket/index.js +10 -15
- package/package.json +2 -3
- package/dist/api/client.d.ts +0 -14
- package/dist/api/client.js +0 -45
- package/dist/bridge/client.d.ts +0 -151
- package/dist/bridge/client.js +0 -434
- package/dist/bridge/const.d.ts +0 -23
- package/dist/bridge/const.js +0 -47
- package/dist/bridge/index.d.ts +0 -4
- package/dist/bridge/index.js +0 -23
- package/dist/bridge/types.d.ts +0 -120
- package/dist/bridge/types.js +0 -18
- package/dist/bridge/utils.d.ts +0 -64
- package/dist/bridge/utils.js +0 -131
- package/dist/gen/common.d.ts +0 -68
- package/dist/gen/common.js +0 -215
- package/dist/gen/nord.d.ts +0 -882
- package/dist/gen/nord.js +0 -6520
- package/dist/idl/bridge.d.ts +0 -569
- package/dist/idl/bridge.js +0 -8
- package/dist/idl/bridge.json +0 -1506
- package/dist/idl/index.d.ts +0 -607
- package/dist/idl/index.js +0 -8
- package/dist/nord/api/actions.d.ts +0 -126
- package/dist/nord/api/actions.js +0 -397
- package/dist/nord/api/core.d.ts +0 -16
- package/dist/nord/api/core.js +0 -81
- package/dist/nord/api/market.d.ts +0 -36
- package/dist/nord/api/market.js +0 -96
- package/dist/nord/api/metrics.d.ts +0 -67
- package/dist/nord/api/metrics.js +0 -229
- package/dist/nord/api/queries.d.ts +0 -46
- package/dist/nord/api/queries.js +0 -109
- package/dist/nord/api/triggers.d.ts +0 -7
- package/dist/nord/api/triggers.js +0 -38
- package/dist/nord/client/Nord.d.ts +0 -396
- package/dist/nord/client/Nord.js +0 -747
- package/dist/nord/client/NordAdmin.d.ts +0 -259
- package/dist/nord/client/NordAdmin.js +0 -395
- package/dist/nord/client/NordClient.d.ts +0 -33
- package/dist/nord/client/NordClient.js +0 -45
- package/dist/nord/client/NordUser.d.ts +0 -362
- package/dist/nord/client/NordUser.js +0 -781
- package/dist/nord/index.d.ts +0 -11
- package/dist/nord/index.js +0 -36
- package/dist/nord/models/Subscriber.d.ts +0 -37
- package/dist/nord/models/Subscriber.js +0 -25
- package/dist/nord/utils/NordError.d.ts +0 -35
- package/dist/nord/utils/NordError.js +0 -49
- package/src/actions.ts +0 -333
- package/src/client/Nord.ts +0 -934
- package/src/client/NordAdmin.ts +0 -484
- package/src/client/NordUser.ts +0 -1122
- package/src/const.ts +0 -34
- package/src/error.ts +0 -76
- package/src/gen/.gitkeep +0 -0
- package/src/gen/nord_pb.ts +0 -5053
- package/src/gen/openapi.ts +0 -2904
- package/src/index.ts +0 -11
- package/src/types.ts +0 -327
- package/src/utils.ts +0 -266
- package/src/websocket/NordWebSocketClient.ts +0 -316
- package/src/websocket/Subscriber.ts +0 -56
- package/src/websocket/events.ts +0 -31
- package/src/websocket/index.ts +0 -105
package/src/client/NordUser.ts
DELETED
|
@@ -1,1122 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
3
|
-
getAssociatedTokenAddress,
|
|
4
|
-
TOKEN_PROGRAM_ID,
|
|
5
|
-
TOKEN_2022_PROGRAM_ID,
|
|
6
|
-
} from "@solana/spl-token";
|
|
7
|
-
import { PublicKey, Transaction, SendOptions } from "@solana/web3.js";
|
|
8
|
-
import Decimal from "decimal.js";
|
|
9
|
-
import * as ed from "@noble/ed25519";
|
|
10
|
-
import { floatToScaledBigIntLossy } from "@n1xyz/proton";
|
|
11
|
-
import {
|
|
12
|
-
FillMode,
|
|
13
|
-
Side,
|
|
14
|
-
SPLTokenInfo,
|
|
15
|
-
QuoteSize,
|
|
16
|
-
TriggerKind,
|
|
17
|
-
fillModeToProtoFillMode,
|
|
18
|
-
} from "../types";
|
|
19
|
-
import * as proto from "../gen/nord_pb";
|
|
20
|
-
import {
|
|
21
|
-
BigIntValue,
|
|
22
|
-
checkedFetch,
|
|
23
|
-
assert,
|
|
24
|
-
findMarket,
|
|
25
|
-
findToken,
|
|
26
|
-
optExpect,
|
|
27
|
-
keypairFromPrivateKey,
|
|
28
|
-
toScaledU64,
|
|
29
|
-
} from "../utils";
|
|
30
|
-
import { create } from "@bufbuild/protobuf";
|
|
31
|
-
import {
|
|
32
|
-
createSession,
|
|
33
|
-
revokeSession,
|
|
34
|
-
atomic,
|
|
35
|
-
expectReceiptKind,
|
|
36
|
-
createAction,
|
|
37
|
-
sendAction,
|
|
38
|
-
AtomicSubaction,
|
|
39
|
-
} from "../actions";
|
|
40
|
-
import { NordError } from "../error";
|
|
41
|
-
import { Nord } from "./Nord";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parameters for individual atomic subactions (user-friendly version)
|
|
45
|
-
*/
|
|
46
|
-
export interface UserAtomicSubaction {
|
|
47
|
-
/** The type of action to perform. */
|
|
48
|
-
kind: "place" | "cancel";
|
|
49
|
-
|
|
50
|
-
/** The market ID to place the order in. */
|
|
51
|
-
marketId?: number;
|
|
52
|
-
|
|
53
|
-
/** The order ID to cancel. */
|
|
54
|
-
orderId?: BigIntValue;
|
|
55
|
-
|
|
56
|
-
/** Order side (bid or ask) */
|
|
57
|
-
side?: Side;
|
|
58
|
-
|
|
59
|
-
/** Fill mode (limit, market, etc.) */
|
|
60
|
-
fillMode?: FillMode;
|
|
61
|
-
|
|
62
|
-
/** Whether the order is reduce-only. */
|
|
63
|
-
isReduceOnly?: boolean;
|
|
64
|
-
|
|
65
|
-
/** The size of the order. */
|
|
66
|
-
size?: Decimal.Value;
|
|
67
|
-
|
|
68
|
-
/** Order price */
|
|
69
|
-
price?: Decimal.Value;
|
|
70
|
-
|
|
71
|
-
/** Quote size object (for market-style placement) */
|
|
72
|
-
quoteSize?: QuoteSize;
|
|
73
|
-
|
|
74
|
-
/** The client order ID of the order. */
|
|
75
|
-
clientOrderId?: BigIntValue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* User class for interacting with the Nord protocol
|
|
80
|
-
*/
|
|
81
|
-
export class NordUser {
|
|
82
|
-
public readonly nord: Nord;
|
|
83
|
-
public readonly sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
|
|
84
|
-
public readonly transactionSignFn: (tx: Transaction) => Promise<Transaction>;
|
|
85
|
-
public sessionId?: bigint;
|
|
86
|
-
public sessionPubKey: PublicKey;
|
|
87
|
-
public publicKey: PublicKey;
|
|
88
|
-
public lastTs = 0;
|
|
89
|
-
|
|
90
|
-
private nonce = 0;
|
|
91
|
-
|
|
92
|
-
/** User balances by token symbol */
|
|
93
|
-
public balances: {
|
|
94
|
-
[key: string]: { accountId: number; balance: number; symbol: string }[];
|
|
95
|
-
} = {};
|
|
96
|
-
|
|
97
|
-
/** User positions by account ID */
|
|
98
|
-
public positions: {
|
|
99
|
-
[key: string]: {
|
|
100
|
-
marketId: number;
|
|
101
|
-
openOrders: number;
|
|
102
|
-
perp?: {
|
|
103
|
-
baseSize: number;
|
|
104
|
-
price: number;
|
|
105
|
-
updatedFundingRateIndex: number;
|
|
106
|
-
fundingPaymentPnl: number;
|
|
107
|
-
sizePricePnl: number;
|
|
108
|
-
isLong: boolean;
|
|
109
|
-
};
|
|
110
|
-
actionId: number;
|
|
111
|
-
}[];
|
|
112
|
-
} = {};
|
|
113
|
-
|
|
114
|
-
/** User margins by account ID */
|
|
115
|
-
public margins: {
|
|
116
|
-
[key: string]: {
|
|
117
|
-
omf: number;
|
|
118
|
-
mf: number;
|
|
119
|
-
imf: number;
|
|
120
|
-
cmf: number;
|
|
121
|
-
mmf: number;
|
|
122
|
-
pon: number;
|
|
123
|
-
pn: number;
|
|
124
|
-
bankruptcy: boolean;
|
|
125
|
-
};
|
|
126
|
-
} = {};
|
|
127
|
-
|
|
128
|
-
/** User's account IDs */
|
|
129
|
-
public accountIds?: number[];
|
|
130
|
-
|
|
131
|
-
/** SPL token information */
|
|
132
|
-
public splTokenInfos: SPLTokenInfo[] = [];
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create a new NordUser instance
|
|
136
|
-
*
|
|
137
|
-
* @param nord - Nord client instance
|
|
138
|
-
* @param sessionSignFn - Function to sign messages with the user's session key
|
|
139
|
-
* @param transactionSignFn - Function to sign transactions with the user's wallet (optional)
|
|
140
|
-
* @param sessionId - Existing session identifier
|
|
141
|
-
* @param sessionPubKey - Session public key
|
|
142
|
-
* @param publicKey - Wallet public key
|
|
143
|
-
* @throws {NordError} If required parameters are missing
|
|
144
|
-
*/
|
|
145
|
-
constructor({
|
|
146
|
-
nord,
|
|
147
|
-
sessionSignFn,
|
|
148
|
-
transactionSignFn,
|
|
149
|
-
sessionId,
|
|
150
|
-
sessionPubKey,
|
|
151
|
-
publicKey,
|
|
152
|
-
}: Readonly<{
|
|
153
|
-
nord: Nord;
|
|
154
|
-
sessionSignFn: (message: Uint8Array) => Promise<Uint8Array>;
|
|
155
|
-
transactionSignFn: (tx: Transaction) => Promise<Transaction>;
|
|
156
|
-
sessionId?: bigint;
|
|
157
|
-
sessionPubKey: Uint8Array;
|
|
158
|
-
publicKey: PublicKey;
|
|
159
|
-
}>) {
|
|
160
|
-
this.nord = nord;
|
|
161
|
-
this.sessionSignFn = sessionSignFn;
|
|
162
|
-
this.transactionSignFn = transactionSignFn;
|
|
163
|
-
this.sessionId = sessionId;
|
|
164
|
-
this.sessionPubKey = new PublicKey(sessionPubKey);
|
|
165
|
-
this.publicKey = publicKey;
|
|
166
|
-
|
|
167
|
-
// Convert tokens from info endpoint to SPLTokenInfo
|
|
168
|
-
if (this.nord.tokens && this.nord.tokens.length > 0) {
|
|
169
|
-
this.splTokenInfos = this.nord.tokens.map((token) => ({
|
|
170
|
-
mint: token.mintAddr, // Use mintAddr as mint
|
|
171
|
-
precision: token.decimals,
|
|
172
|
-
tokenId: token.tokenId,
|
|
173
|
-
name: token.symbol,
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Create a NordUser from a private key
|
|
180
|
-
*
|
|
181
|
-
* @param nord - Nord instance
|
|
182
|
-
* @param privateKey - Private key as string or Uint8Array
|
|
183
|
-
* @returns NordUser instance
|
|
184
|
-
* @throws {NordError} If the private key is invalid
|
|
185
|
-
*/
|
|
186
|
-
static fromPrivateKey(nord: Nord, privateKey: string | Uint8Array): NordUser {
|
|
187
|
-
try {
|
|
188
|
-
const keypair = keypairFromPrivateKey(privateKey);
|
|
189
|
-
const publicKey = keypair.publicKey;
|
|
190
|
-
|
|
191
|
-
const sessionSignFn = async (
|
|
192
|
-
message: Uint8Array,
|
|
193
|
-
): Promise<Uint8Array> => {
|
|
194
|
-
// Use ed25519 to sign the message
|
|
195
|
-
return ed.sign(message, keypair.secretKey.slice(0, 32));
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const transactionSignFn = async (
|
|
199
|
-
tx: Transaction,
|
|
200
|
-
): Promise<Transaction> => {
|
|
201
|
-
tx.sign(keypair);
|
|
202
|
-
return tx;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
return new NordUser({
|
|
206
|
-
nord,
|
|
207
|
-
sessionSignFn,
|
|
208
|
-
transactionSignFn,
|
|
209
|
-
publicKey,
|
|
210
|
-
sessionPubKey: publicKey.toBytes(),
|
|
211
|
-
});
|
|
212
|
-
} catch (error) {
|
|
213
|
-
throw new NordError("Failed to create NordUser from private key", {
|
|
214
|
-
cause: error,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Get the associated token account for a token mint
|
|
221
|
-
*
|
|
222
|
-
* @param mint - Token mint address
|
|
223
|
-
* @returns Associated token account address
|
|
224
|
-
* @throws {NordError} If required parameters are missing or operation fails
|
|
225
|
-
*/
|
|
226
|
-
async getAssociatedTokenAccount(mint: PublicKey): Promise<PublicKey> {
|
|
227
|
-
try {
|
|
228
|
-
// Get the token program ID from the mint account
|
|
229
|
-
const mintAccount = await this.nord.solanaConnection.getAccountInfo(mint);
|
|
230
|
-
if (!mintAccount) {
|
|
231
|
-
throw new NordError("Mint account not found");
|
|
232
|
-
}
|
|
233
|
-
const tokenProgramId = mintAccount.owner;
|
|
234
|
-
|
|
235
|
-
// Validate that the mint is owned by a supported SPL token program
|
|
236
|
-
if (
|
|
237
|
-
!tokenProgramId.equals(TOKEN_PROGRAM_ID) &&
|
|
238
|
-
!tokenProgramId.equals(TOKEN_2022_PROGRAM_ID)
|
|
239
|
-
) {
|
|
240
|
-
throw new NordError(
|
|
241
|
-
"Mint Account is not owned by a supported SPL token program",
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const associatedTokenAddress = await getAssociatedTokenAddress(
|
|
246
|
-
mint,
|
|
247
|
-
this.publicKey,
|
|
248
|
-
false,
|
|
249
|
-
tokenProgramId,
|
|
250
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
251
|
-
);
|
|
252
|
-
return associatedTokenAddress;
|
|
253
|
-
} catch (error) {
|
|
254
|
-
throw new NordError("Failed to get associated token account", {
|
|
255
|
-
cause: error,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Deposit SPL tokens to the app
|
|
262
|
-
*
|
|
263
|
-
* @param amount - Amount to deposit
|
|
264
|
-
* @param tokenId - Token ID
|
|
265
|
-
* @param recipient - Recipient address; defaults to the user's address
|
|
266
|
-
* @returns Transaction signature
|
|
267
|
-
* @deprecated Use deposit instead
|
|
268
|
-
* @throws {NordError} If required parameters are missing or operation fails
|
|
269
|
-
*/
|
|
270
|
-
async depositSpl(
|
|
271
|
-
amount: number,
|
|
272
|
-
tokenId: number,
|
|
273
|
-
recipient?: PublicKey,
|
|
274
|
-
): Promise<string> {
|
|
275
|
-
return this.deposit({ amount, tokenId, recipient });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Deposit SPL tokens to the app
|
|
280
|
-
*
|
|
281
|
-
* @param amount - Amount to deposit
|
|
282
|
-
* @param tokenId - Token ID
|
|
283
|
-
* @param recipient - Recipient address; defaults to the user's address
|
|
284
|
-
* @param sendOptions - Send options for .sendTransaction
|
|
285
|
-
* @returns Transaction signature
|
|
286
|
-
* @throws {NordError} If required parameters are missing or operation fails
|
|
287
|
-
*/
|
|
288
|
-
async deposit({
|
|
289
|
-
amount,
|
|
290
|
-
tokenId,
|
|
291
|
-
recipient,
|
|
292
|
-
sendOptions,
|
|
293
|
-
}: Readonly<{
|
|
294
|
-
amount: number;
|
|
295
|
-
tokenId: number;
|
|
296
|
-
recipient?: PublicKey;
|
|
297
|
-
sendOptions?: SendOptions;
|
|
298
|
-
}>): Promise<string> {
|
|
299
|
-
try {
|
|
300
|
-
// Find the token info
|
|
301
|
-
const tokenInfo = this.splTokenInfos.find((t) => t.tokenId === tokenId);
|
|
302
|
-
if (!tokenInfo) {
|
|
303
|
-
throw new NordError(`Token with ID ${tokenId} not found`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const mint = new PublicKey(tokenInfo.mint);
|
|
307
|
-
const fromAccount = await this.getAssociatedTokenAccount(mint);
|
|
308
|
-
const payer = this.publicKey;
|
|
309
|
-
|
|
310
|
-
const { ix, extraSigner } = await this.nord.protonClient.buildDepositIx({
|
|
311
|
-
payer,
|
|
312
|
-
recipient: recipient ?? payer,
|
|
313
|
-
quantAmount: floatToScaledBigIntLossy(amount, tokenInfo.precision),
|
|
314
|
-
mint,
|
|
315
|
-
sourceTokenAccount: fromAccount,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const { blockhash } =
|
|
319
|
-
await this.nord.solanaConnection.getLatestBlockhash();
|
|
320
|
-
const tx = new Transaction();
|
|
321
|
-
|
|
322
|
-
tx.add(ix);
|
|
323
|
-
tx.recentBlockhash = blockhash;
|
|
324
|
-
tx.feePayer = payer;
|
|
325
|
-
|
|
326
|
-
const signedTx = await this.transactionSignFn(tx);
|
|
327
|
-
signedTx.partialSign(extraSigner);
|
|
328
|
-
|
|
329
|
-
const signature = await this.nord.solanaConnection.sendRawTransaction(
|
|
330
|
-
signedTx.serialize(),
|
|
331
|
-
sendOptions,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
return signature;
|
|
335
|
-
} catch (error) {
|
|
336
|
-
throw new NordError(
|
|
337
|
-
`Failed to deposit ${amount} of token ID ${tokenId}`,
|
|
338
|
-
{ cause: error },
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Get a new nonce for actions
|
|
345
|
-
*
|
|
346
|
-
* @returns Nonce as number
|
|
347
|
-
*/
|
|
348
|
-
getNonce(): number {
|
|
349
|
-
return ++this.nonce;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private async submitSessionAction(
|
|
353
|
-
kind: proto.Action["kind"],
|
|
354
|
-
): Promise<proto.Receipt> {
|
|
355
|
-
return this.submitSignedAction(kind, async (message) => {
|
|
356
|
-
const signature = await this.sessionSignFn(message);
|
|
357
|
-
const signed = new Uint8Array(message.length + signature.length);
|
|
358
|
-
signed.set(message);
|
|
359
|
-
signed.set(signature, message.length);
|
|
360
|
-
return signed;
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Update account IDs for this user
|
|
366
|
-
*
|
|
367
|
-
* @throws {NordError} If the operation fails
|
|
368
|
-
*/
|
|
369
|
-
async updateAccountId(): Promise<void> {
|
|
370
|
-
try {
|
|
371
|
-
if (!this.publicKey) {
|
|
372
|
-
throw new NordError("Public key is required to update account ID");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const resp = await this.nord.getUser({
|
|
376
|
-
pubkey: this.publicKey.toBase58(),
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
if (!resp) {
|
|
380
|
-
throw new NordError(`User ${this.publicKey.toBase58()} not found`);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
this.accountIds = resp.accountIds;
|
|
384
|
-
} catch (error) {
|
|
385
|
-
throw new NordError("Failed to update account ID", { cause: error });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Fetch user information including balances and orders
|
|
391
|
-
*
|
|
392
|
-
* @throws {NordError} If the operation fails
|
|
393
|
-
*/
|
|
394
|
-
async fetchInfo(): Promise<void> {
|
|
395
|
-
type OpenOrder = {
|
|
396
|
-
orderId: number;
|
|
397
|
-
marketId: number;
|
|
398
|
-
side: "ask" | "bid";
|
|
399
|
-
size: number;
|
|
400
|
-
price: number;
|
|
401
|
-
originalOrderSize: number;
|
|
402
|
-
clientOrderId?: number | null;
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
type Balance = {
|
|
406
|
-
tokenId: number;
|
|
407
|
-
token: string;
|
|
408
|
-
amount: number;
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
type Position = {
|
|
412
|
-
marketId: number;
|
|
413
|
-
openOrders: number;
|
|
414
|
-
perp?: {
|
|
415
|
-
baseSize: number;
|
|
416
|
-
price: number;
|
|
417
|
-
updatedFundingRateIndex: number;
|
|
418
|
-
fundingPaymentPnl: number;
|
|
419
|
-
sizePricePnl: number;
|
|
420
|
-
isLong: boolean;
|
|
421
|
-
};
|
|
422
|
-
actionId: number;
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
type Margins = {
|
|
426
|
-
omf: number;
|
|
427
|
-
mf: number;
|
|
428
|
-
imf: number;
|
|
429
|
-
cmf: number;
|
|
430
|
-
mmf: number;
|
|
431
|
-
pon: number;
|
|
432
|
-
pn: number;
|
|
433
|
-
bankruptcy: boolean;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
type Account = {
|
|
437
|
-
updateId: number;
|
|
438
|
-
orders: OpenOrder[];
|
|
439
|
-
positions: Position[];
|
|
440
|
-
balances: Balance[];
|
|
441
|
-
margins: Margins;
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
if (this.accountIds !== undefined) {
|
|
445
|
-
const accountsData: (Account & { accountId: number })[] =
|
|
446
|
-
await Promise.all(
|
|
447
|
-
this.accountIds.map(async (accountId) => {
|
|
448
|
-
const response = await checkedFetch(
|
|
449
|
-
`${this.nord.webServerUrl}/account/${accountId}`,
|
|
450
|
-
);
|
|
451
|
-
const accountData = (await response.json()) as Account;
|
|
452
|
-
// Ensure we have the correct accountId
|
|
453
|
-
return {
|
|
454
|
-
...accountData,
|
|
455
|
-
accountId,
|
|
456
|
-
};
|
|
457
|
-
}),
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
for (const accountData of accountsData) {
|
|
461
|
-
// Process balances
|
|
462
|
-
this.balances[accountData.accountId] = [];
|
|
463
|
-
for (const balance of accountData.balances) {
|
|
464
|
-
this.balances[accountData.accountId].push({
|
|
465
|
-
accountId: accountData.accountId,
|
|
466
|
-
balance: balance.amount,
|
|
467
|
-
symbol: balance.token,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Process positions
|
|
472
|
-
this.positions[accountData.accountId] = accountData.positions;
|
|
473
|
-
|
|
474
|
-
// Process margins
|
|
475
|
-
this.margins[accountData.accountId] = accountData.margins;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Refresh the user's session
|
|
482
|
-
*
|
|
483
|
-
* @throws {NordError} If the operation fails
|
|
484
|
-
*/
|
|
485
|
-
async refreshSession(): Promise<void> {
|
|
486
|
-
const result = await createSession(
|
|
487
|
-
this.nord.httpClient,
|
|
488
|
-
this.transactionSignFn,
|
|
489
|
-
await this.nord.getTimestamp(),
|
|
490
|
-
this.getNonce(),
|
|
491
|
-
{
|
|
492
|
-
userPubkey: this.publicKey,
|
|
493
|
-
sessionPubkey: this.sessionPubKey,
|
|
494
|
-
},
|
|
495
|
-
);
|
|
496
|
-
this.sessionId = result.sessionId;
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Revoke a session
|
|
500
|
-
*
|
|
501
|
-
* @param sessionId - Session ID to revoke
|
|
502
|
-
* @throws {NordError} If the operation fails
|
|
503
|
-
*/
|
|
504
|
-
async revokeSession(sessionId: BigIntValue): Promise<void> {
|
|
505
|
-
try {
|
|
506
|
-
await revokeSession(
|
|
507
|
-
this.nord.httpClient,
|
|
508
|
-
this.transactionSignFn,
|
|
509
|
-
await this.nord.getTimestamp(),
|
|
510
|
-
this.getNonce(),
|
|
511
|
-
{
|
|
512
|
-
userPubkey: this.publicKey,
|
|
513
|
-
sessionId,
|
|
514
|
-
},
|
|
515
|
-
);
|
|
516
|
-
} catch (error) {
|
|
517
|
-
throw new NordError(`Failed to revoke session ${sessionId}`, {
|
|
518
|
-
cause: error,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Checks if the session is valid
|
|
525
|
-
* @private
|
|
526
|
-
* @throws {NordError} If the session is not valid
|
|
527
|
-
*/
|
|
528
|
-
private checkSessionValidity(): void {
|
|
529
|
-
if (this.sessionId === undefined || this.sessionId === BigInt(0)) {
|
|
530
|
-
throw new NordError(
|
|
531
|
-
"Invalid or empty session ID. Please create or refresh your session.",
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Withdraw tokens from the exchange
|
|
538
|
-
*
|
|
539
|
-
* @param tokenId - Token ID to withdraw
|
|
540
|
-
* @param amount - Amount to withdraw
|
|
541
|
-
* @throws {NordError} If the operation fails
|
|
542
|
-
*/
|
|
543
|
-
async withdraw({
|
|
544
|
-
amount,
|
|
545
|
-
tokenId,
|
|
546
|
-
}: Readonly<{
|
|
547
|
-
tokenId: number;
|
|
548
|
-
amount: number;
|
|
549
|
-
}>): Promise<{ actionId: bigint }> {
|
|
550
|
-
try {
|
|
551
|
-
this.checkSessionValidity();
|
|
552
|
-
const token = findToken(this.nord.tokens, tokenId);
|
|
553
|
-
const scaledAmount = toScaledU64(amount, token.decimals);
|
|
554
|
-
if (scaledAmount <= 0n) {
|
|
555
|
-
throw new NordError("Withdraw amount must be positive");
|
|
556
|
-
}
|
|
557
|
-
const receipt = await this.submitSessionAction({
|
|
558
|
-
case: "withdraw",
|
|
559
|
-
value: create(proto.Action_WithdrawSchema, {
|
|
560
|
-
sessionId: BigInt(optExpect(this.sessionId, "No session")),
|
|
561
|
-
tokenId,
|
|
562
|
-
amount: scaledAmount,
|
|
563
|
-
}),
|
|
564
|
-
});
|
|
565
|
-
expectReceiptKind(receipt, "withdrawResult", "withdraw");
|
|
566
|
-
return { actionId: receipt.actionId };
|
|
567
|
-
} catch (error) {
|
|
568
|
-
throw new NordError(
|
|
569
|
-
`Failed to withdraw ${amount} of token ID ${tokenId}`,
|
|
570
|
-
{ cause: error },
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Place an order on the exchange
|
|
577
|
-
*
|
|
578
|
-
* @param marketId - Target market identifier
|
|
579
|
-
* @param side - Order side
|
|
580
|
-
* @param fillMode - Fill mode (limit, market, etc.)
|
|
581
|
-
* @param isReduceOnly - Reduce-only flag
|
|
582
|
-
* @param size - Base size to place
|
|
583
|
-
* @param price - Limit price
|
|
584
|
-
* @param quoteSize - Quote-sized order representation
|
|
585
|
-
* @param accountId - Account executing the order
|
|
586
|
-
* @param clientOrderId - Optional client-specified identifier
|
|
587
|
-
* @returns Object containing actionId, orderId (if posted), fills, and clientOrderId
|
|
588
|
-
* @throws {NordError} If the operation fails
|
|
589
|
-
*/
|
|
590
|
-
async placeOrder({
|
|
591
|
-
marketId,
|
|
592
|
-
side,
|
|
593
|
-
fillMode,
|
|
594
|
-
isReduceOnly,
|
|
595
|
-
size,
|
|
596
|
-
price,
|
|
597
|
-
quoteSize,
|
|
598
|
-
accountId,
|
|
599
|
-
clientOrderId,
|
|
600
|
-
}: Readonly<{
|
|
601
|
-
marketId: number;
|
|
602
|
-
side: Side;
|
|
603
|
-
fillMode: FillMode;
|
|
604
|
-
isReduceOnly: boolean;
|
|
605
|
-
size?: Decimal.Value;
|
|
606
|
-
price?: Decimal.Value;
|
|
607
|
-
quoteSize?: QuoteSize;
|
|
608
|
-
accountId?: number;
|
|
609
|
-
clientOrderId?: BigIntValue;
|
|
610
|
-
}>): Promise<{
|
|
611
|
-
actionId: bigint;
|
|
612
|
-
orderId?: bigint;
|
|
613
|
-
fills: proto.Receipt_Trade[];
|
|
614
|
-
}> {
|
|
615
|
-
try {
|
|
616
|
-
this.checkSessionValidity();
|
|
617
|
-
const market = findMarket(this.nord.markets, marketId);
|
|
618
|
-
if (!market) {
|
|
619
|
-
throw new NordError(`Market with ID ${marketId} not found`);
|
|
620
|
-
}
|
|
621
|
-
const sessionId = optExpect(this.sessionId, "No session");
|
|
622
|
-
const scaledPrice = toScaledU64(price ?? 0, market.priceDecimals);
|
|
623
|
-
const scaledSize = toScaledU64(size ?? 0, market.sizeDecimals);
|
|
624
|
-
const scaledQuote = quoteSize
|
|
625
|
-
? quoteSize.toWire(market.priceDecimals, market.sizeDecimals)
|
|
626
|
-
: undefined;
|
|
627
|
-
assert(
|
|
628
|
-
scaledPrice > 0n || scaledSize > 0n || scaledQuote !== undefined,
|
|
629
|
-
"OrderLimit must include at least one of: size, price, or quoteSize",
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
const receipt = await this.submitSessionAction({
|
|
633
|
-
case: "placeOrder",
|
|
634
|
-
value: create(proto.Action_PlaceOrderSchema, {
|
|
635
|
-
sessionId: BigInt(sessionId),
|
|
636
|
-
senderAccountId: accountId,
|
|
637
|
-
marketId,
|
|
638
|
-
side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
|
|
639
|
-
fillMode: fillModeToProtoFillMode(fillMode),
|
|
640
|
-
isReduceOnly,
|
|
641
|
-
price: scaledPrice,
|
|
642
|
-
size: scaledSize,
|
|
643
|
-
quoteSize:
|
|
644
|
-
scaledQuote === undefined
|
|
645
|
-
? undefined
|
|
646
|
-
: create(proto.QuoteSizeSchema, {
|
|
647
|
-
size: scaledQuote.size,
|
|
648
|
-
price: scaledQuote.price,
|
|
649
|
-
}),
|
|
650
|
-
clientOrderId:
|
|
651
|
-
clientOrderId === undefined ? undefined : BigInt(clientOrderId),
|
|
652
|
-
}),
|
|
653
|
-
});
|
|
654
|
-
expectReceiptKind(receipt, "placeOrderResult", "place order");
|
|
655
|
-
const result = receipt.kind.value;
|
|
656
|
-
return {
|
|
657
|
-
actionId: receipt.actionId,
|
|
658
|
-
orderId: result.posted?.orderId,
|
|
659
|
-
fills: result.fills,
|
|
660
|
-
};
|
|
661
|
-
} catch (error) {
|
|
662
|
-
throw new NordError("Failed to place order", { cause: error });
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Cancel an order
|
|
668
|
-
*
|
|
669
|
-
* @param orderId - Order ID to cancel
|
|
670
|
-
* @param providedAccountId - Account ID that placed the order
|
|
671
|
-
* @returns Object containing actionId, cancelled orderId, and accountId
|
|
672
|
-
* @throws {NordError} If the operation fails
|
|
673
|
-
*/
|
|
674
|
-
async cancelOrder(
|
|
675
|
-
orderId: BigIntValue,
|
|
676
|
-
providedAccountId?: number,
|
|
677
|
-
): Promise<{
|
|
678
|
-
actionId: bigint;
|
|
679
|
-
orderId: bigint;
|
|
680
|
-
accountId: number;
|
|
681
|
-
}> {
|
|
682
|
-
const accountId =
|
|
683
|
-
providedAccountId != null ? providedAccountId : this.accountIds?.[0];
|
|
684
|
-
try {
|
|
685
|
-
this.checkSessionValidity();
|
|
686
|
-
const receipt = await this.submitSessionAction({
|
|
687
|
-
case: "cancelOrderById",
|
|
688
|
-
value: create(proto.Action_CancelOrderByIdSchema, {
|
|
689
|
-
orderId: BigInt(orderId),
|
|
690
|
-
sessionId: BigInt(optExpect(this.sessionId, "No session")),
|
|
691
|
-
senderAccountId: accountId,
|
|
692
|
-
}),
|
|
693
|
-
});
|
|
694
|
-
expectReceiptKind(receipt, "cancelOrderResult", "cancel order");
|
|
695
|
-
return {
|
|
696
|
-
actionId: receipt.actionId,
|
|
697
|
-
orderId: receipt.kind.value.orderId,
|
|
698
|
-
accountId: receipt.kind.value.accountId,
|
|
699
|
-
};
|
|
700
|
-
} catch (error) {
|
|
701
|
-
throw new NordError(`Failed to cancel order ${orderId}`, {
|
|
702
|
-
cause: error,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Add a trigger for the current session
|
|
709
|
-
*
|
|
710
|
-
* @param marketId - Market to watch
|
|
711
|
-
* @param side - Order side for the trigger
|
|
712
|
-
* @param kind - Stop-loss or take-profit trigger type
|
|
713
|
-
* @param triggerPrice - Price that activates the trigger
|
|
714
|
-
* @param limitPrice - Limit price placed once the trigger fires
|
|
715
|
-
* @param accountId - Account executing the trigger
|
|
716
|
-
* @returns Object containing the actionId of the submitted trigger
|
|
717
|
-
* @throws {NordError} If the operation fails
|
|
718
|
-
*/
|
|
719
|
-
async addTrigger({
|
|
720
|
-
marketId,
|
|
721
|
-
side,
|
|
722
|
-
kind,
|
|
723
|
-
triggerPrice,
|
|
724
|
-
limitPrice,
|
|
725
|
-
accountId,
|
|
726
|
-
}: Readonly<{
|
|
727
|
-
marketId: number;
|
|
728
|
-
side: Side;
|
|
729
|
-
kind: TriggerKind;
|
|
730
|
-
triggerPrice: Decimal.Value;
|
|
731
|
-
limitPrice?: Decimal.Value;
|
|
732
|
-
accountId?: number;
|
|
733
|
-
}>): Promise<{ actionId: bigint }> {
|
|
734
|
-
try {
|
|
735
|
-
this.checkSessionValidity();
|
|
736
|
-
const market = findMarket(this.nord.markets, marketId);
|
|
737
|
-
if (!market) {
|
|
738
|
-
throw new NordError(`Market with ID ${marketId} not found`);
|
|
739
|
-
}
|
|
740
|
-
const scaledTriggerPrice = toScaledU64(
|
|
741
|
-
triggerPrice,
|
|
742
|
-
market.priceDecimals,
|
|
743
|
-
);
|
|
744
|
-
assert(scaledTriggerPrice > 0n, "Trigger price must be positive");
|
|
745
|
-
const scaledLimitPrice =
|
|
746
|
-
limitPrice === undefined
|
|
747
|
-
? undefined
|
|
748
|
-
: toScaledU64(limitPrice, market.priceDecimals);
|
|
749
|
-
if (scaledLimitPrice !== undefined) {
|
|
750
|
-
assert(scaledLimitPrice > 0n, "Limit price must be positive");
|
|
751
|
-
}
|
|
752
|
-
const key = create(proto.TriggerKeySchema, {
|
|
753
|
-
kind:
|
|
754
|
-
kind === TriggerKind.StopLoss
|
|
755
|
-
? proto.TriggerKind.STOP_LOSS
|
|
756
|
-
: proto.TriggerKind.TAKE_PROFIT,
|
|
757
|
-
side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
|
|
758
|
-
});
|
|
759
|
-
const prices = create(proto.Action_TriggerPricesSchema, {
|
|
760
|
-
triggerPrice: scaledTriggerPrice,
|
|
761
|
-
limitPrice: scaledLimitPrice,
|
|
762
|
-
});
|
|
763
|
-
const receipt = await this.submitSessionAction({
|
|
764
|
-
case: "addTrigger",
|
|
765
|
-
value: create(proto.Action_AddTriggerSchema, {
|
|
766
|
-
sessionId: BigInt(optExpect(this.sessionId, "No session")),
|
|
767
|
-
marketId,
|
|
768
|
-
key,
|
|
769
|
-
prices,
|
|
770
|
-
accountId,
|
|
771
|
-
}),
|
|
772
|
-
});
|
|
773
|
-
expectReceiptKind(receipt, "triggerAdded", "add trigger");
|
|
774
|
-
return { actionId: receipt.actionId };
|
|
775
|
-
} catch (error) {
|
|
776
|
-
throw new NordError("Failed to add trigger", { cause: error });
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Remove a trigger for the current session
|
|
782
|
-
*
|
|
783
|
-
* @param marketId - Market the trigger belongs to
|
|
784
|
-
* @param side - Order side for the trigger
|
|
785
|
-
* @param kind - Stop-loss or take-profit trigger type
|
|
786
|
-
* @param accountId - Account executing the trigger
|
|
787
|
-
* @returns Object containing the actionId of the removal action
|
|
788
|
-
* @throws {NordError} If the operation fails
|
|
789
|
-
*/
|
|
790
|
-
async removeTrigger({
|
|
791
|
-
marketId,
|
|
792
|
-
side,
|
|
793
|
-
kind,
|
|
794
|
-
accountId,
|
|
795
|
-
}: Readonly<{
|
|
796
|
-
marketId: number;
|
|
797
|
-
side: Side;
|
|
798
|
-
kind: TriggerKind;
|
|
799
|
-
accountId?: number;
|
|
800
|
-
}>): Promise<{ actionId: bigint }> {
|
|
801
|
-
try {
|
|
802
|
-
this.checkSessionValidity();
|
|
803
|
-
const market = findMarket(this.nord.markets, marketId);
|
|
804
|
-
if (!market) {
|
|
805
|
-
throw new NordError(`Market with ID ${marketId} not found`);
|
|
806
|
-
}
|
|
807
|
-
const key = create(proto.TriggerKeySchema, {
|
|
808
|
-
kind:
|
|
809
|
-
kind === TriggerKind.StopLoss
|
|
810
|
-
? proto.TriggerKind.STOP_LOSS
|
|
811
|
-
: proto.TriggerKind.TAKE_PROFIT,
|
|
812
|
-
side: side === Side.Bid ? proto.Side.BID : proto.Side.ASK,
|
|
813
|
-
});
|
|
814
|
-
const receipt = await this.submitSessionAction({
|
|
815
|
-
case: "removeTrigger",
|
|
816
|
-
value: create(proto.Action_RemoveTriggerSchema, {
|
|
817
|
-
sessionId: BigInt(optExpect(this.sessionId, "No session")),
|
|
818
|
-
marketId,
|
|
819
|
-
key,
|
|
820
|
-
accountId,
|
|
821
|
-
}),
|
|
822
|
-
});
|
|
823
|
-
expectReceiptKind(receipt, "triggerRemoved", "remove trigger");
|
|
824
|
-
return { actionId: receipt.actionId };
|
|
825
|
-
} catch (error) {
|
|
826
|
-
throw new NordError("Failed to remove trigger", { cause: error });
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Transfer tokens to another account
|
|
832
|
-
*
|
|
833
|
-
* @param tokenId - Token identifier to move
|
|
834
|
-
* @param amount - Amount to transfer
|
|
835
|
-
* @param fromAccountId - Source account id
|
|
836
|
-
* @param toAccountId - Destination account id
|
|
837
|
-
* @throws {NordError} If the operation fails
|
|
838
|
-
*/
|
|
839
|
-
async transferToAccount({
|
|
840
|
-
tokenId,
|
|
841
|
-
amount,
|
|
842
|
-
fromAccountId,
|
|
843
|
-
toAccountId,
|
|
844
|
-
}: Readonly<{
|
|
845
|
-
tokenId: number;
|
|
846
|
-
amount: Decimal.Value;
|
|
847
|
-
fromAccountId?: number;
|
|
848
|
-
toAccountId?: number;
|
|
849
|
-
}>): Promise<{
|
|
850
|
-
actionId: bigint;
|
|
851
|
-
newAccountId?: number;
|
|
852
|
-
}> {
|
|
853
|
-
try {
|
|
854
|
-
this.checkSessionValidity();
|
|
855
|
-
const token = findToken(this.nord.tokens, tokenId);
|
|
856
|
-
|
|
857
|
-
const scaledAmount = toScaledU64(amount, token.decimals);
|
|
858
|
-
if (scaledAmount <= 0n) {
|
|
859
|
-
throw new NordError("Transfer amount must be positive");
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const receipt = await this.submitSessionAction({
|
|
863
|
-
case: "transfer",
|
|
864
|
-
value: create(proto.Action_TransferSchema, {
|
|
865
|
-
sessionId: BigInt(optExpect(this.sessionId, "No session")),
|
|
866
|
-
fromAccountId: optExpect(fromAccountId, "No source account"),
|
|
867
|
-
toAccountId: optExpect(toAccountId, "No target account"),
|
|
868
|
-
tokenId,
|
|
869
|
-
amount: scaledAmount,
|
|
870
|
-
}),
|
|
871
|
-
});
|
|
872
|
-
expectReceiptKind(receipt, "transferred", "transfer tokens");
|
|
873
|
-
if (receipt.kind.value.accountCreated) {
|
|
874
|
-
assert(
|
|
875
|
-
receipt.kind.value.toUserAccount !== undefined,
|
|
876
|
-
`toAccount must be defined on new account on ${receipt.kind.value}`,
|
|
877
|
-
);
|
|
878
|
-
return {
|
|
879
|
-
actionId: receipt.actionId,
|
|
880
|
-
newAccountId: receipt.kind.value.toUserAccount,
|
|
881
|
-
};
|
|
882
|
-
} else {
|
|
883
|
-
return { actionId: receipt.actionId };
|
|
884
|
-
}
|
|
885
|
-
} catch (error) {
|
|
886
|
-
throw new NordError("Failed to transfer tokens", { cause: error });
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Execute up to four place/cancel operations atomically.
|
|
892
|
-
* Per Market:
|
|
893
|
-
* 1. cancels can only be in the start (one cannot predict future order ids)
|
|
894
|
-
* 2. intermediate trades can trade only
|
|
895
|
-
* 3. placements go last
|
|
896
|
-
*
|
|
897
|
-
* Across Markets, order action can be any
|
|
898
|
-
*
|
|
899
|
-
* @param userActions array of user-friendly subactions
|
|
900
|
-
* @param providedAccountId optional account performing the action (defaults to first account)
|
|
901
|
-
*/
|
|
902
|
-
async atomic(
|
|
903
|
-
userActions: UserAtomicSubaction[],
|
|
904
|
-
providedAccountId?: number,
|
|
905
|
-
): Promise<{
|
|
906
|
-
actionId: bigint;
|
|
907
|
-
results: proto.Receipt_AtomicSubactionResultKind[];
|
|
908
|
-
}> {
|
|
909
|
-
try {
|
|
910
|
-
this.checkSessionValidity();
|
|
911
|
-
|
|
912
|
-
const accountId =
|
|
913
|
-
providedAccountId != null ? providedAccountId : this.accountIds?.[0];
|
|
914
|
-
|
|
915
|
-
if (accountId == null) {
|
|
916
|
-
throw new NordError(
|
|
917
|
-
"Account ID is undefined. Make sure to call updateAccountId() before atomic operations.",
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const apiActions: AtomicSubaction[] = userActions.map((act) => {
|
|
922
|
-
if (act.kind === "place") {
|
|
923
|
-
const market = findMarket(this.nord.markets, act.marketId!);
|
|
924
|
-
if (!market) {
|
|
925
|
-
throw new NordError(`Market ${act.marketId} not found`);
|
|
926
|
-
}
|
|
927
|
-
return {
|
|
928
|
-
kind: "place",
|
|
929
|
-
marketId: act.marketId,
|
|
930
|
-
side: act.side,
|
|
931
|
-
fillMode: act.fillMode,
|
|
932
|
-
isReduceOnly: act.isReduceOnly,
|
|
933
|
-
sizeDecimals: market.sizeDecimals,
|
|
934
|
-
priceDecimals: market.priceDecimals,
|
|
935
|
-
size: act.size,
|
|
936
|
-
price: act.price,
|
|
937
|
-
quoteSize: act.quoteSize,
|
|
938
|
-
clientOrderId: act.clientOrderId,
|
|
939
|
-
} as AtomicSubaction;
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
kind: "cancel",
|
|
943
|
-
orderId: act.orderId,
|
|
944
|
-
} as AtomicSubaction;
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
const result = await atomic(
|
|
948
|
-
this.nord.httpClient,
|
|
949
|
-
this.sessionSignFn,
|
|
950
|
-
await this.nord.getTimestamp(),
|
|
951
|
-
this.getNonce(),
|
|
952
|
-
{
|
|
953
|
-
sessionId: optExpect(this.sessionId, "No session"),
|
|
954
|
-
accountId: accountId,
|
|
955
|
-
actions: apiActions,
|
|
956
|
-
},
|
|
957
|
-
);
|
|
958
|
-
return result;
|
|
959
|
-
} catch (error) {
|
|
960
|
-
throw new NordError("Atomic operation failed", { cause: error });
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Helper function to retry a promise with exponential backoff
|
|
966
|
-
*
|
|
967
|
-
* @param fn - Function to retry
|
|
968
|
-
* @param maxRetries - Maximum number of retries
|
|
969
|
-
* @param initialDelay - Initial delay in milliseconds
|
|
970
|
-
* @returns Promise result
|
|
971
|
-
*/
|
|
972
|
-
private async retryWithBackoff<T>(
|
|
973
|
-
fn: () => Promise<T>,
|
|
974
|
-
maxRetries: number = 3,
|
|
975
|
-
initialDelay: number = 500,
|
|
976
|
-
): Promise<T> {
|
|
977
|
-
let retries = 0;
|
|
978
|
-
let delay = initialDelay;
|
|
979
|
-
|
|
980
|
-
while (true) {
|
|
981
|
-
try {
|
|
982
|
-
return await fn();
|
|
983
|
-
} catch (error) {
|
|
984
|
-
if (retries >= maxRetries) {
|
|
985
|
-
throw error;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Check if error is rate limiting related
|
|
989
|
-
const isRateLimitError =
|
|
990
|
-
error instanceof Error &&
|
|
991
|
-
(error.message.includes("rate limit") ||
|
|
992
|
-
error.message.includes("429") ||
|
|
993
|
-
error.message.includes("too many requests"));
|
|
994
|
-
|
|
995
|
-
if (!isRateLimitError) {
|
|
996
|
-
throw error;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
retries++;
|
|
1000
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1001
|
-
delay *= 2; // Exponential backoff
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/**
|
|
1007
|
-
* Get user's token balances on Solana chain using mintAddr
|
|
1008
|
-
*
|
|
1009
|
-
* @param options - Optional parameters
|
|
1010
|
-
* @param options.includeZeroBalances - Whether to include tokens with zero balance (default: true)
|
|
1011
|
-
* @param options.includeTokenAccounts - Whether to include token account addresses in the result (default: false)
|
|
1012
|
-
* @param options.maxConcurrent - Maximum number of concurrent requests (default: 5)
|
|
1013
|
-
* @param options.maxRetries - Maximum number of retries for rate-limited requests (default: 3)
|
|
1014
|
-
* @returns Object with token balances and optional token account addresses
|
|
1015
|
-
* @throws {NordError} If required parameters are missing or operation fails
|
|
1016
|
-
*/
|
|
1017
|
-
async getSolanaBalances(
|
|
1018
|
-
options: {
|
|
1019
|
-
includeZeroBalances?: boolean;
|
|
1020
|
-
includeTokenAccounts?: boolean;
|
|
1021
|
-
maxConcurrent?: number;
|
|
1022
|
-
maxRetries?: number;
|
|
1023
|
-
} = {},
|
|
1024
|
-
): Promise<{
|
|
1025
|
-
balances: { [symbol: string]: number };
|
|
1026
|
-
tokenAccounts?: { [symbol: string]: string };
|
|
1027
|
-
}> {
|
|
1028
|
-
const {
|
|
1029
|
-
includeZeroBalances = true,
|
|
1030
|
-
includeTokenAccounts = false,
|
|
1031
|
-
maxConcurrent = 5,
|
|
1032
|
-
maxRetries = 3,
|
|
1033
|
-
} = options;
|
|
1034
|
-
|
|
1035
|
-
const balances: { [symbol: string]: number } = {};
|
|
1036
|
-
const tokenAccounts: { [symbol: string]: string } = {};
|
|
1037
|
-
|
|
1038
|
-
try {
|
|
1039
|
-
// Get SOL balance (native token)
|
|
1040
|
-
const solBalance = await this.retryWithBackoff(
|
|
1041
|
-
() => this.nord.solanaConnection.getBalance(this.publicKey),
|
|
1042
|
-
maxRetries,
|
|
1043
|
-
);
|
|
1044
|
-
balances["SOL"] = solBalance / 1e9; // Convert lamports to SOL
|
|
1045
|
-
if (includeTokenAccounts) {
|
|
1046
|
-
tokenAccounts["SOL"] = this.publicKey.toString();
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// Get SPL token balances using mintAddr from Nord tokens
|
|
1050
|
-
if (this.nord.tokens && this.nord.tokens.length > 0) {
|
|
1051
|
-
const tokens = this.nord.tokens.filter((token) => !!token.mintAddr);
|
|
1052
|
-
|
|
1053
|
-
// Process tokens in batches to avoid rate limiting
|
|
1054
|
-
for (let i = 0; i < tokens.length; i += maxConcurrent) {
|
|
1055
|
-
const batch = tokens.slice(i, i + maxConcurrent);
|
|
1056
|
-
|
|
1057
|
-
// Process batch in parallel
|
|
1058
|
-
const batchPromises = batch.map(async (token) => {
|
|
1059
|
-
try {
|
|
1060
|
-
const mint = new PublicKey(token.mintAddr);
|
|
1061
|
-
const associatedTokenAddress = await this.retryWithBackoff(
|
|
1062
|
-
() => getAssociatedTokenAddress(mint, this.publicKey),
|
|
1063
|
-
maxRetries,
|
|
1064
|
-
);
|
|
1065
|
-
|
|
1066
|
-
if (includeTokenAccounts) {
|
|
1067
|
-
tokenAccounts[token.symbol] = associatedTokenAddress.toString();
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
try {
|
|
1071
|
-
const tokenBalance = await this.retryWithBackoff(
|
|
1072
|
-
() =>
|
|
1073
|
-
this.nord.solanaConnection.getTokenAccountBalance(
|
|
1074
|
-
associatedTokenAddress,
|
|
1075
|
-
),
|
|
1076
|
-
maxRetries,
|
|
1077
|
-
);
|
|
1078
|
-
const balance = Number(tokenBalance.value.uiAmount);
|
|
1079
|
-
|
|
1080
|
-
if (balance > 0 || includeZeroBalances) {
|
|
1081
|
-
balances[token.symbol] = balance;
|
|
1082
|
-
}
|
|
1083
|
-
} catch {
|
|
1084
|
-
// Token account might not exist yet, set balance to 0
|
|
1085
|
-
if (includeZeroBalances) {
|
|
1086
|
-
balances[token.symbol] = 0;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
} catch (error) {
|
|
1090
|
-
console.error(
|
|
1091
|
-
`Error getting balance for token ${token.symbol}:`,
|
|
1092
|
-
error,
|
|
1093
|
-
);
|
|
1094
|
-
if (includeZeroBalances) {
|
|
1095
|
-
balances[token.symbol] = 0;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
// Wait for current batch to complete before processing next batch
|
|
1101
|
-
await Promise.all(batchPromises);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
return includeTokenAccounts ? { balances, tokenAccounts } : { balances };
|
|
1106
|
-
} catch (error) {
|
|
1107
|
-
throw new NordError("Failed to get Solana token balances", {
|
|
1108
|
-
cause: error,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
protected async submitSignedAction(
|
|
1114
|
-
kind: proto.Action["kind"],
|
|
1115
|
-
makeSignedMessage: (message: Uint8Array) => Promise<Uint8Array>,
|
|
1116
|
-
): Promise<proto.Receipt> {
|
|
1117
|
-
const nonce = this.getNonce();
|
|
1118
|
-
const currentTimestamp = await this.nord.getTimestamp();
|
|
1119
|
-
const action = createAction(currentTimestamp, nonce, kind);
|
|
1120
|
-
return sendAction(this.nord.httpClient, makeSignedMessage, action);
|
|
1121
|
-
}
|
|
1122
|
-
}
|