@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/README.md +119 -0
- package/dist/cli/commands.d.ts +54 -0
- package/dist/cli/commands.js +1229 -0
- package/dist/cli/config.d.ts +50 -0
- package/dist/cli/config.js +247 -0
- package/dist/cli/devnet.d.ts +81 -0
- package/dist/cli/devnet.js +106 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +155 -0
- package/dist/cli/output.d.ts +102 -0
- package/dist/cli/output.js +251 -0
- package/dist/client.d.ts +121 -0
- package/dist/client.js +691 -0
- package/dist/constants.d.ts +30 -0
- package/dist/constants.js +61 -0
- package/dist/encryption.d.ts +24 -0
- package/dist/encryption.js +90 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +45 -0
- package/dist/idl/shuffle_protocol.json +6333 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +42 -0
- package/dist/pda.d.ts +7 -0
- package/dist/pda.js +59 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +7 -0
- package/package.json +58 -0
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;
|