@shuffle-protocol/sdk 0.1.0

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/client.js ADDED
@@ -0,0 +1,691 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ShuffleClient = void 0;
40
+ const anchor = __importStar(require("@coral-xyz/anchor"));
41
+ const anchor_1 = require("@coral-xyz/anchor");
42
+ const web3_js_1 = require("@solana/web3.js");
43
+ const spl_token_1 = require("@solana/spl-token");
44
+ const client_1 = require("@arcium-hq/client");
45
+ const crypto_1 = require("crypto");
46
+ const constants_1 = require("./constants");
47
+ const pda_1 = require("./pda");
48
+ const encryption_1 = require("./encryption");
49
+ const shuffle_protocol_json_1 = __importDefault(require("./idl/shuffle_protocol.json"));
50
+ class ShuffleClient {
51
+ constructor(config) {
52
+ // Encryption state (set via initEncryption)
53
+ this.cipher = null;
54
+ this.encryptionPublicKey = null;
55
+ this.connection = config.connection;
56
+ this.wallet = config.wallet;
57
+ this.programId = config.programId || constants_1.PROGRAM_ID;
58
+ this.clusterOffset = config.clusterOffset ?? 0; // Default to 0 for localnet
59
+ this.provider = new anchor.AnchorProvider(this.connection, this.wallet, {
60
+ commitment: "confirmed",
61
+ skipPreflight: true,
62
+ });
63
+ // Create a modified IDL with the correct program address
64
+ const idlWithAddress = { ...shuffle_protocol_json_1.default, address: this.programId.toBase58() };
65
+ this.program = new anchor_1.Program(idlWithAddress, this.provider);
66
+ }
67
+ /** Async factory — creates and initializes the client */
68
+ static async create(config) {
69
+ const client = new ShuffleClient(config);
70
+ await client.initialize();
71
+ return client;
72
+ }
73
+ async initialize() {
74
+ this.clusterAccount = (0, client_1.getClusterAccAddress)(this.clusterOffset);
75
+ [this.poolPDA] = (0, pda_1.getPoolPDA)(this.programId);
76
+ [this.batchAccumulatorPDA] = (0, pda_1.getBatchAccumulatorPDA)(this.programId);
77
+ this.mxePublicKey = await (0, encryption_1.fetchMXEPublicKey)(this.provider, this.programId);
78
+ }
79
+ /** Get the MXE public key (needed for cipher creation) */
80
+ getMXEPublicKey() {
81
+ return this.mxePublicKey;
82
+ }
83
+ /** Get the underlying Anchor program */
84
+ getProgram() {
85
+ return this.program;
86
+ }
87
+ /**
88
+ * Initialize encryption from a user's x25519 private key.
89
+ * Creates the cipher and derives the public key.
90
+ * Call this after create() to enable encrypted operations.
91
+ */
92
+ initEncryption(privateKey) {
93
+ const keypair = require("@arcium-hq/client").x25519;
94
+ this.encryptionPublicKey = keypair.getPublicKey(privateKey);
95
+ this.cipher = (0, encryption_1.createCipher)(privateKey, this.mxePublicKey);
96
+ }
97
+ /** Get the encryption public key (available after initEncryption) */
98
+ getEncryptionPublicKey() {
99
+ return this.encryptionPublicKey;
100
+ }
101
+ /** Get the cipher (available after initEncryption) */
102
+ getCipher() {
103
+ return this.cipher;
104
+ }
105
+ _requireEncryption() {
106
+ if (!this.cipher || !this.encryptionPublicKey) {
107
+ throw new Error("Encryption not initialized. Call initEncryption(privateKey) first.");
108
+ }
109
+ return { cipher: this.cipher, pubkey: this.encryptionPublicKey };
110
+ }
111
+ // =========================================================================
112
+ // ACCOUNT METHODS
113
+ // =========================================================================
114
+ /** Create a new user privacy account. Uses internal encryption if initialized. */
115
+ async createUserAccount(encryptionPublicKey) {
116
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
117
+ const enc = this.cipher || (0, encryption_1.createCipher)((0, encryption_1.generateEncryptionKeypair)().privateKey, this.mxePublicKey);
118
+ const owner = this.wallet.publicKey;
119
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, owner);
120
+ const initialNonce = (0, crypto_1.randomBytes)(16);
121
+ const encryptedZero = enc.encrypt([BigInt(0)], initialNonce);
122
+ const initialBalances = [
123
+ Array.from(encryptedZero[0]),
124
+ Array.from(encryptedZero[0]),
125
+ Array.from(encryptedZero[0]),
126
+ Array.from(encryptedZero[0]),
127
+ ];
128
+ const sig = await this.program.methods
129
+ .createUserAccount(Array.from(pubkey), initialBalances, (0, encryption_1.nonceToBN)(initialNonce))
130
+ .accounts({
131
+ payer: owner,
132
+ owner: owner,
133
+ userAccount: userAccountPDA,
134
+ systemProgram: web3_js_1.SystemProgram.programId,
135
+ })
136
+ .rpc({ commitment: "confirmed" });
137
+ return sig;
138
+ }
139
+ /** Fetch UserProfile data for an owner */
140
+ async fetchUserAccount(owner) {
141
+ const target = owner || this.wallet.publicKey;
142
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, target);
143
+ return this.program.account.userProfile.fetch(userAccountPDA);
144
+ }
145
+ /** Check if account exists */
146
+ async accountExists(owner) {
147
+ try {
148
+ await this.fetchUserAccount(owner);
149
+ return true;
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
155
+ // =========================================================================
156
+ // BALANCE METHODS
157
+ // =========================================================================
158
+ /** Deposit tokens into the protocol (add_balance). Uses internal encryption if params omitted. */
159
+ async deposit(assetId, amount, cipher, encryptionPublicKey) {
160
+ const enc = cipher || this._requireEncryption().cipher;
161
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
162
+ const owner = this.wallet.publicKey;
163
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, owner);
164
+ const assetSeed = constants_1.VAULT_ASSET_SEEDS[assetId];
165
+ const [vaultPDA] = (0, pda_1.getVaultPDA)(this.programId, assetSeed);
166
+ // Get the pool to find the correct mint
167
+ const pool = await this.program.account.pool.fetch(this.poolPDA);
168
+ const mints = [pool.usdcMint, pool.tslaMint, pool.spyMint, pool.aaplMint];
169
+ const mint = mints[assetId];
170
+ // Find user's token account for this mint
171
+ const { getAssociatedTokenAddress } = await Promise.resolve().then(() => __importStar(require("@solana/spl-token")));
172
+ const userTokenAccount = await getAssociatedTokenAddress(mint, owner);
173
+ const nonce = (0, crypto_1.randomBytes)(16);
174
+ const encrypted = (0, encryption_1.encryptValue)(enc, BigInt(amount), nonce);
175
+ const computationOffset = this._generateComputationOffset();
176
+ const sig = await this.program.methods
177
+ .addBalance(computationOffset, Array.from(encrypted.ciphertext), Array.from(pubkey), (0, encryption_1.nonceToBN)(nonce), new anchor.BN(amount), assetId)
178
+ .accountsPartial({
179
+ payer: owner,
180
+ user: owner,
181
+ pool: this.poolPDA,
182
+ userAccount: userAccountPDA,
183
+ userTokenAccount,
184
+ vault: vaultPDA,
185
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
186
+ ...this._getArciumAccounts("add_balance", computationOffset),
187
+ })
188
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
189
+ await this._awaitComputation(computationOffset);
190
+ return sig;
191
+ }
192
+ /** Withdraw tokens from the protocol (sub_balance). Uses internal encryption if params omitted. */
193
+ async withdraw(assetId, amount, cipher, encryptionPublicKey) {
194
+ const enc = cipher || this._requireEncryption().cipher;
195
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
196
+ const owner = this.wallet.publicKey;
197
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, owner);
198
+ const assetSeed = constants_1.VAULT_ASSET_SEEDS[assetId];
199
+ const [vaultPDA] = (0, pda_1.getVaultPDA)(this.programId, assetSeed);
200
+ const pool = await this.program.account.pool.fetch(this.poolPDA);
201
+ const mints = [pool.usdcMint, pool.tslaMint, pool.spyMint, pool.aaplMint];
202
+ const mint = mints[assetId];
203
+ const { getAssociatedTokenAddress } = await Promise.resolve().then(() => __importStar(require("@solana/spl-token")));
204
+ const recipientTokenAccount = await getAssociatedTokenAddress(mint, owner);
205
+ const nonce = (0, crypto_1.randomBytes)(16);
206
+ const encrypted = (0, encryption_1.encryptValue)(enc, BigInt(amount), nonce);
207
+ const computationOffset = this._generateComputationOffset();
208
+ const sig = await this.program.methods
209
+ .subBalance(computationOffset, Array.from(encrypted.ciphertext), Array.from(pubkey), (0, encryption_1.nonceToBN)(nonce), new anchor.BN(amount), assetId)
210
+ .accountsPartial({
211
+ payer: owner,
212
+ user: owner,
213
+ pool: this.poolPDA,
214
+ userAccount: userAccountPDA,
215
+ recipientTokenAccount,
216
+ vault: vaultPDA,
217
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
218
+ ...this._getArciumAccounts("sub_balance", computationOffset),
219
+ })
220
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
221
+ await this._awaitComputation(computationOffset);
222
+ return sig;
223
+ }
224
+ /** Decrypt all 4 asset balances from on-chain account. Uses internal cipher if param omitted. */
225
+ async getBalance(cipher, owner) {
226
+ const enc = cipher || this._requireEncryption().cipher;
227
+ const account = await this.fetchUserAccount(owner);
228
+ const nonceToBytes = (n) => {
229
+ const bn = new anchor.BN(n.toString());
230
+ return new Uint8Array(bn.toArray("le", 16));
231
+ };
232
+ return {
233
+ usdc: (0, encryption_1.decryptValue)(enc, new Uint8Array(account.usdcCredit), nonceToBytes(account.usdcNonce)),
234
+ tsla: (0, encryption_1.decryptValue)(enc, new Uint8Array(account.tslaCredit), nonceToBytes(account.tslaNonce)),
235
+ spy: (0, encryption_1.decryptValue)(enc, new Uint8Array(account.spyCredit), nonceToBytes(account.spyNonce)),
236
+ aapl: (0, encryption_1.decryptValue)(enc, new Uint8Array(account.aaplCredit), nonceToBytes(account.aaplNonce)),
237
+ };
238
+ }
239
+ /** Get unshielded (normal SPL token) balances from wallet */
240
+ async getUnshieldedBalances(owner) {
241
+ const userPubkey = owner || this.wallet.publicKey;
242
+ // Fetch pool to get mint addresses
243
+ const poolAccount = await this.program.account.pool.fetch(this.poolPDA);
244
+ const mints = {
245
+ usdc: poolAccount.usdcMint,
246
+ tsla: poolAccount.tslaMint,
247
+ spy: poolAccount.spyMint,
248
+ aapl: poolAccount.aaplMint,
249
+ };
250
+ const getTokenBalance = async (mint) => {
251
+ try {
252
+ const ata = (0, spl_token_1.getAssociatedTokenAddressSync)(mint, userPubkey);
253
+ const account = await (0, spl_token_1.getAccount)(this.connection, ata);
254
+ return account.amount;
255
+ }
256
+ catch (e) {
257
+ // Return 0 if account doesn't exist
258
+ if (e instanceof spl_token_1.TokenAccountNotFoundError) {
259
+ return BigInt(0);
260
+ }
261
+ throw e;
262
+ }
263
+ };
264
+ const [usdc, tsla, spy, aapl] = await Promise.all([
265
+ getTokenBalance(mints.usdc),
266
+ getTokenBalance(mints.tsla),
267
+ getTokenBalance(mints.spy),
268
+ getTokenBalance(mints.aapl),
269
+ ]);
270
+ return { usdc, tsla, spy, aapl };
271
+ }
272
+ /** Internal P2P transfer (USDC only). Uses internal encryption if params omitted. */
273
+ async transfer(recipientPubkey, amount, cipher, encryptionPublicKey) {
274
+ const enc = cipher || this._requireEncryption().cipher;
275
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
276
+ const sender = this.wallet.publicKey;
277
+ const [senderAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, sender);
278
+ const [recipientAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, recipientPubkey);
279
+ const nonce = (0, crypto_1.randomBytes)(16);
280
+ const encrypted = (0, encryption_1.encryptValue)(enc, BigInt(amount), nonce);
281
+ const computationOffset = this._generateComputationOffset();
282
+ const sig = await this.program.methods
283
+ .internalTransfer(computationOffset, Array.from(encrypted.ciphertext), Array.from(pubkey), (0, encryption_1.nonceToBN)(nonce))
284
+ .accountsPartial({
285
+ payer: sender,
286
+ sender: sender,
287
+ senderAccount: senderAccountPDA,
288
+ recipientAccount: recipientAccountPDA,
289
+ ...this._getArciumAccounts("transfer", computationOffset),
290
+ })
291
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
292
+ await this._awaitComputation(computationOffset);
293
+ return sig;
294
+ }
295
+ // =========================================================================
296
+ // ORDER METHODS
297
+ // =========================================================================
298
+ /**
299
+ * Initialize batch state with encrypted zeros.
300
+ * This must be called before the first order of each new batch.
301
+ * After batch execution, the batch state needs to be re-initialized for the next batch.
302
+ */
303
+ async initBatchState() {
304
+ const owner = this.wallet.publicKey;
305
+ const computationOffset = this._generateComputationOffset();
306
+ const sig = await this.program.methods
307
+ .initBatchState(computationOffset)
308
+ .accountsPartial({
309
+ payer: owner,
310
+ batchAccumulator: this.batchAccumulatorPDA,
311
+ ...this._getArciumAccounts("init_batch_state", computationOffset),
312
+ })
313
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
314
+ await this._awaitComputation(computationOffset);
315
+ return sig;
316
+ }
317
+ /**
318
+ * Place an encrypted order in the current batch.
319
+ * Automatically initializes batch state if needed (first order of a new batch).
320
+ * Uses internal encryption if params omitted.
321
+ */
322
+ async placeOrder(pairId, direction, amount, sourceAssetId, cipher, encryptionPublicKey) {
323
+ // Lazy check: If mxe_nonce is 0, batch state needs initialization
324
+ // (mxe_nonce is set by init_batch_state callback, 0 means not yet initialized)
325
+ const batchInfo = await this.getBatchInfo();
326
+ if (batchInfo.mxeNonce === "0") {
327
+ console.log("[SDK] Initializing batch state for new batch...");
328
+ await this.initBatchState();
329
+ console.log("[SDK] Batch state initialized");
330
+ }
331
+ const enc = cipher || this._requireEncryption().cipher;
332
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
333
+ const owner = this.wallet.publicKey;
334
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, owner);
335
+ const orderNonce = (0, crypto_1.randomBytes)(16);
336
+ // Encrypt OrderInput struct fields together in a single call
337
+ // The circuit expects Enc<Shared, OrderInput> where OrderInput = { pair_id: u8, direction: u8, amount: u64 }
338
+ const encryptedOrderInput = enc.encrypt([BigInt(pairId), BigInt(direction), BigInt(amount)], orderNonce);
339
+ const computationOffset = this._generateComputationOffset();
340
+ const sig = await this.program.methods
341
+ .placeOrder(computationOffset, Array.from(encryptedOrderInput[0]), Array.from(encryptedOrderInput[1]), Array.from(encryptedOrderInput[2]), Array.from(pubkey), (0, encryption_1.nonceToBN)(orderNonce), sourceAssetId)
342
+ .accountsPartial({
343
+ payer: owner,
344
+ user: owner,
345
+ userAccount: userAccountPDA,
346
+ batchAccumulator: this.batchAccumulatorPDA,
347
+ ...this._getArciumAccounts("accumulate_order", computationOffset),
348
+ })
349
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
350
+ await this._awaitComputation(computationOffset);
351
+ return sig;
352
+ }
353
+ /** Get current pending order info, or null */
354
+ async getPendingOrder(owner) {
355
+ const account = await this.fetchUserAccount(owner);
356
+ if (!account.pendingOrder)
357
+ return null;
358
+ const order = account.pendingOrder;
359
+ return {
360
+ batchId: order.batchId.toNumber(),
361
+ pairId: Array.from(order.pairId),
362
+ direction: Array.from(order.direction),
363
+ encryptedAmount: Array.from(order.encryptedAmount),
364
+ };
365
+ }
366
+ /**
367
+ * Get decrypted pending order info.
368
+ * Decrypts pairId, direction, and amount using user's cipher.
369
+ * The order nonce is stored on-chain, so no need to save it locally.
370
+ *
371
+ * @param cipher - Optional cipher (uses internal if omitted)
372
+ * @param owner - Optional owner pubkey (uses wallet if omitted)
373
+ */
374
+ async getDecryptedOrder(cipher, owner) {
375
+ const enc = cipher || this._requireEncryption().cipher;
376
+ const account = await this.fetchUserAccount(owner);
377
+ if (!account.pendingOrder)
378
+ return null;
379
+ const order = account.pendingOrder;
380
+ // Get the nonce from the on-chain account
381
+ const orderNonce = new anchor.BN(order.orderNonce.toString());
382
+ const nonceBytes = new Uint8Array(orderNonce.toArray("le", 16));
383
+ // Decrypt the order fields using the user's cipher
384
+ // Orders are encrypted with Enc<Shared,*> so user can decrypt
385
+ // All fields were encrypted together, so decrypt together
386
+ const decryptedFields = enc.decrypt([
387
+ Array.from(order.pairId),
388
+ Array.from(order.direction),
389
+ Array.from(order.encryptedAmount),
390
+ ], nonceBytes);
391
+ return {
392
+ batchId: order.batchId.toNumber(),
393
+ pairId: Number(decryptedFields[0]),
394
+ direction: Number(decryptedFields[1]),
395
+ amount: decryptedFields[2],
396
+ };
397
+ }
398
+ /** Cancel pending order — not yet implemented */
399
+ async cancelOrder() {
400
+ throw new Error("Not implemented (Phase 11)");
401
+ }
402
+ // =========================================================================
403
+ // BATCH EXECUTION
404
+ // =========================================================================
405
+ /** Execute the current batch when 8+ orders are accumulated. Anyone can call this. */
406
+ async executeBatch() {
407
+ const batch = await this.getBatchInfo();
408
+ if (batch.orderCount < 8) {
409
+ throw new Error(`Not enough orders: ${batch.orderCount}/8. Need 8 orders to execute.`);
410
+ }
411
+ const batchId = batch.batchId;
412
+ const [batchLogPDA] = (0, pda_1.getBatchLogPDA)(this.programId, batchId);
413
+ // Derive vault PDAs
414
+ const [vaultUsdcPDA] = (0, pda_1.getVaultPDA)(this.programId, "usdc");
415
+ const [vaultTslaPDA] = (0, pda_1.getVaultPDA)(this.programId, "tsla");
416
+ const [vaultSpyPDA] = (0, pda_1.getVaultPDA)(this.programId, "spy");
417
+ const [vaultAaplPDA] = (0, pda_1.getVaultPDA)(this.programId, "aapl");
418
+ // Derive reserve PDAs
419
+ const [reserveUsdcPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("reserve"), Buffer.from("usdc")], this.programId);
420
+ const [reserveTslaPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("reserve"), Buffer.from("tsla")], this.programId);
421
+ const [reserveSpyPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("reserve"), Buffer.from("spy")], this.programId);
422
+ const [reserveAaplPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("reserve"), Buffer.from("aapl")], this.programId);
423
+ const computationOffset = this._generateComputationOffset();
424
+ const owner = this.wallet.publicKey;
425
+ const sig = await this.program.methods
426
+ .executeBatch(computationOffset)
427
+ .accountsPartial({
428
+ payer: owner,
429
+ caller: owner,
430
+ pool: this.poolPDA,
431
+ batchAccumulator: this.batchAccumulatorPDA,
432
+ batchLog: batchLogPDA,
433
+ // Vault accounts
434
+ vaultUsdc: vaultUsdcPDA,
435
+ vaultTsla: vaultTslaPDA,
436
+ vaultSpy: vaultSpyPDA,
437
+ vaultAapl: vaultAaplPDA,
438
+ // Reserve accounts
439
+ reserveUsdc: reserveUsdcPDA,
440
+ reserveTsla: reserveTslaPDA,
441
+ reserveSpy: reserveSpyPDA,
442
+ reserveAapl: reserveAaplPDA,
443
+ // Token program
444
+ tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
445
+ // Arcium accounts
446
+ ...this._getArciumAccounts("reveal_batch", computationOffset),
447
+ })
448
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
449
+ // Wait for MPC computation
450
+ await this._awaitComputation(computationOffset);
451
+ return sig;
452
+ }
453
+ // =========================================================================
454
+ // SETTLEMENT METHODS
455
+ // =========================================================================
456
+ /** Settle a pending order after batch execution. Uses internal encryption if param omitted. */
457
+ async settleOrder(pairId, direction, encryptionPublicKey) {
458
+ const pubkey = encryptionPublicKey || this._requireEncryption().pubkey;
459
+ const owner = this.wallet.publicKey;
460
+ const [userAccountPDA] = (0, pda_1.getUserAccountPDA)(this.programId, owner);
461
+ const account = await this.fetchUserAccount();
462
+ if (!account.pendingOrder)
463
+ throw new Error("No pending order to settle");
464
+ const batchId = account.pendingOrder.batchId.toNumber();
465
+ const [batchLogPDA] = (0, pda_1.getBatchLogPDA)(this.programId, batchId);
466
+ const settlementNonce = (0, crypto_1.randomBytes)(16);
467
+ const computationOffset = this._generateComputationOffset();
468
+ const sig = await this.program.methods
469
+ .settleOrder(computationOffset, Array.from(pubkey), (0, encryption_1.nonceToBN)(settlementNonce), pairId, direction)
470
+ .accountsPartial({
471
+ payer: owner,
472
+ user: owner,
473
+ userAccount: userAccountPDA,
474
+ batchLog: batchLogPDA,
475
+ ...this._getArciumAccounts("calculate_payout", computationOffset),
476
+ })
477
+ .rpc({ skipPreflight: true, commitment: "confirmed" });
478
+ await this._awaitComputation(computationOffset);
479
+ return sig;
480
+ }
481
+ // =========================================================================
482
+ // QUERY METHODS
483
+ // =========================================================================
484
+ /** Fetch current batch accumulator state */
485
+ async getBatchInfo() {
486
+ const batch = await this.program.account.batchAccumulator.fetch(this.batchAccumulatorPDA);
487
+ return {
488
+ batchId: batch.batchId.toNumber(),
489
+ orderCount: batch.orderCount,
490
+ mxeNonce: batch.mxeNonce.toString(),
491
+ };
492
+ }
493
+ /** Fetch historical batch log */
494
+ async getBatchLog(batchId) {
495
+ const [batchLogPDA] = (0, pda_1.getBatchLogPDA)(this.programId, batchId);
496
+ const log = await this.program.account.batchLog.fetch(batchLogPDA);
497
+ // Note: Anchor uses camelCase field names (converted from Rust's snake_case)
498
+ const results = log.results.map((r) => ({
499
+ totalAIn: r.totalAIn ?? r.total_a_in,
500
+ totalBIn: r.totalBIn ?? r.total_b_in,
501
+ finalPoolA: r.finalPoolA ?? r.final_pool_a,
502
+ finalPoolB: r.finalPoolB ?? r.final_pool_b,
503
+ }));
504
+ return {
505
+ batchId: log.batchId?.toNumber() ?? log.batch_id?.toNumber(),
506
+ results,
507
+ };
508
+ }
509
+ /**
510
+ * Fetch all historical batch logs.
511
+ * Iterates from batch 1 to the current batch ID and returns all found logs.
512
+ * @returns Array of BatchResult for all executed batches
513
+ */
514
+ async getAllBatchLogs() {
515
+ const batchInfo = await this.getBatchInfo();
516
+ const currentBatchId = batchInfo.batchId;
517
+ // Batch IDs start at 1 and increment. Current batchId is the next batch to be executed.
518
+ // So executed batches are 1 to (currentBatchId - 1)
519
+ const logs = [];
520
+ for (let batchId = 1; batchId < currentBatchId; batchId++) {
521
+ try {
522
+ const log = await this.getBatchLog(batchId);
523
+ logs.push(log);
524
+ }
525
+ catch (e) {
526
+ // BatchLog doesn't exist - skip
527
+ // This can happen if batch was never executed or PDA doesn't exist
528
+ continue;
529
+ }
530
+ }
531
+ return logs;
532
+ }
533
+ /**
534
+ * Estimate payout for a pending order after batch execution.
535
+ * Uses client-side calculation: payout = (orderAmount / totalInput) * finalPoolOutput
536
+ *
537
+ * @param cipher - Optional cipher (uses internal if omitted)
538
+ * @param owner - Optional owner pubkey (uses wallet if omitted)
539
+ * @returns EstimatedPayout or null if no pending order or batch not executed
540
+ */
541
+ async estimatePayout(cipher, owner) {
542
+ // Get decrypted order info
543
+ const order = await this.getDecryptedOrder(cipher, owner);
544
+ if (!order)
545
+ return null;
546
+ // Try to fetch batch log (may not exist if batch not executed)
547
+ let batchLog;
548
+ try {
549
+ batchLog = await this.getBatchLog(order.batchId);
550
+ }
551
+ catch (e) {
552
+ // BatchLog doesn't exist yet - batch not executed
553
+ return null;
554
+ }
555
+ const pairResult = batchLog.results[order.pairId];
556
+ // Determine totals based on direction
557
+ // direction 0 = A_to_B (sell A, get B)
558
+ // direction 1 = B_to_A (sell B, get A)
559
+ const totalInput = order.direction === 0
560
+ ? BigInt(pairResult.totalAIn.toString())
561
+ : BigInt(pairResult.totalBIn.toString());
562
+ const finalPoolOutput = order.direction === 0
563
+ ? BigInt(pairResult.finalPoolB.toString())
564
+ : BigInt(pairResult.finalPoolA.toString());
565
+ // Prevent division by zero
566
+ if (totalInput === 0n) {
567
+ return null;
568
+ }
569
+ // Calculate pro-rata payout: (orderAmount * finalPoolOutput) / totalInput
570
+ const orderAmount = order.amount;
571
+ const estimatedPayout = (orderAmount * finalPoolOutput) / totalInput;
572
+ // Determine output asset ID based on pair and direction
573
+ // Simplified: for now, use rough mapping
574
+ // TODO: Implement proper pair -> asset mapping
575
+ const outputAssetId = this._getOutputAssetId(order.pairId, order.direction);
576
+ return {
577
+ batchId: order.batchId,
578
+ pairId: order.pairId,
579
+ direction: order.direction,
580
+ orderAmount,
581
+ totalInput,
582
+ finalPoolOutput,
583
+ estimatedPayout,
584
+ outputAssetId,
585
+ };
586
+ }
587
+ /**
588
+ * Get effective balance for an asset, including pending payout.
589
+ * Combines current on-chain balance with estimated payout from pending order.
590
+ *
591
+ * @param assetId - Asset to check (0=USDC, 1=TSLA, 2=SPY, 3=AAPL)
592
+ * @param cipher - Optional cipher (uses internal if omitted)
593
+ * @param owner - Optional owner pubkey (uses wallet if omitted)
594
+ * @returns EffectiveBalance with current, pending, and total
595
+ */
596
+ async getEffectiveBalance(assetId, cipher, owner) {
597
+ const enc = cipher || this._requireEncryption().cipher;
598
+ // Get current on-chain balances for all assets
599
+ const balances = await this.getBalance(enc, owner);
600
+ // Extract the specific asset balance
601
+ const assetLabels = {
602
+ [constants_1.AssetId.USDC]: 'usdc',
603
+ [constants_1.AssetId.TSLA]: 'tsla',
604
+ [constants_1.AssetId.SPY]: 'spy',
605
+ [constants_1.AssetId.AAPL]: 'aapl',
606
+ };
607
+ const currentBalance = balances[assetLabels[assetId]];
608
+ // Try to estimate pending payout
609
+ const payout = await this.estimatePayout(enc, owner);
610
+ let pendingPayout = 0n;
611
+ let hasPendingOrder = false;
612
+ if (payout && payout.outputAssetId === assetId) {
613
+ pendingPayout = payout.estimatedPayout;
614
+ hasPendingOrder = true;
615
+ }
616
+ else if (payout) {
617
+ // Has pending order but for different asset
618
+ hasPendingOrder = true;
619
+ }
620
+ return {
621
+ currentBalance,
622
+ pendingPayout,
623
+ effectiveBalance: currentBalance + pendingPayout,
624
+ hasPendingOrder,
625
+ };
626
+ }
627
+ /**
628
+ * Helper to determine output asset ID based on pair and direction
629
+ */
630
+ _getOutputAssetId(pairId, direction) {
631
+ // Pair mapping (from constants):
632
+ // TSLA_USDC = 0, SPY_USDC = 1, AAPL_USDC = 2
633
+ // TSLA_SPY = 3, TSLA_AAPL = 4, SPY_AAPL = 5
634
+ //
635
+ // Direction: 0 = A_to_B (sell A, get B), 1 = B_to_A (sell B, get A)
636
+ const pairAssets = [
637
+ [constants_1.AssetId.TSLA, constants_1.AssetId.USDC], // pair 0: TSLA/USDC
638
+ [constants_1.AssetId.SPY, constants_1.AssetId.USDC], // pair 1: SPY/USDC
639
+ [constants_1.AssetId.AAPL, constants_1.AssetId.USDC], // pair 2: AAPL/USDC
640
+ [constants_1.AssetId.TSLA, constants_1.AssetId.SPY], // pair 3: TSLA/SPY
641
+ [constants_1.AssetId.TSLA, constants_1.AssetId.AAPL], // pair 4: TSLA/AAPL
642
+ [constants_1.AssetId.SPY, constants_1.AssetId.AAPL], // pair 5: SPY/AAPL
643
+ ];
644
+ const [assetA, assetB] = pairAssets[pairId] || [constants_1.AssetId.USDC, constants_1.AssetId.USDC];
645
+ // If selling A (direction 0), user gets B. If selling B (direction 1), user gets A.
646
+ return direction === 0 ? assetB : assetA;
647
+ }
648
+ /** Fetch pool account data */
649
+ async getPoolInfo() {
650
+ return this.program.account.pool.fetch(this.poolPDA);
651
+ }
652
+ // =========================================================================
653
+ // INTERNAL HELPERS
654
+ // =========================================================================
655
+ _getArciumAccounts(compDefName, computationOffset) {
656
+ return {
657
+ signPdaAccount: web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("ArciumSignerAccount")], this.programId)[0],
658
+ mxeAccount: (0, client_1.getMXEAccAddress)(this.programId),
659
+ mempoolAccount: (0, client_1.getMempoolAccAddress)(this.clusterOffset),
660
+ executingPool: (0, client_1.getExecutingPoolAccAddress)(this.clusterOffset),
661
+ computationAccount: (0, client_1.getComputationAccAddress)(this.clusterOffset, computationOffset),
662
+ compDefAccount: (0, client_1.getCompDefAccAddress)(this.programId, Buffer.from((0, client_1.getCompDefAccOffset)(compDefName)).readUInt32LE()),
663
+ clusterAccount: this.clusterAccount,
664
+ };
665
+ }
666
+ async _awaitComputation(offset, timeoutMs = 60000, maxRetries = 3) {
667
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
668
+ try {
669
+ const result = await Promise.race([
670
+ (0, client_1.awaitComputationFinalization)(this.provider, offset, this.programId, "confirmed"),
671
+ new Promise((_, reject) => {
672
+ setTimeout(() => reject(new Error(`MPC timeout (attempt ${attempt}/${maxRetries})`)), timeoutMs);
673
+ }),
674
+ ]);
675
+ return result;
676
+ }
677
+ catch (error) {
678
+ if (error.message.includes("timeout") && attempt < maxRetries) {
679
+ await new Promise((r) => setTimeout(r, 2000));
680
+ continue;
681
+ }
682
+ throw error;
683
+ }
684
+ }
685
+ throw new Error("MPC computation failed after all retries");
686
+ }
687
+ _generateComputationOffset() {
688
+ return new anchor.BN((0, crypto_1.randomBytes)(8), "hex");
689
+ }
690
+ }
691
+ exports.ShuffleClient = ShuffleClient;