@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +333 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/psbt.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound Parameters Hash for JoinSplit transactions
|
|
3
|
+
*
|
|
4
|
+
* The operation hash binds transaction metadata to the proof:
|
|
5
|
+
* - treeNumber: Which commitment tree (for multi-tree support)
|
|
6
|
+
* - unshieldAddress: Recipient for public unshield (null = private transfer)
|
|
7
|
+
* - chainId: Prevents cross-chain replay
|
|
8
|
+
* - stealthDataHash: SHA256 of concatenated stealth data (prevents relayer tampering)
|
|
9
|
+
*
|
|
10
|
+
* Hash: SHA256(serialize(params)) mod BN254_SCALAR_FIELD
|
|
11
|
+
*
|
|
12
|
+
* Solana callers must wrap this operation hash with
|
|
13
|
+
* `computeSolanaDomainBoundParamsHash()` before generating a proof.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
17
|
+
import { BN254_FIELD_PRIME, bytesToBigint } from "./crypto";
|
|
18
|
+
import { poseidonHashSync } from "./poseidon";
|
|
19
|
+
|
|
20
|
+
/** Bound params mode: transfer(0), unshield(1), redeem(2) */
|
|
21
|
+
export type BoundParamsMode = 'transfer' | 'unshield' | 'redeem';
|
|
22
|
+
|
|
23
|
+
export interface BoundParams {
|
|
24
|
+
/** Tree number (0 for default) */
|
|
25
|
+
treeNumber: number;
|
|
26
|
+
/** Unshield recipient address (null = private transfer, 32 bytes = public unshield/redeem) */
|
|
27
|
+
unshieldAddress: Uint8Array | null;
|
|
28
|
+
/** Chain ID (prevents cross-chain replay) */
|
|
29
|
+
chainId: bigint;
|
|
30
|
+
/** Mode flag: 'transfer'(0), 'unshield'(1), 'redeem'(2). Defaults to inferred from unshieldAddress. */
|
|
31
|
+
mode?: BoundParamsMode;
|
|
32
|
+
/** SHA256 of concatenated stealth data (prevents relayer from corrupting change outputs) */
|
|
33
|
+
stealthDataHash: Uint8Array;
|
|
34
|
+
/**
|
|
35
|
+
* Requester pubkey (32 bytes) — REQUIRED for redeem. Binds the proof to the signing account
|
|
36
|
+
* that becomes RedemptionRequest.requester so a privileged orderflow actor cannot replay the
|
|
37
|
+
* proof under their own key. Ignored for transfer/unshield.
|
|
38
|
+
*/
|
|
39
|
+
requester?: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SolanaPrivacyDomainKind = "public" | "institution";
|
|
43
|
+
|
|
44
|
+
export interface SolanaPrivacyDomainContext {
|
|
45
|
+
programId: Uint8Array;
|
|
46
|
+
poolState: Uint8Array;
|
|
47
|
+
kind: SolanaPrivacyDomainKind;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SOLANA_DOMAIN_TAG = new TextEncoder().encode("UTXOPIA_DOMAIN_V1");
|
|
51
|
+
|
|
52
|
+
function assert32Bytes(value: Uint8Array, name: string): void {
|
|
53
|
+
if (value.length !== 32) {
|
|
54
|
+
throw new Error(`${name} must be 32 bytes`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function u64le(value: bigint): Uint8Array {
|
|
59
|
+
if (value < 0n || value > 0xffffffffffffffffn) {
|
|
60
|
+
throw new Error("chainId must fit in u64");
|
|
61
|
+
}
|
|
62
|
+
const bytes = new Uint8Array(8);
|
|
63
|
+
let remaining = value;
|
|
64
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
65
|
+
bytes[i] = Number(remaining & 0xffn);
|
|
66
|
+
remaining >>= 8n;
|
|
67
|
+
}
|
|
68
|
+
return bytes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compute the canonical public/institution domain field for Solana.
|
|
73
|
+
*
|
|
74
|
+
* SHA256("UTXOPIA_DOMAIN_V1" || chainIdLE || programId || poolState || kind)
|
|
75
|
+
* reduced modulo BN254 Fr. The on-chain verifier recomputes this from the
|
|
76
|
+
* actual program, pool account, and PoolState.permissioned flag.
|
|
77
|
+
*/
|
|
78
|
+
export function computeSolanaDomainSeparator(
|
|
79
|
+
context: SolanaPrivacyDomainContext,
|
|
80
|
+
chainId: bigint = SOLANA_BOUND_CHAIN_ID,
|
|
81
|
+
): bigint {
|
|
82
|
+
assert32Bytes(context.programId, "programId");
|
|
83
|
+
assert32Bytes(context.poolState, "poolState");
|
|
84
|
+
if (context.kind !== "public" && context.kind !== "institution") {
|
|
85
|
+
throw new Error("kind must be public or institution");
|
|
86
|
+
}
|
|
87
|
+
const preimage = new Uint8Array(SOLANA_DOMAIN_TAG.length + 8 + 32 + 32 + 1);
|
|
88
|
+
let offset = 0;
|
|
89
|
+
preimage.set(SOLANA_DOMAIN_TAG, offset);
|
|
90
|
+
offset += SOLANA_DOMAIN_TAG.length;
|
|
91
|
+
preimage.set(u64le(chainId), offset);
|
|
92
|
+
offset += 8;
|
|
93
|
+
preimage.set(context.programId, offset);
|
|
94
|
+
offset += 32;
|
|
95
|
+
preimage.set(context.poolState, offset);
|
|
96
|
+
offset += 32;
|
|
97
|
+
preimage[offset] = context.kind === "institution" ? 1 : 0;
|
|
98
|
+
return bytesToBigint(sha256(preimage)) % BN254_FIELD_PRIME;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Bind an operation-specific hash to its exact Solana privacy domain.
|
|
103
|
+
*
|
|
104
|
+
* This occupies the existing boundParamsHash public input, so circuit/VK
|
|
105
|
+
* dimensions remain unchanged while proofs become non-replayable across pools.
|
|
106
|
+
*/
|
|
107
|
+
export function computeSolanaDomainBoundParamsHash(
|
|
108
|
+
params: BoundParams,
|
|
109
|
+
context: SolanaPrivacyDomainContext,
|
|
110
|
+
): bigint {
|
|
111
|
+
if (
|
|
112
|
+
params.chainId !== SOLANA_DEVNET_BOUND_CHAIN_ID
|
|
113
|
+
&& params.chainId !== SOLANA_MAINNET_BOUND_CHAIN_ID
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("Solana domain binding requires a supported Solana chain ID");
|
|
116
|
+
}
|
|
117
|
+
return poseidonHashSync([
|
|
118
|
+
computeSolanaDomainSeparator(context, params.chainId),
|
|
119
|
+
computeBoundParamsHash(params),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute SHA256 hash of concatenated stealth data arrays.
|
|
125
|
+
* Returns 32-byte hash, or all zeros if no stealth data.
|
|
126
|
+
*/
|
|
127
|
+
export function computeStealthDataHash(stealthData: Uint8Array[]): Uint8Array {
|
|
128
|
+
// Always SHA256 the concatenation — even for empty arrays.
|
|
129
|
+
// On-chain: sha256(&data[stealth_start..stealth_end]) — empty slice → sha256("")
|
|
130
|
+
const totalLen = stealthData.reduce((sum, sd) => sum + sd.length, 0);
|
|
131
|
+
const concat = new Uint8Array(totalLen);
|
|
132
|
+
let offset = 0;
|
|
133
|
+
for (const sd of stealthData) {
|
|
134
|
+
concat.set(sd, offset);
|
|
135
|
+
offset += sd.length;
|
|
136
|
+
}
|
|
137
|
+
return sha256(concat);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compute the operation-specific bound parameters hash.
|
|
142
|
+
*
|
|
143
|
+
* Solana callers must wrap this with
|
|
144
|
+
* `computeSolanaDomainBoundParamsHash()` to bind the program and pool.
|
|
145
|
+
*
|
|
146
|
+
* Deterministic serialization:
|
|
147
|
+
* - treeNumber: 4 bytes LE
|
|
148
|
+
* - flag: 1 byte (0=transfer, 1=unshield, 2=redeem)
|
|
149
|
+
* - unshieldAddress: 32 bytes (zeros if null)
|
|
150
|
+
* - chainId: 8 bytes LE
|
|
151
|
+
* - stealthDataHash: 32 bytes (SHA256 of concatenated stealth data)
|
|
152
|
+
* - requester: 32 bytes (redeem only — appended, extending the buffer to 109 bytes)
|
|
153
|
+
*
|
|
154
|
+
* Total: 77 bytes (transfer/unshield) or 109 bytes (redeem) → SHA256 → mod BN254
|
|
155
|
+
*/
|
|
156
|
+
export function computeBoundParamsHash(params: BoundParams): bigint {
|
|
157
|
+
const isRedeem = params.mode === 'redeem';
|
|
158
|
+
// Redeem binds the requester pubkey, extending the preimage by 32 bytes (must match the
|
|
159
|
+
// on-chain compute_bound_params_hash_redeem layout).
|
|
160
|
+
const buf = new Uint8Array(isRedeem ? 109 : 77);
|
|
161
|
+
const view = new DataView(buf.buffer);
|
|
162
|
+
|
|
163
|
+
// treeNumber (4 bytes LE)
|
|
164
|
+
view.setUint32(0, params.treeNumber, true);
|
|
165
|
+
|
|
166
|
+
// flag byte: transfer=0, unshield=1, redeem=2
|
|
167
|
+
if (isRedeem) {
|
|
168
|
+
buf[4] = 2;
|
|
169
|
+
} else if (params.mode === 'unshield' || params.unshieldAddress) {
|
|
170
|
+
buf[4] = 1;
|
|
171
|
+
} else {
|
|
172
|
+
buf[4] = 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// unshieldAddress (32 bytes, zeros if null)
|
|
176
|
+
if (params.unshieldAddress) {
|
|
177
|
+
buf.set(params.unshieldAddress.slice(0, 32), 5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// chainId (8 bytes LE)
|
|
181
|
+
const chainIdBuf = new Uint8Array(8);
|
|
182
|
+
let chainId = params.chainId;
|
|
183
|
+
for (let i = 0; i < 8; i++) {
|
|
184
|
+
chainIdBuf[i] = Number(chainId & 0xffn);
|
|
185
|
+
chainId >>= 8n;
|
|
186
|
+
}
|
|
187
|
+
buf.set(chainIdBuf, 37);
|
|
188
|
+
|
|
189
|
+
// stealthDataHash (32 bytes)
|
|
190
|
+
buf.set(params.stealthDataHash.slice(0, 32), 45);
|
|
191
|
+
|
|
192
|
+
// requester (32 bytes) — redeem only
|
|
193
|
+
if (isRedeem) {
|
|
194
|
+
if (!params.requester || params.requester.length !== 32) {
|
|
195
|
+
throw new Error("redeem bound params require a 32-byte requester pubkey");
|
|
196
|
+
}
|
|
197
|
+
buf.set(params.requester.slice(0, 32), 77);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// SHA256 → mod BN254
|
|
201
|
+
const hash = sha256(buf);
|
|
202
|
+
return bytesToBigint(hash) % BN254_FIELD_PRIME;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Canonical chain ids folded into bound-params hashes (must match on-chain). */
|
|
206
|
+
export const SOLANA_MAINNET_BOUND_CHAIN_ID = 101n;
|
|
207
|
+
export const SOLANA_DEVNET_BOUND_CHAIN_ID = 103n;
|
|
208
|
+
/** Backward-compatible devnet alias. Prefer the network-specific constants. */
|
|
209
|
+
export const SOLANA_BOUND_CHAIN_ID = SOLANA_DEVNET_BOUND_CHAIN_ID;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Default bound params for Solana devnet (private transfer)
|
|
213
|
+
*/
|
|
214
|
+
export function createTransferBoundParams(
|
|
215
|
+
stealthDataHash: Uint8Array,
|
|
216
|
+
chainId: bigint = 103n,
|
|
217
|
+
treeNumber: number = 0,
|
|
218
|
+
): BoundParams {
|
|
219
|
+
return {
|
|
220
|
+
treeNumber,
|
|
221
|
+
unshieldAddress: null,
|
|
222
|
+
chainId,
|
|
223
|
+
stealthDataHash,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @deprecated Use createTransferBoundParams instead */
|
|
228
|
+
export const DEFAULT_BOUND_PARAMS: BoundParams = {
|
|
229
|
+
treeNumber: 0,
|
|
230
|
+
unshieldAddress: null,
|
|
231
|
+
chainId: 103n,
|
|
232
|
+
stealthDataHash: new Uint8Array(32),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create bound params for a redeem (JoinSplit → BTC withdrawal, multi-output)
|
|
237
|
+
*
|
|
238
|
+
* The BTC scriptPubKeys are concatenated and SHA-256 hashed into the address field
|
|
239
|
+
* so the proof cryptographically binds ALL withdrawal destinations.
|
|
240
|
+
*
|
|
241
|
+
* For single output: SHA256(script_1) — no special case.
|
|
242
|
+
* For multi-output: SHA256(script_1 || script_2 || ...)
|
|
243
|
+
*
|
|
244
|
+
* `requester` is the 32-byte pubkey of the signer that will submit the redeem (becomes
|
|
245
|
+
* RedemptionRequest.requester); it is bound into the hash so the proof cannot be replayed
|
|
246
|
+
* under a different signer.
|
|
247
|
+
*/
|
|
248
|
+
function u32le(n: number): Uint8Array {
|
|
249
|
+
const b = new Uint8Array(4);
|
|
250
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, true);
|
|
251
|
+
return b;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createRedeemBoundParams(
|
|
255
|
+
btcScripts: Uint8Array | Uint8Array[],
|
|
256
|
+
stealthDataHash: Uint8Array,
|
|
257
|
+
requester: Uint8Array,
|
|
258
|
+
chainId: bigint = 103n,
|
|
259
|
+
treeNumber: number = 0,
|
|
260
|
+
): BoundParams {
|
|
261
|
+
if (!requester || requester.length !== 32) {
|
|
262
|
+
throw new Error("createRedeemBoundParams requires a 32-byte requester pubkey");
|
|
263
|
+
}
|
|
264
|
+
// Normalize to array
|
|
265
|
+
const scripts = btcScripts instanceof Uint8Array ? [btcScripts] : btcScripts;
|
|
266
|
+
// Length-prefixed scripts hash (audit #4): sha256(u32le(count) || per-script
|
|
267
|
+
// [u32le(len) || bytes]). Binds the script boundaries so a redeem proof cannot be
|
|
268
|
+
// replayed with the scripts re-partitioned to the same concatenation. Must match the
|
|
269
|
+
// on-chain Solana `length_prefixed_hash` in compute_bound_params_hash_redeem.
|
|
270
|
+
const lpParts: Uint8Array[] = [u32le(scripts.length)];
|
|
271
|
+
for (const s of scripts) {
|
|
272
|
+
lpParts.push(u32le(s.length), s);
|
|
273
|
+
}
|
|
274
|
+
const lpTotal = lpParts.reduce((sum, p) => sum + p.length, 0);
|
|
275
|
+
const lp = new Uint8Array(lpTotal);
|
|
276
|
+
let off = 0;
|
|
277
|
+
for (const p of lpParts) {
|
|
278
|
+
lp.set(p, off);
|
|
279
|
+
off += p.length;
|
|
280
|
+
}
|
|
281
|
+
const scriptHash = sha256(lp);
|
|
282
|
+
return {
|
|
283
|
+
treeNumber,
|
|
284
|
+
unshieldAddress: scriptHash,
|
|
285
|
+
chainId,
|
|
286
|
+
mode: 'redeem',
|
|
287
|
+
stealthDataHash,
|
|
288
|
+
requester,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Create bound params for an unshield (public withdrawal, multi-output)
|
|
295
|
+
*
|
|
296
|
+
* For multi-output: destinations_hash = SHA256(owner_1 || owner_2 || ...)
|
|
297
|
+
* For single output: SHA256(owner_1) — no special case.
|
|
298
|
+
*/
|
|
299
|
+
export function createUnshieldBoundParams(
|
|
300
|
+
recipientAddresses: Uint8Array | Uint8Array[],
|
|
301
|
+
stealthDataHash: Uint8Array,
|
|
302
|
+
chainId: bigint = 103n,
|
|
303
|
+
treeNumber: number = 0,
|
|
304
|
+
): BoundParams {
|
|
305
|
+
// Normalize to array
|
|
306
|
+
const addrs = recipientAddresses instanceof Uint8Array ? [recipientAddresses] : recipientAddresses;
|
|
307
|
+
// Concatenate all addresses
|
|
308
|
+
const totalLen = addrs.reduce((sum, a) => sum + a.length, 0);
|
|
309
|
+
const concat = new Uint8Array(totalLen);
|
|
310
|
+
let off = 0;
|
|
311
|
+
for (const a of addrs) {
|
|
312
|
+
concat.set(a, off);
|
|
313
|
+
off += a.length;
|
|
314
|
+
}
|
|
315
|
+
const addressHash = sha256(concat);
|
|
316
|
+
return {
|
|
317
|
+
treeNumber,
|
|
318
|
+
unshieldAddress: addressHash,
|
|
319
|
+
chainId,
|
|
320
|
+
stealthDataHash,
|
|
321
|
+
};
|
|
322
|
+
}
|