@utxopia/sdk 0.1.0-alpha.3 → 0.1.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcoin/ika.d.ts +4 -3
- package/dist/bitcoin/ika.js +4 -7
- package/dist/client.d.ts +3 -3
- package/dist/crypto-babyjub.d.ts +0 -4
- package/dist/crypto-babyjub.js +0 -4
- package/dist/crypto.d.ts +2 -5
- package/dist/crypto.js +5 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/instructions.d.ts +0 -113
- package/dist/instructions.js +0 -127
- package/dist/poseidon.d.ts +0 -4
- package/dist/poseidon.js +0 -8
- package/dist/prover/index.d.ts +0 -1
- package/dist/prover/web.d.ts +0 -15
- package/dist/prover/web.js +2 -49
- package/dist/psbt.d.ts +14 -3
- package/dist/psbt.js +23 -14
- package/dist/spend-doc.js +1 -1
- package/dist/stealth.d.ts +6 -12
- package/dist/stealth.js +0 -14
- package/dist/taproot.d.ts +32 -4
- package/dist/taproot.js +54 -32
- package/package.json +1 -32
- package/src/bitcoin/ika.ts +6 -11
- package/src/client.ts +4 -4
- package/src/crypto-babyjub.ts +0 -5
- package/src/crypto.ts +5 -24
- package/src/index.ts +4 -0
- package/src/instructions.ts +0 -214
- package/src/poseidon.ts +0 -9
- package/src/prover/index.ts +0 -8
- package/src/prover/web.ts +3 -72
- package/src/psbt.ts +32 -15
- package/src/spend-doc.ts +1 -2
- package/src/stealth.ts +7 -23
- package/src/taproot.ts +74 -39
package/src/instructions.ts
CHANGED
|
@@ -2191,124 +2191,6 @@ export { hexToBytes, bytesToHex } from "./crypto";
|
|
|
2191
2191
|
// Permissioned Pool Builders
|
|
2192
2192
|
// =============================================================================
|
|
2193
2193
|
|
|
2194
|
-
// ---------------------------------------------------------------------------
|
|
2195
|
-
// initializePermissioned (disc=21)
|
|
2196
|
-
// ---------------------------------------------------------------------------
|
|
2197
|
-
|
|
2198
|
-
/** initializePermissioned instruction options */
|
|
2199
|
-
export interface InitializePermissionedOptions {
|
|
2200
|
-
/** PDA bump for pool state */
|
|
2201
|
-
poolBump: number;
|
|
2202
|
-
/** PDA bump for commitment tree */
|
|
2203
|
-
treeBump: number;
|
|
2204
|
-
/** Deposit fee in basis points (u16 LE) */
|
|
2205
|
-
depositFeeBps: number;
|
|
2206
|
-
/** Withdrawal fee in basis points (u16 LE) */
|
|
2207
|
-
withdrawalFeeBps: number;
|
|
2208
|
-
/** Auditor's Solana pubkey (32 bytes) */
|
|
2209
|
-
auditor: Uint8Array;
|
|
2210
|
-
/** Auditor's viewing public key (32 bytes) */
|
|
2211
|
-
auditorViewingPubkey: Uint8Array;
|
|
2212
|
-
/** Account addresses — same layout as initialize (disc=0) */
|
|
2213
|
-
accounts: {
|
|
2214
|
-
/** 0. pool_state (writable) */
|
|
2215
|
-
poolState: Address;
|
|
2216
|
-
/** 1. commitment_tree (writable) */
|
|
2217
|
-
commitmentTree: Address;
|
|
2218
|
-
/** 2. zkbtc_mint (writable) */
|
|
2219
|
-
zkbtcMint: Address;
|
|
2220
|
-
/** 3. pool_vault (writable) */
|
|
2221
|
-
poolVault: Address;
|
|
2222
|
-
/** 4. deposit_vault (writable) */
|
|
2223
|
-
depositVault: Address;
|
|
2224
|
-
/** 5. authority (signer, writable — pays for storage) */
|
|
2225
|
-
authority: Address;
|
|
2226
|
-
/** 6. system_program (readonly) */
|
|
2227
|
-
systemProgram: Address;
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* Build initializePermissioned instruction data (disc=21).
|
|
2233
|
-
*
|
|
2234
|
-
* Layout (after disc byte — same as initialize plus two 32-byte fields):
|
|
2235
|
-
* pool_bump(1) + tree_bump(1) + deposit_fee_bps(2 LE) + withdrawal_fee_bps(2 LE)
|
|
2236
|
-
* + auditor(32) + auditor_viewing_pubkey(32)
|
|
2237
|
-
* = 70 bytes of payload; 71 bytes total with disc.
|
|
2238
|
-
*/
|
|
2239
|
-
export function buildInitializePermissionedInstructionData(options: {
|
|
2240
|
-
poolBump: number;
|
|
2241
|
-
treeBump: number;
|
|
2242
|
-
depositFeeBps: number;
|
|
2243
|
-
withdrawalFeeBps: number;
|
|
2244
|
-
auditor: Uint8Array;
|
|
2245
|
-
auditorViewingPubkey: Uint8Array;
|
|
2246
|
-
}): Uint8Array {
|
|
2247
|
-
if (options.auditor.length !== 32) {
|
|
2248
|
-
throw new Error(`auditor must be 32 bytes, got ${options.auditor.length}`);
|
|
2249
|
-
}
|
|
2250
|
-
if (options.auditorViewingPubkey.length !== 32) {
|
|
2251
|
-
throw new Error(`auditorViewingPubkey must be 32 bytes, got ${options.auditorViewingPubkey.length}`);
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
// disc(1) + pool_bump(1) + tree_bump(1) + deposit_fee_bps(2) + withdrawal_fee_bps(2) + auditor(32) + auditor_viewing_pubkey(32) = 71
|
|
2255
|
-
const data = new Uint8Array(71);
|
|
2256
|
-
const view = new DataView(data.buffer);
|
|
2257
|
-
let offset = 0;
|
|
2258
|
-
|
|
2259
|
-
data[offset++] = PERMISSIONED_DISC.INITIALIZE_PERMISSIONED; // disc = 21
|
|
2260
|
-
data[offset++] = options.poolBump;
|
|
2261
|
-
data[offset++] = options.treeBump;
|
|
2262
|
-
view.setUint16(offset, options.depositFeeBps, true); offset += 2;
|
|
2263
|
-
view.setUint16(offset, options.withdrawalFeeBps, true); offset += 2;
|
|
2264
|
-
data.set(options.auditor, offset); offset += 32;
|
|
2265
|
-
data.set(options.auditorViewingPubkey, offset);
|
|
2266
|
-
|
|
2267
|
-
return data;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
/**
|
|
2271
|
-
* Build a complete initializePermissioned instruction (disc=21).
|
|
2272
|
-
*
|
|
2273
|
-
* Initializes a pool in permissioned mode; deposits/shields require auditor co-signing.
|
|
2274
|
-
*
|
|
2275
|
-
* Accounts (identical to initialize, disc=0):
|
|
2276
|
-
* 0. pool_state (writable)
|
|
2277
|
-
* 1. commitment_tree (writable)
|
|
2278
|
-
* 2. zkbtc_mint (writable)
|
|
2279
|
-
* 3. pool_vault (writable)
|
|
2280
|
-
* 4. deposit_vault (writable)
|
|
2281
|
-
* 5. authority (writable signer)
|
|
2282
|
-
* 6. system_program (readonly)
|
|
2283
|
-
*/
|
|
2284
|
-
export function buildInitializePermissionedInstruction(
|
|
2285
|
-
options: InitializePermissionedOptions,
|
|
2286
|
-
): Instruction {
|
|
2287
|
-
const config = getConfig();
|
|
2288
|
-
const data = buildInitializePermissionedInstructionData({
|
|
2289
|
-
poolBump: options.poolBump,
|
|
2290
|
-
treeBump: options.treeBump,
|
|
2291
|
-
depositFeeBps: options.depositFeeBps,
|
|
2292
|
-
withdrawalFeeBps: options.withdrawalFeeBps,
|
|
2293
|
-
auditor: options.auditor,
|
|
2294
|
-
auditorViewingPubkey: options.auditorViewingPubkey,
|
|
2295
|
-
});
|
|
2296
|
-
|
|
2297
|
-
return {
|
|
2298
|
-
programAddress: config.utxopiaProgramId,
|
|
2299
|
-
accounts: [
|
|
2300
|
-
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
2301
|
-
{ address: options.accounts.commitmentTree, role: AccountRole.WRITABLE },
|
|
2302
|
-
{ address: options.accounts.zkbtcMint, role: AccountRole.WRITABLE },
|
|
2303
|
-
{ address: options.accounts.poolVault, role: AccountRole.WRITABLE },
|
|
2304
|
-
{ address: options.accounts.depositVault, role: AccountRole.WRITABLE },
|
|
2305
|
-
{ address: options.accounts.authority, role: AccountRole.WRITABLE_SIGNER },
|
|
2306
|
-
{ address: options.accounts.systemProgram, role: AccountRole.READONLY },
|
|
2307
|
-
],
|
|
2308
|
-
data,
|
|
2309
|
-
};
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
2194
|
// ---------------------------------------------------------------------------
|
|
2313
2195
|
// completeDepositPermissioned (disc=22)
|
|
2314
2196
|
// ---------------------------------------------------------------------------
|
|
@@ -2681,102 +2563,6 @@ export function buildRegisterExitDestinationInstruction(options: {
|
|
|
2681
2563
|
};
|
|
2682
2564
|
}
|
|
2683
2565
|
|
|
2684
|
-
// ---------------------------------------------------------------------------
|
|
2685
|
-
// setAuditorFrozen (disc=28)
|
|
2686
|
-
// ---------------------------------------------------------------------------
|
|
2687
|
-
|
|
2688
|
-
/** setAuditorFrozen instruction options */
|
|
2689
|
-
export interface SetAuditorFrozenOptions {
|
|
2690
|
-
/** true = freeze the auditor role; false = un-freeze */
|
|
2691
|
-
frozen: boolean;
|
|
2692
|
-
accounts: {
|
|
2693
|
-
/** 0. pool_state (writable) */
|
|
2694
|
-
poolState: Address;
|
|
2695
|
-
/** 1. auditor (signer) */
|
|
2696
|
-
auditor: Address;
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
/**
|
|
2701
|
-
* Build setAuditorFrozen instruction data (disc=28).
|
|
2702
|
-
*
|
|
2703
|
-
* Layout: disc(1) + frozen(1) — frozen byte: 0 = not frozen, 1 = frozen.
|
|
2704
|
-
*/
|
|
2705
|
-
export function buildSetAuditorFrozenInstructionData(frozen: boolean): Uint8Array {
|
|
2706
|
-
return new Uint8Array([INSTRUCTION.SET_AUDITOR_FROZEN, frozen ? 1 : 0]);
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
/**
|
|
2710
|
-
* Build a complete setAuditorFrozen instruction (disc=28).
|
|
2711
|
-
*
|
|
2712
|
-
* Accounts:
|
|
2713
|
-
* 0. pool_state (writable)
|
|
2714
|
-
* 1. auditor (signer)
|
|
2715
|
-
*/
|
|
2716
|
-
export function buildSetAuditorFrozenInstruction(options: SetAuditorFrozenOptions): Instruction {
|
|
2717
|
-
const config = getConfig();
|
|
2718
|
-
return {
|
|
2719
|
-
programAddress: config.utxopiaProgramId,
|
|
2720
|
-
accounts: [
|
|
2721
|
-
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
2722
|
-
{ address: options.accounts.auditor, role: AccountRole.READONLY_SIGNER },
|
|
2723
|
-
],
|
|
2724
|
-
data: buildSetAuditorFrozenInstructionData(options.frozen),
|
|
2725
|
-
};
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
// ---------------------------------------------------------------------------
|
|
2729
|
-
// setAuditorViewingPubkey (disc=29)
|
|
2730
|
-
// ---------------------------------------------------------------------------
|
|
2731
|
-
|
|
2732
|
-
/** setAuditorViewingPubkey instruction options */
|
|
2733
|
-
export interface SetAuditorViewingPubkeyOptions {
|
|
2734
|
-
/** New 32-byte viewing pubkey for the auditor */
|
|
2735
|
-
viewingPubkey: Uint8Array;
|
|
2736
|
-
accounts: {
|
|
2737
|
-
/** 0. pool_state (writable) */
|
|
2738
|
-
poolState: Address;
|
|
2739
|
-
/** 1. auditor (signer) */
|
|
2740
|
-
auditor: Address;
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
/**
|
|
2745
|
-
* Build setAuditorViewingPubkey instruction data (disc=29).
|
|
2746
|
-
*
|
|
2747
|
-
* Layout: disc(1) + viewing_pubkey(32) = 33 bytes.
|
|
2748
|
-
*/
|
|
2749
|
-
export function buildSetAuditorViewingPubkeyInstructionData(viewingPubkey: Uint8Array): Uint8Array {
|
|
2750
|
-
if (viewingPubkey.length !== 32) {
|
|
2751
|
-
throw new Error(`viewingPubkey must be 32 bytes, got ${viewingPubkey.length}`);
|
|
2752
|
-
}
|
|
2753
|
-
const data = new Uint8Array(33);
|
|
2754
|
-
data[0] = INSTRUCTION.SET_AUDITOR_VIEWING_PUBKEY;
|
|
2755
|
-
data.set(viewingPubkey, 1);
|
|
2756
|
-
return data;
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
/**
|
|
2760
|
-
* Build a complete setAuditorViewingPubkey instruction (disc=29).
|
|
2761
|
-
*
|
|
2762
|
-
* Accounts:
|
|
2763
|
-
* 0. pool_state (writable)
|
|
2764
|
-
* 1. auditor (signer)
|
|
2765
|
-
*/
|
|
2766
|
-
export function buildSetAuditorViewingPubkeyInstruction(
|
|
2767
|
-
options: SetAuditorViewingPubkeyOptions,
|
|
2768
|
-
): Instruction {
|
|
2769
|
-
const config = getConfig();
|
|
2770
|
-
return {
|
|
2771
|
-
programAddress: config.utxopiaProgramId,
|
|
2772
|
-
accounts: [
|
|
2773
|
-
{ address: options.accounts.poolState, role: AccountRole.WRITABLE },
|
|
2774
|
-
{ address: options.accounts.auditor, role: AccountRole.READONLY_SIGNER },
|
|
2775
|
-
],
|
|
2776
|
-
data: buildSetAuditorViewingPubkeyInstructionData(options.viewingPubkey),
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
2566
|
// ---------------------------------------------------------------------------
|
|
2781
2567
|
// rotateAuditor (disc=35)
|
|
2782
2568
|
// ---------------------------------------------------------------------------
|
package/src/poseidon.ts
CHANGED
|
@@ -164,12 +164,3 @@ export function computeTokenId(mintBytes: Uint8Array): bigint {
|
|
|
164
164
|
const reduced = reduceToField(mintBytes);
|
|
165
165
|
return poseidonHashSync([reduced, 0n]);
|
|
166
166
|
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Convenience: compute token_id from a Solana address string
|
|
170
|
-
*/
|
|
171
|
-
export function computeTokenIdFromAddress(mintAddress: string): bigint {
|
|
172
|
-
// Base58 decode — import from @solana/kit if available, otherwise use raw bytes
|
|
173
|
-
// For now, caller should pass raw bytes via computeTokenId
|
|
174
|
-
throw new Error("Use computeTokenId(mintBytes) with raw pubkey bytes");
|
|
175
|
-
}
|
package/src/prover/index.ts
CHANGED
package/src/prover/web.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
computeJoinSplitCommitmentSync,
|
|
16
16
|
computeJoinSplitNullifierSync,
|
|
17
17
|
} from "../poseidon";
|
|
18
|
-
import { BN254_FIELD_PRIME } from "../crypto";
|
|
18
|
+
import { BN254_FIELD_PRIME, bytesToHex } from "../crypto";
|
|
19
19
|
import { TREE_DEPTH } from "../merkle";
|
|
20
20
|
import { getConfig } from "../config";
|
|
21
21
|
import { MAX_SAFE_JOINSPLIT_SIZE } from "../vk-registry";
|
|
@@ -60,13 +60,6 @@ export interface ProofData {
|
|
|
60
60
|
|
|
61
61
|
export type CircuitType = `joinsplit_${number}x${number}`;
|
|
62
62
|
|
|
63
|
-
/** Names of non-JoinSplit auxiliary circuits (selective disclosure). */
|
|
64
|
-
export type AuxCircuitName =
|
|
65
|
-
| "ownership"
|
|
66
|
-
| "range_sum"
|
|
67
|
-
| "range_sum_4"
|
|
68
|
-
| "range_sum_16";
|
|
69
|
-
|
|
70
63
|
// Environment detection
|
|
71
64
|
const isBrowser = typeof window !== "undefined";
|
|
72
65
|
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
@@ -85,13 +78,6 @@ export function setCircuitPath(path: string): void {
|
|
|
85
78
|
artifactDownloadCache.clear();
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
/**
|
|
89
|
-
* Get the current circuit base path
|
|
90
|
-
*/
|
|
91
|
-
export function getCircuitPath(): string {
|
|
92
|
-
return circuitBasePath;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
81
|
// Lazy-loaded snarkjs module
|
|
96
82
|
let snarkjs: any = null;
|
|
97
83
|
|
|
@@ -164,9 +150,6 @@ export function setCircuitArtifactDigests(digests: Record<string, string> | null
|
|
|
164
150
|
artifactBytesCache.clear();
|
|
165
151
|
}
|
|
166
152
|
|
|
167
|
-
const toHex = (buf: ArrayBuffer) =>
|
|
168
|
-
Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
169
|
-
|
|
170
153
|
async function verifyArtifactBytes(url: string, bytes: Uint8Array): Promise<void> {
|
|
171
154
|
if (!artifactDigests) return;
|
|
172
155
|
// Key by path-under-base so one manifest works across origins (local dev vs CDN).
|
|
@@ -177,8 +160,8 @@ async function verifyArtifactBytes(url: string, bytes: Uint8Array): Promise<void
|
|
|
177
160
|
`Regenerate the artifact manifest so it covers every shape this origin serves.`,
|
|
178
161
|
);
|
|
179
162
|
}
|
|
180
|
-
const actual =
|
|
181
|
-
await crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer),
|
|
163
|
+
const actual = bytesToHex(
|
|
164
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer)),
|
|
182
165
|
);
|
|
183
166
|
if (actual !== artifactDigests[key]) {
|
|
184
167
|
throw new Error(
|
|
@@ -717,55 +700,3 @@ export async function cleanup(): Promise<void> {
|
|
|
717
700
|
// ==========================================================================
|
|
718
701
|
// Solana Instruction Building (for Groth16 on-chain verification)
|
|
719
702
|
// ==========================================================================
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Groth16 verifier program ID (from current config)
|
|
723
|
-
*/
|
|
724
|
-
export function getGroth16VerifierProgramId(): Address {
|
|
725
|
-
const config = getConfig();
|
|
726
|
-
return config.groth16VerifierProgramId;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Build instruction data for Groth16 verification
|
|
731
|
-
*/
|
|
732
|
-
export function buildVerifyInstructionData(
|
|
733
|
-
proof: Uint8Array,
|
|
734
|
-
publicSignals: string[],
|
|
735
|
-
vkHash: string
|
|
736
|
-
): Uint8Array {
|
|
737
|
-
const piBytes = publicSignals.flatMap((pi) => {
|
|
738
|
-
const bytes = new Array(32).fill(0);
|
|
739
|
-
const bigint = BigInt(pi);
|
|
740
|
-
for (let i = 31; i >= 0; i--) {
|
|
741
|
-
bytes[i] = Number((bigint >> BigInt((31 - i) * 8)) & 0xFFn);
|
|
742
|
-
}
|
|
743
|
-
return bytes;
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
const cleanHex = vkHash.startsWith("0x") ? vkHash.slice(2) : vkHash;
|
|
747
|
-
const vkHashBytes = new Uint8Array(cleanHex.length / 2);
|
|
748
|
-
for (let i = 0; i < vkHashBytes.length; i++) {
|
|
749
|
-
vkHashBytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const totalSize = proof.length + 4 + piBytes.length + 32;
|
|
753
|
-
const data = new Uint8Array(totalSize);
|
|
754
|
-
let offset = 0;
|
|
755
|
-
|
|
756
|
-
data.set(proof, offset);
|
|
757
|
-
offset += proof.length;
|
|
758
|
-
|
|
759
|
-
const piCount = publicSignals.length;
|
|
760
|
-
data[offset++] = piCount & 0xff;
|
|
761
|
-
data[offset++] = (piCount >> 8) & 0xff;
|
|
762
|
-
data[offset++] = (piCount >> 16) & 0xff;
|
|
763
|
-
data[offset++] = (piCount >> 24) & 0xff;
|
|
764
|
-
|
|
765
|
-
data.set(new Uint8Array(piBytes), offset);
|
|
766
|
-
offset += piBytes.length;
|
|
767
|
-
|
|
768
|
-
data.set(vkHashBytes, offset);
|
|
769
|
-
|
|
770
|
-
return data;
|
|
771
|
-
}
|
package/src/psbt.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import * as btc from "@scure/btc-signer";
|
|
14
14
|
import { hex } from "@scure/base";
|
|
15
|
-
import { DEPOSIT_OP_RETURN_SIZE, createOpReturnScriptFromPayload } from "./taproot";
|
|
15
|
+
import { DEPOSIT_OP_RETURN_SIZE, createOpReturnScriptFromPayload, bech32Hrp, type BitcoinNetwork } from "./taproot";
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Types
|
|
@@ -41,13 +41,20 @@ export interface BuildDepositPsbtParams {
|
|
|
41
41
|
/** Deposit amount in satoshis */
|
|
42
42
|
depositAmountSats: number;
|
|
43
43
|
/** Compact deposit OP_RETURN payload (from buildDepositOpReturn) */
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 73-byte deposit metadata, for the OP_RETURN flow.
|
|
46
|
+
*
|
|
47
|
+
* Omit it for an OP_RETURN-free deposit (`verify_deposit`), where the address
|
|
48
|
+
* itself binds the note keys through its tapleaf. The transaction is then a
|
|
49
|
+
* plain payment, which is what lets an exchange withdrawal fund it.
|
|
50
|
+
*/
|
|
51
|
+
opReturnPayload?: Uint8Array;
|
|
45
52
|
/** Change address (same type as sender) */
|
|
46
53
|
changeAddress: string;
|
|
47
54
|
/** Fee rate in sats/vbyte */
|
|
48
55
|
feeRate: number;
|
|
49
56
|
/** Bitcoin network */
|
|
50
|
-
network?:
|
|
57
|
+
network?: BitcoinNetwork;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/** Result of PSBT construction */
|
|
@@ -98,15 +105,17 @@ export function estimateDepositFee(
|
|
|
98
105
|
feeRate: number,
|
|
99
106
|
inputType: "p2tr" | "p2wpkh" = "p2tr",
|
|
100
107
|
hasChange: boolean = true,
|
|
108
|
+
/** An OP_RETURN-free deposit has one output fewer; defaults true for callers
|
|
109
|
+
* written before that flow existed. */
|
|
110
|
+
hasOpReturn: boolean = true,
|
|
101
111
|
): number {
|
|
102
112
|
const inputVbytes = inputType === "p2tr" ? P2TR_INPUT_VBYTES : P2WPKH_INPUT_VBYTES;
|
|
103
|
-
const outputCount = hasChange ? 3 : 2; // deposit + OP_RETURN + optional change
|
|
104
113
|
|
|
105
114
|
const vsize =
|
|
106
115
|
TX_OVERHEAD_VBYTES +
|
|
107
116
|
numInputs * inputVbytes +
|
|
108
117
|
P2TR_OUTPUT_VBYTES + // deposit output
|
|
109
|
-
OP_RETURN_OUTPUT_VBYTES
|
|
118
|
+
(hasOpReturn ? OP_RETURN_OUTPUT_VBYTES : 0) +
|
|
110
119
|
(hasChange ? P2TR_OUTPUT_VBYTES : 0); // change output
|
|
111
120
|
|
|
112
121
|
return Math.ceil(vsize * feeRate);
|
|
@@ -138,11 +147,16 @@ export function buildDepositPsbt(params: BuildDepositPsbtParams): BuildDepositPs
|
|
|
138
147
|
if (depositAmountSats < DUST_LIMIT) {
|
|
139
148
|
throw new Error(`Deposit amount ${depositAmountSats} is below dust limit ${DUST_LIMIT}`);
|
|
140
149
|
}
|
|
141
|
-
if (opReturnPayload.length !== DEPOSIT_OP_RETURN_SIZE) {
|
|
150
|
+
if (opReturnPayload && opReturnPayload.length !== DEPOSIT_OP_RETURN_SIZE) {
|
|
142
151
|
throw new Error(`OP_RETURN payload must be ${DEPOSIT_OP_RETURN_SIZE} bytes, got ${opReturnPayload.length}`);
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
// Regtest shares testnet's version bytes but not its bech32 hrp, so folding it
|
|
155
|
+
// into TEST_NETWORK leaves every bcrt1 address undecodable.
|
|
156
|
+
const btcNetwork =
|
|
157
|
+
network === "mainnet"
|
|
158
|
+
? btc.NETWORK
|
|
159
|
+
: { ...btc.TEST_NETWORK, bech32: bech32Hrp(network) };
|
|
146
160
|
|
|
147
161
|
// Calculate total input value
|
|
148
162
|
const totalInput = senderUtxos.reduce((sum, u) => sum + u.value, 0);
|
|
@@ -152,11 +166,11 @@ export function buildDepositPsbt(params: BuildDepositPsbtParams): BuildDepositPs
|
|
|
152
166
|
const inputType = firstScript[0] === 0x51 ? "p2tr" : "p2wpkh";
|
|
153
167
|
|
|
154
168
|
// Estimate fee with change
|
|
155
|
-
const feeWithChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, true);
|
|
169
|
+
const feeWithChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, true, Boolean(opReturnPayload));
|
|
156
170
|
const changeAmount = totalInput - depositAmountSats - feeWithChange;
|
|
157
171
|
|
|
158
172
|
// Check if we have enough funds
|
|
159
|
-
const feeWithoutChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, false);
|
|
173
|
+
const feeWithoutChange = estimateDepositFee(senderUtxos.length, feeRate, inputType, false, Boolean(opReturnPayload));
|
|
160
174
|
if (totalInput < depositAmountSats + feeWithoutChange) {
|
|
161
175
|
throw new Error(
|
|
162
176
|
`Insufficient funds: have ${totalInput} sats, need ${depositAmountSats + feeWithoutChange} sats (including fee)`,
|
|
@@ -203,12 +217,15 @@ export function buildDepositPsbt(params: BuildDepositPsbtParams): BuildDepositPs
|
|
|
203
217
|
// Output 1: P2TR deposit
|
|
204
218
|
tx.addOutputAddress(depositAddress, BigInt(depositAmountSats), btcNetwork);
|
|
205
219
|
|
|
206
|
-
// Output 2: OP_RETURN with compact deposit payload.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
// Output 2: OP_RETURN with compact deposit payload — only for that flow. A
|
|
221
|
+
// tweak-bound deposit adds nothing here, so the transaction is indistinguishable
|
|
222
|
+
// from an ordinary payment.
|
|
223
|
+
if (opReturnPayload) {
|
|
224
|
+
tx.addOutput({
|
|
225
|
+
script: createOpReturnScriptFromPayload(opReturnPayload),
|
|
226
|
+
amount: 0n,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
212
229
|
|
|
213
230
|
// Output 3: Change (if above dust)
|
|
214
231
|
if (hasChange) {
|
package/src/spend-doc.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createUnshieldBoundParams,
|
|
19
19
|
type SolanaPrivacyDomainContext,
|
|
20
20
|
} from "./bound-params";
|
|
21
|
+
import { bytesToHex as hex } from "./crypto";
|
|
21
22
|
|
|
22
23
|
export interface SpendDoc {
|
|
23
24
|
mode: "transfer" | "unshield" | "redeem";
|
|
@@ -64,8 +65,6 @@ function fmt(raw: bigint, decimals: number): string {
|
|
|
64
65
|
return `${neg ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
68
|
-
|
|
69
68
|
function sortedValues(v: bigint[]): string {
|
|
70
69
|
return [...v].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(",");
|
|
71
70
|
}
|
package/src/stealth.ts
CHANGED
|
@@ -401,21 +401,21 @@ export interface NonInteractiveDepositWithRefundResult extends NonInteractiveDep
|
|
|
401
401
|
export async function createNonInteractiveDeposit(
|
|
402
402
|
recipientMeta: StealthMetaAddress,
|
|
403
403
|
custodyInternalKey: Uint8Array,
|
|
404
|
-
network?:
|
|
404
|
+
network?: BitcoinNetwork,
|
|
405
405
|
userRefundPubkey?: undefined,
|
|
406
406
|
opReturnContext?: DepositOpReturnContext,
|
|
407
407
|
): Promise<NonInteractiveDepositResult>;
|
|
408
408
|
export async function createNonInteractiveDeposit(
|
|
409
409
|
recipientMeta: StealthMetaAddress,
|
|
410
410
|
custodyInternalKey: Uint8Array,
|
|
411
|
-
network:
|
|
411
|
+
network: BitcoinNetwork,
|
|
412
412
|
userRefundPubkey: Uint8Array,
|
|
413
413
|
opReturnContext: DepositOpReturnContext,
|
|
414
414
|
): Promise<NonInteractiveDepositWithRefundResult>;
|
|
415
415
|
export async function createNonInteractiveDeposit(
|
|
416
416
|
recipientMeta: StealthMetaAddress,
|
|
417
417
|
custodyInternalKey: Uint8Array,
|
|
418
|
-
network:
|
|
418
|
+
network: BitcoinNetwork = "testnet",
|
|
419
419
|
userRefundPubkey?: Uint8Array,
|
|
420
420
|
opReturnContext?: DepositOpReturnContext,
|
|
421
421
|
): Promise<NonInteractiveDepositResult | NonInteractiveDepositWithRefundResult> {
|
|
@@ -491,7 +491,7 @@ export async function createNonInteractiveDeposit(
|
|
|
491
491
|
export async function createDirectVaultDeposit(
|
|
492
492
|
recipientMeta: StealthMetaAddress,
|
|
493
493
|
vaultXOnlyPubkey: Uint8Array,
|
|
494
|
-
network:
|
|
494
|
+
network: BitcoinNetwork = "testnet",
|
|
495
495
|
opReturnContext?: DepositOpReturnContext,
|
|
496
496
|
): Promise<NonInteractiveDepositResult> {
|
|
497
497
|
if (!opReturnContext) {
|
|
@@ -735,7 +735,7 @@ export async function createTweakDeposit(
|
|
|
735
735
|
recipientMeta: StealthMetaAddress,
|
|
736
736
|
vaultXOnlyPubkey: Uint8Array,
|
|
737
737
|
recovery: DepositRecoveryMaterial,
|
|
738
|
-
network:
|
|
738
|
+
network: BitcoinNetwork = "testnet",
|
|
739
739
|
): Promise<TweakDepositResult> {
|
|
740
740
|
if (vaultXOnlyPubkey.length !== 32) {
|
|
741
741
|
throw new Error("vaultXOnlyPubkey must be 32 bytes");
|
|
@@ -781,7 +781,7 @@ export async function createTweakDeposit(
|
|
|
781
781
|
*/
|
|
782
782
|
export async function createDepositFromConfig(
|
|
783
783
|
recipientMeta: StealthMetaAddress,
|
|
784
|
-
network:
|
|
784
|
+
network: BitcoinNetwork = "testnet",
|
|
785
785
|
opReturnContext?: DepositOpReturnContext,
|
|
786
786
|
): Promise<NonInteractiveDepositResult> {
|
|
787
787
|
const config = getConfig();
|
|
@@ -1220,6 +1220,7 @@ export async function scanUnifiedNotes(
|
|
|
1220
1220
|
// ========== Connection Adapter ==========
|
|
1221
1221
|
|
|
1222
1222
|
import type { Address } from "@solana/kit";
|
|
1223
|
+
import type { BitcoinNetwork } from "./taproot";
|
|
1223
1224
|
|
|
1224
1225
|
export interface ConnectionAdapter {
|
|
1225
1226
|
getAccountInfo: (
|
|
@@ -1285,23 +1286,6 @@ export function packStealthOutputForCircuit(output: StealthOutputData): CircuitS
|
|
|
1285
1286
|
};
|
|
1286
1287
|
}
|
|
1287
1288
|
|
|
1288
|
-
/**
|
|
1289
|
-
* Unpack encrypted amount from packed Field element
|
|
1290
|
-
*/
|
|
1291
|
-
export function unpackEncryptedAmountWithSign(packed: bigint): { encryptedAmount: Uint8Array; ySign: boolean } {
|
|
1292
|
-
const ySign = (packed & (1n << 64n)) !== 0n;
|
|
1293
|
-
const amount = packed & ((1n << 64n) - 1n);
|
|
1294
|
-
|
|
1295
|
-
const encryptedAmount = new Uint8Array(8);
|
|
1296
|
-
let temp = amount;
|
|
1297
|
-
for (let i = 0; i < 8; i++) {
|
|
1298
|
-
encryptedAmount[i] = Number(temp & 0xffn);
|
|
1299
|
-
temp >>= 8n;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
return { encryptedAmount, ySign };
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1289
|
/**
|
|
1306
1290
|
* Create stealth output data for a self-send (change output)
|
|
1307
1291
|
*/
|