@utxopia/sdk 0.1.0-alpha.1 → 0.1.0-alpha.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utxopia/sdk",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "UTXOpia public SDK workspace",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,7 +20,7 @@ import { type Address } from "@solana/kit";
20
20
  */
21
21
  export declare function address(input: string): Address;
22
22
  export type NetworkType = "devnet" | "mainnet" | "localnet";
23
- export type AppNetworkId = NetworkType | "devnet-regtest";
23
+ export type AppNetworkId = NetworkType | "devnet-regtest" | "devnet-testnet4";
24
24
  export interface NetworkConfig {
25
25
  /** Network identifier */
26
26
  network: NetworkType;
@@ -99,10 +99,41 @@ export declare const CHADBUFFER_PROGRAM_ID: Address;
99
99
  /** ChadBuffer Program ID for localnet testing */
100
100
  export declare const LOCALNET_CHADBUFFER_PROGRAM_ID: Address;
101
101
  /**
102
- * Fresh multi-pool devnet deployment (2026-07-26).
103
- * The default domain is public; institution pools are selected explicitly.
102
+ * Greenfield devnet + Bitcoin testnet4 deployment (2026-08-26).
103
+ *
104
+ * The previous deployment's programs were closed on chain, so this is not a redeploy and none
105
+ * of the old addresses resolve any more — anything still pinned to CvfSyACR… is talking to a
106
+ * program that no longer exists.
107
+ *
108
+ * Two pools were deployed, each with its own Ika DKG and therefore its own dWallet. The default
109
+ * below is the OPEN pool. Select the verified pool by overriding `zkbtcMint` and
110
+ * `ikaDwalletXOnlyPubkey` — `getConfig` re-derives poolStatePda, commitmentTreePda and poolVault
111
+ * from the mint, so those two values are all a caller needs:
112
+ *
113
+ * verified mint G78CTddWGDaNaSKQayAt7m3pzcMyaUNxgR8y3R34YvEv
114
+ * state Eqn9SmFYtacrdfPE9Shbi8bDXLjCNNz3WhHfqtJnwbKY
115
+ * xonly 16c563baa11bfc8fe93acafe8fe169b954b3ead3ecda7754c615dec9e840b5a5
116
+ *
117
+ * Read off chain, not from the deploy notes: Eqn9SmFY… is the pool whose PoolState flags carry
118
+ * the permissioned bit (0b10 at offset 2). The deployment also contains a THIRD pool —
119
+ * 3chHiDqM… / 7wDtDd1u… — which the deploy notes name as the verified one but which is not
120
+ * permissioned and whose mint has zero supply. Treat it as abandoned; do not wire it up.
104
121
  */
105
122
  export declare const DEVNET_CONFIG: NetworkConfig;
123
+ /**
124
+ * Solana devnet + Bitcoin **regtest** deployment — the environment app.utxopia.com serves.
125
+ *
126
+ * This is a different deployment from DEVNET_CONFIG above, not a Bitcoin-side variation of it:
127
+ * a different program (CvfSyACR…), a different pool, a different Ika dWallet. It used to share
128
+ * DEVNET_CONFIG and differ only by bitcoinNetwork, which worked while one program hosted both.
129
+ * It no longer does — after 2026-08-26 there are two programs — so `devnet-regtest` resolving
130
+ * to DEVNET_CONFIG would silently point this environment at testnet4's program and pool.
131
+ *
132
+ * Pool addresses derived from the mint with the same seeds getConfig uses, then confirmed on
133
+ * devnet: pool_state and commitment_tree owned by CvfSyACR… at 332 and 8816 bytes, the vault by
134
+ * Token-2022 at 170.
135
+ */
136
+ export declare const DEVNET_REGTEST_CONFIG: NetworkConfig;
106
137
  /**
107
138
  * Mainnet Configuration (placeholder - not yet deployed)
108
139
  */
@@ -44,21 +44,42 @@ export const LOCALNET_CHADBUFFER_PROGRAM_ID = address("EgWyMVFZewHmjJ9GGvVBTyaC3
44
44
  // Network Configurations
45
45
  // =============================================================================
46
46
  /**
47
- * Fresh multi-pool devnet deployment (2026-07-26).
48
- * The default domain is public; institution pools are selected explicitly.
47
+ * Greenfield devnet + Bitcoin testnet4 deployment (2026-08-26).
48
+ *
49
+ * The previous deployment's programs were closed on chain, so this is not a redeploy and none
50
+ * of the old addresses resolve any more — anything still pinned to CvfSyACR… is talking to a
51
+ * program that no longer exists.
52
+ *
53
+ * Two pools were deployed, each with its own Ika DKG and therefore its own dWallet. The default
54
+ * below is the OPEN pool. Select the verified pool by overriding `zkbtcMint` and
55
+ * `ikaDwalletXOnlyPubkey` — `getConfig` re-derives poolStatePda, commitmentTreePda and poolVault
56
+ * from the mint, so those two values are all a caller needs:
57
+ *
58
+ * verified mint G78CTddWGDaNaSKQayAt7m3pzcMyaUNxgR8y3R34YvEv
59
+ * state Eqn9SmFYtacrdfPE9Shbi8bDXLjCNNz3WhHfqtJnwbKY
60
+ * xonly 16c563baa11bfc8fe93acafe8fe169b954b3ead3ecda7754c615dec9e840b5a5
61
+ *
62
+ * Read off chain, not from the deploy notes: Eqn9SmFY… is the pool whose PoolState flags carry
63
+ * the permissioned bit (0b10 at offset 2). The deployment also contains a THIRD pool —
64
+ * 3chHiDqM… / 7wDtDd1u… — which the deploy notes name as the verified one but which is not
65
+ * permissioned and whose mint has zero supply. Treat it as abandoned; do not wire it up.
49
66
  */
50
67
  export const DEVNET_CONFIG = {
51
68
  network: "devnet",
52
- utxopiaProgramId: address("CvfSyACR8xemPdeJsB3D8Xh15rKUQ3b5c1PvnmABCBJp"),
69
+ utxopiaProgramId: address("28z2AtKA6aFGrGCh4ns1rmp7vGpWuh6x3H7gXKBcfxur"),
53
70
  policyProgramId: address("9asWYKVriWGpExW5xM44ChHjZtispkLCiWKkM8SQi8Rs"),
54
- btcLightClientProgramId: address("859B7kw1xDyY8rzSXY6pAPNxaAsPWrsaAPJk3iivd43g"),
71
+ // testnet4 light client, deployed 2026-08-26. Must match the `devnet` arm of
72
+ // BTC_LIGHT_CLIENT_PROGRAM_ID in programs/utxopia/src/constants.rs.
73
+ btcLightClientProgramId: address("4LZbktiNsiVAe2bwPCTPNgqiWWgZNUj4T3bDx8GZmehv"),
55
74
  chadbufferProgramId: CHADBUFFER_PROGRAM_ID,
56
75
  token2022ProgramId: TOKEN_2022_PROGRAM_ID,
57
76
  ataProgramId: ATA_PROGRAM_ID,
58
- poolStatePda: address("9xeWc39r3Z176MUpMpaqCGJGneHMj4pfMRv9u6dp2Qgd"),
59
- commitmentTreePda: address("4FvM9dCzDvr39Xu5xRUQc6EEm3UjSikyWUY5Hzpc5C4A"),
60
- zkbtcMint: address("GuruxfN5irYcCyDiKFMeDRTNbP2WeHF1oWjQ8q8Esc16"),
61
- poolVault: address("3GffHxesFsGj4QmgQ8ozs17dZFNTv4AAhdwyR2XZPzkm"),
77
+ // Open pool. Derived and then confirmed on chain: pool_state and commitment_tree are owned by
78
+ // the program (332 / 8816 bytes), the vault by Token-2022 (170 bytes).
79
+ poolStatePda: address("FezM7ksBwftqrd4TtabMa9uXt51eCT5M946YnpSEQZHm"),
80
+ commitmentTreePda: address("CHwJZqNAUag6ARDfmntvmbS4WeAsGX67f2vALxXbB6PP"),
81
+ zkbtcMint: address("87zWstDnNgMig2vk8q8jTrK6YTcyugeRTanfT3LfyU3T"),
82
+ poolVault: address("8VMfH4mDizU6nHybQumxeauyiEL2QTqrGQLXmAr9wpC3"),
62
83
  // RPC Endpoints
63
84
  solanaRpcUrl: "https://api.devnet.solana.com",
64
85
  solanaWsUrl: "wss://api.devnet.solana.com",
@@ -67,8 +88,11 @@ export const DEVNET_CONFIG = {
67
88
  esploraUrl: "https://mempool.space/testnet4/api",
68
89
  // Circuit CDN (Groth16 artifacts: .wasm, .zkey files)
69
90
  circuitCdnUrl: "https://circuit.utxopia.com/circuits/v2/groth16",
70
- // Groth16 Verifier: verification is inline in the UTXOpia program (no separate verifier program)
71
- groth16VerifierProgramId: address("AjbX243s2JMFG2uhfTjKkadjPvQEPgcuyV3vfLJv36MT"), // inline in utxopia program
91
+ // Groth16 verification is inline in the UTXOpia program, so this is the same address.
92
+ // It used to hold a stale third id, which `getConfig` silently overwrote with the program id
93
+ // whenever a programId override was supplied — meaning the constant was only ever read on the
94
+ // no-override path, where it was wrong.
95
+ groth16VerifierProgramId: address("28z2AtKA6aFGrGCh4ns1rmp7vGpWuh6x3H7gXKBcfxur"),
72
96
  // VK Hashes (SHA256 of serialized VK bytes, generated from circom trusted setup)
73
97
  vkHashes: {
74
98
  claim: "7af0e702e7b595fbdb62fd268e6c529481003e07957e0f60e4fb23cd9fe6a77f",
@@ -88,8 +112,12 @@ export const DEVNET_CONFIG = {
88
112
  "1x4": "01728b82e810a8ba604cc66aa6a563444d18f4598c402d11767d0a7e5049a9be",
89
113
  "4x1": "0362b306b17dae916d836d9448a26c97e51b1b0a1a0ed052ebfbd4800e5000cf",
90
114
  },
91
- // Ika dWallet x-only pubkey populated by ./scripts/sync-env.sh from devnet-state.json.
92
- ikaDwalletXOnlyPubkey: "0000000000000000000000000000000000000000000000000000000000000000",
115
+ // Ika dWallet x-only pubkey for the OPEN pool, read from its PoolConfig PDA
116
+ // (GQ5ZfD4tJmgcquHLAHSbmAm72Foi9hf3VduPrroysM1b, offset 68..100) on 2026-08-26.
117
+ // All-zero here used to mean "deposit addresses cannot be derived until sync-env.sh runs";
118
+ // the two pools have distinct dWallets, so a single synced value could only ever be right
119
+ // for one of them. See the verified pool's key in the header comment above.
120
+ ikaDwalletXOnlyPubkey: "3a6ab80ba14bc050f048ee3e0b77d8935adf4fc5e2f5947311f26cc2cb5bd194",
93
121
  // SNS Subdomain Resolution (devnet)
94
122
  snsNameServiceProgramId: "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX", // SPL Name Service (devnet)
95
123
  snsRegistrarProgramId: "snshBoEQ9jx4QoHBpZDQPYdNCtw7RMxJvYrKFEhwaPJ", // SNS Registrar (devnet)
@@ -99,6 +127,32 @@ export const DEVNET_CONFIG = {
99
127
  snsReverseLookupClass: "7NbD1vprif6apthEZAqhRfYuhrqnuderB8qpnfXGCc8H", // Reverse lookup class (devnet)
100
128
  snsStealthDataVersion: 1,
101
129
  };
130
+ /**
131
+ * Solana devnet + Bitcoin **regtest** deployment — the environment app.utxopia.com serves.
132
+ *
133
+ * This is a different deployment from DEVNET_CONFIG above, not a Bitcoin-side variation of it:
134
+ * a different program (CvfSyACR…), a different pool, a different Ika dWallet. It used to share
135
+ * DEVNET_CONFIG and differ only by bitcoinNetwork, which worked while one program hosted both.
136
+ * It no longer does — after 2026-08-26 there are two programs — so `devnet-regtest` resolving
137
+ * to DEVNET_CONFIG would silently point this environment at testnet4's program and pool.
138
+ *
139
+ * Pool addresses derived from the mint with the same seeds getConfig uses, then confirmed on
140
+ * devnet: pool_state and commitment_tree owned by CvfSyACR… at 332 and 8816 bytes, the vault by
141
+ * Token-2022 at 170.
142
+ */
143
+ export const DEVNET_REGTEST_CONFIG = {
144
+ ...DEVNET_CONFIG,
145
+ utxopiaProgramId: address("CvfSyACR8xemPdeJsB3D8Xh15rKUQ3b5c1PvnmABCBJp"),
146
+ groth16VerifierProgramId: address("CvfSyACR8xemPdeJsB3D8Xh15rKUQ3b5c1PvnmABCBJp"),
147
+ btcLightClientProgramId: address("8hCSNKf8ByqZdet2D4SDiZHDrB1u9ohkhqKKzr9i7vfQ"),
148
+ poolStatePda: address("CeEEmE9MvFPZtqcgv1rsXmzNmfvchbs8VEZJGFKZ2Cyj"),
149
+ commitmentTreePda: address("45bCw97GssorJM9b1ZWZLMGy1NJUczcaNVaKASmCRohL"),
150
+ zkbtcMint: address("BJ5SXA33qK8r8BxJD4nQPf72ae9bactiA2Zqo33EcvPu"),
151
+ poolVault: address("JsZ1ipHZWiZYE8kRDXmEkKuiK6KKVxCVJKv1tGnCkM6"),
152
+ ikaDwalletXOnlyPubkey: "243a6c47504f82d168754da9392a9dbcbab9b9f9c515a609227fac4642b2a26f",
153
+ bitcoinNetwork: "regtest",
154
+ esploraUrl: "http://localhost:2140",
155
+ };
102
156
  /**
103
157
  * Mainnet Configuration (placeholder - not yet deployed)
104
158
  */
@@ -221,6 +275,24 @@ function esploraUrlForNetwork(net) {
221
275
  default: return `https://mempool.space/${net}/api`;
222
276
  }
223
277
  }
278
+ /**
279
+ * Base config for an app network id. Two devnet deployments now exist, so this cannot be
280
+ * derived from NetworkType alone — that is what normalizeAppNetwork collapses away.
281
+ */
282
+ function baseConfigForAppNetwork(network) {
283
+ switch (network) {
284
+ case "localnet":
285
+ return LOCALNET_CONFIG;
286
+ case "mainnet":
287
+ return MAINNET_CONFIG;
288
+ case "devnet-regtest":
289
+ return DEVNET_REGTEST_CONFIG;
290
+ case "devnet-testnet4":
291
+ case "devnet":
292
+ default:
293
+ return DEVNET_CONFIG;
294
+ }
295
+ }
224
296
  function normalizeAppNetwork(network) {
225
297
  switch (network) {
226
298
  case "mainnet":
@@ -229,6 +301,7 @@ function normalizeAppNetwork(network) {
229
301
  return "localnet";
230
302
  case "devnet":
231
303
  case "devnet-regtest":
304
+ case "devnet-testnet4":
232
305
  default:
233
306
  return "devnet";
234
307
  }
@@ -239,6 +312,7 @@ function bitcoinNetworkForAppNetwork(network) {
239
312
  case "devnet-regtest":
240
313
  return "regtest";
241
314
  case "devnet":
315
+ case "devnet-testnet4":
242
316
  return "testnet4";
243
317
  case "mainnet":
244
318
  return "mainnet";
@@ -273,7 +347,7 @@ export function setConfig(network) {
273
347
  const baseNetwork = normalizeAppNetwork(network);
274
348
  switch (baseNetwork) {
275
349
  case "devnet":
276
- currentConfig = DEVNET_CONFIG;
350
+ currentConfig = baseConfigForAppNetwork(network);
277
351
  break;
278
352
  case "mainnet":
279
353
  throw new Error("Mainnet is not yet deployed. " +
@@ -318,11 +392,7 @@ export async function initConfig(overrides) {
318
392
  (typeof process !== "undefined" && (process.env?.NEXT_PUBLIC_NETWORK || process.env?.UTXOPIA_NETWORK)) ||
319
393
  "devnet";
320
394
  const networkId = normalizeAppNetwork(appNetworkId);
321
- const baseConfig = networkId === "localnet"
322
- ? LOCALNET_CONFIG
323
- : networkId === "mainnet"
324
- ? MAINNET_CONFIG
325
- : DEVNET_CONFIG;
395
+ const baseConfig = baseConfigForAppNetwork(appNetworkId);
326
396
  const config = { ...baseConfig };
327
397
  const appBitcoinNetwork = bitcoinNetworkForAppNetwork(appNetworkId);
328
398
  const btcNetOverride = typeof process !== "undefined" && process.env?.NEXT_PUBLIC_BTC_NETWORK;
@@ -34,16 +34,17 @@ export { fetchTokenConfig, getTokenId, fetchSupportedTokens, fetchEnabledTokens,
34
34
  export { MAGICBLOCK_DELEGATION_PROGRAM_ID, MAGICBLOCK_DEVNET_ROUTER_URL, MAGICBLOCK_DEVNET_ROUTER_WS_URL, MAGICBLOCK_EPHEMERAL_VAULT_ID, MAGICBLOCK_MAGIC_CONTEXT_ID, MAGICBLOCK_MAGIC_PROGRAM_ID, MAGICBLOCK_MAX_PER_MEMBERS, MAGICBLOCK_PERMISSION_PROGRAM_ID, MAGICBLOCK_PER_MEMBER_FLAGS, MAGICBLOCK_VALIDATOR_IDENTITIES, buildDefaultPrivacyDomain, buildMagicBlockPerMemberFlags, deriveMagicBlockCommitRecordPDA, deriveMagicBlockCommitStatePDA, deriveMagicBlockDelegateBufferPDA, deriveMagicBlockDelegationMetadataPDA, deriveMagicBlockDelegationRecordPDA, deriveMagicBlockPermissionPDA, deriveMagicBlockUndelegateBufferPDA, requiresMagicBlockEndpoint, getMagicBlockEndpoint, getMagicBlockValidatorIdentity, createMagicBlockRouterConnection, type BuildPrivacyDomainOptions, type MagicBlockEndpointConfig, type MagicBlockExecutionMode, type MagicBlockPolicyMode, type MagicBlockPerMemberFlagName, type MagicBlockValidatorRegion, type PrivacyDomainConfig, type PrivacyDomainKind, } from "./magicblock";
35
35
  export { generateNote, createNoteFromSecrets, serializeNote, deserializeNote, noteHasComputedHashes, getNotePublicKeyX, computeNoteCommitment, computeNoteNullifier, formatBtc, parseBtc, deriveNote, deriveNotes, deriveMasterKey, deriveNoteFromMaster, estimateSeedStrength, createNote, prepareWithdrawal, createStealthNote, serializeStealthNote, deserializeStealthNote, type Note, type SerializedNote, type NoteData, type StealthNote, type SerializedStealthNote, createJoinSplitNote, computeJoinSplitNoteNullifier, serializeJoinSplitNote, deserializeJoinSplitNote, type JoinSplitNote, type SerializedJoinSplitNote, } from "./note";
36
36
  export { createMerkleProof, createMerkleProofFromBigints, proofToCircomFormat, proofToOnChainFormat, createEmptyMerkleProof, leafIndexToPathIndices, pathIndicesToLeafIndex, validateMerkleProofStructure, parseMerkleProofResponse, TREE_DEPTH, ROOT_HISTORY_SIZE, MAX_LEAVES, ZERO_VALUE, type MerkleProof, } from "./merkle";
37
- export { deriveTaprootAddress, deriveTaprootAddressWithRefund, buildRefundScript, computeTapLeafHash, verifyTaprootAddress, createP2TRScriptPubkey, parseP2TRScriptPubkey, isValidBitcoinAddress, getInternalKey, createCustomInternalKey, createOpReturnScriptFromPayload, buildDepositOpReturn, parseDepositOpReturn, encodeDepositOpReturnHeader, decodeDepositOpReturnHeader, validateDepositOpReturnContext, computeDepositPoolTag, DEPOSIT_DESTINATION_CHAIN, DEPOSIT_BITCOIN_NETWORK, DEPOSIT_OP_RETURN_VERSION, DEPOSIT_POOL_TAG_SIZE, DEPOSIT_OP_RETURN_SIZE, type DepositDestinationChain, type DepositBitcoinNetwork, type DepositOpReturnContext, type ParsedDepositOpReturn, } from "./taproot";
37
+ export { deriveTaprootAddress, deriveTaprootAddressWithRefund, depositTweakCommitment, buildRefundScript, computeTapLeafHash, verifyTaprootAddress, createP2TRScriptPubkey, parseP2TRScriptPubkey, isValidBitcoinAddress, getInternalKey, createCustomInternalKey, createOpReturnScriptFromPayload, buildDepositOpReturn, parseDepositOpReturn, encodeDepositOpReturnHeader, decodeDepositOpReturnHeader, validateDepositOpReturnContext, computeDepositPoolTag, DEPOSIT_DESTINATION_CHAIN, DEPOSIT_BITCOIN_NETWORK, DEPOSIT_OP_RETURN_VERSION, DEPOSIT_POOL_TAG_SIZE, DEPOSIT_OP_RETURN_SIZE, type DepositDestinationChain, type DepositBitcoinNetwork, type DepositOpReturnContext, type ParsedDepositOpReturn, } from "./taproot";
38
38
  export { encodeClaimLink, decodeClaimLink, parseClaimUrl, } from "./claim-link";
39
39
  export type { ProofData, MerkleProofInput, CircuitType, JoinSplitProofInputs, } from "./prover/web";
40
40
  export { uploadTransactionToBuffer, uploadProofToBuffer, closeBuffer, readBufferData, fetchRawTransaction, fetchMerkleProof, prepareVerifyDeposit, buildMerkleProof, needsBuffer as bufferNeedsBuffer, getProofSource, calculateUploadTransactions, CHADBUFFER_PROGRAM_ID, AUTHORITY_SIZE, MAX_DATA_PER_WRITE, SOLANA_TX_SIZE_LIMIT, type ProofUploadResult, } from "./chadbuffer";
41
41
  export { computeBoundParamsHash, computeSolanaDomainBoundParamsHash, computeSolanaDomainSeparator, computeStealthDataHash, createTransferBoundParams, createUnshieldBoundParams, createRedeemBoundParams, SOLANA_BOUND_CHAIN_ID, SOLANA_DEVNET_BOUND_CHAIN_ID, SOLANA_MAINNET_BOUND_CHAIN_ID, DEFAULT_BOUND_PARAMS, type BoundParams, type BoundParamsMode, type SolanaPrivacyDomainContext, type SolanaPrivacyDomainKind, } from "./bound-params";
42
- export { getConfig, setConfig, createConfig, initConfig, DEVNET_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";
42
+ export { formatSpendDoc, renderSpendDoc, SpendDocMismatch, type SpendDoc, type SpendSignals, } from "./spend-doc";
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";
43
44
  export { POOL_STATE_DISCRIMINATOR, POOL_STATE_LEN, POOL_STATE_OFFSETS, POOL_FLAG, parsePoolState, parsePoolFees, BPS_DENOMINATOR, computeBpsFee, feeShareBps, } from "./pool-state";
44
45
  export type { PoolState, PoolFees } from "./pool-state";
45
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 { createStealthDeposit, createStealthDepositWithKeys, createStealthOutput, createStealthOutputWithKeys, createStealthOutputForCommitment, packStealthOutputForCircuit, scanAnnouncements, scanAnnouncementsViewOnly, scanAnnouncementsViewOnlyMulti, exportViewOnlyKeys, encodeViewOnlyKeys, decodeViewOnlyKeys, prepareClaimInputs, scanUnifiedNotes, scanUnifiedNotesMulti, encryptAmount, decryptAmount, computeNullifierHashForNote, computeNullifierBytes, parseAnnouncementsFromHex, createDepositFromConfig, createDirectVaultDeposit, 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 NonInteractiveDepositWithRefundResult, } from "./stealth";
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, 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 NonInteractiveDepositWithRefundResult, } from "./stealth";
47
48
  export { buildDepositPsbt, estimateDepositFee, fetchUtxos, selectUtxos, type BuildDepositPsbtParams, type BuildDepositPsbtResult, type UtxoDescriptor, } from "./psbt";
48
49
  export { EsploraClient, esploraTestnet, esploraMainnet, type EsploraTransaction, type EsploraVin, type EsploraVout, type EsploraStatus, type EsploraAddressInfo, type EsploraUtxo, type EsploraMerkleProof, type EsploraNetwork, } from "./core/esplora";
49
50
  export { MempoolClient, mempoolTestnet, mempoolMainnet, reverseBytes, type BlockHeader, type TransactionInfo, type SPVProofData, } from "./core/mempool";
@@ -52,7 +53,7 @@ export { setDebug } from "./logger";
52
53
  export { createFetchConnectionAdapter, createConnectionAdapterFromWeb3, createConnectionAdapterFromKit, getConnectionAdapter, clearConnectionAdapterCache, type RpcConfig, type Web3Connection, type KitRpc, } from "./solana/connection";
53
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";
54
55
  export { COMMITMENT_TREE_DISCRIMINATOR, parseCommitmentTreeData, isValidRoot, fetchCommitmentTree, getCommitmentIndex, saveCommitmentIndex, CommitmentTreeIndex, buildCommitmentTreeFromChain, fetchLeafIndexForCommitment, fetchMerkleProofForCommitment, getMerkleProofFromTree, type CommitmentTreeState, type RpcClient, type OnChainMerkleProof, } from "./commitment-tree";
55
- 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, 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, 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, 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
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, isVkRegistryReady, type JoinSplitVkMaterial, type SnarkjsVkeyJson, type ParsedVkRegistry, } from "./vk-registry";
57
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";
58
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";
@@ -94,7 +94,7 @@ export { createMerkleProof, createMerkleProofFromBigints, proofToCircomFormat, p
94
94
  // ==========================================================================
95
95
  // Taproot address utilities
96
96
  // ==========================================================================
97
- export { deriveTaprootAddress, deriveTaprootAddressWithRefund, buildRefundScript, computeTapLeafHash, verifyTaprootAddress, createP2TRScriptPubkey, parseP2TRScriptPubkey, isValidBitcoinAddress, getInternalKey, createCustomInternalKey, createOpReturnScriptFromPayload, buildDepositOpReturn, parseDepositOpReturn, encodeDepositOpReturnHeader, decodeDepositOpReturnHeader, validateDepositOpReturnContext, computeDepositPoolTag, DEPOSIT_DESTINATION_CHAIN, DEPOSIT_BITCOIN_NETWORK, DEPOSIT_OP_RETURN_VERSION, DEPOSIT_POOL_TAG_SIZE, DEPOSIT_OP_RETURN_SIZE, } from "./taproot";
97
+ export { deriveTaprootAddress, deriveTaprootAddressWithRefund, depositTweakCommitment, buildRefundScript, computeTapLeafHash, verifyTaprootAddress, createP2TRScriptPubkey, parseP2TRScriptPubkey, isValidBitcoinAddress, getInternalKey, createCustomInternalKey, createOpReturnScriptFromPayload, buildDepositOpReturn, parseDepositOpReturn, encodeDepositOpReturnHeader, decodeDepositOpReturnHeader, validateDepositOpReturnContext, computeDepositPoolTag, DEPOSIT_DESTINATION_CHAIN, DEPOSIT_BITCOIN_NETWORK, DEPOSIT_OP_RETURN_VERSION, DEPOSIT_POOL_TAG_SIZE, DEPOSIT_OP_RETURN_SIZE, } from "./taproot";
98
98
  // ==========================================================================
99
99
  // Claim link utilities
100
100
  // ==========================================================================
@@ -107,10 +107,11 @@ export { uploadTransactionToBuffer, uploadProofToBuffer, closeBuffer, readBuffer
107
107
  // Bound Parameters (JoinSplit transaction binding)
108
108
  // ==========================================================================
109
109
  export { computeBoundParamsHash, computeSolanaDomainBoundParamsHash, computeSolanaDomainSeparator, computeStealthDataHash, createTransferBoundParams, createUnshieldBoundParams, createRedeemBoundParams, SOLANA_BOUND_CHAIN_ID, SOLANA_DEVNET_BOUND_CHAIN_ID, SOLANA_MAINNET_BOUND_CHAIN_ID, DEFAULT_BOUND_PARAMS, } from "./bound-params";
110
+ export { formatSpendDoc, renderSpendDoc, SpendDocMismatch, } from "./spend-doc";
110
111
  // ==========================================================================
111
112
  // Configuration
112
113
  // ==========================================================================
113
- export { getConfig, setConfig, createConfig, initConfig, DEVNET_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, } from "./config";
114
+ 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, } from "./config";
114
115
  // ==========================================================================
115
116
  // PoolState account layout + fee arithmetic
116
117
  // ==========================================================================
@@ -126,7 +127,7 @@ poolStateSeeds, commitmentTreeSeeds, tokenConfigSeeds, poolConfigSeeds, nullifie
126
127
  // ==========================================================================
127
128
  // Stealth address utilities
128
129
  // ==========================================================================
129
- export { createStealthDeposit, createStealthDepositWithKeys, createStealthOutput, createStealthOutputWithKeys, createStealthOutputForCommitment, packStealthOutputForCircuit, scanAnnouncements, scanAnnouncementsViewOnly, scanAnnouncementsViewOnlyMulti, exportViewOnlyKeys, encodeViewOnlyKeys, decodeViewOnlyKeys, prepareClaimInputs, scanUnifiedNotes, scanUnifiedNotesMulti, encryptAmount, decryptAmount, computeNullifierHashForNote, computeNullifierBytes, parseAnnouncementsFromHex, createDepositFromConfig, createDirectVaultDeposit, isDepositForViewer, isDepositForViewerHex, ANNOUNCEMENT_TYPE_DEPOSIT, ANNOUNCEMENT_TYPE_TRANSFER, createNonInteractiveDeposit, pickIkaCustodyKey, } from "./stealth";
130
+ export { createStealthDeposit, createStealthDepositWithKeys, createStealthOutput, createStealthOutputWithKeys, createStealthOutputForCommitment, packStealthOutputForCircuit, scanAnnouncements, scanAnnouncementsViewOnly, scanAnnouncementsViewOnlyMulti, exportViewOnlyKeys, encodeViewOnlyKeys, decodeViewOnlyKeys, prepareClaimInputs, scanUnifiedNotes, scanUnifiedNotesMulti, encryptAmount, decryptAmount, computeNullifierHashForNote, computeNullifierBytes, parseAnnouncementsFromHex, createDepositFromConfig, createDirectVaultDeposit, createTweakDeposit, isDepositForViewer, isDepositForViewerHex, ANNOUNCEMENT_TYPE_DEPOSIT, ANNOUNCEMENT_TYPE_TRANSFER, createNonInteractiveDeposit, pickIkaCustodyKey, } from "./stealth";
130
131
  // ==========================================================================
131
132
  // PSBT builder for wallet-integrated deposits
132
133
  // ==========================================================================
@@ -180,7 +181,7 @@ buildRotateTreeInstructionData, buildRotateTreeInstruction,
180
181
  // MagicBlock lifecycle
181
182
  buildMagicBlockDelegateInstructionData, buildMagicBlockDelegateInstruction, buildMagicBlockCommitInstructionData, buildMagicBlockCommitInstruction, buildMagicBlockPerPermissionInstructionData, buildMagicBlockPerPermissionInstruction, buildPolicyRequestHash, buildPolicyIntentParts, buildRegisterExitDestinationInstruction, buildRegisterExitDestinationInstructionData, MAX_POLICY_INTENT_PARTS, buildInitializePolicyApprovalInstructionData, buildInitializePolicyApprovalInstruction, buildPolicyApprovalDecisionInstruction, buildPolicyApprovalCommitInstruction, buildCompleteDepositPermissionedInstructionData, buildCompleteDepositPermissionedInstruction, buildShieldPermissionedInstructionData, buildShieldPermissionedInstruction, buildRotateAuditorInstructionData, buildRotateAuditorInstruction,
182
183
  // Verify instruction data builders
183
- buildVerifyTransactionInstructionData, buildCompleteDepositInstructionData,
184
+ buildVerifyTransactionInstructionData, buildCompleteDepositInstructionData, buildVerifyDepositInstructionData,
184
185
  // Pool config (disc 2) builder + parser
185
186
  buildSetPoolConfigInstructionData, parsePoolConfig, POOL_CONFIG_DISCRIMINATOR, POOL_CONFIG_LEN, POOL_SCRIPT_MAX_LEN, } from "./instructions";
186
187
  // ==========================================================================
@@ -32,10 +32,13 @@ export declare const INSTRUCTION_DISCRIMINATORS: {
32
32
  readonly CLAIM_FEES: 10;
33
33
  readonly COMPLETE_DEPOSIT: 11;
34
34
  readonly SHIELD: 12;
35
+ /** OP_RETURN-free deposit: note keys ride in instruction data, proven by the
36
+ * deposit address's Taproot tweak. */
37
+ readonly VERIFY_DEPOSIT: 25;
35
38
  readonly TRANSACT: 13;
36
39
  readonly UNSHIELD: 14;
37
40
  readonly REDEEM: 15;
38
- readonly RESERVED_REQUEST_REDEMPTION: 16;
41
+ readonly FREEZE_VK_REGISTRY: 16;
39
42
  readonly COMPLETE_REDEMPTION: 17;
40
43
  readonly MARK_PROCESSING: 18;
41
44
  readonly CANCEL_REDEMPTION: 19;
@@ -113,6 +116,17 @@ export interface CompleteRedemptionInstructionOptions {
113
116
  poolVault: Address;
114
117
  completionReceipt: Address;
115
118
  poolConfig: Address;
119
+ /** HeightIndex PDA for the VerifiedTransaction's block —
120
+ * `deriveHeightIndexPDA(blockHeight, config.btcLightClientProgramId)`.
121
+ *
122
+ * REQUIRED. The program re-checks that the proof's block is still the canonical one at
123
+ * that height before it settles (audit_1 F-BTC-04): a VerifiedTransaction records a merkle
124
+ * proof that was valid once and is never invalidated, and the confirmation count is taken
125
+ * against a tip that only grows, so neither notices a reorg. Omitting this fails with
126
+ * InvalidSpvProof — the program locates the account by address, so its position in the
127
+ * list does not matter, but its absence is an error rather than a skipped check.
128
+ */
129
+ heightIndex: Address;
116
130
  /** Change UTXO PDA. Required when poolScript is non-empty. */
117
131
  changeUtxo?: Address;
118
132
  /** zkBTC TokenConfig PDA (credits protocol revenue) */
@@ -135,6 +149,17 @@ export interface ApproveRedemptionSigningInstructionOptions {
135
149
  redemptionRequest: Address;
136
150
  authority: Address;
137
151
  poolConfig: Address;
152
+ /** HeightIndex PDA for the VerifiedTransaction's block —
153
+ * `deriveHeightIndexPDA(blockHeight, config.btcLightClientProgramId)`.
154
+ *
155
+ * REQUIRED. The program re-checks that the proof's block is still the canonical one at
156
+ * that height before it settles (audit_1 F-BTC-04): a VerifiedTransaction records a merkle
157
+ * proof that was valid once and is never invalidated, and the confirmation count is taken
158
+ * against a tip that only grows, so neither notices a reorg. Omitting this fails with
159
+ * InvalidSpvProof — the program locates the account by address, so its position in the
160
+ * list does not matter, but its absence is an error rather than a skipped check.
161
+ */
162
+ heightIndex: Address;
138
163
  ikaProgram: Address;
139
164
  ikaCoordinator: Address;
140
165
  ikaMessageApproval: Address;
@@ -732,6 +757,38 @@ export declare function buildCompleteDepositInstructionData(params: {
732
757
  depositTxSize: number;
733
758
  depositTxid: Uint8Array;
734
759
  }): Uint8Array;
760
+ /**
761
+ * Build utxopia verify_deposit instruction data (disc=25).
762
+ *
763
+ * The OP_RETURN-free deposit path. `notePublicKey` + `ephemeralPubkey` travel in
764
+ * instruction data instead of in the Bitcoin transaction, and the program proves
765
+ * them against the deposit output's Taproot tweak — a different key pair derives
766
+ * a different address, which the funding transaction did not pay. Nothing marks
767
+ * the deposit as a UTXOpia transaction on chain, so any wallet or exchange that
768
+ * can send to a P2TR address can fund it.
769
+ *
770
+ * The address must be derived from `depositTweakCommitment(npk, eph)`, NOT from
771
+ * the note key alone — the program hashes both, so that a caller cannot swap in
772
+ * an ephemeral key that leaves the note undiscoverable.
773
+ *
774
+ * Sweep mode only: `depositTxSize` must be non-zero. The receipt PDA is seeded
775
+ * `["deposit_receipt", txid, vout]`, so pass `depositVout` to
776
+ * `deriveDepositReceiptPDA` for this flow.
777
+ *
778
+ * Layout: disc(1) + sweep_txid(32) + block_height(u64 LE) + sweep_tx_size(u32 LE)
779
+ * + deposit_tx_size(u32 LE) + deposit_txid(32) + ephemeral_pubkey(32)
780
+ * + note_public_key(32) + deposit_vout(u32 LE) = 149 bytes
781
+ */
782
+ export declare function buildVerifyDepositInstructionData(params: {
783
+ sweepTxid: Uint8Array;
784
+ blockHeight: number;
785
+ sweepTxSize: number;
786
+ depositTxSize: number;
787
+ depositTxid: Uint8Array;
788
+ ephemeralPubkey: Uint8Array;
789
+ notePublicKey: Uint8Array;
790
+ depositVout: number;
791
+ }): Uint8Array;
735
792
  /** PoolConfig account discriminator (0x0a) */
736
793
  export declare const POOL_CONFIG_DISCRIMINATOR = 10;
737
794
  /** Serialized PoolConfig account length (bytes) */
@@ -901,6 +958,17 @@ export interface CompleteDepositPermissionedOptions {
901
958
  tokenConfig: Address;
902
959
  /** 14. pool_config PDA (readonly) */
903
960
  poolConfig: Address;
961
+ /** HeightIndex PDA for the VerifiedTransaction's block —
962
+ * `deriveHeightIndexPDA(blockHeight, config.btcLightClientProgramId)`.
963
+ *
964
+ * REQUIRED. The program re-checks that the proof's block is still the canonical one at
965
+ * that height before it settles (audit_1 F-BTC-04): a VerifiedTransaction records a merkle
966
+ * proof that was valid once and is never invalidated, and the confirmation count is taken
967
+ * against a tip that only grows, so neither notices a reorg. Omitting this fails with
968
+ * InvalidSpvProof — the program locates the account by address, so its position in the
969
+ * list does not matter, but its absence is an error rather than a skipped check.
970
+ */
971
+ heightIndex: Address;
904
972
  /** 15. one-time PolicyApproval (writable) */
905
973
  policyApproval: Address;
906
974
  };
@@ -944,6 +1012,8 @@ export declare function buildCompleteDepositPermissionedInstructionData(options:
944
1012
  * 13. token_config (writable)
945
1013
  * 14. pool_config (readonly)
946
1014
  * 15. policy_approval (writable)
1015
+ * 16. policy_program (readonly)
1016
+ * 17. height_index (readonly) — canonicality re-check, located by address
947
1017
  */
948
1018
  export declare function buildCompleteDepositPermissionedInstruction(options: CompleteDepositPermissionedOptions): Instruction;
949
1019
  /** shieldPermissioned instruction options */
@@ -33,15 +33,19 @@ const INSTRUCTION = {
33
33
  REGISTER_TOKEN: 8,
34
34
  UPDATE_TOKEN_CONFIG: 9,
35
35
  CLAIM_FEES: 10,
36
- // Deposit (11-12)
36
+ // Deposit (11-12, 25)
37
37
  COMPLETE_DEPOSIT: 11,
38
38
  SHIELD: 12,
39
+ /** OP_RETURN-free deposit: note keys ride in instruction data, proven by the
40
+ * deposit address's Taproot tweak. */
41
+ VERIFY_DEPOSIT: 25,
39
42
  // JoinSplit (13-15) — all share n_in + n_out + n_pub + proof_source header
40
43
  TRANSACT: 13,
41
44
  UNSHIELD: 14,
42
45
  REDEEM: 15,
43
- // Redemption lifecycle (16-19)
44
- RESERVED_REQUEST_REDEMPTION: 16,
46
+ // VK registry freeze (16) — NOT part of the redemption range below
47
+ FREEZE_VK_REGISTRY: 16,
48
+ // Redemption lifecycle (17-19)
45
49
  COMPLETE_REDEMPTION: 17,
46
50
  MARK_PROCESSING: 18,
47
51
  CANCEL_REDEMPTION: 19,
@@ -308,6 +312,9 @@ export function buildCompleteRedemptionInstruction(options) {
308
312
  }
309
313
  }
310
314
  accounts.push({ address: options.accounts.tokenConfig, role: AccountRole.WRITABLE });
315
+ // Located by address, so the trailing position is free — this instruction already has a
316
+ // variable tail (change UTXO, consumed UTXOs) and the program scans rather than indexing.
317
+ accounts.push({ address: options.accounts.heightIndex, role: AccountRole.READONLY });
311
318
  return {
312
319
  programAddress: config.utxopiaProgramId,
313
320
  accounts,
@@ -1297,6 +1304,67 @@ export function buildCompleteDepositInstructionData(params) {
1297
1304
  return data;
1298
1305
  }
1299
1306
  // =============================================================================
1307
+ // UTXOpia Verify Deposit (disc=25) — OP_RETURN-free
1308
+ // =============================================================================
1309
+ /**
1310
+ * Build utxopia verify_deposit instruction data (disc=25).
1311
+ *
1312
+ * The OP_RETURN-free deposit path. `notePublicKey` + `ephemeralPubkey` travel in
1313
+ * instruction data instead of in the Bitcoin transaction, and the program proves
1314
+ * them against the deposit output's Taproot tweak — a different key pair derives
1315
+ * a different address, which the funding transaction did not pay. Nothing marks
1316
+ * the deposit as a UTXOpia transaction on chain, so any wallet or exchange that
1317
+ * can send to a P2TR address can fund it.
1318
+ *
1319
+ * The address must be derived from `depositTweakCommitment(npk, eph)`, NOT from
1320
+ * the note key alone — the program hashes both, so that a caller cannot swap in
1321
+ * an ephemeral key that leaves the note undiscoverable.
1322
+ *
1323
+ * Sweep mode only: `depositTxSize` must be non-zero. The receipt PDA is seeded
1324
+ * `["deposit_receipt", txid, vout]`, so pass `depositVout` to
1325
+ * `deriveDepositReceiptPDA` for this flow.
1326
+ *
1327
+ * Layout: disc(1) + sweep_txid(32) + block_height(u64 LE) + sweep_tx_size(u32 LE)
1328
+ * + deposit_tx_size(u32 LE) + deposit_txid(32) + ephemeral_pubkey(32)
1329
+ * + note_public_key(32) + deposit_vout(u32 LE) = 149 bytes
1330
+ */
1331
+ export function buildVerifyDepositInstructionData(params) {
1332
+ for (const [name, value] of [
1333
+ ["sweepTxid", params.sweepTxid],
1334
+ ["depositTxid", params.depositTxid],
1335
+ ["ephemeralPubkey", params.ephemeralPubkey],
1336
+ ["notePublicKey", params.notePublicKey],
1337
+ ]) {
1338
+ if (value.length !== 32) {
1339
+ throw new Error(`${name} must be 32 bytes, got ${value.length}`);
1340
+ }
1341
+ }
1342
+ if (params.depositTxSize === 0) {
1343
+ throw new Error("verify_deposit is sweep-mode only: depositTxSize must be non-zero");
1344
+ }
1345
+ const data = new Uint8Array(149);
1346
+ const view = new DataView(data.buffer);
1347
+ let offset = 0;
1348
+ data[offset++] = INSTRUCTION.VERIFY_DEPOSIT;
1349
+ data.set(params.sweepTxid, offset);
1350
+ offset += 32;
1351
+ view.setBigUint64(offset, BigInt(params.blockHeight), true);
1352
+ offset += 8;
1353
+ view.setUint32(offset, params.sweepTxSize, true);
1354
+ offset += 4;
1355
+ view.setUint32(offset, params.depositTxSize, true);
1356
+ offset += 4;
1357
+ data.set(params.depositTxid, offset);
1358
+ offset += 32;
1359
+ data.set(params.ephemeralPubkey, offset);
1360
+ offset += 32;
1361
+ data.set(params.notePublicKey, offset);
1362
+ offset += 32;
1363
+ view.setUint32(offset, params.depositVout, true);
1364
+ offset += 4;
1365
+ return data;
1366
+ }
1367
+ // =============================================================================
1300
1368
  // UTXOpia Set Pool Config (disc=2)
1301
1369
  // =============================================================================
1302
1370
  /** PoolConfig account discriminator (0x0a) */
@@ -1523,6 +1591,8 @@ export function buildCompleteDepositPermissionedInstructionData(options) {
1523
1591
  * 13. token_config (writable)
1524
1592
  * 14. pool_config (readonly)
1525
1593
  * 15. policy_approval (writable)
1594
+ * 16. policy_program (readonly)
1595
+ * 17. height_index (readonly) — canonicality re-check, located by address
1526
1596
  */
1527
1597
  export function buildCompleteDepositPermissionedInstruction(options) {
1528
1598
  const config = getConfig();
@@ -1554,6 +1624,7 @@ export function buildCompleteDepositPermissionedInstruction(options) {
1554
1624
  { address: options.accounts.poolConfig, role: AccountRole.READONLY },
1555
1625
  { address: options.accounts.policyApproval, role: AccountRole.WRITABLE },
1556
1626
  { address: config.policyProgramId ?? config.utxopiaProgramId, role: AccountRole.READONLY },
1627
+ { address: options.accounts.heightIndex, role: AccountRole.READONLY },
1557
1628
  ],
1558
1629
  data,
1559
1630
  };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * "What am I proving?" — a canonical sentence for a shielded spend.
3
+ *
4
+ * The text is not the point. The point is that `renderSpendDoc` recomputes the
5
+ * proof's public signals from the numbers it is about to print and throws if
6
+ * they disagree, so a UI cannot caption a proof with an amount or a destination
7
+ * the proof does not actually contain.
8
+ *
9
+ * Nothing here changes the circuit. Every value below is already bound to the
10
+ * user's spending key by the in-circuit EdDSA over
11
+ * Poseidon(merkleRoot, boundParamsHash, nullifiers.., commitmentsOut..).
12
+ */
13
+ import { type SolanaPrivacyDomainContext } from "./bound-params";
14
+ export interface SpendDoc {
15
+ mode: "transfer" | "unshield" | "redeem";
16
+ /** Display label, e.g. "Solana Devnet". */
17
+ network: string;
18
+ /** Display label, e.g. "zkBTC". */
19
+ asset: string;
20
+ decimals: number;
21
+ /** Destination as shown to the user (a .sol name, a BTC address, a pubkey). */
22
+ recipient: string;
23
+ /**
24
+ * The destination bytes actually folded into boundParamsHash: 32-byte Solana
25
+ * owner for `unshield`, raw scriptPubKey for `redeem`. Omitted for `transfer`,
26
+ * where the destination is private and provably absent from the signals.
27
+ */
28
+ recipientBytes?: Uint8Array;
29
+ /** Raw units the recipient receives. */
30
+ amount: bigint;
31
+ relayerFee: bigint;
32
+ change: bigint;
33
+ }
34
+ /** The public signals of the proof about to be generated, plus what built them. */
35
+ export interface SpendSignals {
36
+ /** `outputs.map(o => o.value)` from JoinSplitProofInputs. */
37
+ outputValues: bigint[];
38
+ /** The boundParamsHash going into the proof. */
39
+ boundParamsHash: bigint;
40
+ stealthDataHash: Uint8Array;
41
+ chainId: bigint;
42
+ domain: SolanaPrivacyDomainContext;
43
+ /** Redeem only: the on-chain requester bound into the proof. */
44
+ requester?: Uint8Array;
45
+ treeNumber?: number;
46
+ }
47
+ export declare class SpendDocMismatch extends Error {
48
+ }
49
+ /**
50
+ * The statement itself. Show this before the user commits; pass the same `doc`
51
+ * to `renderSpendDoc` when the proof is built so the string they read is the
52
+ * string that gets checked.
53
+ */
54
+ export declare function formatSpendDoc(doc: SpendDoc): string;
55
+ /**
56
+ * Render the statement, or throw if it does not describe `signals`.
57
+ *
58
+ * Checked: every amount on screen is an output value of the proof and there are
59
+ * no other outputs; the destination reproduces boundParamsHash.
60
+ * Not checked: that `recipient` (a label) names `recipientBytes` — the doc
61
+ * prints the bound bytes so that stays verifiable by eye.
62
+ */
63
+ export declare function renderSpendDoc(doc: SpendDoc, signals: SpendSignals): string;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * "What am I proving?" — a canonical sentence for a shielded spend.
3
+ *
4
+ * The text is not the point. The point is that `renderSpendDoc` recomputes the
5
+ * proof's public signals from the numbers it is about to print and throws if
6
+ * they disagree, so a UI cannot caption a proof with an amount or a destination
7
+ * the proof does not actually contain.
8
+ *
9
+ * Nothing here changes the circuit. Every value below is already bound to the
10
+ * user's spending key by the in-circuit EdDSA over
11
+ * Poseidon(merkleRoot, boundParamsHash, nullifiers.., commitmentsOut..).
12
+ */
13
+ import { computeSolanaDomainBoundParamsHash, createRedeemBoundParams, createTransferBoundParams, createUnshieldBoundParams, } from "./bound-params";
14
+ export class SpendDocMismatch extends Error {
15
+ }
16
+ function fmt(raw, decimals) {
17
+ const neg = raw < 0n;
18
+ const s = (neg ? -raw : raw).toString().padStart(decimals + 1, "0");
19
+ const whole = s.slice(0, s.length - decimals);
20
+ const frac = decimals === 0 ? "" : s.slice(s.length - decimals).replace(/0+$/, "");
21
+ return `${neg ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
22
+ }
23
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
24
+ function sortedValues(v) {
25
+ return [...v].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(",");
26
+ }
27
+ function expectedBoundParamsHash(doc, s) {
28
+ const tree = s.treeNumber ?? 0;
29
+ switch (doc.mode) {
30
+ case "transfer":
31
+ return computeSolanaDomainBoundParamsHash(createTransferBoundParams(s.stealthDataHash, s.chainId, tree), s.domain);
32
+ case "unshield":
33
+ if (!doc.recipientBytes)
34
+ throw new SpendDocMismatch("unshield doc has no recipientBytes");
35
+ return computeSolanaDomainBoundParamsHash(createUnshieldBoundParams(doc.recipientBytes, s.stealthDataHash, s.chainId, tree), s.domain);
36
+ case "redeem":
37
+ if (!doc.recipientBytes)
38
+ throw new SpendDocMismatch("redeem doc has no recipientBytes");
39
+ if (!s.requester)
40
+ throw new SpendDocMismatch("redeem doc has no requester");
41
+ return computeSolanaDomainBoundParamsHash(createRedeemBoundParams(doc.recipientBytes, s.stealthDataHash, s.requester, s.chainId, tree), s.domain);
42
+ }
43
+ }
44
+ /**
45
+ * The statement itself. Show this before the user commits; pass the same `doc`
46
+ * to `renderSpendDoc` when the proof is built so the string they read is the
47
+ * string that gets checked.
48
+ */
49
+ export function formatSpendDoc(doc) {
50
+ const amt = (v) => `${fmt(v, doc.decimals)} ${doc.asset}`;
51
+ const action = doc.mode === "redeem"
52
+ ? `Withdraw ${amt(doc.amount)} to Bitcoin`
53
+ : doc.mode === "unshield"
54
+ ? `Unshield ${amt(doc.amount)}`
55
+ : `Send ${amt(doc.amount)} privately`;
56
+ const lines = [
57
+ "UTXOpia Proof",
58
+ "",
59
+ "I AM PROVING",
60
+ action,
61
+ "",
62
+ "DETAILS",
63
+ `Network: ${doc.network}`,
64
+ `Amount leaving the pool: ${amt(doc.amount)}`,
65
+ `To: ${doc.recipient}`,
66
+ ];
67
+ if (doc.recipientBytes)
68
+ lines.push(`Bound destination: ${hex(doc.recipientBytes)}`);
69
+ if (doc.relayerFee > 0n)
70
+ lines.push(`Relayer fee: ${amt(doc.relayerFee)}`);
71
+ if (doc.change > 0n)
72
+ lines.push(`Change back to me: ${amt(doc.change)}`);
73
+ lines.push("", "ENFORCED BY THE PROOF", "Amounts: these are every output this proof creates", doc.mode === "transfer"
74
+ ? "Destination: private — not in the public signals. Confirm it with the recipient."
75
+ : "Destination: bound into boundParamsHash and re-derived onchain");
76
+ if (doc.mode !== "transfer") {
77
+ lines.push("Protocol fee: deducted onchain from pool policy, not part of this statement");
78
+ }
79
+ lines.push("", "Protocol: utxopia-spend-doc-v1");
80
+ return lines.join("\n");
81
+ }
82
+ /**
83
+ * Render the statement, or throw if it does not describe `signals`.
84
+ *
85
+ * Checked: every amount on screen is an output value of the proof and there are
86
+ * no other outputs; the destination reproduces boundParamsHash.
87
+ * Not checked: that `recipient` (a label) names `recipientBytes` — the doc
88
+ * prints the bound bytes so that stays verifiable by eye.
89
+ */
90
+ export function renderSpendDoc(doc, signals) {
91
+ const shown = [doc.amount, doc.relayerFee, doc.change].filter((v) => v > 0n);
92
+ if (sortedValues(shown) !== sortedValues(signals.outputValues)) {
93
+ throw new SpendDocMismatch(`amounts do not match the proof outputs: doc [${sortedValues(shown)}] vs proof [${sortedValues(signals.outputValues)}]`);
94
+ }
95
+ const expected = expectedBoundParamsHash(doc, signals);
96
+ if (expected !== signals.boundParamsHash) {
97
+ throw new SpendDocMismatch("destination does not match the proof's boundParamsHash");
98
+ }
99
+ return formatSpendDoc(doc);
100
+ }
@@ -200,6 +200,34 @@ export declare function createNonInteractiveDeposit(recipientMeta: StealthMetaAd
200
200
  * credits the note from that transaction.
201
201
  */
202
202
  export declare function createDirectVaultDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?: "mainnet" | "testnet" | "regtest", opReturnContext?: DepositOpReturnContext): Promise<NonInteractiveDepositResult>;
203
+ /** A deposit whose address alone binds the note keys — no OP_RETURN. */
204
+ export interface TweakDepositResult {
205
+ /** Taproot address to send BTC to */
206
+ btcAddress: string;
207
+ /** 32-byte x-only output key for the deposit P2TR output */
208
+ depositOutputKey: Uint8Array;
209
+ /** 32-byte note public key */
210
+ npk: Uint8Array;
211
+ /** 32-byte Ed25519 ephemeral public key */
212
+ ephemeralPub: Uint8Array;
213
+ /** sha256(npk || ephemeralPub) — the commitment the address is tweaked by */
214
+ tweakCommitment: Uint8Array;
215
+ }
216
+ /**
217
+ * Create a deposit for the OP_RETURN-free flow (`verify_deposit`, disc 25).
218
+ *
219
+ * The transaction carries nothing but a payment, so anything that can send to a
220
+ * P2TR address can fund it — a hardware wallet, an exchange withdrawal, a faucet.
221
+ * The note keys are recovered from instruction data at completion time and proven
222
+ * against this address's Taproot tweak, so substituting either key derives a
223
+ * different address that the funding transaction never paid.
224
+ *
225
+ * Sweep mode: the pool sweeps this address into its own custody, and that sweep
226
+ * is what gets SPV-verified. Register the address with the tracker BEFORE any
227
+ * coins are sent — a deposit with no OP_RETURN is invisible to block scanning,
228
+ * so an unregistered address is one nobody is watching.
229
+ */
230
+ export declare function createTweakDeposit(recipientMeta: StealthMetaAddress, vaultXOnlyPubkey: Uint8Array, network?: "mainnet" | "testnet" | "regtest"): Promise<TweakDepositResult>;
203
231
  /**
204
232
  * Create a non-interactive deposit using the current SDK config.
205
233
  *
@@ -248,6 +248,42 @@ export async function createDirectVaultDeposit(recipientMeta, vaultXOnlyPubkey,
248
248
  ephemeralPub,
249
249
  };
250
250
  }
251
+ /**
252
+ * Create a deposit for the OP_RETURN-free flow (`verify_deposit`, disc 25).
253
+ *
254
+ * The transaction carries nothing but a payment, so anything that can send to a
255
+ * P2TR address can fund it — a hardware wallet, an exchange withdrawal, a faucet.
256
+ * The note keys are recovered from instruction data at completion time and proven
257
+ * against this address's Taproot tweak, so substituting either key derives a
258
+ * different address that the funding transaction never paid.
259
+ *
260
+ * Sweep mode: the pool sweeps this address into its own custody, and that sweep
261
+ * is what gets SPV-verified. Register the address with the tracker BEFORE any
262
+ * coins are sent — a deposit with no OP_RETURN is invisible to block scanning,
263
+ * so an unregistered address is one nobody is watching.
264
+ */
265
+ export async function createTweakDeposit(recipientMeta, vaultXOnlyPubkey, network = "testnet") {
266
+ if (vaultXOnlyPubkey.length !== 32) {
267
+ throw new Error("vaultXOnlyPubkey must be 32 bytes");
268
+ }
269
+ const viewingPubKey = new Uint8Array(recipientMeta.viewingPubKey);
270
+ const ephemeral = ed25519GenerateKeyPair();
271
+ const sharedSecret = x25519Ecdh(ephemeral.privKey, viewingPubKey);
272
+ const stealthScalar = deriveStealthScalar(sharedSecret);
273
+ const recipientMPK = bytesToBigint(recipientMeta.mpk);
274
+ const npk = bigintToBytes(computeNPKSync(recipientMPK, stealthScalar));
275
+ const ephemeralPub = new Uint8Array(ephemeral.pubKey);
276
+ const { depositTweakCommitment, deriveTaprootAddress } = await import("./taproot");
277
+ const tweakCommitment = depositTweakCommitment(npk, ephemeralPub);
278
+ const { address, outputKey } = deriveTaprootAddress(tweakCommitment, network, vaultXOnlyPubkey);
279
+ return {
280
+ btcAddress: address,
281
+ depositOutputKey: outputKey,
282
+ npk,
283
+ ephemeralPub,
284
+ tweakCommitment,
285
+ };
286
+ }
251
287
  /**
252
288
  * Create a non-interactive deposit using the current SDK config.
253
289
  *
@@ -23,6 +23,17 @@ export declare function deriveTaprootAddress(commitment: Uint8Array, network?: "
23
23
  outputKey: Uint8Array;
24
24
  tweak: Uint8Array;
25
25
  };
26
+ /**
27
+ * Commitment a `verify_deposit` (disc 25) deposit address is derived from.
28
+ *
29
+ * Both keys are hashed in. Binding the note key alone would leave the ephemeral
30
+ * pubkey caller-chosen on the Solana side: the credited amount and owner would
31
+ * still be right, but a substituted ephemeral key makes the stealth announcement
32
+ * undecryptable and the recipient never finds their note.
33
+ *
34
+ * Feed the result to `deriveTaprootAddress` as the commitment.
35
+ */
36
+ export declare function depositTweakCommitment(notePublicKey: Uint8Array, ephemeralPubkey: Uint8Array): Uint8Array;
26
37
  /**
27
38
  * Verify that a Taproot address is correctly derived from a commitment
28
39
  *
@@ -70,6 +70,25 @@ export function deriveTaprootAddress(commitment, network = "testnet", internalKe
70
70
  tweak,
71
71
  };
72
72
  }
73
+ /**
74
+ * Commitment a `verify_deposit` (disc 25) deposit address is derived from.
75
+ *
76
+ * Both keys are hashed in. Binding the note key alone would leave the ephemeral
77
+ * pubkey caller-chosen on the Solana side: the credited amount and owner would
78
+ * still be right, but a substituted ephemeral key makes the stealth announcement
79
+ * undecryptable and the recipient never finds their note.
80
+ *
81
+ * Feed the result to `deriveTaprootAddress` as the commitment.
82
+ */
83
+ export function depositTweakCommitment(notePublicKey, ephemeralPubkey) {
84
+ if (notePublicKey.length !== 32 || ephemeralPubkey.length !== 32) {
85
+ throw new Error("notePublicKey and ephemeralPubkey must be 32 bytes");
86
+ }
87
+ const material = new Uint8Array(64);
88
+ material.set(notePublicKey, 0);
89
+ material.set(ephemeralPubkey, 32);
90
+ return sha256(material);
91
+ }
73
92
  /**
74
93
  * Verify that a Taproot address is correctly derived from a commitment
75
94
  *