@utxopia/sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- package/packages/sdk/dist/vk-registry.js +212 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDA (Program Derived Address) Derivation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized module for all UTXOpia PDA derivations.
|
|
5
|
+
* Prevents code duplication across api.ts, pda.ts, etc.
|
|
6
|
+
*
|
|
7
|
+
* @module pda
|
|
8
|
+
*/
|
|
9
|
+
import { getAddressEncoder, getProgramDerivedAddress, } from "@solana/kit";
|
|
10
|
+
import { UTXOPIA_PROGRAM_ID, UTXOPIA_POLICY_PROGRAM_ID, BTC_LIGHT_CLIENT_PROGRAM_ID, } from "./config";
|
|
11
|
+
function seedBytes(value, label) {
|
|
12
|
+
const bytes = typeof value === "string"
|
|
13
|
+
? new Uint8Array(getAddressEncoder().encode(value))
|
|
14
|
+
: value;
|
|
15
|
+
if (bytes.length !== 32)
|
|
16
|
+
throw new Error(`${label} must be 32 bytes`);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// PDA Seeds
|
|
21
|
+
// =============================================================================
|
|
22
|
+
export const PDA_SEEDS = {
|
|
23
|
+
POOL_STATE: "pool_state",
|
|
24
|
+
COMMITMENT_TREE: "commitment_tree",
|
|
25
|
+
LIGHT_CLIENT: "btc_light_client",
|
|
26
|
+
BLOCK_HEADER: "block",
|
|
27
|
+
HEIGHT_INDEX: "height_index",
|
|
28
|
+
VERIFIED_TX: "verified_tx",
|
|
29
|
+
DEPOSIT: "deposit",
|
|
30
|
+
NULLIFIER: "nullifier",
|
|
31
|
+
POLICY_APPROVAL: "policy_approval",
|
|
32
|
+
EXIT_DESTINATION: "exit_destination",
|
|
33
|
+
VK_REGISTRY: "vk_registry",
|
|
34
|
+
TOKEN_CONFIG: "token_config",
|
|
35
|
+
POOL_CONFIG: "pool_config",
|
|
36
|
+
};
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Seed builders — the single definition of every program-derived address
|
|
39
|
+
//
|
|
40
|
+
// These are the authority. Each mirrors the seed array the on-chain program
|
|
41
|
+
// derives with, and every derivation in this file is built on them, so there is
|
|
42
|
+
// one place to change when the program changes.
|
|
43
|
+
//
|
|
44
|
+
// They are exported and synchronous on purpose. `getProgramDerivedAddress` is
|
|
45
|
+
// async, but consumers on @solana/web3.js need `findProgramAddressSync` and
|
|
46
|
+
// `PublicKey`. Without a sync seam those consumers end up re-declaring the seeds
|
|
47
|
+
// themselves — which is exactly how the web app's copy drifted out of sync with
|
|
48
|
+
// the program twice (the nullifier and the redemption request both lost their
|
|
49
|
+
// pool scope, and both failed only on chain, after a proof had been paid for).
|
|
50
|
+
// Take the seeds from here and do your own address math with your own types.
|
|
51
|
+
// =============================================================================
|
|
52
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
53
|
+
const u32le = (n, label) => {
|
|
54
|
+
if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) {
|
|
55
|
+
throw new Error(`${label} must be a u32, got ${n}`);
|
|
56
|
+
}
|
|
57
|
+
const b = new Uint8Array(4);
|
|
58
|
+
new DataView(b.buffer).setUint32(0, n, true);
|
|
59
|
+
return b;
|
|
60
|
+
};
|
|
61
|
+
const u64le = (n) => {
|
|
62
|
+
const b = new Uint8Array(8);
|
|
63
|
+
new DataView(b.buffer).setBigUint64(0, n, true);
|
|
64
|
+
return b;
|
|
65
|
+
};
|
|
66
|
+
/** `["pool_state", pool_id]` — pool_id is the pool's zkBTC mint. */
|
|
67
|
+
export function poolStateSeeds(poolId) {
|
|
68
|
+
return [enc(PDA_SEEDS.POOL_STATE), seedBytes(poolId, "poolId")];
|
|
69
|
+
}
|
|
70
|
+
/** `["commitment_tree", pool_state, tree_index_le]`. */
|
|
71
|
+
export function commitmentTreeSeeds(poolState, treeIndex = 0) {
|
|
72
|
+
return [
|
|
73
|
+
enc(PDA_SEEDS.COMMITMENT_TREE),
|
|
74
|
+
seedBytes(poolState, "poolState"),
|
|
75
|
+
u32le(treeIndex, "treeIndex"),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/** `["token_config", pool_state, mint]`. */
|
|
79
|
+
export function tokenConfigSeeds(poolState, mint) {
|
|
80
|
+
return [
|
|
81
|
+
enc(PDA_SEEDS.TOKEN_CONFIG),
|
|
82
|
+
seedBytes(poolState, "poolState"),
|
|
83
|
+
seedBytes(mint, "mint"),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
/** `["pool_config", pool_state]`. */
|
|
87
|
+
export function poolConfigSeeds(poolState) {
|
|
88
|
+
return [enc(PDA_SEEDS.POOL_CONFIG), seedBytes(poolState, "poolState")];
|
|
89
|
+
}
|
|
90
|
+
/** `["nullifier", pool_state, nullifier]` on tree 0, and
|
|
91
|
+
* `["nullifier", pool_state, tree_index_le, nullifier]` after a rotation.
|
|
92
|
+
*
|
|
93
|
+
* A nullifier is Poseidon(nullifyingKey, leafIndex), so it names a note only
|
|
94
|
+
* within one pool and one tree — leaf indices restart at 0 in each new tree.
|
|
95
|
+
* Drop either scope and two distinct notes collapse onto one PDA, where
|
|
96
|
+
* spending either strands the other. Tree 0 keeps the shorter seeds so records
|
|
97
|
+
* already on chain stay reachable (`joinsplit_common.rs`). */
|
|
98
|
+
export function nullifierRecordSeeds(nullifierHash, poolState, treeIndex = 0) {
|
|
99
|
+
const seeds = [
|
|
100
|
+
enc(PDA_SEEDS.NULLIFIER),
|
|
101
|
+
seedBytes(poolState, "poolState"),
|
|
102
|
+
];
|
|
103
|
+
if (treeIndex !== 0)
|
|
104
|
+
seeds.push(u32le(treeIndex, "treeIndex"));
|
|
105
|
+
seeds.push(nullifierHash);
|
|
106
|
+
return seeds;
|
|
107
|
+
}
|
|
108
|
+
/** `["redemption", pool_state, user, nonce_le]` (`redeem.rs`). */
|
|
109
|
+
export function redemptionRequestSeeds(poolState, userPubkey, nonce) {
|
|
110
|
+
return [
|
|
111
|
+
enc("redemption"),
|
|
112
|
+
seedBytes(poolState, "poolState"),
|
|
113
|
+
seedBytes(userPubkey, "userPubkey"),
|
|
114
|
+
u64le(nonce),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
/** `["vk_registry", [n_inputs], [n_outputs]]`. */
|
|
118
|
+
export function vkRegistrySeeds(nInputs, nOutputs) {
|
|
119
|
+
return [enc(PDA_SEEDS.VK_REGISTRY), Uint8Array.of(nInputs), Uint8Array.of(nOutputs)];
|
|
120
|
+
}
|
|
121
|
+
/** `["deposit_receipt", txid]`, or `[..., vout_le]` for the OP_RETURN-free
|
|
122
|
+
* `verify_deposit` flow. See deriveDepositReceiptPDA for which is which. */
|
|
123
|
+
export function depositReceiptSeeds(depositTxid, depositVout) {
|
|
124
|
+
if (depositTxid.length !== 32) {
|
|
125
|
+
throw new Error(`depositTxid must be 32 bytes, got ${depositTxid.length}`);
|
|
126
|
+
}
|
|
127
|
+
const seeds = [enc("deposit_receipt"), depositTxid];
|
|
128
|
+
if (depositVout !== undefined)
|
|
129
|
+
seeds.push(u32le(depositVout, "depositVout"));
|
|
130
|
+
return seeds;
|
|
131
|
+
}
|
|
132
|
+
/** `["policy_approval", pool_state, request_hash, nonce]`. */
|
|
133
|
+
export function policyApprovalSeeds(poolState, requestHash, nonce) {
|
|
134
|
+
if (requestHash.length !== 32 || nonce.length !== 32) {
|
|
135
|
+
throw new Error("requestHash and nonce must be 32 bytes");
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
enc(PDA_SEEDS.POLICY_APPROVAL),
|
|
139
|
+
seedBytes(poolState, "poolState"),
|
|
140
|
+
requestHash,
|
|
141
|
+
nonce,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
/** `["exit_destination", pool_state, [kind], key]`. */
|
|
145
|
+
export function exitDestinationSeeds(poolState, kind, key) {
|
|
146
|
+
if (key.length !== 32)
|
|
147
|
+
throw new Error("exit destination key must be 32 bytes");
|
|
148
|
+
if (kind !== EXIT_KIND_SOLANA_OWNER && kind !== EXIT_KIND_BTC_SCRIPT) {
|
|
149
|
+
throw new Error("unknown exit destination kind");
|
|
150
|
+
}
|
|
151
|
+
return [
|
|
152
|
+
enc(PDA_SEEDS.EXIT_DESTINATION),
|
|
153
|
+
seedBytes(poolState, "poolState"),
|
|
154
|
+
Uint8Array.of(kind),
|
|
155
|
+
key,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
/** `["btc_light_client"]`. */
|
|
159
|
+
export function lightClientSeeds() {
|
|
160
|
+
return [enc(PDA_SEEDS.LIGHT_CLIENT)];
|
|
161
|
+
}
|
|
162
|
+
/** `["block", block_hash]`. */
|
|
163
|
+
export function blockHeaderSeeds(blockHash) {
|
|
164
|
+
// A short hash still derives *an* address, just not the block's — the caller would then read
|
|
165
|
+
// or write the wrong account with no error anywhere. Every other 32-byte seed here is checked.
|
|
166
|
+
return [enc(PDA_SEEDS.BLOCK_HEADER), seedBytes(blockHash, "blockHash")];
|
|
167
|
+
}
|
|
168
|
+
/** `["height_index", height_le(8)]`. */
|
|
169
|
+
export function heightIndexSeeds(height) {
|
|
170
|
+
return [enc(PDA_SEEDS.HEIGHT_INDEX), u64le(BigInt(height))];
|
|
171
|
+
}
|
|
172
|
+
/** `["verified_tx", block_hash, txid]`. */
|
|
173
|
+
export function verifiedTransactionSeeds(blockHash, txid) {
|
|
174
|
+
return [enc(PDA_SEEDS.VERIFIED_TX), blockHash, txid];
|
|
175
|
+
}
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Core UTXOpia PDAs
|
|
178
|
+
// =============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Derive Pool State PDA
|
|
181
|
+
*/
|
|
182
|
+
export async function derivePoolStatePDA(poolId, programId = UTXOPIA_PROGRAM_ID) {
|
|
183
|
+
const result = await getProgramDerivedAddress({
|
|
184
|
+
programAddress: programId,
|
|
185
|
+
seeds: poolStateSeeds(poolId),
|
|
186
|
+
});
|
|
187
|
+
return [result[0], result[1]];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Derive Commitment Tree PDA
|
|
191
|
+
*
|
|
192
|
+
* @param treeIndex - Tree rotation index (default 0).
|
|
193
|
+
*/
|
|
194
|
+
export async function deriveCommitmentTreePDA(poolState, programId = UTXOPIA_PROGRAM_ID, treeIndex) {
|
|
195
|
+
const result = await getProgramDerivedAddress({
|
|
196
|
+
programAddress: programId,
|
|
197
|
+
seeds: commitmentTreeSeeds(poolState, treeIndex ?? 0),
|
|
198
|
+
});
|
|
199
|
+
return [result[0], result[1]];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Derive TokenConfig PDA for a specific mint
|
|
203
|
+
* Seeds: ["token_config", mint_pubkey_bytes]
|
|
204
|
+
*/
|
|
205
|
+
export async function deriveTokenConfigPDA(poolState, mintPubkey, programId = UTXOPIA_PROGRAM_ID) {
|
|
206
|
+
const result = await getProgramDerivedAddress({
|
|
207
|
+
programAddress: programId,
|
|
208
|
+
seeds: tokenConfigSeeds(poolState, mintPubkey),
|
|
209
|
+
});
|
|
210
|
+
return [result[0], result[1]];
|
|
211
|
+
}
|
|
212
|
+
/** Derive ["pool_config", pool_state]. */
|
|
213
|
+
export async function derivePoolConfigPDA(poolState, programId = UTXOPIA_PROGRAM_ID) {
|
|
214
|
+
const result = await getProgramDerivedAddress({
|
|
215
|
+
programAddress: programId,
|
|
216
|
+
seeds: poolConfigSeeds(poolState),
|
|
217
|
+
});
|
|
218
|
+
return [result[0], result[1]];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Derive Nullifier Record PDA.
|
|
222
|
+
*
|
|
223
|
+
* Seeds are `["nullifier", pool_state, nullifier]` on tree 0, and
|
|
224
|
+
* `["nullifier", pool_state, tree_index_le, nullifier]` on any tree a rotation
|
|
225
|
+
* created. Both scopes exist because a nullifier is Poseidon(nullifyingKey,
|
|
226
|
+
* leafIndex) and so identifies a note only within one pool and one tree:
|
|
227
|
+
*
|
|
228
|
+
* - Without the pool, the same seed spending into two vaults derives one PDA,
|
|
229
|
+
* and spending in either strands the twin note in the other.
|
|
230
|
+
* - Without the tree, the same happens across a rotation, because leaf indices
|
|
231
|
+
* restart at 0 in every new tree.
|
|
232
|
+
*
|
|
233
|
+
* Tree 0 keeps the shorter seeds so the records already on chain stay reachable
|
|
234
|
+
* — re-deriving them would make every already-spent note spendable again.
|
|
235
|
+
*/
|
|
236
|
+
export async function deriveNullifierRecordPDA(nullifierHash, poolState, treeIndex = 0, programId = UTXOPIA_PROGRAM_ID) {
|
|
237
|
+
const result = await getProgramDerivedAddress({
|
|
238
|
+
programAddress: programId,
|
|
239
|
+
seeds: nullifierRecordSeeds(nullifierHash, poolState, treeIndex),
|
|
240
|
+
});
|
|
241
|
+
return [result[0], result[1]];
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Derive one-time PolicyApproval PDA.
|
|
245
|
+
* Seeds: ["policy_approval", pool_state, request_hash, nonce]
|
|
246
|
+
*/
|
|
247
|
+
export async function derivePolicyApprovalPDA(poolState, requestHash, nonce, programId = UTXOPIA_POLICY_PROGRAM_ID) {
|
|
248
|
+
const result = await getProgramDerivedAddress({
|
|
249
|
+
programAddress: programId,
|
|
250
|
+
seeds: policyApprovalSeeds(poolState, requestHash, nonce),
|
|
251
|
+
});
|
|
252
|
+
return [result[0], result[1]];
|
|
253
|
+
}
|
|
254
|
+
/** Destination kinds in the exit registry. Kind is a PDA seed, so a Solana
|
|
255
|
+
* owner and a BTC script hash sharing the same 32 bytes stay distinct. */
|
|
256
|
+
export const EXIT_KIND_SOLANA_OWNER = 0;
|
|
257
|
+
export const EXIT_KIND_BTC_SCRIPT = 1;
|
|
258
|
+
/**
|
|
259
|
+
* Derive an ExitDestination PDA — the append-only registry of destinations a
|
|
260
|
+
* permissioned pool's ragequit path may pay.
|
|
261
|
+
*
|
|
262
|
+
* Seeds: ["exit_destination", pool_state, [kind], key]
|
|
263
|
+
*
|
|
264
|
+
* `key` is the recipient token account's OWNER for `EXIT_KIND_SOLANA_OWNER`,
|
|
265
|
+
* or `sha256(btcScript)` for `EXIT_KIND_BTC_SCRIPT`.
|
|
266
|
+
*/
|
|
267
|
+
export async function deriveExitDestinationPDA(poolState, kind, key, programId) {
|
|
268
|
+
const result = await getProgramDerivedAddress({
|
|
269
|
+
programAddress: programId,
|
|
270
|
+
seeds: exitDestinationSeeds(poolState, kind, key),
|
|
271
|
+
});
|
|
272
|
+
return [result[0], result[1]];
|
|
273
|
+
}
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// BTC Light Client PDAs
|
|
276
|
+
// =============================================================================
|
|
277
|
+
/**
|
|
278
|
+
* Derive BTC Light Client PDA
|
|
279
|
+
*/
|
|
280
|
+
export async function deriveLightClientPDA(programId = BTC_LIGHT_CLIENT_PROGRAM_ID) {
|
|
281
|
+
const result = await getProgramDerivedAddress({
|
|
282
|
+
programAddress: programId,
|
|
283
|
+
seeds: lightClientSeeds(),
|
|
284
|
+
});
|
|
285
|
+
return [result[0], result[1]];
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Derive Block Header PDA (hash-based)
|
|
289
|
+
* Seeds: ["block", blockHash(32)]
|
|
290
|
+
*/
|
|
291
|
+
export async function deriveBlockHeaderPDA(blockHash, programId = BTC_LIGHT_CLIENT_PROGRAM_ID) {
|
|
292
|
+
const result = await getProgramDerivedAddress({
|
|
293
|
+
programAddress: programId,
|
|
294
|
+
seeds: blockHeaderSeeds(blockHash),
|
|
295
|
+
});
|
|
296
|
+
return [result[0], result[1]];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Derive HeightIndex PDA
|
|
300
|
+
* Seeds: ["height_index", height_le_bytes(8)]
|
|
301
|
+
*/
|
|
302
|
+
export async function deriveHeightIndexPDA(height, programId = BTC_LIGHT_CLIENT_PROGRAM_ID) {
|
|
303
|
+
const result = await getProgramDerivedAddress({
|
|
304
|
+
programAddress: programId,
|
|
305
|
+
seeds: heightIndexSeeds(height),
|
|
306
|
+
});
|
|
307
|
+
return [result[0], result[1]];
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Derive VerifiedTransaction PDA (btc-light-client)
|
|
311
|
+
*
|
|
312
|
+
* Seeds: ["verified_tx", blockHash(32), txid(32)]
|
|
313
|
+
*/
|
|
314
|
+
export async function deriveVerifiedTransactionPDA(blockHash, txid, programId = BTC_LIGHT_CLIENT_PROGRAM_ID) {
|
|
315
|
+
const result = await getProgramDerivedAddress({
|
|
316
|
+
programAddress: programId,
|
|
317
|
+
seeds: verifiedTransactionSeeds(blockHash, txid),
|
|
318
|
+
});
|
|
319
|
+
return [result[0], result[1]];
|
|
320
|
+
}
|
|
321
|
+
// =============================================================================
|
|
322
|
+
// Redemption Request PDAs
|
|
323
|
+
// =============================================================================
|
|
324
|
+
/**
|
|
325
|
+
* Derive Redemption Request PDA
|
|
326
|
+
*
|
|
327
|
+
* Seeds: ["redemption", pool_state, user_pubkey(32), nonce_le(8)]
|
|
328
|
+
*/
|
|
329
|
+
export async function deriveRedemptionRequestPDA(poolState, userPubkey, nonce, programId = UTXOPIA_PROGRAM_ID) {
|
|
330
|
+
const result = await getProgramDerivedAddress({
|
|
331
|
+
programAddress: programId,
|
|
332
|
+
seeds: redemptionRequestSeeds(poolState, userPubkey, nonce),
|
|
333
|
+
});
|
|
334
|
+
return [result[0], result[1]];
|
|
335
|
+
}
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// VK Registry PDAs
|
|
338
|
+
// =============================================================================
|
|
339
|
+
/**
|
|
340
|
+
* Derive VK Registry PDA for a JoinSplit variant
|
|
341
|
+
*
|
|
342
|
+
* Seeds: ["vk_registry", &[n_inputs], &[n_outputs]]
|
|
343
|
+
*/
|
|
344
|
+
export async function deriveVkRegistryPDA(nInputs, nOutputs, programId = UTXOPIA_PROGRAM_ID) {
|
|
345
|
+
const result = await getProgramDerivedAddress({
|
|
346
|
+
programAddress: programId,
|
|
347
|
+
seeds: vkRegistrySeeds(nInputs, nOutputs),
|
|
348
|
+
});
|
|
349
|
+
return [result[0], result[1]];
|
|
350
|
+
}
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// Deposit Receipt PDAs
|
|
353
|
+
// =============================================================================
|
|
354
|
+
/**
|
|
355
|
+
* Derive Deposit Receipt PDA.
|
|
356
|
+
*
|
|
357
|
+
* Two on-chain schemes exist:
|
|
358
|
+
* - `complete_deposit` (disc 11, the active direct-vault flow): seeds ["deposit_receipt", txid] —
|
|
359
|
+
* call WITHOUT `depositVout`.
|
|
360
|
+
* - `verify_deposit` (disc 25, OP_RETURN-free flow): seeds ["deposit_receipt", txid, vout(4 LE)] —
|
|
361
|
+
* pass `depositVout` so a funding tx with multiple independent deposit outputs gets one receipt
|
|
362
|
+
* per output (each creditable once) instead of the first output blocking the rest.
|
|
363
|
+
*/
|
|
364
|
+
export async function deriveDepositReceiptPDA(depositTxid, depositVout, programId = UTXOPIA_PROGRAM_ID) {
|
|
365
|
+
const result = await getProgramDerivedAddress({
|
|
366
|
+
programAddress: programId,
|
|
367
|
+
seeds: depositReceiptSeeds(depositTxid, depositVout),
|
|
368
|
+
});
|
|
369
|
+
return [result[0], result[1]];
|
|
370
|
+
}
|
|
371
|
+
// =============================================================================
|
|
372
|
+
// Utility Functions
|
|
373
|
+
// =============================================================================
|
|
374
|
+
/**
|
|
375
|
+
* Convert bigint commitment to bytes for PDA derivation
|
|
376
|
+
*/
|
|
377
|
+
export function commitmentToBytes(commitment) {
|
|
378
|
+
const hex = commitment.toString(16).padStart(64, "0");
|
|
379
|
+
const bytes = new Uint8Array(32);
|
|
380
|
+
for (let i = 0; i < 32; i++) {
|
|
381
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
382
|
+
}
|
|
383
|
+
return bytes;
|
|
384
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PoolState account layout and fee arithmetic.
|
|
3
|
+
*
|
|
4
|
+
* Both mirror the program (`state/pool.rs`, `utils/fees.rs`), so both belong
|
|
5
|
+
* here rather than in an app: a consumer that re-derives either gets it right
|
|
6
|
+
* until the struct grows a field, and then reads the wrong bytes with no error.
|
|
7
|
+
* TokenConfig and CommitmentTree are already decoded in this package; this
|
|
8
|
+
* closes the gap for the third account.
|
|
9
|
+
*
|
|
10
|
+
* @module pool-state
|
|
11
|
+
*/
|
|
12
|
+
export declare const POOL_STATE_DISCRIMINATOR = 1;
|
|
13
|
+
/** `core::mem::size_of::<PoolState>()`. Fields are appended into `_reserved`,
|
|
14
|
+
* so the length is stable across the additions made so far. */
|
|
15
|
+
export declare const POOL_STATE_LEN = 332;
|
|
16
|
+
/** Byte offsets of every field, in declaration order (`#[repr(C)]`). */
|
|
17
|
+
export declare const POOL_STATE_OFFSETS: {
|
|
18
|
+
readonly discriminator: 0;
|
|
19
|
+
readonly bump: 1;
|
|
20
|
+
readonly flags: 2;
|
|
21
|
+
readonly authority: 4;
|
|
22
|
+
readonly zkbtcMint: 36;
|
|
23
|
+
readonly poolVault: 68;
|
|
24
|
+
readonly depositVault: 100;
|
|
25
|
+
readonly depositCount: 132;
|
|
26
|
+
readonly totalMinted: 140;
|
|
27
|
+
readonly totalBurned: 148;
|
|
28
|
+
readonly pendingRedemptions: 156;
|
|
29
|
+
readonly lastUpdate: 164;
|
|
30
|
+
readonly minDeposit: 172;
|
|
31
|
+
readonly maxDeposit: 180;
|
|
32
|
+
readonly totalShielded: 188;
|
|
33
|
+
readonly serviceFeeBase: 196;
|
|
34
|
+
readonly feePool: 204;
|
|
35
|
+
readonly pendingMinDeposit: 212;
|
|
36
|
+
readonly pendingMaxDeposit: 220;
|
|
37
|
+
readonly pendingServiceFee: 228;
|
|
38
|
+
readonly pendingExecuteAfter: 236;
|
|
39
|
+
readonly depositFeeBps: 244;
|
|
40
|
+
readonly withdrawalFeeBps: 246;
|
|
41
|
+
readonly totalBtcHeld: 248;
|
|
42
|
+
readonly utxoCount: 256;
|
|
43
|
+
readonly activeTreeIndex: 258;
|
|
44
|
+
readonly utxoCountHi: 262;
|
|
45
|
+
readonly auditor: 264;
|
|
46
|
+
readonly auditorViewingPubkey: 296;
|
|
47
|
+
readonly nullifierCount: 328;
|
|
48
|
+
};
|
|
49
|
+
/** `flags` bits, from `state/pool.rs`. */
|
|
50
|
+
export declare const POOL_FLAG: {
|
|
51
|
+
readonly PAUSED: number;
|
|
52
|
+
readonly PERMISSIONED: number;
|
|
53
|
+
readonly AUDITOR_FROZEN: number;
|
|
54
|
+
readonly VK_REGISTRY_FROZEN: number;
|
|
55
|
+
};
|
|
56
|
+
export interface PoolFees {
|
|
57
|
+
depositFeeBps: number;
|
|
58
|
+
withdrawalFeeBps: number;
|
|
59
|
+
}
|
|
60
|
+
export interface PoolState extends PoolFees {
|
|
61
|
+
bump: number;
|
|
62
|
+
paused: boolean;
|
|
63
|
+
permissioned: boolean;
|
|
64
|
+
auditorFrozen: boolean;
|
|
65
|
+
vkRegistryFrozen: boolean;
|
|
66
|
+
zkbtcMint: Uint8Array;
|
|
67
|
+
poolVault: Uint8Array;
|
|
68
|
+
depositCount: bigint;
|
|
69
|
+
totalMinted: bigint;
|
|
70
|
+
totalBurned: bigint;
|
|
71
|
+
pendingRedemptions: bigint;
|
|
72
|
+
minDeposit: bigint;
|
|
73
|
+
maxDeposit: bigint;
|
|
74
|
+
totalShielded: bigint;
|
|
75
|
+
serviceFeeBase: bigint;
|
|
76
|
+
feePool: bigint;
|
|
77
|
+
totalBtcHeld: bigint;
|
|
78
|
+
activeTreeIndex: number;
|
|
79
|
+
auditor: Uint8Array;
|
|
80
|
+
auditorViewingPubkey: Uint8Array;
|
|
81
|
+
/** Counts only records created since the counter was added; older pools
|
|
82
|
+
* start from zero, so treat it as a floor rather than a total. */
|
|
83
|
+
nullifierCount: number;
|
|
84
|
+
/** `utxo_count` widened by `utxo_count_hi`, which was carved out later. */
|
|
85
|
+
utxoCount: number;
|
|
86
|
+
}
|
|
87
|
+
/** Fees only — the common case, and readable from a truncated account. */
|
|
88
|
+
export declare function parsePoolFees(data: Uint8Array): PoolFees | null;
|
|
89
|
+
/** Full decode. Returns null rather than throwing: callers poll this against
|
|
90
|
+
* whatever the RPC hands back, including a pool from an older deployment. */
|
|
91
|
+
export declare function parsePoolState(data: Uint8Array): PoolState | null;
|
|
92
|
+
export declare const BPS_DENOMINATOR = 10000n;
|
|
93
|
+
/**
|
|
94
|
+
* Compute an on-chain basis-point fee. Unshield withdrawals enforce a one-unit
|
|
95
|
+
* minimum; deposits use plain floor division. Quoting a fee any other way shows
|
|
96
|
+
* the user a number the program will not charge.
|
|
97
|
+
*/
|
|
98
|
+
export declare function computeBpsFee(amount: bigint, bps: number, minimumOne?: boolean): bigint;
|
|
99
|
+
/** What share of `gross` a fee represents, in basis points. */
|
|
100
|
+
export declare function feeShareBps(fee: bigint, gross: bigint): number;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PoolState account layout and fee arithmetic.
|
|
3
|
+
*
|
|
4
|
+
* Both mirror the program (`state/pool.rs`, `utils/fees.rs`), so both belong
|
|
5
|
+
* here rather than in an app: a consumer that re-derives either gets it right
|
|
6
|
+
* until the struct grows a field, and then reads the wrong bytes with no error.
|
|
7
|
+
* TokenConfig and CommitmentTree are already decoded in this package; this
|
|
8
|
+
* closes the gap for the third account.
|
|
9
|
+
*
|
|
10
|
+
* @module pool-state
|
|
11
|
+
*/
|
|
12
|
+
export const POOL_STATE_DISCRIMINATOR = 0x01;
|
|
13
|
+
/** `core::mem::size_of::<PoolState>()`. Fields are appended into `_reserved`,
|
|
14
|
+
* so the length is stable across the additions made so far. */
|
|
15
|
+
export const POOL_STATE_LEN = 332;
|
|
16
|
+
/** Byte offsets of every field, in declaration order (`#[repr(C)]`). */
|
|
17
|
+
export const POOL_STATE_OFFSETS = {
|
|
18
|
+
discriminator: 0,
|
|
19
|
+
bump: 1,
|
|
20
|
+
flags: 2,
|
|
21
|
+
authority: 4,
|
|
22
|
+
zkbtcMint: 36,
|
|
23
|
+
poolVault: 68,
|
|
24
|
+
depositVault: 100,
|
|
25
|
+
depositCount: 132,
|
|
26
|
+
totalMinted: 140,
|
|
27
|
+
totalBurned: 148,
|
|
28
|
+
pendingRedemptions: 156,
|
|
29
|
+
lastUpdate: 164,
|
|
30
|
+
minDeposit: 172,
|
|
31
|
+
maxDeposit: 180,
|
|
32
|
+
totalShielded: 188,
|
|
33
|
+
serviceFeeBase: 196,
|
|
34
|
+
feePool: 204,
|
|
35
|
+
pendingMinDeposit: 212,
|
|
36
|
+
pendingMaxDeposit: 220,
|
|
37
|
+
pendingServiceFee: 228,
|
|
38
|
+
pendingExecuteAfter: 236,
|
|
39
|
+
depositFeeBps: 244,
|
|
40
|
+
withdrawalFeeBps: 246,
|
|
41
|
+
totalBtcHeld: 248,
|
|
42
|
+
utxoCount: 256,
|
|
43
|
+
activeTreeIndex: 258,
|
|
44
|
+
utxoCountHi: 262,
|
|
45
|
+
auditor: 264,
|
|
46
|
+
auditorViewingPubkey: 296,
|
|
47
|
+
nullifierCount: 328,
|
|
48
|
+
};
|
|
49
|
+
/** `flags` bits, from `state/pool.rs`. */
|
|
50
|
+
export const POOL_FLAG = {
|
|
51
|
+
PAUSED: 1 << 0,
|
|
52
|
+
PERMISSIONED: 1 << 1,
|
|
53
|
+
AUDITOR_FROZEN: 1 << 2,
|
|
54
|
+
VK_REGISTRY_FROZEN: 1 << 3,
|
|
55
|
+
};
|
|
56
|
+
const view = (data) => new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
57
|
+
/** Fees only — the common case, and readable from a truncated account. */
|
|
58
|
+
export function parsePoolFees(data) {
|
|
59
|
+
if (data.length < POOL_STATE_OFFSETS.withdrawalFeeBps + 2 ||
|
|
60
|
+
data[0] !== POOL_STATE_DISCRIMINATOR) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const v = view(data);
|
|
64
|
+
return {
|
|
65
|
+
depositFeeBps: v.getUint16(POOL_STATE_OFFSETS.depositFeeBps, true),
|
|
66
|
+
withdrawalFeeBps: v.getUint16(POOL_STATE_OFFSETS.withdrawalFeeBps, true),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Full decode. Returns null rather than throwing: callers poll this against
|
|
70
|
+
* whatever the RPC hands back, including a pool from an older deployment. */
|
|
71
|
+
export function parsePoolState(data) {
|
|
72
|
+
if (data.length < POOL_STATE_LEN || data[0] !== POOL_STATE_DISCRIMINATOR) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const v = view(data);
|
|
76
|
+
const o = POOL_STATE_OFFSETS;
|
|
77
|
+
const flags = data[o.flags];
|
|
78
|
+
const bytes = (at, len = 32) => data.subarray(at, at + len);
|
|
79
|
+
return {
|
|
80
|
+
bump: data[o.bump],
|
|
81
|
+
paused: (flags & POOL_FLAG.PAUSED) !== 0,
|
|
82
|
+
permissioned: (flags & POOL_FLAG.PERMISSIONED) !== 0,
|
|
83
|
+
auditorFrozen: (flags & POOL_FLAG.AUDITOR_FROZEN) !== 0,
|
|
84
|
+
vkRegistryFrozen: (flags & POOL_FLAG.VK_REGISTRY_FROZEN) !== 0,
|
|
85
|
+
zkbtcMint: bytes(o.zkbtcMint),
|
|
86
|
+
poolVault: bytes(o.poolVault),
|
|
87
|
+
depositCount: v.getBigUint64(o.depositCount, true),
|
|
88
|
+
totalMinted: v.getBigUint64(o.totalMinted, true),
|
|
89
|
+
totalBurned: v.getBigUint64(o.totalBurned, true),
|
|
90
|
+
pendingRedemptions: v.getBigUint64(o.pendingRedemptions, true),
|
|
91
|
+
minDeposit: v.getBigUint64(o.minDeposit, true),
|
|
92
|
+
maxDeposit: v.getBigUint64(o.maxDeposit, true),
|
|
93
|
+
totalShielded: v.getBigUint64(o.totalShielded, true),
|
|
94
|
+
serviceFeeBase: v.getBigUint64(o.serviceFeeBase, true),
|
|
95
|
+
feePool: v.getBigUint64(o.feePool, true),
|
|
96
|
+
depositFeeBps: v.getUint16(o.depositFeeBps, true),
|
|
97
|
+
withdrawalFeeBps: v.getUint16(o.withdrawalFeeBps, true),
|
|
98
|
+
totalBtcHeld: v.getBigUint64(o.totalBtcHeld, true),
|
|
99
|
+
activeTreeIndex: v.getUint32(o.activeTreeIndex, true),
|
|
100
|
+
utxoCount: v.getUint16(o.utxoCount, true) + (v.getUint16(o.utxoCountHi, true) << 16),
|
|
101
|
+
auditor: bytes(o.auditor),
|
|
102
|
+
auditorViewingPubkey: bytes(o.auditorViewingPubkey),
|
|
103
|
+
nullifierCount: v.getUint32(o.nullifierCount, true),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Fee arithmetic
|
|
108
|
+
// =============================================================================
|
|
109
|
+
export const BPS_DENOMINATOR = 10000n;
|
|
110
|
+
/**
|
|
111
|
+
* Compute an on-chain basis-point fee. Unshield withdrawals enforce a one-unit
|
|
112
|
+
* minimum; deposits use plain floor division. Quoting a fee any other way shows
|
|
113
|
+
* the user a number the program will not charge.
|
|
114
|
+
*/
|
|
115
|
+
export function computeBpsFee(amount, bps, minimumOne = true) {
|
|
116
|
+
if (amount <= 0n || bps <= 0)
|
|
117
|
+
return 0n;
|
|
118
|
+
const fee = (amount * BigInt(bps)) / BPS_DENOMINATOR;
|
|
119
|
+
return fee > 0n || !minimumOne ? fee : 1n;
|
|
120
|
+
}
|
|
121
|
+
/** What share of `gross` a fee represents, in basis points. */
|
|
122
|
+
export function feeShareBps(fee, gross) {
|
|
123
|
+
if (fee <= 0n || gross <= 0n)
|
|
124
|
+
return 0;
|
|
125
|
+
return Number((fee * BPS_DENOMINATOR) / gross);
|
|
126
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poseidon Hash - BN254 compatible with circom circuits and Solana's sol_poseidon
|
|
3
|
+
*
|
|
4
|
+
* Uses poseidon-lite: pure JavaScript, zero dependencies, works in
|
|
5
|
+
* Browser, Node.js, and React Native (no WASM required).
|
|
6
|
+
*
|
|
7
|
+
* UNIFIED MODEL:
|
|
8
|
+
* - Commitment = Poseidon(pub_key_x, amount)
|
|
9
|
+
* - Nullifier = Poseidon(priv_key, leaf_index)
|
|
10
|
+
* - Nullifier Hash = Poseidon(nullifier)
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Initialize poseidon (no-op — poseidon-lite needs no initialization)
|
|
14
|
+
* Kept for backward API compatibility.
|
|
15
|
+
*/
|
|
16
|
+
export declare function initPoseidon(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Whether Poseidon can hash right now.
|
|
19
|
+
*
|
|
20
|
+
* Always true: poseidon-lite is pure synchronous JS with nothing to load, so `initPoseidon()`
|
|
21
|
+
* exists only so callers written against a WASM-backed hasher keep working. Kept honest rather
|
|
22
|
+
* than tracking a flag — the previous version lived in note.ts over its own state and reported
|
|
23
|
+
* false no matter what you called.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isPoseidonReady(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Synchronous Poseidon hash (no initialization required with poseidon-lite)
|
|
28
|
+
*/
|
|
29
|
+
export declare function poseidonHashSync(inputs: bigint[]): bigint;
|
|
30
|
+
export declare const BN254_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
31
|
+
export declare function computeUnifiedCommitmentSync(pubKeyX: bigint, amount: bigint): bigint;
|
|
32
|
+
export declare function computeNullifierSync(privKey: bigint, leafIndex: bigint): bigint;
|
|
33
|
+
export declare function hashNullifierSync(nullifier: bigint): bigint;
|
|
34
|
+
export declare function computeMPKSync(pkX: bigint, pkY: bigint, nullifyingKey: bigint): bigint;
|
|
35
|
+
export declare function computeNPKSync(mpk: bigint, random: bigint): bigint;
|
|
36
|
+
export declare function computeJoinSplitCommitmentSync(npk: bigint, token: bigint, amount: bigint): bigint;
|
|
37
|
+
export declare function computeJoinSplitNullifierSync(nullifyingKey: bigint, leafIndex: bigint): bigint;
|
|
38
|
+
/**
|
|
39
|
+
* Reduce a 32-byte big-endian value to BN254 scalar field (mod p).
|
|
40
|
+
* Must match on-chain reduce_to_field_exact in crypto.rs.
|
|
41
|
+
*/
|
|
42
|
+
export declare function reduceToField(bytes: Uint8Array): bigint;
|
|
43
|
+
/**
|
|
44
|
+
* Compute token_id from mint address: Poseidon(reduce_to_field(mint), 0)
|
|
45
|
+
*
|
|
46
|
+
* Must match on-chain compute_token_id in crypto.rs which uses
|
|
47
|
+
* poseidon2_hash(reduce_to_field_exact(mint), [0u8; 32]).
|
|
48
|
+
*/
|
|
49
|
+
export declare function computeTokenId(mintBytes: Uint8Array): bigint;
|
|
50
|
+
/**
|
|
51
|
+
* Convenience: compute token_id from a Solana address string
|
|
52
|
+
*/
|
|
53
|
+
export declare function computeTokenIdFromAddress(mintAddress: string): bigint;
|