@utxopia/sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- package/packages/sdk/dist/vk-registry.js +212 -0
|
@@ -0,0 +1,1760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXOPIA Instruction Builders (JoinSplit Architecture)
|
|
3
|
+
*
|
|
4
|
+
* Low-level instruction building for UTXOPIA operations.
|
|
5
|
+
* All Groth16 proofs are verified inline using BN254 pairing syscalls.
|
|
6
|
+
*
|
|
7
|
+
* @module instructions
|
|
8
|
+
*/
|
|
9
|
+
import { AccountRole, } from "@solana/kit";
|
|
10
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
11
|
+
import { address, getConfig, TOKEN_2022_PROGRAM_ID } from "./config";
|
|
12
|
+
import { resolveAuditorCiphertext } from "./auditor-ciphertext";
|
|
13
|
+
import { MAGICBLOCK_EPHEMERAL_VAULT_ID, MAGICBLOCK_DELEGATION_PROGRAM_ID, MAGICBLOCK_MAGIC_CONTEXT_ID, MAGICBLOCK_MAGIC_PROGRAM_ID, MAGICBLOCK_MAX_PER_MEMBERS, MAGICBLOCK_PERMISSION_PROGRAM_ID, MAGICBLOCK_PER_MEMBER_FLAGS, } from "./magicblock";
|
|
14
|
+
/** System program address */
|
|
15
|
+
const SYSTEM_PROGRAM_ADDRESS = address("11111111111111111111111111111111");
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/** Instruction discriminators — sequential 0-19 (must match contracts/programs/utxopia/src/lib.rs) */
|
|
20
|
+
const INSTRUCTION = {
|
|
21
|
+
// Core (0-2)
|
|
22
|
+
INITIALIZE: 0,
|
|
23
|
+
SET_PAUSED: 1,
|
|
24
|
+
SET_POOL_CONFIG: 2,
|
|
25
|
+
// Pool updates (3-5)
|
|
26
|
+
PROPOSE_POOL_UPDATE: 3,
|
|
27
|
+
EXECUTE_POOL_UPDATE: 4,
|
|
28
|
+
CANCEL_POOL_UPDATE: 5,
|
|
29
|
+
// VK admin (6-7)
|
|
30
|
+
INIT_VK_REGISTRY: 6,
|
|
31
|
+
UPDATE_VK_REGISTRY: 7,
|
|
32
|
+
// Multi-token (8-10)
|
|
33
|
+
REGISTER_TOKEN: 8,
|
|
34
|
+
UPDATE_TOKEN_CONFIG: 9,
|
|
35
|
+
CLAIM_FEES: 10,
|
|
36
|
+
// Deposit (11-12)
|
|
37
|
+
COMPLETE_DEPOSIT: 11,
|
|
38
|
+
SHIELD: 12,
|
|
39
|
+
// JoinSplit (13-15) — all share n_in + n_out + n_pub + proof_source header
|
|
40
|
+
TRANSACT: 13,
|
|
41
|
+
UNSHIELD: 14,
|
|
42
|
+
REDEEM: 15,
|
|
43
|
+
// Redemption lifecycle (16-19)
|
|
44
|
+
RESERVED_REQUEST_REDEMPTION: 16,
|
|
45
|
+
COMPLETE_REDEMPTION: 17,
|
|
46
|
+
MARK_PROCESSING: 18,
|
|
47
|
+
CANCEL_REDEMPTION: 19,
|
|
48
|
+
// Tree management (20)
|
|
49
|
+
ROTATE_TREE: 20,
|
|
50
|
+
// 21-23 are permissioned-pool ops — see PERMISSIONED_DISC
|
|
51
|
+
APPROVE_REDEMPTION_SIGNING: 27,
|
|
52
|
+
// Auditor-only setters (28-29) — utxopia program, permissioned pools
|
|
53
|
+
SET_AUDITOR_FROZEN: 28,
|
|
54
|
+
SET_AUDITOR_VIEWING_PUBKEY: 29,
|
|
55
|
+
// MagicBlock ER/PER lifecycle helpers (32-33)
|
|
56
|
+
MAGICBLOCK_DELEGATE: 32,
|
|
57
|
+
MAGICBLOCK_COMMIT: 33,
|
|
58
|
+
MAGICBLOCK_PER_PERMISSION: 34,
|
|
59
|
+
ROTATE_AUDITOR: 35,
|
|
60
|
+
INITIALIZE_POLICY_APPROVAL: 36,
|
|
61
|
+
POLICY_APPROVAL_DECISION: 37,
|
|
62
|
+
POLICY_APPROVAL_COMMIT: 38,
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Discriminants for permissioned-pool instructions (utxopia program only).
|
|
66
|
+
* Values must match programs/utxopia/src/lib.rs exactly.
|
|
67
|
+
*/
|
|
68
|
+
const PERMISSIONED_DISC = {
|
|
69
|
+
INITIALIZE_PERMISSIONED: 21,
|
|
70
|
+
COMPLETE_DEPOSIT_PERMISSIONED: 22,
|
|
71
|
+
SHIELD_PERMISSIONED: 23,
|
|
72
|
+
REGISTER_EXIT_DESTINATION: 39,
|
|
73
|
+
};
|
|
74
|
+
/** Export instruction discriminators for consumers */
|
|
75
|
+
export const INSTRUCTION_DISCRIMINATORS = INSTRUCTION;
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Utilities
|
|
78
|
+
// =============================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Simple base58 decoding for addresses
|
|
81
|
+
*/
|
|
82
|
+
function bs58Decode(str) {
|
|
83
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
84
|
+
const ALPHABET_MAP = new Map();
|
|
85
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
86
|
+
ALPHABET_MAP.set(ALPHABET[i], i);
|
|
87
|
+
}
|
|
88
|
+
let num = BigInt(0);
|
|
89
|
+
for (const char of str) {
|
|
90
|
+
const val = ALPHABET_MAP.get(char);
|
|
91
|
+
if (val === undefined) {
|
|
92
|
+
throw new Error(`Invalid base58 character: ${char}`);
|
|
93
|
+
}
|
|
94
|
+
num = num * BigInt(58) + BigInt(val);
|
|
95
|
+
}
|
|
96
|
+
// Count leading zeros
|
|
97
|
+
let leadingZeros = 0;
|
|
98
|
+
for (const char of str) {
|
|
99
|
+
if (char === "1") {
|
|
100
|
+
leadingZeros++;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Convert to bytes
|
|
107
|
+
const bytes = [];
|
|
108
|
+
while (num > BigInt(0)) {
|
|
109
|
+
bytes.unshift(Number(num % BigInt(256)));
|
|
110
|
+
num = num / BigInt(256);
|
|
111
|
+
}
|
|
112
|
+
// Add leading zeros
|
|
113
|
+
for (let i = 0; i < leadingZeros; i++) {
|
|
114
|
+
bytes.unshift(0);
|
|
115
|
+
}
|
|
116
|
+
// Ensure 32 bytes for Solana addresses
|
|
117
|
+
while (bytes.length < 32) {
|
|
118
|
+
bytes.unshift(0);
|
|
119
|
+
}
|
|
120
|
+
return new Uint8Array(bytes);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Convert Address to bytes
|
|
124
|
+
*/
|
|
125
|
+
function addressToBytes(addr) {
|
|
126
|
+
return bs58Decode(addr.toString());
|
|
127
|
+
}
|
|
128
|
+
const STEALTH_DATA_PER_OUTPUT = 72; // ephemeral_pub(32) + encrypted_amount(8) + encrypted_token_id(32)
|
|
129
|
+
function assertStealthDataRecordLengths(stealthData) {
|
|
130
|
+
for (let i = 0; i < stealthData.length; i++) {
|
|
131
|
+
if (stealthData[i].length !== STEALTH_DATA_PER_OUTPUT) {
|
|
132
|
+
throw new Error(`Stealth data ${i} must be ${STEALTH_DATA_PER_OUTPUT} bytes, got ${stealthData[i].length}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build shield instruction data (disc=12).
|
|
138
|
+
*
|
|
139
|
+
* Layout (after disc stripped by entrypoint):
|
|
140
|
+
* - amount: u64 LE (8 bytes)
|
|
141
|
+
* - npk: [u8; 32]
|
|
142
|
+
* - ephemeral_pub: [u8; 32]
|
|
143
|
+
*/
|
|
144
|
+
export function buildShieldInstructionData(options) {
|
|
145
|
+
const data = new Uint8Array(73);
|
|
146
|
+
data[0] = INSTRUCTION.SHIELD;
|
|
147
|
+
const view = new DataView(data.buffer);
|
|
148
|
+
view.setBigUint64(1, options.amount, true);
|
|
149
|
+
data.set(options.npk.slice(0, 32), 9);
|
|
150
|
+
data.set(options.ephemeralPub.slice(0, 32), 41);
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build a complete shield instruction (disc=12).
|
|
155
|
+
*
|
|
156
|
+
* Shields SPL tokens into the privacy pool. Works with both
|
|
157
|
+
* legacy Token program (wSOL) and Token-2022 (USDC, USDT, etc.).
|
|
158
|
+
*/
|
|
159
|
+
export function buildShieldInstruction(options) {
|
|
160
|
+
const config = getConfig();
|
|
161
|
+
const data = buildShieldInstructionData({
|
|
162
|
+
amount: options.amount,
|
|
163
|
+
npk: options.npk,
|
|
164
|
+
ephemeralPub: options.ephemeralPub,
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
programAddress: config.utxopiaProgramId,
|
|
168
|
+
accounts: [
|
|
169
|
+
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
170
|
+
{ address: options.accounts.userTokenAccount, role: AccountRole.WRITABLE },
|
|
171
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
172
|
+
{ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE },
|
|
173
|
+
{ address: options.accounts.vault, role: AccountRole.WRITABLE },
|
|
174
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
175
|
+
{ address: options.accounts.tokenProgram, role: AccountRole.READONLY },
|
|
176
|
+
],
|
|
177
|
+
data,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function buildApproveRedemptionSigningInstructionData(options) {
|
|
181
|
+
if (options.btcSighash.length !== 32) {
|
|
182
|
+
throw new Error("btcSighash must be exactly 32 bytes");
|
|
183
|
+
}
|
|
184
|
+
if (options.ikaMessageDigest && options.ikaMessageDigest.length !== 32) {
|
|
185
|
+
throw new Error("ikaMessageDigest must be exactly 32 bytes");
|
|
186
|
+
}
|
|
187
|
+
const data = new Uint8Array(1 + 32 + (options.ikaMessageDigest ? 32 : 0) + 8);
|
|
188
|
+
const view = new DataView(data.buffer);
|
|
189
|
+
let offset = 0;
|
|
190
|
+
data[offset++] = INSTRUCTION.APPROVE_REDEMPTION_SIGNING;
|
|
191
|
+
data.set(options.btcSighash, offset);
|
|
192
|
+
offset += 32;
|
|
193
|
+
if (options.ikaMessageDigest) {
|
|
194
|
+
data.set(options.ikaMessageDigest, offset);
|
|
195
|
+
offset += 32;
|
|
196
|
+
}
|
|
197
|
+
view.setBigUint64(offset, BigInt(options.minerFeeSats), true);
|
|
198
|
+
return data;
|
|
199
|
+
}
|
|
200
|
+
export function buildApproveRedemptionSigningInstruction(options) {
|
|
201
|
+
const config = getConfig();
|
|
202
|
+
return {
|
|
203
|
+
programAddress: config.utxopiaProgramId,
|
|
204
|
+
accounts: [
|
|
205
|
+
{ address: options.accounts.poolState, role: AccountRole.READONLY },
|
|
206
|
+
{ address: options.accounts.redemptionRequest, role: AccountRole.READONLY },
|
|
207
|
+
{ address: options.accounts.authority, role: AccountRole.READONLY_SIGNER },
|
|
208
|
+
{ address: options.accounts.poolConfig, role: AccountRole.READONLY },
|
|
209
|
+
{ address: options.accounts.ikaProgram, role: AccountRole.READONLY },
|
|
210
|
+
{ address: options.accounts.ikaCoordinator, role: AccountRole.READONLY },
|
|
211
|
+
{ address: options.accounts.ikaMessageApproval, role: AccountRole.WRITABLE },
|
|
212
|
+
{ address: options.accounts.ikaDwallet, role: AccountRole.READONLY },
|
|
213
|
+
{ address: options.accounts.callerProgram, role: AccountRole.READONLY },
|
|
214
|
+
{ address: options.accounts.cpiAuthority, role: AccountRole.READONLY },
|
|
215
|
+
{ address: options.accounts.ikaPayer, role: AccountRole.WRITABLE_SIGNER },
|
|
216
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
217
|
+
],
|
|
218
|
+
data: buildApproveRedemptionSigningInstructionData({
|
|
219
|
+
btcSighash: options.btcSighash,
|
|
220
|
+
ikaMessageDigest: options.ikaMessageDigest,
|
|
221
|
+
minerFeeSats: options.minerFeeSats,
|
|
222
|
+
}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build instruction data for COMPLETE_REDEMPTION (disc 17)
|
|
227
|
+
*
|
|
228
|
+
* Layout (after disc stripped):
|
|
229
|
+
* - btc_txid: [u8; 32]
|
|
230
|
+
* - tx_size: u32 LE
|
|
231
|
+
* - pool_script_len: u8
|
|
232
|
+
* - pool_script: [u8; 0-34]
|
|
233
|
+
* - consumed_utxo_count: u8
|
|
234
|
+
*/
|
|
235
|
+
export function buildCompleteRedemptionInstructionData(options) {
|
|
236
|
+
const { btcTxid, txSize, poolScript, consumedUtxoCount } = options;
|
|
237
|
+
const totalLen = 1 + 32 + 4 + 1 + poolScript.length + 1;
|
|
238
|
+
const data = new Uint8Array(totalLen);
|
|
239
|
+
const view = new DataView(data.buffer);
|
|
240
|
+
let offset = 0;
|
|
241
|
+
data[offset++] = INSTRUCTION.COMPLETE_REDEMPTION;
|
|
242
|
+
data.set(btcTxid, offset);
|
|
243
|
+
offset += 32;
|
|
244
|
+
view.setUint32(offset, txSize, true);
|
|
245
|
+
offset += 4;
|
|
246
|
+
data[offset++] = poolScript.length;
|
|
247
|
+
if (poolScript.length > 0) {
|
|
248
|
+
data.set(poolScript, offset);
|
|
249
|
+
offset += poolScript.length;
|
|
250
|
+
}
|
|
251
|
+
data[offset++] = consumedUtxoCount;
|
|
252
|
+
return data;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Build a complete redemption instruction
|
|
256
|
+
*
|
|
257
|
+
* Accounts (14 base + optional change + variable consumed UTXOs):
|
|
258
|
+
* 0. pool_state (writable)
|
|
259
|
+
* 1. redemption_request (writable)
|
|
260
|
+
* 2. authority (signer)
|
|
261
|
+
* 3. rent_recipient (readonly)
|
|
262
|
+
* 4. verified_transaction (readonly)
|
|
263
|
+
* 5. light_client (readonly)
|
|
264
|
+
* 6. tx_buffer (readonly)
|
|
265
|
+
* 7. zkbtc_mint (writable)
|
|
266
|
+
* 8. pool_vault (writable)
|
|
267
|
+
* 9. token_program (readonly)
|
|
268
|
+
* 10. completion_receipt (writable)
|
|
269
|
+
* 11. system_program (readonly)
|
|
270
|
+
* 12. pool_config (readonly)
|
|
271
|
+
* 13. change_utxo (writable, only when pool_script is non-empty)
|
|
272
|
+
* 13/14..+N consumed_utxos (writable)
|
|
273
|
+
* final. token_config (writable)
|
|
274
|
+
*/
|
|
275
|
+
export function buildCompleteRedemptionInstruction(options) {
|
|
276
|
+
const config = getConfig();
|
|
277
|
+
const data = buildCompleteRedemptionInstructionData({
|
|
278
|
+
btcTxid: options.btcTxid,
|
|
279
|
+
txSize: options.txSize,
|
|
280
|
+
poolScript: options.poolScript,
|
|
281
|
+
consumedUtxoCount: options.consumedUtxoCount,
|
|
282
|
+
});
|
|
283
|
+
const accounts = [
|
|
284
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
285
|
+
{ address: options.accounts.redemptionRequest, role: AccountRole.WRITABLE },
|
|
286
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
287
|
+
{ address: options.accounts.rentRecipient, role: AccountRole.READONLY },
|
|
288
|
+
{ address: options.accounts.verifiedTransaction, role: AccountRole.READONLY },
|
|
289
|
+
{ address: options.accounts.lightClient, role: AccountRole.READONLY },
|
|
290
|
+
{ address: options.accounts.txBuffer, role: AccountRole.READONLY },
|
|
291
|
+
{ address: options.accounts.zkbtcMint, role: AccountRole.WRITABLE },
|
|
292
|
+
{ address: options.accounts.poolVault, role: AccountRole.WRITABLE },
|
|
293
|
+
{ address: options.accounts.tokenProgram ?? TOKEN_2022_PROGRAM_ID, role: AccountRole.READONLY },
|
|
294
|
+
{ address: options.accounts.completionReceipt, role: AccountRole.WRITABLE },
|
|
295
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
296
|
+
{ address: options.accounts.poolConfig, role: AccountRole.READONLY },
|
|
297
|
+
];
|
|
298
|
+
if (options.poolScript.length > 0) {
|
|
299
|
+
if (!options.accounts.changeUtxo) {
|
|
300
|
+
throw new Error("changeUtxo is required when poolScript is non-empty");
|
|
301
|
+
}
|
|
302
|
+
accounts.push({ address: options.accounts.changeUtxo, role: AccountRole.WRITABLE });
|
|
303
|
+
}
|
|
304
|
+
// Append consumed UTXO PDAs
|
|
305
|
+
if (options.accounts.consumedUtxos) {
|
|
306
|
+
for (const utxo of options.accounts.consumedUtxos) {
|
|
307
|
+
accounts.push({ address: utxo, role: AccountRole.WRITABLE });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
accounts.push({ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE });
|
|
311
|
+
return {
|
|
312
|
+
programAddress: config.utxopiaProgramId,
|
|
313
|
+
accounts,
|
|
314
|
+
data,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Build cancel redemption instruction data.
|
|
319
|
+
*
|
|
320
|
+
* Layout (after disc stripped by entrypoint):
|
|
321
|
+
* - npk: [u8; 32]
|
|
322
|
+
* - utxo_count: u8 (number of reserved UtxoRecord accounts that follow; 0 for Pending)
|
|
323
|
+
*/
|
|
324
|
+
export function buildCancelRedemptionInstructionData(options) {
|
|
325
|
+
const { npk, reservedUtxoCount } = options;
|
|
326
|
+
if (npk.length !== 32) {
|
|
327
|
+
throw new Error("npk must be 32 bytes");
|
|
328
|
+
}
|
|
329
|
+
// disc(1) + npk(32) + utxo_count(1)
|
|
330
|
+
const data = new Uint8Array(1 + 32 + 1);
|
|
331
|
+
let offset = 0;
|
|
332
|
+
data[offset++] = INSTRUCTION.CANCEL_REDEMPTION;
|
|
333
|
+
data.set(npk, offset);
|
|
334
|
+
offset += 32;
|
|
335
|
+
data[offset++] = reservedUtxoCount;
|
|
336
|
+
return data;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Build a cancel redemption instruction.
|
|
340
|
+
*
|
|
341
|
+
* Accounts (6 base + variable):
|
|
342
|
+
* 0. user (writable signer)
|
|
343
|
+
* 1. pool_state (writable)
|
|
344
|
+
* 2. redemption_request (writable)
|
|
345
|
+
* 3. commitment_tree (writable)
|
|
346
|
+
* 4. system_program (readonly)
|
|
347
|
+
* 5. token_config (writable)
|
|
348
|
+
* 6..6+N reserved UtxoRecord PDAs (writable) — Processing cancels only
|
|
349
|
+
*/
|
|
350
|
+
export function buildCancelRedemptionInstruction(options) {
|
|
351
|
+
const config = getConfig();
|
|
352
|
+
const reservedUtxos = options.accounts.reservedUtxos ?? [];
|
|
353
|
+
const data = buildCancelRedemptionInstructionData({
|
|
354
|
+
npk: options.npk,
|
|
355
|
+
reservedUtxoCount: reservedUtxos.length,
|
|
356
|
+
});
|
|
357
|
+
const accounts = [
|
|
358
|
+
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
359
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
360
|
+
{ address: options.accounts.redemptionRequest, role: AccountRole.WRITABLE },
|
|
361
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
362
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
363
|
+
{ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE },
|
|
364
|
+
];
|
|
365
|
+
for (const utxo of reservedUtxos) {
|
|
366
|
+
accounts.push({ address: utxo, role: AccountRole.WRITABLE });
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
programAddress: config.utxopiaProgramId,
|
|
370
|
+
accounts,
|
|
371
|
+
data,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Build transact instruction data (JoinSplit)
|
|
376
|
+
*
|
|
377
|
+
* Layout (after disc stripped by entrypoint):
|
|
378
|
+
* - n_inputs: u8
|
|
379
|
+
* - n_outputs: u8
|
|
380
|
+
* - n_public_outputs: u8 (always 0 for transact)
|
|
381
|
+
* - proof_source: u8 (0=inline, 1=buffer account)
|
|
382
|
+
* - proof: [u8; 256] (only if proof_source=0)
|
|
383
|
+
* - merkle_root: [u8; 32]
|
|
384
|
+
* - bound_params_hash: [u8; 32]
|
|
385
|
+
* - nullifiers: [[u8; 32]; n_inputs]
|
|
386
|
+
* - commitments_out: [[u8; 32]; n_outputs]
|
|
387
|
+
* - stealth_data: [ephemeral_pub(32) + encrypted_amount(8) + encrypted_token_id(32)] x n_outputs
|
|
388
|
+
*/
|
|
389
|
+
export function buildTransactInstructionData(options) {
|
|
390
|
+
const { nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash, nullifiers, commitmentsOut, stealthData, senderMemos } = options;
|
|
391
|
+
const proofSource = options.proofSource ?? 0;
|
|
392
|
+
if (proofSource === 0 && (!proofBytes || proofBytes.length !== 256)) {
|
|
393
|
+
throw new Error(`Inline mode requires 256-byte proof, got ${proofBytes?.length ?? 0}`);
|
|
394
|
+
}
|
|
395
|
+
if (nullifiers.length !== nInputs) {
|
|
396
|
+
throw new Error(`Expected ${nInputs} nullifiers, got ${nullifiers.length}`);
|
|
397
|
+
}
|
|
398
|
+
if (commitmentsOut.length !== nOutputs) {
|
|
399
|
+
throw new Error(`Expected ${nOutputs} commitments, got ${commitmentsOut.length}`);
|
|
400
|
+
}
|
|
401
|
+
if (stealthData.length !== nOutputs) {
|
|
402
|
+
throw new Error(`Expected ${nOutputs} stealth data entries, got ${stealthData.length}`);
|
|
403
|
+
}
|
|
404
|
+
assertStealthDataRecordLengths(stealthData);
|
|
405
|
+
if (senderMemos != null) {
|
|
406
|
+
throw new Error("senderMemos are disabled until a proof-bound protocol version is available");
|
|
407
|
+
}
|
|
408
|
+
const proofSize = proofSource === 0 ? 256 : 0;
|
|
409
|
+
const totalSize = 1 + 4 + proofSize + 32 + 32 + nInputs * 32 + nOutputs * 32 + nOutputs * STEALTH_DATA_PER_OUTPUT;
|
|
410
|
+
const data = new Uint8Array(totalSize);
|
|
411
|
+
let offset = 0;
|
|
412
|
+
// Discriminator
|
|
413
|
+
data[offset++] = INSTRUCTION.TRANSACT;
|
|
414
|
+
// Header (4 bytes)
|
|
415
|
+
data[offset++] = nInputs;
|
|
416
|
+
data[offset++] = nOutputs;
|
|
417
|
+
data[offset++] = 0; // n_public_outputs = 0 for transact
|
|
418
|
+
data[offset++] = proofSource;
|
|
419
|
+
// Proof (256 bytes, only in inline mode)
|
|
420
|
+
if (proofSource === 0 && proofBytes) {
|
|
421
|
+
data.set(proofBytes, offset);
|
|
422
|
+
offset += 256;
|
|
423
|
+
}
|
|
424
|
+
// Merkle root (32 bytes)
|
|
425
|
+
data.set(merkleRoot, offset);
|
|
426
|
+
offset += 32;
|
|
427
|
+
// Bound params hash (32 bytes)
|
|
428
|
+
data.set(boundParamsHash, offset);
|
|
429
|
+
offset += 32;
|
|
430
|
+
// Nullifiers
|
|
431
|
+
for (const nullifier of nullifiers) {
|
|
432
|
+
data.set(nullifier, offset);
|
|
433
|
+
offset += 32;
|
|
434
|
+
}
|
|
435
|
+
// Output commitments
|
|
436
|
+
for (const commitment of commitmentsOut) {
|
|
437
|
+
data.set(commitment, offset);
|
|
438
|
+
offset += 32;
|
|
439
|
+
}
|
|
440
|
+
// Stealth data (ephemeral_pub + encrypted_amount per output)
|
|
441
|
+
for (const sd of stealthData) {
|
|
442
|
+
data.set(sd, offset);
|
|
443
|
+
offset += STEALTH_DATA_PER_OUTPUT;
|
|
444
|
+
}
|
|
445
|
+
return data;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Build a complete JoinSplit transact instruction
|
|
449
|
+
*
|
|
450
|
+
* Accounts:
|
|
451
|
+
* 0. pool_state (writable)
|
|
452
|
+
* 1. commitment_tree (writable)
|
|
453
|
+
* 2. vk_registry (read)
|
|
454
|
+
* 3. user (signer)
|
|
455
|
+
* 4. system_program (read)
|
|
456
|
+
* 5..5+N nullifier_records (writable)
|
|
457
|
+
*/
|
|
458
|
+
export function buildTransactInstruction(options) {
|
|
459
|
+
const config = getConfig();
|
|
460
|
+
const data = buildTransactInstructionData({
|
|
461
|
+
nInputs: options.nInputs,
|
|
462
|
+
nOutputs: options.nOutputs,
|
|
463
|
+
proofBytes: options.proofBytes,
|
|
464
|
+
merkleRoot: options.merkleRoot,
|
|
465
|
+
boundParamsHash: options.boundParamsHash,
|
|
466
|
+
nullifiers: options.nullifiers,
|
|
467
|
+
commitmentsOut: options.commitmentsOut,
|
|
468
|
+
stealthData: options.stealthData,
|
|
469
|
+
senderMemos: options.senderMemos,
|
|
470
|
+
});
|
|
471
|
+
const accounts = [
|
|
472
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
473
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
474
|
+
{ address: options.accounts.vkRegistry, role: AccountRole.READONLY },
|
|
475
|
+
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
476
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
477
|
+
];
|
|
478
|
+
// Nullifier records (writable PDAs)
|
|
479
|
+
for (const nr of options.accounts.nullifierRecords) {
|
|
480
|
+
accounts.push({ address: nr, role: AccountRole.WRITABLE });
|
|
481
|
+
}
|
|
482
|
+
if (options.accounts.policyApproval) {
|
|
483
|
+
accounts.push({ address: options.accounts.policyApproval, role: AccountRole.WRITABLE });
|
|
484
|
+
accounts.push({
|
|
485
|
+
address: config.policyProgramId ?? config.utxopiaProgramId,
|
|
486
|
+
role: AccountRole.READONLY,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
programAddress: config.utxopiaProgramId,
|
|
491
|
+
accounts,
|
|
492
|
+
data,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// =============================================================================
|
|
496
|
+
// JoinSplit + BTC Redeem Instruction Builder (disc=16)
|
|
497
|
+
// =============================================================================
|
|
498
|
+
/**
|
|
499
|
+
* Build instruction data for REDEEM (disc=15) — atomic JoinSplit + BTC withdrawal (multi-output)
|
|
500
|
+
*
|
|
501
|
+
* Combines Groth16 proof verification with RedemptionRequest PDA creation.
|
|
502
|
+
* Supports 1..3 public outputs, each creating a separate RedemptionRequest.
|
|
503
|
+
*
|
|
504
|
+
* Layout (4-byte common header):
|
|
505
|
+
* - n_inputs: u8
|
|
506
|
+
* - n_outputs: u8
|
|
507
|
+
* - n_public_outputs: u8 (1..3)
|
|
508
|
+
* - proof_source: u8 (0=inline, 1=buffer account)
|
|
509
|
+
* - proof: [u8; 256] (only if proof_source=0)
|
|
510
|
+
* - merkle_root: [u8; 32]
|
|
511
|
+
* - bound_params_hash: [u8; 32]
|
|
512
|
+
* - nullifiers: [[u8; 32]; n_inputs]
|
|
513
|
+
* - commitments_out: [[u8; 32]; n_outputs]
|
|
514
|
+
* - stealth_data: [ephemeral_pub(32) + encrypted_amount(8) + encrypted_token_id(32)] x n_tree_outputs
|
|
515
|
+
* - For each public output: amount(8) + script_len(1) + script(var) + nonce(8)
|
|
516
|
+
*/
|
|
517
|
+
export function buildRedeemInstructionData(options) {
|
|
518
|
+
const { nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash, nullifiers, commitmentsOut, stealthData, redeemAmounts, btcScripts, requestNonces, } = options;
|
|
519
|
+
const nPublicOutputs = options.nPublicOutputs ?? redeemAmounts.length;
|
|
520
|
+
const proofSource = options.proofSource ?? 0;
|
|
521
|
+
if (proofSource === 0 && (!proofBytes || proofBytes.length !== 256)) {
|
|
522
|
+
throw new Error(`Inline mode requires 256-byte proof, got ${proofBytes?.length ?? 0}`);
|
|
523
|
+
}
|
|
524
|
+
if (nPublicOutputs < 1 || nPublicOutputs > 3) {
|
|
525
|
+
throw new Error(`nPublicOutputs must be 1-3, got ${nPublicOutputs}`);
|
|
526
|
+
}
|
|
527
|
+
const nTreeOutputs = nOutputs - nPublicOutputs;
|
|
528
|
+
if (nTreeOutputs < 0) {
|
|
529
|
+
throw new Error(`nOutputs (${nOutputs}) must be >= nPublicOutputs (${nPublicOutputs})`);
|
|
530
|
+
}
|
|
531
|
+
if (nullifiers.length !== nInputs) {
|
|
532
|
+
throw new Error(`Expected ${nInputs} nullifiers, got ${nullifiers.length}`);
|
|
533
|
+
}
|
|
534
|
+
if (commitmentsOut.length !== nOutputs) {
|
|
535
|
+
throw new Error(`Expected ${nOutputs} commitments, got ${commitmentsOut.length}`);
|
|
536
|
+
}
|
|
537
|
+
if (stealthData.length !== nTreeOutputs) {
|
|
538
|
+
throw new Error(`Expected ${nTreeOutputs} stealth data entries, got ${stealthData.length}`);
|
|
539
|
+
}
|
|
540
|
+
assertStealthDataRecordLengths(stealthData);
|
|
541
|
+
if (redeemAmounts.length !== nPublicOutputs) {
|
|
542
|
+
throw new Error(`Expected ${nPublicOutputs} redeem amounts, got ${redeemAmounts.length}`);
|
|
543
|
+
}
|
|
544
|
+
if (btcScripts.length !== nPublicOutputs) {
|
|
545
|
+
throw new Error(`Expected ${nPublicOutputs} BTC scripts, got ${btcScripts.length}`);
|
|
546
|
+
}
|
|
547
|
+
if (requestNonces.length !== nPublicOutputs) {
|
|
548
|
+
throw new Error(`Expected ${nPublicOutputs} request nonces, got ${requestNonces.length}`);
|
|
549
|
+
}
|
|
550
|
+
for (let k = 0; k < nPublicOutputs; k++) {
|
|
551
|
+
if (btcScripts[k].length === 0 || btcScripts[k].length > 62) {
|
|
552
|
+
throw new Error(`BTC script[${k}] must be 1-62 bytes, got ${btcScripts[k].length}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const proofSize = proofSource === 0 ? 256 : 0;
|
|
556
|
+
let totalScriptLen = 0;
|
|
557
|
+
for (const s of btcScripts)
|
|
558
|
+
totalScriptLen += s.length;
|
|
559
|
+
const totalSize = 1 + 4 + proofSize + 32 + 32
|
|
560
|
+
+ (nInputs * 32) + (nOutputs * 32) + (nTreeOutputs * STEALTH_DATA_PER_OUTPUT)
|
|
561
|
+
+ nPublicOutputs * (8 + 1 + 8) + totalScriptLen;
|
|
562
|
+
const data = new Uint8Array(totalSize);
|
|
563
|
+
const view = new DataView(data.buffer);
|
|
564
|
+
let offset = 0;
|
|
565
|
+
// Discriminator
|
|
566
|
+
data[offset++] = INSTRUCTION.REDEEM;
|
|
567
|
+
// Header (4 bytes)
|
|
568
|
+
data[offset++] = nInputs;
|
|
569
|
+
data[offset++] = nOutputs;
|
|
570
|
+
data[offset++] = nPublicOutputs;
|
|
571
|
+
data[offset++] = proofSource;
|
|
572
|
+
// Proof (256 bytes, only in inline mode)
|
|
573
|
+
if (proofSource === 0 && proofBytes) {
|
|
574
|
+
data.set(proofBytes, offset);
|
|
575
|
+
offset += 256;
|
|
576
|
+
}
|
|
577
|
+
// Merkle root (32 bytes)
|
|
578
|
+
data.set(merkleRoot, offset);
|
|
579
|
+
offset += 32;
|
|
580
|
+
// Bound params hash (32 bytes)
|
|
581
|
+
data.set(boundParamsHash, offset);
|
|
582
|
+
offset += 32;
|
|
583
|
+
// Nullifiers
|
|
584
|
+
for (const nullifier of nullifiers) {
|
|
585
|
+
data.set(nullifier, offset);
|
|
586
|
+
offset += 32;
|
|
587
|
+
}
|
|
588
|
+
// Output commitments (all n_outputs, last n_public_outputs = redeem)
|
|
589
|
+
for (const commitment of commitmentsOut) {
|
|
590
|
+
data.set(commitment, offset);
|
|
591
|
+
offset += 32;
|
|
592
|
+
}
|
|
593
|
+
// Stealth data for tree outputs only (72 bytes each)
|
|
594
|
+
for (const sd of stealthData) {
|
|
595
|
+
data.set(sd, offset);
|
|
596
|
+
offset += STEALTH_DATA_PER_OUTPUT;
|
|
597
|
+
}
|
|
598
|
+
// Per-output redeem data: amount(8) + script_len(1) + script(var) + nonce(8)
|
|
599
|
+
for (let k = 0; k < nPublicOutputs; k++) {
|
|
600
|
+
view.setBigUint64(offset, redeemAmounts[k], true);
|
|
601
|
+
offset += 8;
|
|
602
|
+
data[offset++] = btcScripts[k].length;
|
|
603
|
+
data.set(btcScripts[k], offset);
|
|
604
|
+
offset += btcScripts[k].length;
|
|
605
|
+
view.setBigUint64(offset, requestNonces[k], true);
|
|
606
|
+
offset += 8;
|
|
607
|
+
}
|
|
608
|
+
return data;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Build unshield instruction data (multi-output, disc=14).
|
|
612
|
+
*
|
|
613
|
+
* Layout (4-byte common header):
|
|
614
|
+
* - disc(1) + n_inputs(1) + n_outputs(1) + n_public_outputs(1) + proof_source(1)
|
|
615
|
+
* - proof(256) if inline
|
|
616
|
+
* - merkle_root(32) + bound_params_hash(32)
|
|
617
|
+
* - nullifiers(N*32) + commitments_out(M*32)
|
|
618
|
+
* - stealth_data(n_tree_outputs * 72)
|
|
619
|
+
* - amounts[P] (each u64 LE)
|
|
620
|
+
*
|
|
621
|
+
* Recipients come from the accounts array. Ordinary SPL outputs use token
|
|
622
|
+
* accounts; native-SOL outputs use the recipient accounts directly.
|
|
623
|
+
*/
|
|
624
|
+
export function buildUnshieldInstructionData(options) {
|
|
625
|
+
const { nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash, nullifiers, commitmentsOut, stealthData, unshieldAmounts } = options;
|
|
626
|
+
const nPublicOutputs = options.nPublicOutputs ?? unshieldAmounts.length;
|
|
627
|
+
const proofSource = options.proofSource ?? 0;
|
|
628
|
+
if (proofSource === 0 && (!proofBytes || proofBytes.length !== 256)) {
|
|
629
|
+
throw new Error(`Inline mode requires 256-byte proof, got ${proofBytes?.length ?? 0}`);
|
|
630
|
+
}
|
|
631
|
+
if (nPublicOutputs < 1 || nPublicOutputs > 3) {
|
|
632
|
+
throw new Error(`nPublicOutputs must be 1-3, got ${nPublicOutputs}`);
|
|
633
|
+
}
|
|
634
|
+
if (nullifiers.length !== nInputs) {
|
|
635
|
+
throw new Error(`Expected ${nInputs} nullifiers, got ${nullifiers.length}`);
|
|
636
|
+
}
|
|
637
|
+
if (commitmentsOut.length !== nOutputs) {
|
|
638
|
+
throw new Error(`Expected ${nOutputs} commitments, got ${commitmentsOut.length}`);
|
|
639
|
+
}
|
|
640
|
+
const nTreeOutputs = nOutputs - nPublicOutputs;
|
|
641
|
+
if (nTreeOutputs < 0) {
|
|
642
|
+
throw new Error(`nOutputs (${nOutputs}) must be >= nPublicOutputs (${nPublicOutputs})`);
|
|
643
|
+
}
|
|
644
|
+
if (stealthData.length !== nTreeOutputs) {
|
|
645
|
+
throw new Error(`Expected ${nTreeOutputs} stealth data entries (tree outputs), got ${stealthData.length}`);
|
|
646
|
+
}
|
|
647
|
+
assertStealthDataRecordLengths(stealthData);
|
|
648
|
+
if (unshieldAmounts.length !== nPublicOutputs) {
|
|
649
|
+
throw new Error(`Expected ${nPublicOutputs} unshield amounts, got ${unshieldAmounts.length}`);
|
|
650
|
+
}
|
|
651
|
+
const proofSize = proofSource === 0 ? 256 : 0;
|
|
652
|
+
const totalSize = 1 + 4 + proofSize + 32 + 32 + (nInputs * 32) + (nOutputs * 32) + (nTreeOutputs * STEALTH_DATA_PER_OUTPUT) + (nPublicOutputs * 8);
|
|
653
|
+
const data = new Uint8Array(totalSize);
|
|
654
|
+
const view = new DataView(data.buffer);
|
|
655
|
+
let offset = 0;
|
|
656
|
+
// Discriminator
|
|
657
|
+
data[offset++] = INSTRUCTION.UNSHIELD;
|
|
658
|
+
// Header (4 bytes)
|
|
659
|
+
data[offset++] = nInputs;
|
|
660
|
+
data[offset++] = nOutputs;
|
|
661
|
+
data[offset++] = nPublicOutputs;
|
|
662
|
+
data[offset++] = proofSource;
|
|
663
|
+
// Proof (256 bytes, only in inline mode)
|
|
664
|
+
if (proofSource === 0 && proofBytes) {
|
|
665
|
+
data.set(proofBytes, offset);
|
|
666
|
+
offset += 256;
|
|
667
|
+
}
|
|
668
|
+
// Merkle root (32 bytes)
|
|
669
|
+
data.set(merkleRoot, offset);
|
|
670
|
+
offset += 32;
|
|
671
|
+
// Bound params hash (32 bytes)
|
|
672
|
+
data.set(boundParamsHash, offset);
|
|
673
|
+
offset += 32;
|
|
674
|
+
// Nullifiers
|
|
675
|
+
for (const nullifier of nullifiers) {
|
|
676
|
+
data.set(nullifier, offset);
|
|
677
|
+
offset += 32;
|
|
678
|
+
}
|
|
679
|
+
// Output commitments (all n_outputs, last nPublicOutputs = burn)
|
|
680
|
+
for (const commitment of commitmentsOut) {
|
|
681
|
+
data.set(commitment, offset);
|
|
682
|
+
offset += 32;
|
|
683
|
+
}
|
|
684
|
+
// Stealth data for tree outputs only
|
|
685
|
+
for (const sd of stealthData) {
|
|
686
|
+
data.set(sd, offset);
|
|
687
|
+
offset += STEALTH_DATA_PER_OUTPUT;
|
|
688
|
+
}
|
|
689
|
+
// Per-output unshield amounts (u64 LE each)
|
|
690
|
+
for (const amount of unshieldAmounts) {
|
|
691
|
+
view.setBigUint64(offset, amount, true);
|
|
692
|
+
offset += 8;
|
|
693
|
+
}
|
|
694
|
+
return data;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Build a complete unshield instruction (multi-output, disc=14)
|
|
698
|
+
*
|
|
699
|
+
* Accounts:
|
|
700
|
+
* 0. pool_state (read)
|
|
701
|
+
* 1. commitment_tree (writable)
|
|
702
|
+
* 2. vk_registry (read)
|
|
703
|
+
* 3. user (signer)
|
|
704
|
+
* 4. system_program (read)
|
|
705
|
+
* 5. token_config (writable)
|
|
706
|
+
* 6. vault (writable)
|
|
707
|
+
* 7. token_program (read)
|
|
708
|
+
* 8..8+P public destinations (SPL token accounts or native-SOL recipients)
|
|
709
|
+
* 8+P..8+P+N nullifier_records (writable)
|
|
710
|
+
*/
|
|
711
|
+
export function buildUnshieldInstruction(options) {
|
|
712
|
+
const config = getConfig();
|
|
713
|
+
const nPublicOutputs = options.nPublicOutputs ?? options.unshieldAmounts.length;
|
|
714
|
+
const data = buildUnshieldInstructionData({
|
|
715
|
+
nInputs: options.nInputs,
|
|
716
|
+
nOutputs: options.nOutputs,
|
|
717
|
+
nPublicOutputs,
|
|
718
|
+
proofBytes: options.proofBytes,
|
|
719
|
+
merkleRoot: options.merkleRoot,
|
|
720
|
+
boundParamsHash: options.boundParamsHash,
|
|
721
|
+
nullifiers: options.nullifiers,
|
|
722
|
+
commitmentsOut: options.commitmentsOut,
|
|
723
|
+
stealthData: options.stealthData,
|
|
724
|
+
unshieldAmounts: options.unshieldAmounts,
|
|
725
|
+
});
|
|
726
|
+
const accounts = [
|
|
727
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
728
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
729
|
+
{ address: options.accounts.vkRegistry, role: AccountRole.READONLY },
|
|
730
|
+
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
731
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
732
|
+
{ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE },
|
|
733
|
+
{ address: options.accounts.vault, role: AccountRole.WRITABLE },
|
|
734
|
+
{ address: options.accounts.tokenProgram ?? TOKEN_2022_PROGRAM_ID, role: AccountRole.READONLY },
|
|
735
|
+
];
|
|
736
|
+
// Public destinations (one per public output)
|
|
737
|
+
for (const rta of options.accounts.recipientTokenAccounts) {
|
|
738
|
+
accounts.push({ address: rta, role: AccountRole.WRITABLE });
|
|
739
|
+
}
|
|
740
|
+
// Nullifier records (writable PDAs)
|
|
741
|
+
for (const nr of options.accounts.nullifierRecords) {
|
|
742
|
+
accounts.push({ address: nr, role: AccountRole.WRITABLE });
|
|
743
|
+
}
|
|
744
|
+
if (options.accounts.policyApproval) {
|
|
745
|
+
accounts.push({ address: options.accounts.policyApproval, role: AccountRole.WRITABLE });
|
|
746
|
+
accounts.push({
|
|
747
|
+
address: config.policyProgramId ?? config.utxopiaProgramId,
|
|
748
|
+
role: AccountRole.READONLY,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
programAddress: config.utxopiaProgramId,
|
|
753
|
+
accounts,
|
|
754
|
+
data,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Build propose_pool_update instruction data
|
|
759
|
+
*
|
|
760
|
+
* Layout: discriminator(1) + min_deposit(8) + max_deposit(8) + service_fee(8) + [service_fee_bps(2)] = 25 or 27 bytes
|
|
761
|
+
*/
|
|
762
|
+
export function buildProposePoolUpdateInstructionData(minDeposit, maxDeposit, serviceFee, serviceFeeBps) {
|
|
763
|
+
const hasBps = serviceFeeBps !== undefined;
|
|
764
|
+
const data = new Uint8Array(hasBps ? 27 : 25);
|
|
765
|
+
const view = new DataView(data.buffer);
|
|
766
|
+
data[0] = INSTRUCTION.PROPOSE_POOL_UPDATE;
|
|
767
|
+
view.setBigUint64(1, minDeposit, true);
|
|
768
|
+
view.setBigUint64(9, maxDeposit, true);
|
|
769
|
+
view.setBigUint64(17, serviceFee, true);
|
|
770
|
+
if (hasBps) {
|
|
771
|
+
view.setUint16(25, serviceFeeBps, true);
|
|
772
|
+
}
|
|
773
|
+
return data;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Build a complete propose_pool_update instruction
|
|
777
|
+
*
|
|
778
|
+
* Accounts:
|
|
779
|
+
* 0. pool_state (writable)
|
|
780
|
+
* 1. authority (signer)
|
|
781
|
+
*/
|
|
782
|
+
export function buildProposePoolUpdateInstruction(options) {
|
|
783
|
+
const config = getConfig();
|
|
784
|
+
const data = buildProposePoolUpdateInstructionData(options.minDeposit, options.maxDeposit, options.serviceFee, options.serviceFeeBps);
|
|
785
|
+
return {
|
|
786
|
+
programAddress: config.utxopiaProgramId,
|
|
787
|
+
accounts: [
|
|
788
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
789
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
790
|
+
],
|
|
791
|
+
data,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Build execute_pool_update instruction data
|
|
796
|
+
*
|
|
797
|
+
* Layout: discriminator(1) = 1 byte
|
|
798
|
+
*/
|
|
799
|
+
export function buildExecutePoolUpdateInstructionData() {
|
|
800
|
+
return new Uint8Array([INSTRUCTION.EXECUTE_POOL_UPDATE]);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Build a complete execute_pool_update instruction (authority-only)
|
|
804
|
+
*
|
|
805
|
+
* Accounts:
|
|
806
|
+
* 0. pool_state (writable)
|
|
807
|
+
* 1. authority (signer)
|
|
808
|
+
*/
|
|
809
|
+
export function buildExecutePoolUpdateInstruction(options) {
|
|
810
|
+
const config = getConfig();
|
|
811
|
+
return {
|
|
812
|
+
programAddress: config.utxopiaProgramId,
|
|
813
|
+
accounts: [
|
|
814
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
815
|
+
{ address: options.accounts.authority, role: AccountRole.READONLY_SIGNER },
|
|
816
|
+
],
|
|
817
|
+
data: buildExecutePoolUpdateInstructionData(),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Build cancel_pool_update instruction data
|
|
822
|
+
*
|
|
823
|
+
* Layout: discriminator(1) = 1 byte
|
|
824
|
+
*/
|
|
825
|
+
export function buildCancelPoolUpdateInstructionData() {
|
|
826
|
+
return new Uint8Array([INSTRUCTION.CANCEL_POOL_UPDATE]);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Build a complete cancel_pool_update instruction
|
|
830
|
+
*
|
|
831
|
+
* Accounts:
|
|
832
|
+
* 0. pool_state (writable)
|
|
833
|
+
* 1. authority (signer)
|
|
834
|
+
*/
|
|
835
|
+
export function buildCancelPoolUpdateInstruction(options) {
|
|
836
|
+
const config = getConfig();
|
|
837
|
+
return {
|
|
838
|
+
programAddress: config.utxopiaProgramId,
|
|
839
|
+
accounts: [
|
|
840
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
841
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
842
|
+
],
|
|
843
|
+
data: buildCancelPoolUpdateInstructionData(),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Build rotate_tree instruction data (disc=20, no payload)
|
|
848
|
+
*/
|
|
849
|
+
export function buildRotateTreeInstructionData() {
|
|
850
|
+
return new Uint8Array([INSTRUCTION.ROTATE_TREE]);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Build a complete rotate_tree instruction
|
|
854
|
+
*
|
|
855
|
+
* Accounts:
|
|
856
|
+
* 0. pool_state (writable)
|
|
857
|
+
* 1. current_tree (writable) — must be full
|
|
858
|
+
* 2. new_tree (writable) — to be created
|
|
859
|
+
* 3. authority (signer)
|
|
860
|
+
* 4. system_program
|
|
861
|
+
*/
|
|
862
|
+
export function buildRotateTreeInstruction(options) {
|
|
863
|
+
const config = getConfig();
|
|
864
|
+
return {
|
|
865
|
+
programAddress: config.utxopiaProgramId,
|
|
866
|
+
accounts: [
|
|
867
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
868
|
+
{ address: options.accounts.currentTree, role: AccountRole.WRITABLE },
|
|
869
|
+
{ address: options.accounts.newTree, role: AccountRole.WRITABLE },
|
|
870
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
871
|
+
{ address: options.accounts.systemProgram, role: AccountRole.READONLY },
|
|
872
|
+
],
|
|
873
|
+
data: buildRotateTreeInstructionData(),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function magicBlockDelegateTargetByte(target) {
|
|
877
|
+
if (target === "policyApproval")
|
|
878
|
+
return 2;
|
|
879
|
+
throw new Error(`Unsupported MagicBlock delegate target: ${target}`);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Build magicblock_delegate instruction data (disc=32).
|
|
883
|
+
*/
|
|
884
|
+
export function buildMagicBlockDelegateInstructionData(options) {
|
|
885
|
+
if (!Number.isInteger(options.commitFrequencyMs) || options.commitFrequencyMs < 0) {
|
|
886
|
+
throw new Error("commitFrequencyMs must be a non-negative u32");
|
|
887
|
+
}
|
|
888
|
+
if (options.commitFrequencyMs > 0xffffffff) {
|
|
889
|
+
throw new Error("commitFrequencyMs must fit in u32");
|
|
890
|
+
}
|
|
891
|
+
const data = new Uint8Array(options.validator ? 38 : 6);
|
|
892
|
+
const view = new DataView(data.buffer);
|
|
893
|
+
data[0] = INSTRUCTION.MAGICBLOCK_DELEGATE;
|
|
894
|
+
data[1] = magicBlockDelegateTargetByte(options.target);
|
|
895
|
+
view.setUint32(2, options.commitFrequencyMs, true);
|
|
896
|
+
if (options.validator) {
|
|
897
|
+
data.set(addressToBytes(options.validator), 6);
|
|
898
|
+
}
|
|
899
|
+
return data;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Build a complete magicblock_delegate instruction.
|
|
903
|
+
*/
|
|
904
|
+
export function buildMagicBlockDelegateInstruction(options) {
|
|
905
|
+
const config = getConfig();
|
|
906
|
+
return {
|
|
907
|
+
programAddress: config.policyProgramId ?? config.utxopiaProgramId,
|
|
908
|
+
accounts: [
|
|
909
|
+
{ address: options.accounts.payer, role: AccountRole.WRITABLE_SIGNER },
|
|
910
|
+
{ address: options.accounts.authority, role: AccountRole.READONLY_SIGNER },
|
|
911
|
+
{ address: options.accounts.poolState, role: AccountRole.READONLY },
|
|
912
|
+
{ address: options.accounts.delegatedAccount, role: AccountRole.WRITABLE },
|
|
913
|
+
{
|
|
914
|
+
address: options.accounts.ownerProgram ?? config.policyProgramId ?? config.utxopiaProgramId,
|
|
915
|
+
role: AccountRole.READONLY,
|
|
916
|
+
},
|
|
917
|
+
{ address: options.accounts.buffer, role: AccountRole.WRITABLE },
|
|
918
|
+
{ address: options.accounts.delegationRecord, role: AccountRole.WRITABLE },
|
|
919
|
+
{ address: options.accounts.delegationMetadata, role: AccountRole.WRITABLE },
|
|
920
|
+
{ address: options.accounts.systemProgram ?? SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
921
|
+
{ address: MAGICBLOCK_DELEGATION_PROGRAM_ID, role: AccountRole.READONLY },
|
|
922
|
+
],
|
|
923
|
+
data: buildMagicBlockDelegateInstructionData({
|
|
924
|
+
target: options.target,
|
|
925
|
+
commitFrequencyMs: options.commitFrequencyMs,
|
|
926
|
+
validator: options.validator,
|
|
927
|
+
}),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Build magicblock_commit instruction data (disc=33).
|
|
932
|
+
*/
|
|
933
|
+
export function buildMagicBlockCommitInstructionData(options) {
|
|
934
|
+
if (options.nullifierHashes.length === 0 ||
|
|
935
|
+
options.nullifierHashes.length > 10) {
|
|
936
|
+
throw new Error("MagicBlock commits require 1-10 nullifier hashes");
|
|
937
|
+
}
|
|
938
|
+
for (const hash of options.nullifierHashes) {
|
|
939
|
+
if (hash.length !== 32) {
|
|
940
|
+
throw new Error("Each MagicBlock commit nullifier hash must be 32 bytes");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const data = new Uint8Array(4 + options.nullifierHashes.length * 32);
|
|
944
|
+
data[0] = INSTRUCTION.MAGICBLOCK_COMMIT;
|
|
945
|
+
data[1] = 1;
|
|
946
|
+
data[2] = options.allowUndelegation ? 1 : 0;
|
|
947
|
+
data[3] = options.nullifierHashes.length;
|
|
948
|
+
options.nullifierHashes.forEach((hash, index) => data.set(hash, 4 + index * 32));
|
|
949
|
+
return data;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Build a complete magicblock_commit instruction.
|
|
953
|
+
*/
|
|
954
|
+
export function buildMagicBlockCommitInstruction(options) {
|
|
955
|
+
const config = getConfig();
|
|
956
|
+
if (options.accounts.nullifierAccounts.length !== options.nullifierHashes.length) {
|
|
957
|
+
throw new Error("Nullifier account and hash counts must match");
|
|
958
|
+
}
|
|
959
|
+
const accounts = [
|
|
960
|
+
{ address: options.accounts.payer, role: AccountRole.READONLY_SIGNER },
|
|
961
|
+
{
|
|
962
|
+
address: options.accounts.authority ?? options.accounts.payer,
|
|
963
|
+
role: AccountRole.READONLY_SIGNER,
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
address: options.accounts.magicContext ?? MAGICBLOCK_MAGIC_CONTEXT_ID,
|
|
967
|
+
role: AccountRole.WRITABLE,
|
|
968
|
+
},
|
|
969
|
+
{ address: options.accounts.magicProgram ?? MAGICBLOCK_MAGIC_PROGRAM_ID, role: AccountRole.READONLY },
|
|
970
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
971
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
972
|
+
];
|
|
973
|
+
for (const nullifier of options.accounts.nullifierAccounts) {
|
|
974
|
+
accounts.push({ address: nullifier, role: AccountRole.WRITABLE });
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
programAddress: config.utxopiaProgramId,
|
|
978
|
+
accounts,
|
|
979
|
+
data: buildMagicBlockCommitInstructionData({
|
|
980
|
+
nullifierHashes: options.nullifierHashes,
|
|
981
|
+
allowUndelegation: options.allowUndelegation,
|
|
982
|
+
}),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function magicBlockPerOperationByte(operation) {
|
|
986
|
+
if (operation === "create")
|
|
987
|
+
return 0;
|
|
988
|
+
if (operation === "update")
|
|
989
|
+
return 1;
|
|
990
|
+
if (operation === "close")
|
|
991
|
+
return 2;
|
|
992
|
+
throw new Error(`Unsupported MagicBlock PER operation: ${operation}`);
|
|
993
|
+
}
|
|
994
|
+
export function buildMagicBlockPerPermissionInstructionData(options) {
|
|
995
|
+
const members = options.members ?? [];
|
|
996
|
+
if (options.operation === "close") {
|
|
997
|
+
if (members.length !== 0) {
|
|
998
|
+
throw new Error("Closing a MagicBlock PER permission does not accept members");
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
if (members.length === 0 || members.length > MAGICBLOCK_MAX_PER_MEMBERS) {
|
|
1003
|
+
throw new Error(`MagicBlock PER permissions require 1-${MAGICBLOCK_MAX_PER_MEMBERS} members`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!members.some((member) => (member.flags & MAGICBLOCK_PER_MEMBER_FLAGS.authority) !== 0)) {
|
|
1006
|
+
throw new Error("MagicBlock PER permissions must retain an authority member");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const allowedFlags = Object.values(MAGICBLOCK_PER_MEMBER_FLAGS).reduce((combined, flag) => combined | flag, 0);
|
|
1010
|
+
const data = new Uint8Array(4 + members.length * 33);
|
|
1011
|
+
data[0] = INSTRUCTION.MAGICBLOCK_PER_PERMISSION;
|
|
1012
|
+
data[1] = magicBlockPerOperationByte(options.operation);
|
|
1013
|
+
data[2] = magicBlockDelegateTargetByte(options.target);
|
|
1014
|
+
data[3] = members.length;
|
|
1015
|
+
members.forEach((member, index) => {
|
|
1016
|
+
if (!Number.isInteger(member.flags) || member.flags < 0 || member.flags > 0xff) {
|
|
1017
|
+
throw new Error("MagicBlock PER member flags must fit in u8");
|
|
1018
|
+
}
|
|
1019
|
+
if ((member.flags & ~allowedFlags) !== 0) {
|
|
1020
|
+
throw new Error("MagicBlock PER member flags contain unsupported bits");
|
|
1021
|
+
}
|
|
1022
|
+
const offset = 4 + index * 33;
|
|
1023
|
+
data[offset] = member.flags;
|
|
1024
|
+
data.set(addressToBytes(member.address), offset + 1);
|
|
1025
|
+
});
|
|
1026
|
+
return data;
|
|
1027
|
+
}
|
|
1028
|
+
export function buildMagicBlockPerPermissionInstruction(options) {
|
|
1029
|
+
const config = getConfig();
|
|
1030
|
+
return {
|
|
1031
|
+
programAddress: config.policyProgramId ?? config.utxopiaProgramId,
|
|
1032
|
+
accounts: [
|
|
1033
|
+
{ address: options.accounts.authority, role: AccountRole.READONLY_SIGNER },
|
|
1034
|
+
{ address: options.accounts.poolState, role: AccountRole.READONLY },
|
|
1035
|
+
{ address: options.accounts.permissionedAccount, role: AccountRole.WRITABLE },
|
|
1036
|
+
{ address: options.accounts.permission, role: AccountRole.WRITABLE },
|
|
1037
|
+
{
|
|
1038
|
+
address: options.accounts.ephemeralVault ?? MAGICBLOCK_EPHEMERAL_VAULT_ID,
|
|
1039
|
+
role: AccountRole.WRITABLE,
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
address: options.accounts.magicProgram ?? MAGICBLOCK_MAGIC_PROGRAM_ID,
|
|
1043
|
+
role: AccountRole.READONLY,
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
address: options.accounts.permissionProgram ?? MAGICBLOCK_PERMISSION_PROGRAM_ID,
|
|
1047
|
+
role: AccountRole.READONLY,
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
data: buildMagicBlockPerPermissionInstructionData(options),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/** Must match `MAX_INTENT_PARTS` in the asset program. */
|
|
1054
|
+
export const MAX_POLICY_INTENT_PARTS = 3;
|
|
1055
|
+
/**
|
|
1056
|
+
* Build the intent parts a spend's policy approval commits to.
|
|
1057
|
+
*
|
|
1058
|
+
* The layout mirrors what the asset program hashes, and deliberately carries no
|
|
1059
|
+
* proof machinery: the merkle root, output commitments and stealth data all move
|
|
1060
|
+
* when a spend is re-proved, and a spend has to be re-proved whenever the root
|
|
1061
|
+
* advances — which is exactly while the authority is deciding. Binding them
|
|
1062
|
+
* would make every approval expire the moment someone else deposits.
|
|
1063
|
+
*
|
|
1064
|
+
* Amounts and BTC scripts are encoded exactly as the instruction encodes them,
|
|
1065
|
+
* because the program hashes the same trailing bytes it parses.
|
|
1066
|
+
*/
|
|
1067
|
+
export function buildPolicyIntentParts(options) {
|
|
1068
|
+
const nullifiers = concatBytes(options.nullifiers);
|
|
1069
|
+
if (options.action === INSTRUCTION.TRANSACT) {
|
|
1070
|
+
// An internal transfer reveals no amount and no external destination, so the
|
|
1071
|
+
// only thing being decided is whether these notes may be spent at all.
|
|
1072
|
+
return [nullifiers];
|
|
1073
|
+
}
|
|
1074
|
+
if (options.action === INSTRUCTION.UNSHIELD) {
|
|
1075
|
+
const amounts = new Uint8Array(options.unshieldAmounts.length * 8);
|
|
1076
|
+
const view = new DataView(amounts.buffer);
|
|
1077
|
+
options.unshieldAmounts.forEach((amount, i) => view.setBigUint64(i * 8, amount, true));
|
|
1078
|
+
for (const owner of options.recipientOwners) {
|
|
1079
|
+
if (owner.length !== 32)
|
|
1080
|
+
throw new Error("recipient owner must be 32 bytes");
|
|
1081
|
+
}
|
|
1082
|
+
return [nullifiers, amounts, concatBytes(options.recipientOwners)];
|
|
1083
|
+
}
|
|
1084
|
+
// redeem: amount(8) + script_len(1) + script(var) + nonce(8), per output
|
|
1085
|
+
const { redeemAmounts, btcScripts, requestNonces } = options;
|
|
1086
|
+
if (redeemAmounts.length !== btcScripts.length || btcScripts.length !== requestNonces.length) {
|
|
1087
|
+
throw new Error("redeem amounts, scripts and nonces must be the same length");
|
|
1088
|
+
}
|
|
1089
|
+
const size = redeemAmounts.length * 17 + btcScripts.reduce((n, s) => n + s.length, 0);
|
|
1090
|
+
const outputs = new Uint8Array(size);
|
|
1091
|
+
const view = new DataView(outputs.buffer);
|
|
1092
|
+
let offset = 0;
|
|
1093
|
+
for (let k = 0; k < redeemAmounts.length; k++) {
|
|
1094
|
+
view.setBigUint64(offset, redeemAmounts[k], true);
|
|
1095
|
+
offset += 8;
|
|
1096
|
+
outputs[offset++] = btcScripts[k].length;
|
|
1097
|
+
outputs.set(btcScripts[k], offset);
|
|
1098
|
+
offset += btcScripts[k].length;
|
|
1099
|
+
view.setBigUint64(offset, requestNonces[k], true);
|
|
1100
|
+
offset += 8;
|
|
1101
|
+
}
|
|
1102
|
+
return [nullifiers, outputs];
|
|
1103
|
+
}
|
|
1104
|
+
function concatBytes(chunks) {
|
|
1105
|
+
const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
|
|
1106
|
+
let offset = 0;
|
|
1107
|
+
for (const chunk of chunks) {
|
|
1108
|
+
out.set(chunk, offset);
|
|
1109
|
+
offset += chunk.length;
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Mirror of `compute_policy_request_hash` in the asset program.
|
|
1115
|
+
*
|
|
1116
|
+
* The approval commits to what the authority decides — which notes are spent,
|
|
1117
|
+
* how much leaves, and where it goes — never to the whole instruction. Binding
|
|
1118
|
+
* the payload would tie an approval to one exact proof, and a spend has to be
|
|
1119
|
+
* re-proved whenever the merkle root moves on, which is precisely while the
|
|
1120
|
+
* authority is deciding.
|
|
1121
|
+
*
|
|
1122
|
+
* Parts, in the order the program hashes them:
|
|
1123
|
+
* - `transact`: [nullifiers]
|
|
1124
|
+
* - `unshield`: [nullifiers, amounts, recipientOwners]
|
|
1125
|
+
* - `redeem`: [nullifiers, amountsScriptsAndNonces]
|
|
1126
|
+
* - value entry (`shield` / `completeDeposit`): [instructionDataWithoutDiscriminator]
|
|
1127
|
+
*
|
|
1128
|
+
* Each part is folded to a fixed 32 bytes before the parts are joined, so their
|
|
1129
|
+
* boundaries cannot be slid. Any drift from the on-chain version makes every
|
|
1130
|
+
* Verified spend fail with PolicyApprovalMismatch.
|
|
1131
|
+
*/
|
|
1132
|
+
export function buildPolicyRequestHash(options) {
|
|
1133
|
+
if (options.intentParts.length < 1 ||
|
|
1134
|
+
options.intentParts.length > MAX_POLICY_INTENT_PARTS) {
|
|
1135
|
+
throw new Error(`Policy intent must have 1-${MAX_POLICY_INTENT_PARTS} parts`);
|
|
1136
|
+
}
|
|
1137
|
+
const domain = new TextEncoder().encode("UTXOPIA_POLICY_APPROVAL_V1");
|
|
1138
|
+
const chunks = [
|
|
1139
|
+
domain,
|
|
1140
|
+
addressToBytes(options.programId),
|
|
1141
|
+
addressToBytes(options.poolState),
|
|
1142
|
+
addressToBytes(options.actor),
|
|
1143
|
+
Uint8Array.of(options.action),
|
|
1144
|
+
Uint8Array.of(options.intentParts.length),
|
|
1145
|
+
...options.intentParts.map((part) => sha256(part)),
|
|
1146
|
+
];
|
|
1147
|
+
const length = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
1148
|
+
const preimage = new Uint8Array(length);
|
|
1149
|
+
let offset = 0;
|
|
1150
|
+
for (const chunk of chunks) {
|
|
1151
|
+
preimage.set(chunk, offset);
|
|
1152
|
+
offset += chunk.length;
|
|
1153
|
+
}
|
|
1154
|
+
return sha256(preimage);
|
|
1155
|
+
}
|
|
1156
|
+
export function buildInitializePolicyApprovalInstructionData(options) {
|
|
1157
|
+
if (!Number.isInteger(options.action) || options.action < 0 || options.action > 0xff) {
|
|
1158
|
+
throw new Error("Policy approval action must fit in u8");
|
|
1159
|
+
}
|
|
1160
|
+
if (options.requestHash.length !== 32 || options.nonce.length !== 32) {
|
|
1161
|
+
throw new Error("Policy approval requestHash and nonce must be 32 bytes");
|
|
1162
|
+
}
|
|
1163
|
+
const data = new Uint8Array(106);
|
|
1164
|
+
const view = new DataView(data.buffer);
|
|
1165
|
+
data[0] = INSTRUCTION.INITIALIZE_POLICY_APPROVAL;
|
|
1166
|
+
data[1] = options.action;
|
|
1167
|
+
view.setBigUint64(2, options.expiresAtSlot, true);
|
|
1168
|
+
data.set(addressToBytes(options.actor), 10);
|
|
1169
|
+
data.set(options.requestHash, 42);
|
|
1170
|
+
data.set(options.nonce, 74);
|
|
1171
|
+
return data;
|
|
1172
|
+
}
|
|
1173
|
+
export function buildInitializePolicyApprovalInstruction(options) {
|
|
1174
|
+
const config = getConfig();
|
|
1175
|
+
return {
|
|
1176
|
+
programAddress: config.policyProgramId ?? config.utxopiaProgramId,
|
|
1177
|
+
accounts: [
|
|
1178
|
+
{ address: options.accounts.payer, role: AccountRole.WRITABLE_SIGNER },
|
|
1179
|
+
{ address: options.accounts.poolState, role: AccountRole.READONLY },
|
|
1180
|
+
{ address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
|
|
1181
|
+
{
|
|
1182
|
+
address: options.accounts.systemProgram ?? SYSTEM_PROGRAM_ADDRESS,
|
|
1183
|
+
role: AccountRole.READONLY,
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
data: buildInitializePolicyApprovalInstructionData(options),
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
export function buildPolicyApprovalDecisionInstruction(options) {
|
|
1190
|
+
const config = getConfig();
|
|
1191
|
+
return {
|
|
1192
|
+
programAddress: config.policyProgramId ?? config.utxopiaProgramId,
|
|
1193
|
+
accounts: [
|
|
1194
|
+
{ address: options.accounts.policyAuthority, role: AccountRole.READONLY_SIGNER },
|
|
1195
|
+
{ address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
|
|
1196
|
+
],
|
|
1197
|
+
data: new Uint8Array([
|
|
1198
|
+
INSTRUCTION.POLICY_APPROVAL_DECISION,
|
|
1199
|
+
options.decision === "approve" ? 1 : 2,
|
|
1200
|
+
]),
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
export function buildPolicyApprovalCommitInstruction(options) {
|
|
1204
|
+
const config = getConfig();
|
|
1205
|
+
return {
|
|
1206
|
+
programAddress: config.policyProgramId ?? config.utxopiaProgramId,
|
|
1207
|
+
accounts: [
|
|
1208
|
+
{ address: options.accounts.payer, role: AccountRole.READONLY_SIGNER },
|
|
1209
|
+
{
|
|
1210
|
+
address: options.accounts.magicContext ?? MAGICBLOCK_MAGIC_CONTEXT_ID,
|
|
1211
|
+
role: AccountRole.WRITABLE,
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
address: options.accounts.magicProgram ?? MAGICBLOCK_MAGIC_PROGRAM_ID,
|
|
1215
|
+
role: AccountRole.READONLY,
|
|
1216
|
+
},
|
|
1217
|
+
{ address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
|
|
1218
|
+
],
|
|
1219
|
+
data: new Uint8Array([INSTRUCTION.POLICY_APPROVAL_COMMIT]),
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
// =============================================================================
|
|
1223
|
+
// Redemption Request PDA Derivation
|
|
1224
|
+
// =============================================================================
|
|
1225
|
+
// deriveRedemptionRequestPDA lived here too, over inlined copies of the same seeds pda.ts
|
|
1226
|
+
// already builds — and it returned `address: userBytes`, the caller's own pubkey rather than the
|
|
1227
|
+
// derived address, leaving a comment telling you to derive it yourself. Nothing used it. Use
|
|
1228
|
+
// `deriveRedemptionRequestPDA` from pda.ts, which returns the real PDA.
|
|
1229
|
+
// =============================================================================
|
|
1230
|
+
// BTC Light Client Verify Transaction (disc=2)
|
|
1231
|
+
// =============================================================================
|
|
1232
|
+
/**
|
|
1233
|
+
* Build btc-light-client verify_transaction instruction data (disc=2)
|
|
1234
|
+
*
|
|
1235
|
+
* Layout (after disc byte):
|
|
1236
|
+
* txid(32) + block_hash(32) + tx_size(u32 LE) + merkle_proof(variable)
|
|
1237
|
+
*
|
|
1238
|
+
* Merkle proof sub-layout:
|
|
1239
|
+
* proof_txid(32) + path_bits(u32 LE) + path_len(u8) + tx_index(u32 LE) + siblings(32 * path_len)
|
|
1240
|
+
*/
|
|
1241
|
+
export function buildVerifyTransactionInstructionData(params) {
|
|
1242
|
+
const { txid, blockHash, txSize, txIndex, merkleSiblings, pathBits } = params;
|
|
1243
|
+
const pathLen = merkleSiblings.length;
|
|
1244
|
+
// disc(1) + txid(32) + blockHash(32) + txSize(4) + proofTxid(32) + pathBits(4) + pathLen(1) + txIndex(4) + siblings(32*N)
|
|
1245
|
+
const totalSize = 1 + 32 + 32 + 4 + 32 + 4 + 1 + 4 + 32 * pathLen;
|
|
1246
|
+
const data = new Uint8Array(totalSize);
|
|
1247
|
+
const view = new DataView(data.buffer);
|
|
1248
|
+
let offset = 0;
|
|
1249
|
+
data[offset++] = 2; // discriminator for verify_transaction
|
|
1250
|
+
data.set(txid, offset);
|
|
1251
|
+
offset += 32;
|
|
1252
|
+
data.set(blockHash, offset);
|
|
1253
|
+
offset += 32;
|
|
1254
|
+
view.setUint32(offset, txSize, true);
|
|
1255
|
+
offset += 4;
|
|
1256
|
+
// Merkle proof sub-layout
|
|
1257
|
+
data.set(txid, offset);
|
|
1258
|
+
offset += 32; // proof_txid = txid
|
|
1259
|
+
view.setUint32(offset, pathBits, true);
|
|
1260
|
+
offset += 4;
|
|
1261
|
+
data[offset++] = pathLen;
|
|
1262
|
+
view.setUint32(offset, txIndex, true);
|
|
1263
|
+
offset += 4;
|
|
1264
|
+
for (const sibling of merkleSiblings) {
|
|
1265
|
+
data.set(sibling, offset);
|
|
1266
|
+
offset += 32;
|
|
1267
|
+
}
|
|
1268
|
+
return data;
|
|
1269
|
+
}
|
|
1270
|
+
// =============================================================================
|
|
1271
|
+
// UTXOpia Complete Deposit (disc=11)
|
|
1272
|
+
// =============================================================================
|
|
1273
|
+
/**
|
|
1274
|
+
* Build utxopia complete_deposit instruction data (disc=11)
|
|
1275
|
+
*
|
|
1276
|
+
* npk + ephemeral_pub are extracted ON-CHAIN from the deposit TX OP_RETURN.
|
|
1277
|
+
* Amount is extracted from the SPV-verified sweep TX.
|
|
1278
|
+
*
|
|
1279
|
+
* Layout: disc(1) + sweep_txid(32) + block_height(u64 LE)
|
|
1280
|
+
* + sweep_tx_size(u32 LE) + deposit_tx_size(u32 LE) + deposit_txid(32) = 81 bytes
|
|
1281
|
+
*/
|
|
1282
|
+
export function buildCompleteDepositInstructionData(params) {
|
|
1283
|
+
const data = new Uint8Array(81);
|
|
1284
|
+
const view = new DataView(data.buffer);
|
|
1285
|
+
let offset = 0;
|
|
1286
|
+
data[offset++] = INSTRUCTION.COMPLETE_DEPOSIT;
|
|
1287
|
+
data.set(params.sweepTxid, offset);
|
|
1288
|
+
offset += 32;
|
|
1289
|
+
view.setBigUint64(offset, BigInt(params.blockHeight), true);
|
|
1290
|
+
offset += 8;
|
|
1291
|
+
view.setUint32(offset, params.sweepTxSize, true);
|
|
1292
|
+
offset += 4;
|
|
1293
|
+
view.setUint32(offset, params.depositTxSize, true);
|
|
1294
|
+
offset += 4;
|
|
1295
|
+
data.set(params.depositTxid, offset);
|
|
1296
|
+
offset += 32;
|
|
1297
|
+
return data;
|
|
1298
|
+
}
|
|
1299
|
+
// =============================================================================
|
|
1300
|
+
// UTXOpia Set Pool Config (disc=2)
|
|
1301
|
+
// =============================================================================
|
|
1302
|
+
/** PoolConfig account discriminator (0x0a) */
|
|
1303
|
+
export const POOL_CONFIG_DISCRIMINATOR = 0x0a;
|
|
1304
|
+
/** Serialized PoolConfig account length (bytes) */
|
|
1305
|
+
export const POOL_CONFIG_LEN = 129;
|
|
1306
|
+
/** Max pool_script (P2TR scriptPubKey) length */
|
|
1307
|
+
export const POOL_SCRIPT_MAX_LEN = 34;
|
|
1308
|
+
/**
|
|
1309
|
+
* Build set_pool_config instruction data (disc=2).
|
|
1310
|
+
*
|
|
1311
|
+
* Strict payload — the program rejects any other shape:
|
|
1312
|
+
* disc(1)
|
|
1313
|
+
* + pool_script_len(1)
|
|
1314
|
+
* + pool_script(N, 1..=34)
|
|
1315
|
+
* + ika_dwallet(32)
|
|
1316
|
+
* + ika_dwallet_xonly_pubkey(32)
|
|
1317
|
+
* + cpi_authority_bump(1)
|
|
1318
|
+
*
|
|
1319
|
+
* `group_pub_key` is no longer part of PoolConfig and must not be sent.
|
|
1320
|
+
*/
|
|
1321
|
+
export function buildSetPoolConfigInstructionData(params) {
|
|
1322
|
+
const { poolScript, ikaDwallet, ikaDwalletXonlyPubkey, cpiAuthorityBump } = params;
|
|
1323
|
+
if (poolScript.length < 1 || poolScript.length > POOL_SCRIPT_MAX_LEN) {
|
|
1324
|
+
throw new Error(`poolScript length must be 1..=${POOL_SCRIPT_MAX_LEN}, got ${poolScript.length}`);
|
|
1325
|
+
}
|
|
1326
|
+
if (ikaDwallet.length !== 32) {
|
|
1327
|
+
throw new Error(`ikaDwallet must be 32 bytes, got ${ikaDwallet.length}`);
|
|
1328
|
+
}
|
|
1329
|
+
if (ikaDwalletXonlyPubkey.length !== 32) {
|
|
1330
|
+
throw new Error(`ikaDwalletXonlyPubkey must be 32 bytes, got ${ikaDwalletXonlyPubkey.length}`);
|
|
1331
|
+
}
|
|
1332
|
+
const data = new Uint8Array(1 + 1 + poolScript.length + 32 + 32 + 1);
|
|
1333
|
+
let offset = 0;
|
|
1334
|
+
data[offset++] = INSTRUCTION.SET_POOL_CONFIG;
|
|
1335
|
+
data[offset++] = poolScript.length;
|
|
1336
|
+
data.set(poolScript, offset);
|
|
1337
|
+
offset += poolScript.length;
|
|
1338
|
+
data.set(ikaDwallet, offset);
|
|
1339
|
+
offset += 32;
|
|
1340
|
+
data.set(ikaDwalletXonlyPubkey, offset);
|
|
1341
|
+
offset += 32;
|
|
1342
|
+
data[offset++] = cpiAuthorityBump;
|
|
1343
|
+
return data;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Parse a PoolConfig account.
|
|
1347
|
+
*
|
|
1348
|
+
* Layout (fixed offsets — pool_script is a 34-byte field regardless of len):
|
|
1349
|
+
* disc(1) @0, pool_script_len(1) @1, pool_script(34) @2,
|
|
1350
|
+
* ika_dwallet(32) @36, ika_dwallet_xonly_pubkey(32) @68,
|
|
1351
|
+
* cpi_authority_bump(1) @100, reserved(28) @101
|
|
1352
|
+
*/
|
|
1353
|
+
export function parsePoolConfig(data) {
|
|
1354
|
+
if (data.length < POOL_CONFIG_LEN) {
|
|
1355
|
+
throw new Error(`PoolConfig account too small: ${data.length} < ${POOL_CONFIG_LEN}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (data[0] !== POOL_CONFIG_DISCRIMINATOR) {
|
|
1358
|
+
throw new Error(`Invalid PoolConfig discriminator: 0x${data[0].toString(16)}`);
|
|
1359
|
+
}
|
|
1360
|
+
const poolScriptLen = data[1];
|
|
1361
|
+
if (poolScriptLen > POOL_SCRIPT_MAX_LEN) {
|
|
1362
|
+
throw new Error(`Invalid pool script length: ${poolScriptLen}`);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
discriminator: data[0],
|
|
1366
|
+
poolScriptLen,
|
|
1367
|
+
poolScript: data.subarray(2, 2 + poolScriptLen),
|
|
1368
|
+
ikaDwallet: data.subarray(36, 68),
|
|
1369
|
+
ikaDwalletXonlyPubkey: data.subarray(68, 100),
|
|
1370
|
+
cpiAuthorityBump: data[100],
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
// =============================================================================
|
|
1374
|
+
// Utility Exports
|
|
1375
|
+
// =============================================================================
|
|
1376
|
+
/**
|
|
1377
|
+
* Bigint to 32-byte Uint8Array (big-endian)
|
|
1378
|
+
*/
|
|
1379
|
+
export function bigintTo32Bytes(value) {
|
|
1380
|
+
const hex = value.toString(16).padStart(64, "0");
|
|
1381
|
+
const bytes = new Uint8Array(32);
|
|
1382
|
+
for (let i = 0; i < 32; i++) {
|
|
1383
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1384
|
+
}
|
|
1385
|
+
return bytes;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* 32-byte Uint8Array to bigint (big-endian)
|
|
1389
|
+
*/
|
|
1390
|
+
export function bytes32ToBigint(bytes) {
|
|
1391
|
+
if (bytes.length !== 32) {
|
|
1392
|
+
throw new Error("Expected 32 bytes");
|
|
1393
|
+
}
|
|
1394
|
+
let hex = "0x";
|
|
1395
|
+
for (let i = 0; i < 32; i++) {
|
|
1396
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
1397
|
+
}
|
|
1398
|
+
return BigInt(hex);
|
|
1399
|
+
}
|
|
1400
|
+
// hexToBytes / bytesToHex live in ./crypto (single source); re-exported here
|
|
1401
|
+
// to preserve this module's public surface.
|
|
1402
|
+
export { hexToBytes, bytesToHex } from "./crypto";
|
|
1403
|
+
/**
|
|
1404
|
+
* Build initializePermissioned instruction data (disc=21).
|
|
1405
|
+
*
|
|
1406
|
+
* Layout (after disc byte — same as initialize plus two 32-byte fields):
|
|
1407
|
+
* pool_bump(1) + tree_bump(1) + deposit_fee_bps(2 LE) + withdrawal_fee_bps(2 LE)
|
|
1408
|
+
* + auditor(32) + auditor_viewing_pubkey(32)
|
|
1409
|
+
* = 70 bytes of payload; 71 bytes total with disc.
|
|
1410
|
+
*/
|
|
1411
|
+
export function buildInitializePermissionedInstructionData(options) {
|
|
1412
|
+
if (options.auditor.length !== 32) {
|
|
1413
|
+
throw new Error(`auditor must be 32 bytes, got ${options.auditor.length}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (options.auditorViewingPubkey.length !== 32) {
|
|
1416
|
+
throw new Error(`auditorViewingPubkey must be 32 bytes, got ${options.auditorViewingPubkey.length}`);
|
|
1417
|
+
}
|
|
1418
|
+
// disc(1) + pool_bump(1) + tree_bump(1) + deposit_fee_bps(2) + withdrawal_fee_bps(2) + auditor(32) + auditor_viewing_pubkey(32) = 71
|
|
1419
|
+
const data = new Uint8Array(71);
|
|
1420
|
+
const view = new DataView(data.buffer);
|
|
1421
|
+
let offset = 0;
|
|
1422
|
+
data[offset++] = PERMISSIONED_DISC.INITIALIZE_PERMISSIONED; // disc = 21
|
|
1423
|
+
data[offset++] = options.poolBump;
|
|
1424
|
+
data[offset++] = options.treeBump;
|
|
1425
|
+
view.setUint16(offset, options.depositFeeBps, true);
|
|
1426
|
+
offset += 2;
|
|
1427
|
+
view.setUint16(offset, options.withdrawalFeeBps, true);
|
|
1428
|
+
offset += 2;
|
|
1429
|
+
data.set(options.auditor, offset);
|
|
1430
|
+
offset += 32;
|
|
1431
|
+
data.set(options.auditorViewingPubkey, offset);
|
|
1432
|
+
return data;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Build a complete initializePermissioned instruction (disc=21).
|
|
1436
|
+
*
|
|
1437
|
+
* Initializes a pool in permissioned mode; deposits/shields require auditor co-signing.
|
|
1438
|
+
*
|
|
1439
|
+
* Accounts (identical to initialize, disc=0):
|
|
1440
|
+
* 0. pool_state (writable)
|
|
1441
|
+
* 1. commitment_tree (writable)
|
|
1442
|
+
* 2. zkbtc_mint (writable)
|
|
1443
|
+
* 3. pool_vault (writable)
|
|
1444
|
+
* 4. deposit_vault (writable)
|
|
1445
|
+
* 5. authority (writable signer)
|
|
1446
|
+
* 6. system_program (readonly)
|
|
1447
|
+
*/
|
|
1448
|
+
export function buildInitializePermissionedInstruction(options) {
|
|
1449
|
+
const config = getConfig();
|
|
1450
|
+
const data = buildInitializePermissionedInstructionData({
|
|
1451
|
+
poolBump: options.poolBump,
|
|
1452
|
+
treeBump: options.treeBump,
|
|
1453
|
+
depositFeeBps: options.depositFeeBps,
|
|
1454
|
+
withdrawalFeeBps: options.withdrawalFeeBps,
|
|
1455
|
+
auditor: options.auditor,
|
|
1456
|
+
auditorViewingPubkey: options.auditorViewingPubkey,
|
|
1457
|
+
});
|
|
1458
|
+
return {
|
|
1459
|
+
programAddress: config.utxopiaProgramId,
|
|
1460
|
+
accounts: [
|
|
1461
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1462
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
1463
|
+
{ address: options.accounts.zkbtcMint, role: AccountRole.WRITABLE },
|
|
1464
|
+
{ address: options.accounts.poolVault, role: AccountRole.WRITABLE },
|
|
1465
|
+
{ address: options.accounts.depositVault, role: AccountRole.WRITABLE },
|
|
1466
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
1467
|
+
{ address: options.accounts.systemProgram, role: AccountRole.READONLY },
|
|
1468
|
+
],
|
|
1469
|
+
data,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Build completeDepositPermissioned instruction data (disc=22).
|
|
1474
|
+
*
|
|
1475
|
+
* Layout:
|
|
1476
|
+
* disc(1) + CompleteDepositData fixed header (80 bytes) + auditorCiphertext (variable)
|
|
1477
|
+
*
|
|
1478
|
+
* The 80-byte header is identical to complete_deposit (disc=11).
|
|
1479
|
+
*/
|
|
1480
|
+
export function buildCompleteDepositPermissionedInstructionData(options) {
|
|
1481
|
+
// disc(1) + sweep_txid(32) + block_height(8) + sweep_tx_size(4) + deposit_tx_size(4) + deposit_txid(32) + auditorCiphertext(variable)
|
|
1482
|
+
const auditorCiphertext = resolveAuditorCiphertext(options.auditorCiphertext);
|
|
1483
|
+
const headerSize = 1 + 32 + 8 + 4 + 4 + 32; // 81 bytes
|
|
1484
|
+
const data = new Uint8Array(headerSize + auditorCiphertext.length);
|
|
1485
|
+
const view = new DataView(data.buffer);
|
|
1486
|
+
let offset = 0;
|
|
1487
|
+
data[offset++] = PERMISSIONED_DISC.COMPLETE_DEPOSIT_PERMISSIONED; // disc = 22
|
|
1488
|
+
data.set(options.sweepTxid, offset);
|
|
1489
|
+
offset += 32;
|
|
1490
|
+
view.setBigUint64(offset, BigInt(options.blockHeight), true);
|
|
1491
|
+
offset += 8;
|
|
1492
|
+
view.setUint32(offset, options.sweepTxSize, true);
|
|
1493
|
+
offset += 4;
|
|
1494
|
+
view.setUint32(offset, options.depositTxSize, true);
|
|
1495
|
+
offset += 4;
|
|
1496
|
+
data.set(options.depositTxid, offset);
|
|
1497
|
+
offset += 32;
|
|
1498
|
+
if (auditorCiphertext.length > 0) {
|
|
1499
|
+
data.set(auditorCiphertext, offset);
|
|
1500
|
+
}
|
|
1501
|
+
return data;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Build a complete completeDepositPermissioned instruction (disc=22).
|
|
1505
|
+
*
|
|
1506
|
+
* Same accounts as complete_deposit (disc=11), plus a one-time PolicyApproval
|
|
1507
|
+
* appended at account index 15.
|
|
1508
|
+
*
|
|
1509
|
+
* Accounts:
|
|
1510
|
+
* 0. pool_state (writable)
|
|
1511
|
+
* 1. verified_transaction(readonly)
|
|
1512
|
+
* 2. light_client (readonly)
|
|
1513
|
+
* 3. commitment_tree (writable)
|
|
1514
|
+
* 4. tx_buffer (readonly)
|
|
1515
|
+
* 5. authority (writable signer)
|
|
1516
|
+
* 6. system_program (readonly)
|
|
1517
|
+
* 7. zkbtc_mint (writable)
|
|
1518
|
+
* 8. pool_vault (writable)
|
|
1519
|
+
* 9. token_program (readonly)
|
|
1520
|
+
* 10. deposit_tx_buffer (readonly)
|
|
1521
|
+
* 11. deposit_receipt (writable)
|
|
1522
|
+
* 12. utxo_record (writable)
|
|
1523
|
+
* 13. token_config (writable)
|
|
1524
|
+
* 14. pool_config (readonly)
|
|
1525
|
+
* 15. policy_approval (writable)
|
|
1526
|
+
*/
|
|
1527
|
+
export function buildCompleteDepositPermissionedInstruction(options) {
|
|
1528
|
+
const config = getConfig();
|
|
1529
|
+
const data = buildCompleteDepositPermissionedInstructionData({
|
|
1530
|
+
sweepTxid: options.sweepTxid,
|
|
1531
|
+
blockHeight: options.blockHeight,
|
|
1532
|
+
sweepTxSize: options.sweepTxSize,
|
|
1533
|
+
depositTxSize: options.depositTxSize,
|
|
1534
|
+
depositTxid: options.depositTxid,
|
|
1535
|
+
auditorCiphertext: options.auditorCiphertext,
|
|
1536
|
+
});
|
|
1537
|
+
return {
|
|
1538
|
+
programAddress: config.utxopiaProgramId,
|
|
1539
|
+
accounts: [
|
|
1540
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1541
|
+
{ address: options.accounts.verifiedTransaction, role: AccountRole.READONLY },
|
|
1542
|
+
{ address: options.accounts.lightClient, role: AccountRole.READONLY },
|
|
1543
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
1544
|
+
{ address: options.accounts.txBuffer, role: AccountRole.READONLY },
|
|
1545
|
+
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
1546
|
+
{ address: options.accounts.systemProgram, role: AccountRole.READONLY },
|
|
1547
|
+
{ address: options.accounts.zkbtcMint, role: AccountRole.WRITABLE },
|
|
1548
|
+
{ address: options.accounts.poolVault, role: AccountRole.WRITABLE },
|
|
1549
|
+
{ address: options.accounts.tokenProgram, role: AccountRole.READONLY },
|
|
1550
|
+
{ address: options.accounts.depositTxBuffer, role: AccountRole.READONLY },
|
|
1551
|
+
{ address: options.accounts.depositReceipt, role: AccountRole.WRITABLE },
|
|
1552
|
+
{ address: options.accounts.utxoRecord, role: AccountRole.WRITABLE },
|
|
1553
|
+
{ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE },
|
|
1554
|
+
{ address: options.accounts.poolConfig, role: AccountRole.READONLY },
|
|
1555
|
+
{ address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
|
|
1556
|
+
{ address: config.policyProgramId ?? config.utxopiaProgramId, role: AccountRole.READONLY },
|
|
1557
|
+
],
|
|
1558
|
+
data,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Build shieldPermissioned instruction data (disc=23).
|
|
1563
|
+
*
|
|
1564
|
+
* Layout:
|
|
1565
|
+
* disc(1) + shield header (72 bytes) + auditorCiphertext (variable)
|
|
1566
|
+
*
|
|
1567
|
+
* The 72-byte header is identical to shield (disc=12):
|
|
1568
|
+
* amount(8 LE) + npk(32) + ephemeral_pub(32)
|
|
1569
|
+
*/
|
|
1570
|
+
export function buildShieldPermissionedInstructionData(options) {
|
|
1571
|
+
// disc(1) + amount(8) + npk(32) + ephemeral_pub(32) + auditorCiphertext(variable)
|
|
1572
|
+
const auditorCiphertext = resolveAuditorCiphertext(options.auditorCiphertext);
|
|
1573
|
+
const headerSize = 1 + 8 + 32 + 32; // 73 bytes
|
|
1574
|
+
const data = new Uint8Array(headerSize + auditorCiphertext.length);
|
|
1575
|
+
const view = new DataView(data.buffer);
|
|
1576
|
+
let offset = 0;
|
|
1577
|
+
data[offset++] = PERMISSIONED_DISC.SHIELD_PERMISSIONED; // disc = 23
|
|
1578
|
+
view.setBigUint64(offset, options.amount, true);
|
|
1579
|
+
offset += 8;
|
|
1580
|
+
data.set(options.npk.slice(0, 32), offset);
|
|
1581
|
+
offset += 32;
|
|
1582
|
+
data.set(options.ephemeralPub.slice(0, 32), offset);
|
|
1583
|
+
offset += 32;
|
|
1584
|
+
if (auditorCiphertext.length > 0) {
|
|
1585
|
+
data.set(auditorCiphertext, offset);
|
|
1586
|
+
}
|
|
1587
|
+
return data;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Build a complete shieldPermissioned instruction (disc=23).
|
|
1591
|
+
*
|
|
1592
|
+
* Same accounts as shield (disc=12), plus three appended.
|
|
1593
|
+
*
|
|
1594
|
+
* Accounts:
|
|
1595
|
+
* 0. user (writable signer)
|
|
1596
|
+
* 1. user_token_account(writable)
|
|
1597
|
+
* 2. pool_state (writable)
|
|
1598
|
+
* 3. token_config (writable)
|
|
1599
|
+
* 4. vault (writable)
|
|
1600
|
+
* 5. commitment_tree (writable)
|
|
1601
|
+
* 6. token_program (readonly)
|
|
1602
|
+
* 7. policy_approval (writable)
|
|
1603
|
+
* 8. policy_program (readonly)
|
|
1604
|
+
* 9. exit_destination (readonly) — the depositor's registered exit
|
|
1605
|
+
*/
|
|
1606
|
+
export function buildShieldPermissionedInstruction(options) {
|
|
1607
|
+
const config = getConfig();
|
|
1608
|
+
const data = buildShieldPermissionedInstructionData({
|
|
1609
|
+
amount: options.amount,
|
|
1610
|
+
npk: options.npk,
|
|
1611
|
+
ephemeralPub: options.ephemeralPub,
|
|
1612
|
+
auditorCiphertext: options.auditorCiphertext,
|
|
1613
|
+
});
|
|
1614
|
+
return {
|
|
1615
|
+
programAddress: config.utxopiaProgramId,
|
|
1616
|
+
accounts: [
|
|
1617
|
+
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
1618
|
+
{ address: options.accounts.userTokenAccount, role: AccountRole.WRITABLE },
|
|
1619
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1620
|
+
{ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE },
|
|
1621
|
+
{ address: options.accounts.vault, role: AccountRole.WRITABLE },
|
|
1622
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
1623
|
+
{ address: options.accounts.tokenProgram, role: AccountRole.READONLY },
|
|
1624
|
+
{ address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
|
|
1625
|
+
{ address: config.policyProgramId ?? config.utxopiaProgramId, role: AccountRole.READONLY },
|
|
1626
|
+
{ address: options.accounts.exitDestination, role: AccountRole.READONLY },
|
|
1627
|
+
],
|
|
1628
|
+
data,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
// ---------------------------------------------------------------------------
|
|
1632
|
+
// registerExitDestination (disc=39)
|
|
1633
|
+
// ---------------------------------------------------------------------------
|
|
1634
|
+
/**
|
|
1635
|
+
* Build a registerExitDestination instruction (disc=39) — auditor-only.
|
|
1636
|
+
*
|
|
1637
|
+
* Adds one entry to a permissioned pool's append-only exit registry. Entries can
|
|
1638
|
+
* be added but never removed: a removable entry would hand the auditor back the
|
|
1639
|
+
* ability to prevent a withdrawal, which is the one power the design withholds.
|
|
1640
|
+
*
|
|
1641
|
+
* Register a depositor's address BEFORE their first `shieldPermissioned` — the
|
|
1642
|
+
* program refuses a depositor who has no exit.
|
|
1643
|
+
*
|
|
1644
|
+
* Data: kind(1) + key(32)
|
|
1645
|
+
* Accounts: 0. auditor (writable signer, pays rent)
|
|
1646
|
+
* 1. pool_state (readonly)
|
|
1647
|
+
* 2. exit_destination PDA (writable, uninitialized)
|
|
1648
|
+
* 3. system_program (readonly)
|
|
1649
|
+
*/
|
|
1650
|
+
/**
|
|
1651
|
+
* Build registerExitDestination instruction data (disc=39).
|
|
1652
|
+
*
|
|
1653
|
+
* Layout: disc(1) + kind(1) + key(32)
|
|
1654
|
+
*/
|
|
1655
|
+
export function buildRegisterExitDestinationInstructionData(options) {
|
|
1656
|
+
if (options.key.length !== 32) {
|
|
1657
|
+
throw new Error("exit destination key must be 32 bytes");
|
|
1658
|
+
}
|
|
1659
|
+
const data = new Uint8Array(34);
|
|
1660
|
+
data[0] = PERMISSIONED_DISC.REGISTER_EXIT_DESTINATION;
|
|
1661
|
+
data[1] = options.kind;
|
|
1662
|
+
data.set(options.key, 2);
|
|
1663
|
+
return data;
|
|
1664
|
+
}
|
|
1665
|
+
export function buildRegisterExitDestinationInstruction(options) {
|
|
1666
|
+
const config = getConfig();
|
|
1667
|
+
const data = buildRegisterExitDestinationInstructionData(options);
|
|
1668
|
+
return {
|
|
1669
|
+
programAddress: config.utxopiaProgramId,
|
|
1670
|
+
accounts: [
|
|
1671
|
+
{ address: options.accounts.auditor, role: AccountRole.WRITABLE_SIGNER },
|
|
1672
|
+
{ address: options.accounts.poolState, role: AccountRole.READONLY },
|
|
1673
|
+
{ address: options.accounts.exitDestination, role: AccountRole.WRITABLE },
|
|
1674
|
+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
1675
|
+
],
|
|
1676
|
+
data,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Build setAuditorFrozen instruction data (disc=28).
|
|
1681
|
+
*
|
|
1682
|
+
* Layout: disc(1) + frozen(1) — frozen byte: 0 = not frozen, 1 = frozen.
|
|
1683
|
+
*/
|
|
1684
|
+
export function buildSetAuditorFrozenInstructionData(frozen) {
|
|
1685
|
+
return new Uint8Array([INSTRUCTION.SET_AUDITOR_FROZEN, frozen ? 1 : 0]);
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Build a complete setAuditorFrozen instruction (disc=28).
|
|
1689
|
+
*
|
|
1690
|
+
* Accounts:
|
|
1691
|
+
* 0. pool_state (writable)
|
|
1692
|
+
* 1. auditor (signer)
|
|
1693
|
+
*/
|
|
1694
|
+
export function buildSetAuditorFrozenInstruction(options) {
|
|
1695
|
+
const config = getConfig();
|
|
1696
|
+
return {
|
|
1697
|
+
programAddress: config.utxopiaProgramId,
|
|
1698
|
+
accounts: [
|
|
1699
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1700
|
+
{ address: options.accounts.auditor, role: AccountRole.READONLY_SIGNER },
|
|
1701
|
+
],
|
|
1702
|
+
data: buildSetAuditorFrozenInstructionData(options.frozen),
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Build setAuditorViewingPubkey instruction data (disc=29).
|
|
1707
|
+
*
|
|
1708
|
+
* Layout: disc(1) + viewing_pubkey(32) = 33 bytes.
|
|
1709
|
+
*/
|
|
1710
|
+
export function buildSetAuditorViewingPubkeyInstructionData(viewingPubkey) {
|
|
1711
|
+
if (viewingPubkey.length !== 32) {
|
|
1712
|
+
throw new Error(`viewingPubkey must be 32 bytes, got ${viewingPubkey.length}`);
|
|
1713
|
+
}
|
|
1714
|
+
const data = new Uint8Array(33);
|
|
1715
|
+
data[0] = INSTRUCTION.SET_AUDITOR_VIEWING_PUBKEY;
|
|
1716
|
+
data.set(viewingPubkey, 1);
|
|
1717
|
+
return data;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Build a complete setAuditorViewingPubkey instruction (disc=29).
|
|
1721
|
+
*
|
|
1722
|
+
* Accounts:
|
|
1723
|
+
* 0. pool_state (writable)
|
|
1724
|
+
* 1. auditor (signer)
|
|
1725
|
+
*/
|
|
1726
|
+
export function buildSetAuditorViewingPubkeyInstruction(options) {
|
|
1727
|
+
const config = getConfig();
|
|
1728
|
+
return {
|
|
1729
|
+
programAddress: config.utxopiaProgramId,
|
|
1730
|
+
accounts: [
|
|
1731
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1732
|
+
{ address: options.accounts.auditor, role: AccountRole.READONLY_SIGNER },
|
|
1733
|
+
],
|
|
1734
|
+
data: buildSetAuditorViewingPubkeyInstructionData(options.viewingPubkey),
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
export function buildRotateAuditorInstructionData(auditor, viewingPubkey) {
|
|
1738
|
+
if (auditor.length !== 32 || auditor.every((byte) => byte === 0)) {
|
|
1739
|
+
throw new Error("auditor must be a nonzero 32-byte public key");
|
|
1740
|
+
}
|
|
1741
|
+
if (viewingPubkey.length !== 32 || viewingPubkey.every((byte) => byte === 0)) {
|
|
1742
|
+
throw new Error("viewingPubkey must be a nonzero 32-byte public key");
|
|
1743
|
+
}
|
|
1744
|
+
const data = new Uint8Array(65);
|
|
1745
|
+
data[0] = INSTRUCTION.ROTATE_AUDITOR;
|
|
1746
|
+
data.set(auditor, 1);
|
|
1747
|
+
data.set(viewingPubkey, 33);
|
|
1748
|
+
return data;
|
|
1749
|
+
}
|
|
1750
|
+
export function buildRotateAuditorInstruction(options) {
|
|
1751
|
+
const config = getConfig();
|
|
1752
|
+
return {
|
|
1753
|
+
programAddress: config.utxopiaProgramId,
|
|
1754
|
+
accounts: [
|
|
1755
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1756
|
+
{ address: options.accounts.authority, role: AccountRole.READONLY_SIGNER },
|
|
1757
|
+
],
|
|
1758
|
+
data: buildRotateAuditorInstructionData(options.auditor, options.viewingPubkey),
|
|
1759
|
+
};
|
|
1760
|
+
}
|