@utxopia/sdk 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +12 -19
- package/dist/commitment-tree.js +10 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/instructions.d.ts +89 -11
- package/dist/instructions.js +117 -9
- package/dist/pda.d.ts +13 -0
- package/dist/pda.js +25 -0
- package/package.json +1 -1
- package/src/client.ts +13 -17
- package/src/commitment-tree.ts +10 -1
- package/src/index.ts +8 -0
- package/src/instructions.ts +175 -18
- package/src/pda.ts +34 -0
package/dist/client.js
CHANGED
|
@@ -25,6 +25,7 @@ import { setupKeysFromWallet, setupKeysFromSeed, setupKeysFromAuthSignature, rec
|
|
|
25
25
|
import { scanUnifiedNotesMulti, scanAnnouncementsViewOnlyMulti, computeNullifierHashForNote, computeNullifierBytes, isDepositForViewerHex, createDepositFromConfig, createTweakDeposit, depositViewingNode, createStealthOutputWithKeys, } from "./stealth";
|
|
26
26
|
import { selectUtxos } from "./psbt";
|
|
27
27
|
import { hexToBytes } from "./crypto";
|
|
28
|
+
import { getAddressEncoder } from "@solana/kit";
|
|
28
29
|
import { EventClient } from "./event-client";
|
|
29
30
|
// ─── Client ─────────────────────────────────────────────────────────
|
|
30
31
|
let _instance = null;
|
|
@@ -183,25 +184,17 @@ export class UTXOpiaClient {
|
|
|
183
184
|
const cached = this._tokenIdCache.get(mintAddress);
|
|
184
185
|
if (cached !== undefined)
|
|
185
186
|
return cached;
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
bytes = new PublicKey(mintAddress).toBytes();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// Lazy on purpose: a base58 mint padded to 64 is not hex, so computing
|
|
202
|
-
// this eagerly threw before the branch above ever ran.
|
|
203
|
-
bytes = hexToBytes(mintAddress.padStart(64, "0"));
|
|
204
|
-
}
|
|
187
|
+
// 64 hex chars is a raw token id; anything else is a base58 mint.
|
|
188
|
+
//
|
|
189
|
+
// Decoded with getAddressEncoder, the same path pda.ts uses, because
|
|
190
|
+
// require("@solana/web3.js") is not callable from an ESM browser bundle —
|
|
191
|
+
// it threw for every base58 mint, and the hex fallback behind it threw
|
|
192
|
+
// again on the same input. There is no fallback now: both branches are
|
|
193
|
+
// total for their input, and a malformed mint should raise, not be padded
|
|
194
|
+
// into a different token's id.
|
|
195
|
+
const bytes = mintAddress.length === 64 && /^[0-9a-fA-F]+$/.test(mintAddress)
|
|
196
|
+
? hexToBytes(mintAddress)
|
|
197
|
+
: new Uint8Array(getAddressEncoder().encode(mintAddress));
|
|
205
198
|
const tokenId = computeTokenId(bytes);
|
|
206
199
|
this._tokenIdCache.set(mintAddress, tokenId);
|
|
207
200
|
return tokenId;
|
package/dist/commitment-tree.js
CHANGED
|
@@ -189,7 +189,16 @@ export class CommitmentTreeIndex {
|
|
|
189
189
|
if (leafIndex >= MAX_LEAVES) {
|
|
190
190
|
throw new Error("Tree is full");
|
|
191
191
|
}
|
|
192
|
-
// Store in map for lookup
|
|
192
|
+
// Store in map for lookup.
|
|
193
|
+
//
|
|
194
|
+
// Test-only helper, and this map is why: a commitment is NOT unique in the
|
|
195
|
+
// tree. Paying the same BTC deposit address twice for the same amount yields
|
|
196
|
+
// byte-identical commitments (complete_deposit hashes note_public_key,
|
|
197
|
+
// token_id and shielded_amount), so a second add overwrites the first and
|
|
198
|
+
// getMerkleProof then returns only the later leaf. Production never takes
|
|
199
|
+
// this path — announcements dedupe on leafIndex and proofs come from
|
|
200
|
+
// /api/tree/proof/:leaf_index — but do not promote this class to a wallet
|
|
201
|
+
// path without keying on leafIndex instead.
|
|
193
202
|
const commitmentHex = commitment.toString(16).padStart(64, "0");
|
|
194
203
|
this.commitments.set(commitmentHex, { index: leafIndex, amount });
|
|
195
204
|
// Add to leaves array
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { formatSpendDoc, renderSpendDoc, SpendDocMismatch, type SpendDoc, type S
|
|
|
43
43
|
export { getConfig, setConfig, createConfig, initConfig, DEVNET_CONFIG, DEVNET_REGTEST_CONFIG, MAINNET_CONFIG, LOCALNET_CONFIG, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ATA_PROGRAM_ID, UTXOPIA_POLICY_PROGRAM_ID, SDK_VERSION, DEPLOYMENT_INFO, JOINSPLIT_TREE_DEPTH, type NetworkConfig, type NetworkType, } from "./config";
|
|
44
44
|
export { POOL_STATE_DISCRIMINATOR, POOL_STATE_LEN, POOL_STATE_OFFSETS, POOL_FLAG, parsePoolState, parsePoolFees, BPS_DENOMINATOR, computeBpsFee, feeShareBps, } from "./pool-state";
|
|
45
45
|
export type { PoolState, PoolFees } from "./pool-state";
|
|
46
|
-
export { PDA_SEEDS, poolStateSeeds, commitmentTreeSeeds, tokenConfigSeeds, poolConfigSeeds, nullifierRecordSeeds, redemptionRequestSeeds, vkRegistrySeeds, depositReceiptSeeds, policyApprovalSeeds, exitDestinationSeeds, lightClientSeeds, blockHeaderSeeds, heightIndexSeeds, verifiedTransactionSeeds, derivePoolStatePDA, deriveCommitmentTreePDA, deriveNullifierRecordPDA, derivePolicyApprovalPDA, deriveExitDestinationPDA, EXIT_KIND_SOLANA_OWNER, EXIT_KIND_BTC_SCRIPT, deriveLightClientPDA, deriveBlockHeaderPDA, deriveHeightIndexPDA, deriveVkRegistryPDA, deriveRedemptionRequestPDA, deriveTokenConfigPDA, derivePoolConfigPDA, deriveDepositReceiptPDA, commitmentToBytes, } from "./pda";
|
|
46
|
+
export { PDA_SEEDS, poolStateSeeds, commitmentTreeSeeds, tokenConfigSeeds, poolConfigSeeds, nullifierRecordSeeds, redemptionRequestSeeds, vkRegistrySeeds, depositReceiptSeeds, policyApprovalSeeds, exitDestinationSeeds, lightClientSeeds, blockHeaderSeeds, heightIndexSeeds, verifiedTransactionSeeds, derivePoolStatePDA, deriveCommitmentTreePDA, deriveNullifierRecordPDA, deriveQueuedLeafPDA, queuedLeafSeeds, derivePolicyApprovalPDA, deriveExitDestinationPDA, EXIT_KIND_SOLANA_OWNER, EXIT_KIND_BTC_SCRIPT, deriveLightClientPDA, deriveBlockHeaderPDA, deriveHeightIndexPDA, deriveVkRegistryPDA, deriveRedemptionRequestPDA, deriveTokenConfigPDA, derivePoolConfigPDA, deriveDepositReceiptPDA, commitmentToBytes, } from "./pda";
|
|
47
47
|
export { createStealthDeposit, createStealthDepositWithKeys, createStealthOutput, createStealthOutputWithKeys, createStealthOutputForCommitment, packStealthOutputForCircuit, scanAnnouncements, scanAnnouncementsViewOnly, scanAnnouncementsViewOnlyMulti, exportViewOnlyKeys, encodeViewOnlyKeys, decodeViewOnlyKeys, prepareClaimInputs, scanUnifiedNotes, scanUnifiedNotesMulti, encryptAmount, decryptAmount, computeNullifierHashForNote, computeNullifierBytes, parseAnnouncementsFromHex, createDepositFromConfig, createDirectVaultDeposit, createTweakDeposit, depositEphemeralKeyPair, depositViewingNode, outgoingViewingNode, outgoingEphemeralKeyPair, findNextSendIndex, isDepositForViewer, isDepositForViewerHex, ANNOUNCEMENT_TYPE_DEPOSIT, ANNOUNCEMENT_TYPE_TRANSFER, type StealthDeposit, type StealthOutputData, type StealthOutputWithKeys, type CircuitStealthOutput, type ScannedNote, type ClaimInputs as StealthClaimInputs, type OnChainStealthAnnouncement, type ConnectionAdapter, type ViewOnlyKeys, type ViewOnlyScannedNote, createNonInteractiveDeposit, pickIkaCustodyKey, type NonInteractiveDepositResult, type TweakDepositResult, type DepositRecoveryMaterial, type OutgoingRecoveryMaterial, type NonInteractiveDepositWithRefundResult, } from "./stealth";
|
|
48
48
|
export { buildDepositPsbt, estimateDepositFee, fetchUtxos, selectUtxos, type BuildDepositPsbtParams, type BuildDepositPsbtResult, type UtxoDescriptor, } from "./psbt";
|
|
49
49
|
export { EsploraClient, esploraTestnet, esploraMainnet, type EsploraTransaction, type EsploraVin, type EsploraVout, type EsploraStatus, type EsploraAddressInfo, type EsploraUtxo, type EsploraMerkleProof, type EsploraNetwork, } from "./core/esplora";
|
|
@@ -53,7 +53,7 @@ export { setDebug } from "./logger";
|
|
|
53
53
|
export { createFetchConnectionAdapter, createConnectionAdapterFromWeb3, createConnectionAdapterFromKit, getConnectionAdapter, clearConnectionAdapterCache, type RpcConfig, type Web3Connection, type KitRpc, } from "./solana/connection";
|
|
54
54
|
export { resolveSnsName, resolveStealthName, parseSnsStealthData, isSnsStealthAddress, isAuditorDisclosable, SnsComplianceFlags, SNS_COMPLIANCE_AUDITOR_OFFSET, SNS_COMPLIANCE_AUDITOR_BYTES, deriveParentDomainKey, SNS_STEALTH_DATA_SIZE, type SnsStealthAddress, } from "./sns-resolver";
|
|
55
55
|
export { COMMITMENT_TREE_DISCRIMINATOR, parseCommitmentTreeData, isValidRoot, fetchCommitmentTree, getCommitmentIndex, saveCommitmentIndex, CommitmentTreeIndex, buildCommitmentTreeFromChain, fetchLeafIndexForCommitment, fetchMerkleProofForCommitment, getMerkleProofFromTree, type CommitmentTreeState, type RpcClient, type OnChainMerkleProof, } from "./commitment-tree";
|
|
56
|
-
export { INSTRUCTION_DISCRIMINATORS, buildShieldInstructionData, buildShieldInstruction, type ShieldInstructionOptions, buildApproveRedemptionSigningInstructionData, buildApproveRedemptionSigningInstruction, buildCancelRedemptionInstructionData, buildCancelRedemptionInstruction, type CancelRedemptionInstructionOptions, bigintTo32Bytes, bytes32ToBigint, buildTransactInstructionData, buildTransactInstruction, buildRedeemInstructionData, buildUnshieldInstructionData, buildUnshieldInstruction, buildProposePoolUpdateInstructionData, buildProposePoolUpdateInstruction, buildExecutePoolUpdateInstructionData, buildExecutePoolUpdateInstruction, buildCancelPoolUpdateInstructionData, buildCancelPoolUpdateInstruction, buildRotateTreeInstructionData, buildRotateTreeInstruction, type RotateTreeOptions, buildMagicBlockDelegateInstructionData, buildMagicBlockDelegateInstruction, buildMagicBlockCommitInstructionData, buildMagicBlockCommitInstruction, buildMagicBlockPerPermissionInstructionData, buildMagicBlockPerPermissionInstruction, buildPolicyRequestHash, buildPolicyIntentParts, buildRegisterExitDestinationInstruction, buildRegisterExitDestinationInstructionData, MAX_POLICY_INTENT_PARTS, buildInitializePolicyApprovalInstructionData, buildInitializePolicyApprovalInstruction, buildPolicyApprovalDecisionInstruction, buildPolicyApprovalCommitInstruction, buildCompleteDepositPermissionedInstructionData, buildCompleteDepositPermissionedInstruction, buildShieldPermissionedInstructionData, buildShieldPermissionedInstruction, buildRotateAuditorInstructionData, buildRotateAuditorInstruction, type MagicBlockDelegateTarget, type MagicBlockDelegateInstructionOptions, type MagicBlockCommitInstructionOptions, type MagicBlockPerPermissionOperation, type MagicBlockPerPermissionMember, type MagicBlockPerPermissionInstructionOptions, type PolicyApprovalDecision, type InitializePolicyApprovalOptions, type CompleteDepositPermissionedOptions, type ShieldPermissionedInstructionOptions, type RotateAuditorOptions, buildVerifyTransactionInstructionData, buildCompleteDepositInstructionData, buildVerifyDepositInstructionData, buildVerifyDepositPermissionedInstructionData, buildSetPoolConfigInstructionData, parsePoolConfig, type ParsedPoolConfig, POOL_CONFIG_DISCRIMINATOR, POOL_CONFIG_LEN, POOL_SCRIPT_MAX_LEN, type Instruction, type ApproveRedemptionSigningInstructionOptions, type TransactInstructionOptions, type UnshieldInstructionOptions, type ProposePoolUpdateOptions, type ExecutePoolUpdateOptions, type CancelPoolUpdateOptions, } from "./instructions";
|
|
56
|
+
export { INSTRUCTION_DISCRIMINATORS, buildShieldInstructionData, buildShieldInstruction, type ShieldInstructionOptions, buildApproveRedemptionSigningInstructionData, buildApproveRedemptionSigningInstruction, buildCancelRedemptionInstructionData, buildCancelRedemptionInstruction, type CancelRedemptionInstructionOptions, bigintTo32Bytes, bytes32ToBigint, JSFLAGS, joinSplitFlags, buildMergeQueuedLeavesInstruction, MAX_MERGE_LEAVES, buildTransactInstructionData, buildTransactInstruction, buildRedeemInstructionData, buildUnshieldInstructionData, buildUnshieldInstruction, buildProposePoolUpdateInstructionData, buildProposePoolUpdateInstruction, buildExecutePoolUpdateInstructionData, buildExecutePoolUpdateInstruction, buildCancelPoolUpdateInstructionData, buildCancelPoolUpdateInstruction, buildRotateTreeInstructionData, buildRotateTreeInstruction, type RotateTreeOptions, buildMagicBlockDelegateInstructionData, buildMagicBlockDelegateInstruction, buildMagicBlockCommitInstructionData, buildMagicBlockCommitInstruction, buildMagicBlockPerPermissionInstructionData, buildMagicBlockPerPermissionInstruction, buildPolicyRequestHash, buildPolicyIntentParts, buildRegisterExitDestinationInstruction, buildRegisterExitDestinationInstructionData, MAX_POLICY_INTENT_PARTS, buildInitializePolicyApprovalInstructionData, buildInitializePolicyApprovalInstruction, buildPolicyApprovalDecisionInstruction, buildPolicyApprovalCommitInstruction, buildCompleteDepositPermissionedInstructionData, buildCompleteDepositPermissionedInstruction, buildShieldPermissionedInstructionData, buildShieldPermissionedInstruction, buildRotateAuditorInstructionData, buildRotateAuditorInstruction, type MagicBlockDelegateTarget, type JoinSplitTailOptions, type PolicyTailKind, type MagicBlockDelegateInstructionOptions, type MagicBlockCommitInstructionOptions, type MagicBlockPerPermissionOperation, type MagicBlockPerPermissionMember, type MagicBlockPerPermissionInstructionOptions, type PolicyApprovalDecision, type InitializePolicyApprovalOptions, type CompleteDepositPermissionedOptions, type ShieldPermissionedInstructionOptions, type RotateAuditorOptions, buildVerifyTransactionInstructionData, buildCompleteDepositInstructionData, buildVerifyDepositInstructionData, buildVerifyDepositPermissionedInstructionData, buildSetPoolConfigInstructionData, parsePoolConfig, type ParsedPoolConfig, POOL_CONFIG_DISCRIMINATOR, POOL_CONFIG_LEN, POOL_SCRIPT_MAX_LEN, type Instruction, type ApproveRedemptionSigningInstructionOptions, type TransactInstructionOptions, type UnshieldInstructionOptions, type ProposePoolUpdateOptions, type ExecutePoolUpdateOptions, type CancelPoolUpdateOptions, } from "./instructions";
|
|
57
57
|
export { VK_REGISTRY_DISCRIMINATOR, VK_REGISTRY_LEN, MAX_IC_POINTS, MAX_SAFE_JOINSPLIT_SIZE, INIT_VK_REGISTRY_DISCRIMINATOR, UPDATE_VK_REGISTRY_DISCRIMINATOR, joinSplitNumPublicInputs, computeVkHash, vkeyJsonToVkMaterial, buildVkRegistryData, parseVkRegistry, assertVkRegistryForShape, assertVkeyMatchesRegistry, isVkRegistryReady, type JoinSplitVkMaterial, type SnarkjsVkeyJson, type ParsedVkRegistry, } from "./vk-registry";
|
|
58
58
|
export { fetchExplorerDeposits, fetchExplorerTransfers, fetchExplorerRedemptions, parseNullifierRecord, parseRedemptionRequest, NULLIFIER_RECORD_SIZE, REDEMPTION_REQUEST_SIZE, NULLIFIER_RECORD_DISCRIMINATOR, REDEMPTION_REQUEST_DISCRIMINATOR, OPERATION_TYPE_LABELS, type ExplorerDeposit, type ExplorerTransferEvent, type ExplorerRedemption, type IndexerLeaf, } from "./explorer";
|
|
59
59
|
export { parseProgramEvents, parseNullifierSpentEvent, parseStealthAnnouncementEvent, parseSenderMemoEvent, parseBtcOriginAttestationEvent, parseAuditorCiphertextEvent, EVENT_NULLIFIER_SPENT, EVENT_STEALTH_ANNOUNCEMENT, EVENT_NULLIFIERS_BATCH, EVENT_ANNOUNCEMENTS_BATCH, EVENT_SENDER_MEMO, EVENT_BTC_ORIGIN_ATTESTATION, EVENT_AUDITOR_CIPHERTEXT, type NullifierSpentEvent, type StealthAnnouncementEvent, type SenderMemoEvent, type BtcOriginAttestationEvent, type AuditorCiphertextEvent, type ProgramEvent, } from "./events";
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,7 @@ export { PDA_SEEDS,
|
|
|
123
123
|
// Seed builders — the single definition of every PDA. Consumers on
|
|
124
124
|
// @solana/web3.js should derive from these with findProgramAddressSync rather
|
|
125
125
|
// than restating the seeds, which is how copies drift from the program.
|
|
126
|
-
poolStateSeeds, commitmentTreeSeeds, tokenConfigSeeds, poolConfigSeeds, nullifierRecordSeeds, redemptionRequestSeeds, vkRegistrySeeds, depositReceiptSeeds, policyApprovalSeeds, exitDestinationSeeds, lightClientSeeds, blockHeaderSeeds, heightIndexSeeds, verifiedTransactionSeeds, derivePoolStatePDA, deriveCommitmentTreePDA, deriveNullifierRecordPDA, derivePolicyApprovalPDA, deriveExitDestinationPDA, EXIT_KIND_SOLANA_OWNER, EXIT_KIND_BTC_SCRIPT, deriveLightClientPDA, deriveBlockHeaderPDA, deriveHeightIndexPDA, deriveVkRegistryPDA, deriveRedemptionRequestPDA, deriveTokenConfigPDA, derivePoolConfigPDA, deriveDepositReceiptPDA, commitmentToBytes, } from "./pda";
|
|
126
|
+
poolStateSeeds, commitmentTreeSeeds, tokenConfigSeeds, poolConfigSeeds, nullifierRecordSeeds, redemptionRequestSeeds, vkRegistrySeeds, depositReceiptSeeds, policyApprovalSeeds, exitDestinationSeeds, lightClientSeeds, blockHeaderSeeds, heightIndexSeeds, verifiedTransactionSeeds, derivePoolStatePDA, deriveCommitmentTreePDA, deriveNullifierRecordPDA, deriveQueuedLeafPDA, queuedLeafSeeds, derivePolicyApprovalPDA, deriveExitDestinationPDA, EXIT_KIND_SOLANA_OWNER, EXIT_KIND_BTC_SCRIPT, deriveLightClientPDA, deriveBlockHeaderPDA, deriveHeightIndexPDA, deriveVkRegistryPDA, deriveRedemptionRequestPDA, deriveTokenConfigPDA, derivePoolConfigPDA, deriveDepositReceiptPDA, commitmentToBytes, } from "./pda";
|
|
127
127
|
// ==========================================================================
|
|
128
128
|
// Stealth address utilities
|
|
129
129
|
// ==========================================================================
|
|
@@ -169,7 +169,7 @@ buildShieldInstructionData, buildShieldInstruction, buildApproveRedemptionSignin
|
|
|
169
169
|
// Cancel redemption instruction
|
|
170
170
|
buildCancelRedemptionInstructionData, buildCancelRedemptionInstruction, bigintTo32Bytes, bytes32ToBigint,
|
|
171
171
|
// JoinSplit transact instruction
|
|
172
|
-
buildTransactInstructionData, buildTransactInstruction,
|
|
172
|
+
JSFLAGS, joinSplitFlags, buildMergeQueuedLeavesInstruction, MAX_MERGE_LEAVES, buildTransactInstructionData, buildTransactInstruction,
|
|
173
173
|
// JoinSplit + BTC redeem instruction
|
|
174
174
|
buildRedeemInstructionData,
|
|
175
175
|
// Public unshield instruction
|
package/dist/instructions.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare const INSTRUCTION_DISCRIMINATORS: {
|
|
|
46
46
|
readonly APPROVE_REDEMPTION_SIGNING: 27;
|
|
47
47
|
readonly SET_AUDITOR_FROZEN: 28;
|
|
48
48
|
readonly SET_AUDITOR_VIEWING_PUBKEY: 29;
|
|
49
|
+
readonly MERGE_QUEUED_LEAVES: 40;
|
|
49
50
|
readonly MAGICBLOCK_DELEGATE: 32;
|
|
50
51
|
readonly MAGICBLOCK_COMMIT: 33;
|
|
51
52
|
readonly MAGICBLOCK_PER_PERMISSION: 34;
|
|
@@ -280,6 +281,12 @@ export interface TransactInstructionOptions {
|
|
|
280
281
|
accounts: {
|
|
281
282
|
poolState: Address;
|
|
282
283
|
commitmentTree: Address;
|
|
284
|
+
/**
|
|
285
|
+
* Supply one `QueuedLeaf` PDA per output to defer placement. Doing so also
|
|
286
|
+
* flips poolState and commitmentTree to read-only, which is what lets two
|
|
287
|
+
* spends run in the same slot.
|
|
288
|
+
*/
|
|
289
|
+
queuedLeaves?: Address[];
|
|
283
290
|
vkRegistry: Address;
|
|
284
291
|
user: Address;
|
|
285
292
|
/** Nullifier record PDAs (one per input) */
|
|
@@ -288,6 +295,50 @@ export interface TransactInstructionOptions {
|
|
|
288
295
|
policyApproval?: Address;
|
|
289
296
|
};
|
|
290
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* What a JoinSplit declares it is passing, mirroring `jsflags` on-chain.
|
|
300
|
+
*
|
|
301
|
+
* This byte used to be `proof_source`, valued 0 or 1. Bit 0 keeps that exact
|
|
302
|
+
* meaning so no instruction-data offset moved; the rest replaced the program's
|
|
303
|
+
* positional heuristics, which recovered the optional account tail by counting
|
|
304
|
+
* backwards from the end and by asking whether an account "looked like" a
|
|
305
|
+
* commitment tree or carried the policy program's id.
|
|
306
|
+
*
|
|
307
|
+
* A flag only says which slot to read. The program still validates owner,
|
|
308
|
+
* signer and seeds on every slot, so declaring the wrong thing breaks your own
|
|
309
|
+
* transaction rather than buying anything.
|
|
310
|
+
*/
|
|
311
|
+
export declare const JSFLAGS: {
|
|
312
|
+
readonly PROOF_IN_BUFFER: number;
|
|
313
|
+
readonly RELAYER: number;
|
|
314
|
+
readonly FROZEN_SOURCE_TREE: number;
|
|
315
|
+
readonly POLICY: number;
|
|
316
|
+
readonly QUEUED_LEAVES: number;
|
|
317
|
+
readonly RAGEQUIT: number;
|
|
318
|
+
};
|
|
319
|
+
/** Which permissioned tail a spend carries. */
|
|
320
|
+
export type PolicyTailKind = "none" | "verified" | "ragequit";
|
|
321
|
+
export interface JoinSplitTailOptions {
|
|
322
|
+
/** 0=inline proof (default), 1=proof in a separate ChadBuffer account */
|
|
323
|
+
proofSource?: 0 | 1;
|
|
324
|
+
/** A relayer signs and pays instead of the note owner. transact only. */
|
|
325
|
+
hasRelayer?: boolean;
|
|
326
|
+
/** A rotated-out CommitmentTree proves membership of pre-rotation notes. */
|
|
327
|
+
hasFrozenSourceTree?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Permissioned tail. "verified" appends (approval, policyProgram);
|
|
330
|
+
* "ragequit" appends one registered ExitDestination per public output.
|
|
331
|
+
* Must match the pool: the program cross-checks against pool.permissioned().
|
|
332
|
+
*/
|
|
333
|
+
policyTail?: PolicyTailKind;
|
|
334
|
+
/** Outputs are queued as QueuedLeaf PDAs instead of inserted inline. */
|
|
335
|
+
hasQueuedLeaves?: boolean;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Assemble the flags byte. Kept in one place so the data builders and the
|
|
339
|
+
* account builders cannot disagree about what was declared.
|
|
340
|
+
*/
|
|
341
|
+
export declare function joinSplitFlags(options: JoinSplitTailOptions): number;
|
|
291
342
|
/**
|
|
292
343
|
* Build transact instruction data (JoinSplit)
|
|
293
344
|
*
|
|
@@ -313,11 +364,9 @@ export declare function buildTransactInstructionData(options: {
|
|
|
313
364
|
nullifiers: Uint8Array[];
|
|
314
365
|
commitmentsOut: Uint8Array[];
|
|
315
366
|
stealthData: Uint8Array[];
|
|
316
|
-
/** 0=inline proof (default), 1=proof in separate ChadBuffer account */
|
|
317
|
-
proofSource?: 0 | 1;
|
|
318
367
|
/** Reserved. Sender memos are rejected until they are proof-bound. */
|
|
319
368
|
senderMemos?: Uint8Array[];
|
|
320
|
-
}): Uint8Array;
|
|
369
|
+
} & JoinSplitTailOptions): Uint8Array;
|
|
321
370
|
/**
|
|
322
371
|
* Build a complete JoinSplit transact instruction
|
|
323
372
|
*
|
|
@@ -368,9 +417,7 @@ export declare function buildRedeemInstructionData(options: {
|
|
|
368
417
|
btcScripts: Uint8Array[];
|
|
369
418
|
/** Unique request nonce(s) — single or array */
|
|
370
419
|
requestNonces: bigint[];
|
|
371
|
-
|
|
372
|
-
proofSource?: 0 | 1;
|
|
373
|
-
}): Uint8Array;
|
|
420
|
+
} & JoinSplitTailOptions): Uint8Array;
|
|
374
421
|
/** Unshield instruction options (multi-output) */
|
|
375
422
|
export interface UnshieldInstructionOptions {
|
|
376
423
|
/** Number of input notes being spent */
|
|
@@ -439,9 +486,7 @@ export declare function buildUnshieldInstructionData(options: {
|
|
|
439
486
|
stealthData: Uint8Array[];
|
|
440
487
|
/** Amount(s) being unshielded — single or array */
|
|
441
488
|
unshieldAmounts: bigint[];
|
|
442
|
-
|
|
443
|
-
proofSource?: 0 | 1;
|
|
444
|
-
}): Uint8Array;
|
|
489
|
+
} & JoinSplitTailOptions): Uint8Array;
|
|
445
490
|
/**
|
|
446
491
|
* Build a complete unshield instruction (multi-output, disc=14)
|
|
447
492
|
*
|
|
@@ -572,7 +617,8 @@ export interface MagicBlockDelegateInstructionOptions {
|
|
|
572
617
|
};
|
|
573
618
|
target: MagicBlockDelegateTarget;
|
|
574
619
|
commitFrequencyMs: number;
|
|
575
|
-
validator
|
|
620
|
+
/** Required: the PER's TEE validator. An unpinned delegation is rejected on-chain. */
|
|
621
|
+
validator: Address;
|
|
576
622
|
}
|
|
577
623
|
export interface MagicBlockCommitInstructionOptions {
|
|
578
624
|
accounts: {
|
|
@@ -613,7 +659,7 @@ export interface MagicBlockPerPermissionInstructionOptions {
|
|
|
613
659
|
export declare function buildMagicBlockDelegateInstructionData(options: {
|
|
614
660
|
target: MagicBlockDelegateTarget;
|
|
615
661
|
commitFrequencyMs: number;
|
|
616
|
-
validator
|
|
662
|
+
validator: Address;
|
|
617
663
|
}): Uint8Array;
|
|
618
664
|
/**
|
|
619
665
|
* Build a complete magicblock_delegate instruction.
|
|
@@ -1097,3 +1143,35 @@ export interface RotateAuditorOptions {
|
|
|
1097
1143
|
}
|
|
1098
1144
|
export declare function buildRotateAuditorInstructionData(auditor: Uint8Array, viewingPubkey: Uint8Array): Uint8Array;
|
|
1099
1145
|
export declare function buildRotateAuditorInstruction(options: RotateAuditorOptions): Instruction;
|
|
1146
|
+
export interface MergeQueuedLeavesOptions {
|
|
1147
|
+
accounts: {
|
|
1148
|
+
/** Pays the fee. A relayer, or the holder taking the escape hatch. */
|
|
1149
|
+
caller: Address;
|
|
1150
|
+
poolState: Address;
|
|
1151
|
+
commitmentTree: Address;
|
|
1152
|
+
};
|
|
1153
|
+
/**
|
|
1154
|
+
* The leaves to place, in the order they should land: leaf `i` gets
|
|
1155
|
+
* `first_leaf_index + i`. Each needs the payer recorded in the account —
|
|
1156
|
+
* rent goes there, never to the caller, so racing to merge earns nothing.
|
|
1157
|
+
*/
|
|
1158
|
+
leaves: {
|
|
1159
|
+
queuedLeaf: Address;
|
|
1160
|
+
rentRecipient: Address;
|
|
1161
|
+
}[];
|
|
1162
|
+
}
|
|
1163
|
+
/** Matches MAX_MERGE_LEAVES on-chain: two accounts per leaf under the 64-account cap. */
|
|
1164
|
+
export declare const MAX_MERGE_LEAVES = 24;
|
|
1165
|
+
/**
|
|
1166
|
+
* Place queued commitments into the tree and close their accounts.
|
|
1167
|
+
*
|
|
1168
|
+
* Permissionless: anyone may call this, which is what makes a QueuedLeaf
|
|
1169
|
+
* recoverable without a timeout — no operator can strand a note, because the
|
|
1170
|
+
* holder can merge it themselves.
|
|
1171
|
+
*
|
|
1172
|
+
* Prefer having a relayer call it. Self-merging links a Solana identity to one
|
|
1173
|
+
* specific leaf and the timing says the holder is in a hurry to spend it, so it
|
|
1174
|
+
* is the escape hatch rather than the path. Merge cadence is a privacy
|
|
1175
|
+
* parameter, not only a UX one.
|
|
1176
|
+
*/
|
|
1177
|
+
export declare function buildMergeQueuedLeavesInstruction(options: MergeQueuedLeavesOptions): Instruction;
|
package/dist/instructions.js
CHANGED
|
@@ -58,6 +58,7 @@ const INSTRUCTION = {
|
|
|
58
58
|
SET_AUDITOR_FROZEN: 28,
|
|
59
59
|
SET_AUDITOR_VIEWING_PUBKEY: 29,
|
|
60
60
|
// MagicBlock ER/PER lifecycle helpers (32-33)
|
|
61
|
+
MERGE_QUEUED_LEAVES: 40,
|
|
61
62
|
MAGICBLOCK_DELEGATE: 32,
|
|
62
63
|
MAGICBLOCK_COMMIT: 33,
|
|
63
64
|
MAGICBLOCK_PER_PERMISSION: 34,
|
|
@@ -381,6 +382,47 @@ export function buildCancelRedemptionInstruction(options) {
|
|
|
381
382
|
data,
|
|
382
383
|
};
|
|
383
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* What a JoinSplit declares it is passing, mirroring `jsflags` on-chain.
|
|
387
|
+
*
|
|
388
|
+
* This byte used to be `proof_source`, valued 0 or 1. Bit 0 keeps that exact
|
|
389
|
+
* meaning so no instruction-data offset moved; the rest replaced the program's
|
|
390
|
+
* positional heuristics, which recovered the optional account tail by counting
|
|
391
|
+
* backwards from the end and by asking whether an account "looked like" a
|
|
392
|
+
* commitment tree or carried the policy program's id.
|
|
393
|
+
*
|
|
394
|
+
* A flag only says which slot to read. The program still validates owner,
|
|
395
|
+
* signer and seeds on every slot, so declaring the wrong thing breaks your own
|
|
396
|
+
* transaction rather than buying anything.
|
|
397
|
+
*/
|
|
398
|
+
export const JSFLAGS = {
|
|
399
|
+
PROOF_IN_BUFFER: 1 << 0,
|
|
400
|
+
RELAYER: 1 << 1,
|
|
401
|
+
FROZEN_SOURCE_TREE: 1 << 2,
|
|
402
|
+
POLICY: 1 << 3,
|
|
403
|
+
QUEUED_LEAVES: 1 << 4,
|
|
404
|
+
RAGEQUIT: 1 << 5,
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Assemble the flags byte. Kept in one place so the data builders and the
|
|
408
|
+
* account builders cannot disagree about what was declared.
|
|
409
|
+
*/
|
|
410
|
+
export function joinSplitFlags(options) {
|
|
411
|
+
let flags = 0;
|
|
412
|
+
if ((options.proofSource ?? 0) === 1)
|
|
413
|
+
flags |= JSFLAGS.PROOF_IN_BUFFER;
|
|
414
|
+
if (options.hasRelayer)
|
|
415
|
+
flags |= JSFLAGS.RELAYER;
|
|
416
|
+
if (options.hasFrozenSourceTree)
|
|
417
|
+
flags |= JSFLAGS.FROZEN_SOURCE_TREE;
|
|
418
|
+
if (options.policyTail === "verified")
|
|
419
|
+
flags |= JSFLAGS.POLICY;
|
|
420
|
+
if (options.policyTail === "ragequit")
|
|
421
|
+
flags |= JSFLAGS.RAGEQUIT;
|
|
422
|
+
if (options.hasQueuedLeaves)
|
|
423
|
+
flags |= JSFLAGS.QUEUED_LEAVES;
|
|
424
|
+
return flags;
|
|
425
|
+
}
|
|
384
426
|
/**
|
|
385
427
|
* Build transact instruction data (JoinSplit)
|
|
386
428
|
*
|
|
@@ -425,7 +467,7 @@ export function buildTransactInstructionData(options) {
|
|
|
425
467
|
data[offset++] = nInputs;
|
|
426
468
|
data[offset++] = nOutputs;
|
|
427
469
|
data[offset++] = 0; // n_public_outputs = 0 for transact
|
|
428
|
-
data[offset++] =
|
|
470
|
+
data[offset++] = joinSplitFlags(options);
|
|
429
471
|
// Proof (256 bytes, only in inline mode)
|
|
430
472
|
if (proofSource === 0 && proofBytes) {
|
|
431
473
|
data.set(proofBytes, offset);
|
|
@@ -467,6 +509,11 @@ export function buildTransactInstructionData(options) {
|
|
|
467
509
|
*/
|
|
468
510
|
export function buildTransactInstruction(options) {
|
|
469
511
|
const config = getConfig();
|
|
512
|
+
const queuedLeaves = options.accounts.queuedLeaves ?? [];
|
|
513
|
+
const queued = queuedLeaves.length > 0;
|
|
514
|
+
if (queued && queuedLeaves.length !== options.nOutputs) {
|
|
515
|
+
throw new Error(`Queued placement needs one QueuedLeaf PDA per output: expected ${options.nOutputs}, got ${queuedLeaves.length}`);
|
|
516
|
+
}
|
|
470
517
|
const data = buildTransactInstructionData({
|
|
471
518
|
nInputs: options.nInputs,
|
|
472
519
|
nOutputs: options.nOutputs,
|
|
@@ -477,10 +524,21 @@ export function buildTransactInstruction(options) {
|
|
|
477
524
|
commitmentsOut: options.commitmentsOut,
|
|
478
525
|
stealthData: options.stealthData,
|
|
479
526
|
senderMemos: options.senderMemos,
|
|
527
|
+
// The account list below appends the policy pair exactly when the caller
|
|
528
|
+
// supplies an approval, so the declared tail is derived from the same fact
|
|
529
|
+
// rather than asked for twice.
|
|
530
|
+
policyTail: options.accounts.policyApproval ? "verified" : "none",
|
|
531
|
+
hasQueuedLeaves: queued,
|
|
480
532
|
});
|
|
533
|
+
// A queued spend writes neither shared account — the tree is read for
|
|
534
|
+
// is_valid_root, pool_state for its policy flags — and the program REJECTS
|
|
535
|
+
// them as writable. Sealevel serialises on the declared metas, not on what
|
|
536
|
+
// the program does, so leaving them writable would take both locks and
|
|
537
|
+
// silently cost the parallelism the queue exists for.
|
|
538
|
+
const sharedRole = queued ? AccountRole.READONLY : AccountRole.WRITABLE;
|
|
481
539
|
const accounts = [
|
|
482
|
-
{ address: options.accounts.poolState, role:
|
|
483
|
-
{ address: options.accounts.commitmentTree, role:
|
|
540
|
+
{ address: options.accounts.poolState, role: sharedRole },
|
|
541
|
+
{ address: options.accounts.commitmentTree, role: sharedRole },
|
|
484
542
|
{ address: options.accounts.vkRegistry, role: AccountRole.READONLY },
|
|
485
543
|
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
486
544
|
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
@@ -496,6 +554,11 @@ export function buildTransactInstruction(options) {
|
|
|
496
554
|
role: AccountRole.READONLY,
|
|
497
555
|
});
|
|
498
556
|
}
|
|
557
|
+
// Order matches resolve_joinsplit_tail: relayer, source tree, policy, queued
|
|
558
|
+
// leaves, proof buffer. One PDA per output, in output order.
|
|
559
|
+
for (const leaf of queuedLeaves) {
|
|
560
|
+
accounts.push({ address: leaf, role: AccountRole.WRITABLE });
|
|
561
|
+
}
|
|
499
562
|
return {
|
|
500
563
|
programAddress: config.utxopiaProgramId,
|
|
501
564
|
accounts,
|
|
@@ -578,7 +641,7 @@ export function buildRedeemInstructionData(options) {
|
|
|
578
641
|
data[offset++] = nInputs;
|
|
579
642
|
data[offset++] = nOutputs;
|
|
580
643
|
data[offset++] = nPublicOutputs;
|
|
581
|
-
data[offset++] =
|
|
644
|
+
data[offset++] = joinSplitFlags(options);
|
|
582
645
|
// Proof (256 bytes, only in inline mode)
|
|
583
646
|
if (proofSource === 0 && proofBytes) {
|
|
584
647
|
data.set(proofBytes, offset);
|
|
@@ -669,7 +732,7 @@ export function buildUnshieldInstructionData(options) {
|
|
|
669
732
|
data[offset++] = nInputs;
|
|
670
733
|
data[offset++] = nOutputs;
|
|
671
734
|
data[offset++] = nPublicOutputs;
|
|
672
|
-
data[offset++] =
|
|
735
|
+
data[offset++] = joinSplitFlags(options);
|
|
673
736
|
// Proof (256 bytes, only in inline mode)
|
|
674
737
|
if (proofSource === 0 && proofBytes) {
|
|
675
738
|
data.set(proofBytes, offset);
|
|
@@ -898,14 +961,12 @@ export function buildMagicBlockDelegateInstructionData(options) {
|
|
|
898
961
|
if (options.commitFrequencyMs > 0xffffffff) {
|
|
899
962
|
throw new Error("commitFrequencyMs must fit in u32");
|
|
900
963
|
}
|
|
901
|
-
const data = new Uint8Array(
|
|
964
|
+
const data = new Uint8Array(38);
|
|
902
965
|
const view = new DataView(data.buffer);
|
|
903
966
|
data[0] = INSTRUCTION.MAGICBLOCK_DELEGATE;
|
|
904
967
|
data[1] = magicBlockDelegateTargetByte(options.target);
|
|
905
968
|
view.setUint32(2, options.commitFrequencyMs, true);
|
|
906
|
-
|
|
907
|
-
data.set(addressToBytes(options.validator), 6);
|
|
908
|
-
}
|
|
969
|
+
data.set(addressToBytes(options.validator), 6);
|
|
909
970
|
return data;
|
|
910
971
|
}
|
|
911
972
|
/**
|
|
@@ -1732,3 +1793,50 @@ export function buildRotateAuditorInstruction(options) {
|
|
|
1732
1793
|
data: buildRotateAuditorInstructionData(options.auditor, options.viewingPubkey),
|
|
1733
1794
|
};
|
|
1734
1795
|
}
|
|
1796
|
+
/** Matches MAX_MERGE_LEAVES on-chain: two accounts per leaf under the 64-account cap. */
|
|
1797
|
+
export const MAX_MERGE_LEAVES = 24;
|
|
1798
|
+
/**
|
|
1799
|
+
* Place queued commitments into the tree and close their accounts.
|
|
1800
|
+
*
|
|
1801
|
+
* Permissionless: anyone may call this, which is what makes a QueuedLeaf
|
|
1802
|
+
* recoverable without a timeout — no operator can strand a note, because the
|
|
1803
|
+
* holder can merge it themselves.
|
|
1804
|
+
*
|
|
1805
|
+
* Prefer having a relayer call it. Self-merging links a Solana identity to one
|
|
1806
|
+
* specific leaf and the timing says the holder is in a hurry to spend it, so it
|
|
1807
|
+
* is the escape hatch rather than the path. Merge cadence is a privacy
|
|
1808
|
+
* parameter, not only a UX one.
|
|
1809
|
+
*/
|
|
1810
|
+
export function buildMergeQueuedLeavesInstruction(options) {
|
|
1811
|
+
const config = getConfig();
|
|
1812
|
+
if (options.leaves.length === 0) {
|
|
1813
|
+
throw new Error("merge_queued_leaves needs at least one leaf");
|
|
1814
|
+
}
|
|
1815
|
+
if (options.leaves.length > MAX_MERGE_LEAVES) {
|
|
1816
|
+
throw new Error(`merge_queued_leaves takes at most ${MAX_MERGE_LEAVES} leaves, got ${options.leaves.length}`);
|
|
1817
|
+
}
|
|
1818
|
+
// Duplicates would insert one commitment at two leaf indices, and nullifiers
|
|
1819
|
+
// are derived from (key, leaf index) — the program refuses it, but catching it
|
|
1820
|
+
// here names the problem instead of surfacing NullifierAlreadyUsed.
|
|
1821
|
+
const seen = new Set();
|
|
1822
|
+
for (const { queuedLeaf } of options.leaves) {
|
|
1823
|
+
if (seen.has(queuedLeaf)) {
|
|
1824
|
+
throw new Error(`Duplicate queued leaf in one merge: ${queuedLeaf}`);
|
|
1825
|
+
}
|
|
1826
|
+
seen.add(queuedLeaf);
|
|
1827
|
+
}
|
|
1828
|
+
const accounts = [
|
|
1829
|
+
{ address: options.accounts.caller, role: AccountRole.WRITABLE_SIGNER },
|
|
1830
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
1831
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
1832
|
+
];
|
|
1833
|
+
for (const { queuedLeaf, rentRecipient } of options.leaves) {
|
|
1834
|
+
accounts.push({ address: queuedLeaf, role: AccountRole.WRITABLE });
|
|
1835
|
+
accounts.push({ address: rentRecipient, role: AccountRole.WRITABLE });
|
|
1836
|
+
}
|
|
1837
|
+
return {
|
|
1838
|
+
programAddress: config.utxopiaProgramId,
|
|
1839
|
+
accounts,
|
|
1840
|
+
data: Uint8Array.of(INSTRUCTION.MERGE_QUEUED_LEAVES),
|
|
1841
|
+
};
|
|
1842
|
+
}
|
package/dist/pda.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const PDA_SEEDS: {
|
|
|
21
21
|
readonly VK_REGISTRY: "vk_registry";
|
|
22
22
|
readonly TOKEN_CONFIG: "token_config";
|
|
23
23
|
readonly POOL_CONFIG: "pool_config";
|
|
24
|
+
readonly QUEUED_LEAF: "queued_leaf";
|
|
24
25
|
};
|
|
25
26
|
/** `["pool_state", pool_id]` — pool_id is the pool's zkBTC mint. */
|
|
26
27
|
export declare function poolStateSeeds(poolId: Address | Uint8Array): Uint8Array[];
|
|
@@ -92,6 +93,18 @@ export declare function derivePoolConfigPDA(poolState: Address | Uint8Array, pro
|
|
|
92
93
|
* — re-deriving them would make every already-spent note spendable again.
|
|
93
94
|
*/
|
|
94
95
|
export declare function deriveNullifierRecordPDA(nullifierHash: Uint8Array, poolState: Address | Uint8Array, treeIndex?: number, programId?: Address): Promise<[Address, number]>;
|
|
96
|
+
/**
|
|
97
|
+
* Seeds for a `QueuedLeaf`: `["queued_leaf", pool_state, commitment]`.
|
|
98
|
+
*
|
|
99
|
+
* The commitment is the seed because it is already unique per output — which
|
|
100
|
+
* makes a second attempt to queue the same commitment fail on account creation
|
|
101
|
+
* rather than silently produce a second leaf for one note.
|
|
102
|
+
*/
|
|
103
|
+
export declare function queuedLeafSeeds(poolState: Address | Uint8Array, commitment: Uint8Array): Uint8Array[];
|
|
104
|
+
/**
|
|
105
|
+
* Derive the `QueuedLeaf` PDA a queued spend creates for one output.
|
|
106
|
+
*/
|
|
107
|
+
export declare function deriveQueuedLeafPDA(poolState: Address | Uint8Array, commitment: Uint8Array, programId?: Address): Promise<[Address, number]>;
|
|
95
108
|
/**
|
|
96
109
|
* Derive one-time PolicyApproval PDA.
|
|
97
110
|
* Seeds: ["policy_approval", pool_state, request_hash, nonce]
|
package/dist/pda.js
CHANGED
|
@@ -33,6 +33,7 @@ export const PDA_SEEDS = {
|
|
|
33
33
|
VK_REGISTRY: "vk_registry",
|
|
34
34
|
TOKEN_CONFIG: "token_config",
|
|
35
35
|
POOL_CONFIG: "pool_config",
|
|
36
|
+
QUEUED_LEAF: "queued_leaf",
|
|
36
37
|
};
|
|
37
38
|
// =============================================================================
|
|
38
39
|
// Seed builders — the single definition of every program-derived address
|
|
@@ -240,6 +241,30 @@ export async function deriveNullifierRecordPDA(nullifierHash, poolState, treeInd
|
|
|
240
241
|
});
|
|
241
242
|
return [result[0], result[1]];
|
|
242
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Seeds for a `QueuedLeaf`: `["queued_leaf", pool_state, commitment]`.
|
|
246
|
+
*
|
|
247
|
+
* The commitment is the seed because it is already unique per output — which
|
|
248
|
+
* makes a second attempt to queue the same commitment fail on account creation
|
|
249
|
+
* rather than silently produce a second leaf for one note.
|
|
250
|
+
*/
|
|
251
|
+
export function queuedLeafSeeds(poolState, commitment) {
|
|
252
|
+
return [
|
|
253
|
+
enc(PDA_SEEDS.QUEUED_LEAF),
|
|
254
|
+
seedBytes(poolState, "poolState"),
|
|
255
|
+
seedBytes(commitment, "commitment"),
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Derive the `QueuedLeaf` PDA a queued spend creates for one output.
|
|
260
|
+
*/
|
|
261
|
+
export async function deriveQueuedLeafPDA(poolState, commitment, programId = UTXOPIA_PROGRAM_ID) {
|
|
262
|
+
const result = await getProgramDerivedAddress({
|
|
263
|
+
programAddress: programId,
|
|
264
|
+
seeds: queuedLeafSeeds(poolState, commitment),
|
|
265
|
+
});
|
|
266
|
+
return [result[0], result[1]];
|
|
267
|
+
}
|
|
243
268
|
/**
|
|
244
269
|
* Derive one-time PolicyApproval PDA.
|
|
245
270
|
* Seeds: ["policy_approval", pool_state, request_hash, nonce]
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "./stealth";
|
|
56
56
|
import { selectUtxos, type UtxoDescriptor } from "./psbt";
|
|
57
57
|
import { hexToBytes, bytesToHex, bigintToBytes } from "./crypto";
|
|
58
|
+
import { getAddressEncoder, type Address } from "@solana/kit";
|
|
58
59
|
import { EventClient } from "./event-client";
|
|
59
60
|
import { type DepositOpReturnContext, type BitcoinNetwork } from "./taproot";
|
|
60
61
|
|
|
@@ -269,23 +270,18 @@ export class UTXOpiaClient {
|
|
|
269
270
|
const cached = this._tokenIdCache.get(mintAddress);
|
|
270
271
|
if (cached !== undefined) return cached;
|
|
271
272
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
} catch {
|
|
285
|
-
// Lazy on purpose: a base58 mint padded to 64 is not hex, so computing
|
|
286
|
-
// this eagerly threw before the branch above ever ran.
|
|
287
|
-
bytes = hexToBytes(mintAddress.padStart(64, "0"));
|
|
288
|
-
}
|
|
273
|
+
// 64 hex chars is a raw token id; anything else is a base58 mint.
|
|
274
|
+
//
|
|
275
|
+
// Decoded with getAddressEncoder, the same path pda.ts uses, because
|
|
276
|
+
// require("@solana/web3.js") is not callable from an ESM browser bundle —
|
|
277
|
+
// it threw for every base58 mint, and the hex fallback behind it threw
|
|
278
|
+
// again on the same input. There is no fallback now: both branches are
|
|
279
|
+
// total for their input, and a malformed mint should raise, not be padded
|
|
280
|
+
// into a different token's id.
|
|
281
|
+
const bytes =
|
|
282
|
+
mintAddress.length === 64 && /^[0-9a-fA-F]+$/.test(mintAddress)
|
|
283
|
+
? hexToBytes(mintAddress)
|
|
284
|
+
: new Uint8Array(getAddressEncoder().encode(mintAddress as Address));
|
|
289
285
|
|
|
290
286
|
const tokenId = computeTokenId(bytes);
|
|
291
287
|
this._tokenIdCache.set(mintAddress, tokenId);
|
package/src/commitment-tree.ts
CHANGED
|
@@ -230,7 +230,16 @@ export class CommitmentTreeIndex {
|
|
|
230
230
|
throw new Error("Tree is full");
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
// Store in map for lookup
|
|
233
|
+
// Store in map for lookup.
|
|
234
|
+
//
|
|
235
|
+
// Test-only helper, and this map is why: a commitment is NOT unique in the
|
|
236
|
+
// tree. Paying the same BTC deposit address twice for the same amount yields
|
|
237
|
+
// byte-identical commitments (complete_deposit hashes note_public_key,
|
|
238
|
+
// token_id and shielded_amount), so a second add overwrites the first and
|
|
239
|
+
// getMerkleProof then returns only the later leaf. Production never takes
|
|
240
|
+
// this path — announcements dedupe on leafIndex and proofs come from
|
|
241
|
+
// /api/tree/proof/:leaf_index — but do not promote this class to a wallet
|
|
242
|
+
// path without keying on leafIndex instead.
|
|
234
243
|
const commitmentHex = commitment.toString(16).padStart(64, "0");
|
|
235
244
|
this.commitments.set(commitmentHex, { index: leafIndex, amount });
|
|
236
245
|
|
package/src/index.ts
CHANGED
|
@@ -504,6 +504,8 @@ export {
|
|
|
504
504
|
derivePoolStatePDA,
|
|
505
505
|
deriveCommitmentTreePDA,
|
|
506
506
|
deriveNullifierRecordPDA,
|
|
507
|
+
deriveQueuedLeafPDA,
|
|
508
|
+
queuedLeafSeeds,
|
|
507
509
|
derivePolicyApprovalPDA,
|
|
508
510
|
deriveExitDestinationPDA,
|
|
509
511
|
EXIT_KIND_SOLANA_OWNER,
|
|
@@ -717,6 +719,10 @@ export {
|
|
|
717
719
|
bigintTo32Bytes,
|
|
718
720
|
bytes32ToBigint,
|
|
719
721
|
// JoinSplit transact instruction
|
|
722
|
+
JSFLAGS,
|
|
723
|
+
joinSplitFlags,
|
|
724
|
+
buildMergeQueuedLeavesInstruction,
|
|
725
|
+
MAX_MERGE_LEAVES,
|
|
720
726
|
buildTransactInstructionData,
|
|
721
727
|
buildTransactInstruction,
|
|
722
728
|
// JoinSplit + BTC redeem instruction
|
|
@@ -758,6 +764,8 @@ export {
|
|
|
758
764
|
buildRotateAuditorInstructionData,
|
|
759
765
|
buildRotateAuditorInstruction,
|
|
760
766
|
type MagicBlockDelegateTarget,
|
|
767
|
+
type JoinSplitTailOptions,
|
|
768
|
+
type PolicyTailKind,
|
|
761
769
|
type MagicBlockDelegateInstructionOptions,
|
|
762
770
|
type MagicBlockCommitInstructionOptions,
|
|
763
771
|
type MagicBlockPerPermissionOperation,
|
package/src/instructions.ts
CHANGED
|
@@ -85,6 +85,7 @@ const INSTRUCTION = {
|
|
|
85
85
|
SET_AUDITOR_FROZEN: 28,
|
|
86
86
|
SET_AUDITOR_VIEWING_PUBKEY: 29,
|
|
87
87
|
// MagicBlock ER/PER lifecycle helpers (32-33)
|
|
88
|
+
MERGE_QUEUED_LEAVES: 40,
|
|
88
89
|
MAGICBLOCK_DELEGATE: 32,
|
|
89
90
|
MAGICBLOCK_COMMIT: 33,
|
|
90
91
|
MAGICBLOCK_PER_PERMISSION: 34,
|
|
@@ -619,6 +620,12 @@ export interface TransactInstructionOptions {
|
|
|
619
620
|
accounts: {
|
|
620
621
|
poolState: Address;
|
|
621
622
|
commitmentTree: Address;
|
|
623
|
+
/**
|
|
624
|
+
* Supply one `QueuedLeaf` PDA per output to defer placement. Doing so also
|
|
625
|
+
* flips poolState and commitmentTree to read-only, which is what lets two
|
|
626
|
+
* spends run in the same slot.
|
|
627
|
+
*/
|
|
628
|
+
queuedLeaves?: Address[];
|
|
622
629
|
vkRegistry: Address;
|
|
623
630
|
user: Address;
|
|
624
631
|
/** Nullifier record PDAs (one per input) */
|
|
@@ -628,6 +635,63 @@ export interface TransactInstructionOptions {
|
|
|
628
635
|
};
|
|
629
636
|
}
|
|
630
637
|
|
|
638
|
+
/**
|
|
639
|
+
* What a JoinSplit declares it is passing, mirroring `jsflags` on-chain.
|
|
640
|
+
*
|
|
641
|
+
* This byte used to be `proof_source`, valued 0 or 1. Bit 0 keeps that exact
|
|
642
|
+
* meaning so no instruction-data offset moved; the rest replaced the program's
|
|
643
|
+
* positional heuristics, which recovered the optional account tail by counting
|
|
644
|
+
* backwards from the end and by asking whether an account "looked like" a
|
|
645
|
+
* commitment tree or carried the policy program's id.
|
|
646
|
+
*
|
|
647
|
+
* A flag only says which slot to read. The program still validates owner,
|
|
648
|
+
* signer and seeds on every slot, so declaring the wrong thing breaks your own
|
|
649
|
+
* transaction rather than buying anything.
|
|
650
|
+
*/
|
|
651
|
+
export const JSFLAGS = {
|
|
652
|
+
PROOF_IN_BUFFER: 1 << 0,
|
|
653
|
+
RELAYER: 1 << 1,
|
|
654
|
+
FROZEN_SOURCE_TREE: 1 << 2,
|
|
655
|
+
POLICY: 1 << 3,
|
|
656
|
+
QUEUED_LEAVES: 1 << 4,
|
|
657
|
+
RAGEQUIT: 1 << 5,
|
|
658
|
+
} as const;
|
|
659
|
+
|
|
660
|
+
/** Which permissioned tail a spend carries. */
|
|
661
|
+
export type PolicyTailKind = "none" | "verified" | "ragequit";
|
|
662
|
+
|
|
663
|
+
export interface JoinSplitTailOptions {
|
|
664
|
+
/** 0=inline proof (default), 1=proof in a separate ChadBuffer account */
|
|
665
|
+
proofSource?: 0 | 1;
|
|
666
|
+
/** A relayer signs and pays instead of the note owner. transact only. */
|
|
667
|
+
hasRelayer?: boolean;
|
|
668
|
+
/** A rotated-out CommitmentTree proves membership of pre-rotation notes. */
|
|
669
|
+
hasFrozenSourceTree?: boolean;
|
|
670
|
+
/**
|
|
671
|
+
* Permissioned tail. "verified" appends (approval, policyProgram);
|
|
672
|
+
* "ragequit" appends one registered ExitDestination per public output.
|
|
673
|
+
* Must match the pool: the program cross-checks against pool.permissioned().
|
|
674
|
+
*/
|
|
675
|
+
policyTail?: PolicyTailKind;
|
|
676
|
+
/** Outputs are queued as QueuedLeaf PDAs instead of inserted inline. */
|
|
677
|
+
hasQueuedLeaves?: boolean;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Assemble the flags byte. Kept in one place so the data builders and the
|
|
682
|
+
* account builders cannot disagree about what was declared.
|
|
683
|
+
*/
|
|
684
|
+
export function joinSplitFlags(options: JoinSplitTailOptions): number {
|
|
685
|
+
let flags = 0;
|
|
686
|
+
if ((options.proofSource ?? 0) === 1) flags |= JSFLAGS.PROOF_IN_BUFFER;
|
|
687
|
+
if (options.hasRelayer) flags |= JSFLAGS.RELAYER;
|
|
688
|
+
if (options.hasFrozenSourceTree) flags |= JSFLAGS.FROZEN_SOURCE_TREE;
|
|
689
|
+
if (options.policyTail === "verified") flags |= JSFLAGS.POLICY;
|
|
690
|
+
if (options.policyTail === "ragequit") flags |= JSFLAGS.RAGEQUIT;
|
|
691
|
+
if (options.hasQueuedLeaves) flags |= JSFLAGS.QUEUED_LEAVES;
|
|
692
|
+
return flags;
|
|
693
|
+
}
|
|
694
|
+
|
|
631
695
|
/**
|
|
632
696
|
* Build transact instruction data (JoinSplit)
|
|
633
697
|
*
|
|
@@ -653,11 +717,9 @@ export function buildTransactInstructionData(options: {
|
|
|
653
717
|
nullifiers: Uint8Array[];
|
|
654
718
|
commitmentsOut: Uint8Array[];
|
|
655
719
|
stealthData: Uint8Array[];
|
|
656
|
-
/** 0=inline proof (default), 1=proof in separate ChadBuffer account */
|
|
657
|
-
proofSource?: 0 | 1;
|
|
658
720
|
/** Reserved. Sender memos are rejected until they are proof-bound. */
|
|
659
721
|
senderMemos?: Uint8Array[];
|
|
660
|
-
}): Uint8Array {
|
|
722
|
+
} & JoinSplitTailOptions): Uint8Array {
|
|
661
723
|
const { nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash, nullifiers, commitmentsOut, stealthData, senderMemos } = options;
|
|
662
724
|
const proofSource = options.proofSource ?? 0;
|
|
663
725
|
|
|
@@ -693,7 +755,7 @@ export function buildTransactInstructionData(options: {
|
|
|
693
755
|
data[offset++] = nInputs;
|
|
694
756
|
data[offset++] = nOutputs;
|
|
695
757
|
data[offset++] = 0; // n_public_outputs = 0 for transact
|
|
696
|
-
data[offset++] =
|
|
758
|
+
data[offset++] = joinSplitFlags(options);
|
|
697
759
|
|
|
698
760
|
// Proof (256 bytes, only in inline mode)
|
|
699
761
|
if (proofSource === 0 && proofBytes) {
|
|
@@ -743,6 +805,13 @@ export function buildTransactInstructionData(options: {
|
|
|
743
805
|
*/
|
|
744
806
|
export function buildTransactInstruction(options: TransactInstructionOptions): Instruction {
|
|
745
807
|
const config = getConfig();
|
|
808
|
+
const queuedLeaves = options.accounts.queuedLeaves ?? [];
|
|
809
|
+
const queued = queuedLeaves.length > 0;
|
|
810
|
+
if (queued && queuedLeaves.length !== options.nOutputs) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`Queued placement needs one QueuedLeaf PDA per output: expected ${options.nOutputs}, got ${queuedLeaves.length}`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
746
815
|
|
|
747
816
|
const data = buildTransactInstructionData({
|
|
748
817
|
nInputs: options.nInputs,
|
|
@@ -754,11 +823,23 @@ export function buildTransactInstruction(options: TransactInstructionOptions): I
|
|
|
754
823
|
commitmentsOut: options.commitmentsOut,
|
|
755
824
|
stealthData: options.stealthData,
|
|
756
825
|
senderMemos: options.senderMemos,
|
|
826
|
+
// The account list below appends the policy pair exactly when the caller
|
|
827
|
+
// supplies an approval, so the declared tail is derived from the same fact
|
|
828
|
+
// rather than asked for twice.
|
|
829
|
+
policyTail: options.accounts.policyApproval ? "verified" : "none",
|
|
830
|
+
hasQueuedLeaves: queued,
|
|
757
831
|
});
|
|
758
832
|
|
|
833
|
+
// A queued spend writes neither shared account — the tree is read for
|
|
834
|
+
// is_valid_root, pool_state for its policy flags — and the program REJECTS
|
|
835
|
+
// them as writable. Sealevel serialises on the declared metas, not on what
|
|
836
|
+
// the program does, so leaving them writable would take both locks and
|
|
837
|
+
// silently cost the parallelism the queue exists for.
|
|
838
|
+
const sharedRole = queued ? AccountRole.READONLY : AccountRole.WRITABLE;
|
|
839
|
+
|
|
759
840
|
const accounts: Instruction["accounts"] = [
|
|
760
|
-
{ address: options.accounts.poolState, role:
|
|
761
|
-
{ address: options.accounts.commitmentTree, role:
|
|
841
|
+
{ address: options.accounts.poolState, role: sharedRole },
|
|
842
|
+
{ address: options.accounts.commitmentTree, role: sharedRole },
|
|
762
843
|
{ address: options.accounts.vkRegistry, role: AccountRole.READONLY },
|
|
763
844
|
{ address: options.accounts.user, role: AccountRole.WRITABLE_SIGNER },
|
|
764
845
|
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
|
|
@@ -775,6 +856,11 @@ export function buildTransactInstruction(options: TransactInstructionOptions): I
|
|
|
775
856
|
role: AccountRole.READONLY,
|
|
776
857
|
});
|
|
777
858
|
}
|
|
859
|
+
// Order matches resolve_joinsplit_tail: relayer, source tree, policy, queued
|
|
860
|
+
// leaves, proof buffer. One PDA per output, in output order.
|
|
861
|
+
for (const leaf of queuedLeaves) {
|
|
862
|
+
accounts.push({ address: leaf, role: AccountRole.WRITABLE });
|
|
863
|
+
}
|
|
778
864
|
|
|
779
865
|
return {
|
|
780
866
|
programAddress: config.utxopiaProgramId,
|
|
@@ -826,8 +912,7 @@ export function buildRedeemInstructionData(options: {
|
|
|
826
912
|
/** Unique request nonce(s) — single or array */
|
|
827
913
|
requestNonces: bigint[];
|
|
828
914
|
/** 0=inline proof (default), 1=proof in separate ChadBuffer account */
|
|
829
|
-
|
|
830
|
-
}): Uint8Array {
|
|
915
|
+
} & JoinSplitTailOptions): Uint8Array {
|
|
831
916
|
const {
|
|
832
917
|
nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash,
|
|
833
918
|
nullifiers, commitmentsOut, stealthData, redeemAmounts, btcScripts, requestNonces,
|
|
@@ -888,7 +973,7 @@ export function buildRedeemInstructionData(options: {
|
|
|
888
973
|
data[offset++] = nInputs;
|
|
889
974
|
data[offset++] = nOutputs;
|
|
890
975
|
data[offset++] = nPublicOutputs;
|
|
891
|
-
data[offset++] =
|
|
976
|
+
data[offset++] = joinSplitFlags(options);
|
|
892
977
|
|
|
893
978
|
// Proof (256 bytes, only in inline mode)
|
|
894
979
|
if (proofSource === 0 && proofBytes) {
|
|
@@ -1010,8 +1095,7 @@ export function buildUnshieldInstructionData(options: {
|
|
|
1010
1095
|
/** Amount(s) being unshielded — single or array */
|
|
1011
1096
|
unshieldAmounts: bigint[];
|
|
1012
1097
|
/** 0=inline proof (default), 1=proof in separate ChadBuffer account */
|
|
1013
|
-
|
|
1014
|
-
}): Uint8Array {
|
|
1098
|
+
} & JoinSplitTailOptions): Uint8Array {
|
|
1015
1099
|
const { nInputs, nOutputs, proofBytes, merkleRoot, boundParamsHash, nullifiers, commitmentsOut, stealthData, unshieldAmounts } = options;
|
|
1016
1100
|
const nPublicOutputs = options.nPublicOutputs ?? unshieldAmounts.length;
|
|
1017
1101
|
const proofSource = options.proofSource ?? 0;
|
|
@@ -1054,7 +1138,7 @@ export function buildUnshieldInstructionData(options: {
|
|
|
1054
1138
|
data[offset++] = nInputs;
|
|
1055
1139
|
data[offset++] = nOutputs;
|
|
1056
1140
|
data[offset++] = nPublicOutputs;
|
|
1057
|
-
data[offset++] =
|
|
1141
|
+
data[offset++] = joinSplitFlags(options);
|
|
1058
1142
|
|
|
1059
1143
|
// Proof (256 bytes, only in inline mode)
|
|
1060
1144
|
if (proofSource === 0 && proofBytes) {
|
|
@@ -1380,7 +1464,8 @@ export interface MagicBlockDelegateInstructionOptions {
|
|
|
1380
1464
|
};
|
|
1381
1465
|
target: MagicBlockDelegateTarget;
|
|
1382
1466
|
commitFrequencyMs: number;
|
|
1383
|
-
validator
|
|
1467
|
+
/** Required: the PER's TEE validator. An unpinned delegation is rejected on-chain. */
|
|
1468
|
+
validator: Address;
|
|
1384
1469
|
}
|
|
1385
1470
|
|
|
1386
1471
|
export interface MagicBlockCommitInstructionOptions {
|
|
@@ -1431,7 +1516,7 @@ function magicBlockDelegateTargetByte(target: MagicBlockDelegateTarget): number
|
|
|
1431
1516
|
export function buildMagicBlockDelegateInstructionData(options: {
|
|
1432
1517
|
target: MagicBlockDelegateTarget;
|
|
1433
1518
|
commitFrequencyMs: number;
|
|
1434
|
-
validator
|
|
1519
|
+
validator: Address;
|
|
1435
1520
|
}): Uint8Array {
|
|
1436
1521
|
if (!Number.isInteger(options.commitFrequencyMs) || options.commitFrequencyMs < 0) {
|
|
1437
1522
|
throw new Error("commitFrequencyMs must be a non-negative u32");
|
|
@@ -1440,14 +1525,12 @@ export function buildMagicBlockDelegateInstructionData(options: {
|
|
|
1440
1525
|
throw new Error("commitFrequencyMs must fit in u32");
|
|
1441
1526
|
}
|
|
1442
1527
|
|
|
1443
|
-
const data = new Uint8Array(
|
|
1528
|
+
const data = new Uint8Array(38);
|
|
1444
1529
|
const view = new DataView(data.buffer);
|
|
1445
1530
|
data[0] = INSTRUCTION.MAGICBLOCK_DELEGATE;
|
|
1446
1531
|
data[1] = magicBlockDelegateTargetByte(options.target);
|
|
1447
1532
|
view.setUint32(2, options.commitFrequencyMs, true);
|
|
1448
|
-
|
|
1449
|
-
data.set(addressToBytes(options.validator), 6);
|
|
1450
|
-
}
|
|
1533
|
+
data.set(addressToBytes(options.validator), 6);
|
|
1451
1534
|
return data;
|
|
1452
1535
|
}
|
|
1453
1536
|
|
|
@@ -2604,3 +2687,77 @@ export function buildRotateAuditorInstruction(options: RotateAuditorOptions): In
|
|
|
2604
2687
|
data: buildRotateAuditorInstructionData(options.auditor, options.viewingPubkey),
|
|
2605
2688
|
};
|
|
2606
2689
|
}
|
|
2690
|
+
|
|
2691
|
+
// =============================================================================
|
|
2692
|
+
// merge_queued_leaves (disc=40)
|
|
2693
|
+
// =============================================================================
|
|
2694
|
+
|
|
2695
|
+
export interface MergeQueuedLeavesOptions {
|
|
2696
|
+
accounts: {
|
|
2697
|
+
/** Pays the fee. A relayer, or the holder taking the escape hatch. */
|
|
2698
|
+
caller: Address;
|
|
2699
|
+
poolState: Address;
|
|
2700
|
+
commitmentTree: Address;
|
|
2701
|
+
};
|
|
2702
|
+
/**
|
|
2703
|
+
* The leaves to place, in the order they should land: leaf `i` gets
|
|
2704
|
+
* `first_leaf_index + i`. Each needs the payer recorded in the account —
|
|
2705
|
+
* rent goes there, never to the caller, so racing to merge earns nothing.
|
|
2706
|
+
*/
|
|
2707
|
+
leaves: { queuedLeaf: Address; rentRecipient: Address }[];
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
/** Matches MAX_MERGE_LEAVES on-chain: two accounts per leaf under the 64-account cap. */
|
|
2711
|
+
export const MAX_MERGE_LEAVES = 24;
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* Place queued commitments into the tree and close their accounts.
|
|
2715
|
+
*
|
|
2716
|
+
* Permissionless: anyone may call this, which is what makes a QueuedLeaf
|
|
2717
|
+
* recoverable without a timeout — no operator can strand a note, because the
|
|
2718
|
+
* holder can merge it themselves.
|
|
2719
|
+
*
|
|
2720
|
+
* Prefer having a relayer call it. Self-merging links a Solana identity to one
|
|
2721
|
+
* specific leaf and the timing says the holder is in a hurry to spend it, so it
|
|
2722
|
+
* is the escape hatch rather than the path. Merge cadence is a privacy
|
|
2723
|
+
* parameter, not only a UX one.
|
|
2724
|
+
*/
|
|
2725
|
+
export function buildMergeQueuedLeavesInstruction(
|
|
2726
|
+
options: MergeQueuedLeavesOptions
|
|
2727
|
+
): Instruction {
|
|
2728
|
+
const config = getConfig();
|
|
2729
|
+
if (options.leaves.length === 0) {
|
|
2730
|
+
throw new Error("merge_queued_leaves needs at least one leaf");
|
|
2731
|
+
}
|
|
2732
|
+
if (options.leaves.length > MAX_MERGE_LEAVES) {
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
`merge_queued_leaves takes at most ${MAX_MERGE_LEAVES} leaves, got ${options.leaves.length}`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
// Duplicates would insert one commitment at two leaf indices, and nullifiers
|
|
2738
|
+
// are derived from (key, leaf index) — the program refuses it, but catching it
|
|
2739
|
+
// here names the problem instead of surfacing NullifierAlreadyUsed.
|
|
2740
|
+
const seen = new Set<string>();
|
|
2741
|
+
for (const { queuedLeaf } of options.leaves) {
|
|
2742
|
+
if (seen.has(queuedLeaf)) {
|
|
2743
|
+
throw new Error(`Duplicate queued leaf in one merge: ${queuedLeaf}`);
|
|
2744
|
+
}
|
|
2745
|
+
seen.add(queuedLeaf);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
const accounts: Instruction["accounts"] = [
|
|
2749
|
+
{ address: options.accounts.caller, role: AccountRole.WRITABLE_SIGNER },
|
|
2750
|
+
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
2751
|
+
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
2752
|
+
];
|
|
2753
|
+
for (const { queuedLeaf, rentRecipient } of options.leaves) {
|
|
2754
|
+
accounts.push({ address: queuedLeaf, role: AccountRole.WRITABLE });
|
|
2755
|
+
accounts.push({ address: rentRecipient, role: AccountRole.WRITABLE });
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
return {
|
|
2759
|
+
programAddress: config.utxopiaProgramId,
|
|
2760
|
+
accounts,
|
|
2761
|
+
data: Uint8Array.of(INSTRUCTION.MERGE_QUEUED_LEAVES),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
package/src/pda.ts
CHANGED
|
@@ -46,6 +46,7 @@ export const PDA_SEEDS = {
|
|
|
46
46
|
VK_REGISTRY: "vk_registry",
|
|
47
47
|
TOKEN_CONFIG: "token_config",
|
|
48
48
|
POOL_CONFIG: "pool_config",
|
|
49
|
+
QUEUED_LEAF: "queued_leaf",
|
|
49
50
|
} as const;
|
|
50
51
|
|
|
51
52
|
// =============================================================================
|
|
@@ -322,6 +323,39 @@ export async function deriveNullifierRecordPDA(
|
|
|
322
323
|
return [result[0], result[1]];
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Seeds for a `QueuedLeaf`: `["queued_leaf", pool_state, commitment]`.
|
|
328
|
+
*
|
|
329
|
+
* The commitment is the seed because it is already unique per output — which
|
|
330
|
+
* makes a second attempt to queue the same commitment fail on account creation
|
|
331
|
+
* rather than silently produce a second leaf for one note.
|
|
332
|
+
*/
|
|
333
|
+
export function queuedLeafSeeds(
|
|
334
|
+
poolState: Address | Uint8Array,
|
|
335
|
+
commitment: Uint8Array
|
|
336
|
+
): Uint8Array[] {
|
|
337
|
+
return [
|
|
338
|
+
enc(PDA_SEEDS.QUEUED_LEAF),
|
|
339
|
+
seedBytes(poolState, "poolState"),
|
|
340
|
+
seedBytes(commitment, "commitment"),
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Derive the `QueuedLeaf` PDA a queued spend creates for one output.
|
|
346
|
+
*/
|
|
347
|
+
export async function deriveQueuedLeafPDA(
|
|
348
|
+
poolState: Address | Uint8Array,
|
|
349
|
+
commitment: Uint8Array,
|
|
350
|
+
programId: Address = UTXOPIA_PROGRAM_ID
|
|
351
|
+
): Promise<[Address, number]> {
|
|
352
|
+
const result = await getProgramDerivedAddress({
|
|
353
|
+
programAddress: programId,
|
|
354
|
+
seeds: queuedLeafSeeds(poolState, commitment),
|
|
355
|
+
});
|
|
356
|
+
return [result[0], result[1]];
|
|
357
|
+
}
|
|
358
|
+
|
|
325
359
|
/**
|
|
326
360
|
* Derive one-time PolicyApproval PDA.
|
|
327
361
|
* Seeds: ["policy_approval", pool_state, request_hash, nonce]
|