@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ika dWallet → Bitcoin P2TR address derivation.
|
|
3
|
+
*
|
|
4
|
+
* UTXOpia v2 custody is held by an Ika dWallet on Solana. The dWallet's
|
|
5
|
+
* compressed secp256k1 public key (or its x-only form) identifies the Bitcoin
|
|
6
|
+
* key that Ika can sign for. Ika pre-alpha signs for the raw x-only key; it
|
|
7
|
+
* does not currently produce signatures for UTXOpia's per-deposit tweaked keys.
|
|
8
|
+
*/
|
|
9
|
+
import { taggedHash, hexToBytes, bytesToHex } from "../crypto";
|
|
10
|
+
import { bech32m } from "bech32";
|
|
11
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
12
|
+
/**
|
|
13
|
+
* Derive the BIP-341 P2TR (key-path-only) address controlled by an Ika dWallet.
|
|
14
|
+
*
|
|
15
|
+
* BIP-341 (no script tree):
|
|
16
|
+
* t = h_tapTweak(P)
|
|
17
|
+
* Q = P + t*G
|
|
18
|
+
* output_key = x(Q)
|
|
19
|
+
* address = bech32m(hrp, [witness_version=1, ...words(output_key)])
|
|
20
|
+
*
|
|
21
|
+
* @param ref The Ika dWallet reference (literal pubkey, or future async id).
|
|
22
|
+
* @param network "mainnet" | "testnet" | "regtest"
|
|
23
|
+
* @returns The P2TR address (`bc1p…` / `tb1p…` / `bcrt1p…`)
|
|
24
|
+
*/
|
|
25
|
+
export function deriveCustodyAddressFromIkaDWallet(ref, network) {
|
|
26
|
+
const xonly = extractXOnly(ref);
|
|
27
|
+
const tweak = taggedHash("TapTweak", xonly);
|
|
28
|
+
const tweakScalar = bytesToBigIntBe(tweak);
|
|
29
|
+
// Lift x-only to a full point with even y (BIP-340 convention).
|
|
30
|
+
const internalPoint = secp256k1.Point.fromHex("02" + bytesToHex(xonly));
|
|
31
|
+
const tweakPoint = secp256k1.Point.BASE.multiply(tweakScalar);
|
|
32
|
+
const outputPoint = internalPoint.add(tweakPoint);
|
|
33
|
+
// Drop the 1-byte parity prefix to get the x-only output key.
|
|
34
|
+
const outputKey = hexToBytes(outputPoint.toHex(true).slice(2));
|
|
35
|
+
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
36
|
+
const words = bech32m.toWords(outputKey);
|
|
37
|
+
return bech32m.encode(hrp, [1, ...words]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Encode a raw x-only public key as a P2TR witness program.
|
|
41
|
+
*
|
|
42
|
+
* This intentionally does not apply BIP-341's no-script TapTweak. It is the
|
|
43
|
+
* direct-vault address used by the current Ika pre-alpha mock signer, because
|
|
44
|
+
* the signer returns Schnorr signatures for the dWallet's raw x-only key.
|
|
45
|
+
*/
|
|
46
|
+
export function deriveRawXOnlyP2TRAddress(xonlyPubkey, network) {
|
|
47
|
+
if (xonlyPubkey.length !== 32) {
|
|
48
|
+
throw new Error("xonlyPubkey must be 32 bytes");
|
|
49
|
+
}
|
|
50
|
+
const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
|
|
51
|
+
return bech32m.encode(hrp, [1, ...bech32m.toWords(xonlyPubkey)]);
|
|
52
|
+
}
|
|
53
|
+
function extractXOnly(ref) {
|
|
54
|
+
if (ref.type === "literal-xonly") {
|
|
55
|
+
if (ref.xonlyPubkey.length !== 32) {
|
|
56
|
+
throw new Error("xonlyPubkey must be 32 bytes");
|
|
57
|
+
}
|
|
58
|
+
return ref.xonlyPubkey;
|
|
59
|
+
}
|
|
60
|
+
if (ref.type === "literal-compressed") {
|
|
61
|
+
if (ref.compressedPubkey.length !== 33) {
|
|
62
|
+
throw new Error("compressedPubkey must be 33 bytes");
|
|
63
|
+
}
|
|
64
|
+
return ref.compressedPubkey.subarray(1);
|
|
65
|
+
}
|
|
66
|
+
throw new Error("deriveCustodyAddressFromIkaDWallet: 'id' resolution requires the Ika SDK; " +
|
|
67
|
+
"resolve dwalletId → pubkey first and pass via literal-xonly or literal-compressed");
|
|
68
|
+
}
|
|
69
|
+
function bytesToBigIntBe(bytes) {
|
|
70
|
+
let n = 0n;
|
|
71
|
+
for (const b of bytes)
|
|
72
|
+
n = (n << 8n) | BigInt(b);
|
|
73
|
+
return n;
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deriveCustodyAddressFromIkaDWallet, deriveRawXOnlyP2TRAddress, } from "./ika";
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
/** Bound params mode: transfer(0), unshield(1), redeem(2) */
|
|
16
|
+
export type BoundParamsMode = 'transfer' | 'unshield' | 'redeem';
|
|
17
|
+
export interface BoundParams {
|
|
18
|
+
/** Tree number (0 for default) */
|
|
19
|
+
treeNumber: number;
|
|
20
|
+
/** Unshield recipient address (null = private transfer, 32 bytes = public unshield/redeem) */
|
|
21
|
+
unshieldAddress: Uint8Array | null;
|
|
22
|
+
/** Chain ID (prevents cross-chain replay) */
|
|
23
|
+
chainId: bigint;
|
|
24
|
+
/** Mode flag: 'transfer'(0), 'unshield'(1), 'redeem'(2). Defaults to inferred from unshieldAddress. */
|
|
25
|
+
mode?: BoundParamsMode;
|
|
26
|
+
/** SHA256 of concatenated stealth data (prevents relayer from corrupting change outputs) */
|
|
27
|
+
stealthDataHash: Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Requester pubkey (32 bytes) — REQUIRED for redeem. Binds the proof to the signing account
|
|
30
|
+
* that becomes RedemptionRequest.requester so a privileged orderflow actor cannot replay the
|
|
31
|
+
* proof under their own key. Ignored for transfer/unshield.
|
|
32
|
+
*/
|
|
33
|
+
requester?: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export type SolanaPrivacyDomainKind = "public" | "institution";
|
|
36
|
+
export interface SolanaPrivacyDomainContext {
|
|
37
|
+
programId: Uint8Array;
|
|
38
|
+
poolState: Uint8Array;
|
|
39
|
+
kind: SolanaPrivacyDomainKind;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compute the canonical public/institution domain field for Solana.
|
|
43
|
+
*
|
|
44
|
+
* SHA256("UTXOPIA_DOMAIN_V1" || chainIdLE || programId || poolState || kind)
|
|
45
|
+
* reduced modulo BN254 Fr. The on-chain verifier recomputes this from the
|
|
46
|
+
* actual program, pool account, and PoolState.permissioned flag.
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeSolanaDomainSeparator(context: SolanaPrivacyDomainContext, chainId?: bigint): bigint;
|
|
49
|
+
/**
|
|
50
|
+
* Bind an operation-specific hash to its exact Solana privacy domain.
|
|
51
|
+
*
|
|
52
|
+
* This occupies the existing boundParamsHash public input, so circuit/VK
|
|
53
|
+
* dimensions remain unchanged while proofs become non-replayable across pools.
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeSolanaDomainBoundParamsHash(params: BoundParams, context: SolanaPrivacyDomainContext): bigint;
|
|
56
|
+
/**
|
|
57
|
+
* Compute SHA256 hash of concatenated stealth data arrays.
|
|
58
|
+
* Returns 32-byte hash, or all zeros if no stealth data.
|
|
59
|
+
*/
|
|
60
|
+
export declare function computeStealthDataHash(stealthData: Uint8Array[]): Uint8Array;
|
|
61
|
+
/**
|
|
62
|
+
* Compute the operation-specific bound parameters hash.
|
|
63
|
+
*
|
|
64
|
+
* Solana callers must wrap this with
|
|
65
|
+
* `computeSolanaDomainBoundParamsHash()` to bind the program and pool.
|
|
66
|
+
*
|
|
67
|
+
* Deterministic serialization:
|
|
68
|
+
* - treeNumber: 4 bytes LE
|
|
69
|
+
* - flag: 1 byte (0=transfer, 1=unshield, 2=redeem)
|
|
70
|
+
* - unshieldAddress: 32 bytes (zeros if null)
|
|
71
|
+
* - chainId: 8 bytes LE
|
|
72
|
+
* - stealthDataHash: 32 bytes (SHA256 of concatenated stealth data)
|
|
73
|
+
* - requester: 32 bytes (redeem only — appended, extending the buffer to 109 bytes)
|
|
74
|
+
*
|
|
75
|
+
* Total: 77 bytes (transfer/unshield) or 109 bytes (redeem) → SHA256 → mod BN254
|
|
76
|
+
*/
|
|
77
|
+
export declare function computeBoundParamsHash(params: BoundParams): bigint;
|
|
78
|
+
/** Canonical chain ids folded into bound-params hashes (must match on-chain). */
|
|
79
|
+
export declare const SOLANA_MAINNET_BOUND_CHAIN_ID = 101n;
|
|
80
|
+
export declare const SOLANA_DEVNET_BOUND_CHAIN_ID = 103n;
|
|
81
|
+
/** Backward-compatible devnet alias. Prefer the network-specific constants. */
|
|
82
|
+
export declare const SOLANA_BOUND_CHAIN_ID = 103n;
|
|
83
|
+
/**
|
|
84
|
+
* Default bound params for Solana devnet (private transfer)
|
|
85
|
+
*/
|
|
86
|
+
export declare function createTransferBoundParams(stealthDataHash: Uint8Array, chainId?: bigint, treeNumber?: number): BoundParams;
|
|
87
|
+
/** @deprecated Use createTransferBoundParams instead */
|
|
88
|
+
export declare const DEFAULT_BOUND_PARAMS: BoundParams;
|
|
89
|
+
export declare function createRedeemBoundParams(btcScripts: Uint8Array | Uint8Array[], stealthDataHash: Uint8Array, requester: Uint8Array, chainId?: bigint, treeNumber?: number): BoundParams;
|
|
90
|
+
/**
|
|
91
|
+
* Create bound params for an unshield (public withdrawal, multi-output)
|
|
92
|
+
*
|
|
93
|
+
* For multi-output: destinations_hash = SHA256(owner_1 || owner_2 || ...)
|
|
94
|
+
* For single output: SHA256(owner_1) — no special case.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createUnshieldBoundParams(recipientAddresses: Uint8Array | Uint8Array[], stealthDataHash: Uint8Array, chainId?: bigint, treeNumber?: number): BoundParams;
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
16
|
+
import { BN254_FIELD_PRIME, bytesToBigint } from "./crypto";
|
|
17
|
+
import { poseidonHashSync } from "./poseidon";
|
|
18
|
+
const SOLANA_DOMAIN_TAG = new TextEncoder().encode("UTXOPIA_DOMAIN_V1");
|
|
19
|
+
function assert32Bytes(value, name) {
|
|
20
|
+
if (value.length !== 32) {
|
|
21
|
+
throw new Error(`${name} must be 32 bytes`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function u64le(value) {
|
|
25
|
+
if (value < 0n || value > 0xffffffffffffffffn) {
|
|
26
|
+
throw new Error("chainId must fit in u64");
|
|
27
|
+
}
|
|
28
|
+
const bytes = new Uint8Array(8);
|
|
29
|
+
let remaining = value;
|
|
30
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
31
|
+
bytes[i] = Number(remaining & 0xffn);
|
|
32
|
+
remaining >>= 8n;
|
|
33
|
+
}
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compute the canonical public/institution domain field for Solana.
|
|
38
|
+
*
|
|
39
|
+
* SHA256("UTXOPIA_DOMAIN_V1" || chainIdLE || programId || poolState || kind)
|
|
40
|
+
* reduced modulo BN254 Fr. The on-chain verifier recomputes this from the
|
|
41
|
+
* actual program, pool account, and PoolState.permissioned flag.
|
|
42
|
+
*/
|
|
43
|
+
export function computeSolanaDomainSeparator(context, chainId = SOLANA_BOUND_CHAIN_ID) {
|
|
44
|
+
assert32Bytes(context.programId, "programId");
|
|
45
|
+
assert32Bytes(context.poolState, "poolState");
|
|
46
|
+
if (context.kind !== "public" && context.kind !== "institution") {
|
|
47
|
+
throw new Error("kind must be public or institution");
|
|
48
|
+
}
|
|
49
|
+
const preimage = new Uint8Array(SOLANA_DOMAIN_TAG.length + 8 + 32 + 32 + 1);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
preimage.set(SOLANA_DOMAIN_TAG, offset);
|
|
52
|
+
offset += SOLANA_DOMAIN_TAG.length;
|
|
53
|
+
preimage.set(u64le(chainId), offset);
|
|
54
|
+
offset += 8;
|
|
55
|
+
preimage.set(context.programId, offset);
|
|
56
|
+
offset += 32;
|
|
57
|
+
preimage.set(context.poolState, offset);
|
|
58
|
+
offset += 32;
|
|
59
|
+
preimage[offset] = context.kind === "institution" ? 1 : 0;
|
|
60
|
+
return bytesToBigint(sha256(preimage)) % BN254_FIELD_PRIME;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Bind an operation-specific hash to its exact Solana privacy domain.
|
|
64
|
+
*
|
|
65
|
+
* This occupies the existing boundParamsHash public input, so circuit/VK
|
|
66
|
+
* dimensions remain unchanged while proofs become non-replayable across pools.
|
|
67
|
+
*/
|
|
68
|
+
export function computeSolanaDomainBoundParamsHash(params, context) {
|
|
69
|
+
if (params.chainId !== SOLANA_DEVNET_BOUND_CHAIN_ID
|
|
70
|
+
&& params.chainId !== SOLANA_MAINNET_BOUND_CHAIN_ID) {
|
|
71
|
+
throw new Error("Solana domain binding requires a supported Solana chain ID");
|
|
72
|
+
}
|
|
73
|
+
return poseidonHashSync([
|
|
74
|
+
computeSolanaDomainSeparator(context, params.chainId),
|
|
75
|
+
computeBoundParamsHash(params),
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute SHA256 hash of concatenated stealth data arrays.
|
|
80
|
+
* Returns 32-byte hash, or all zeros if no stealth data.
|
|
81
|
+
*/
|
|
82
|
+
export function computeStealthDataHash(stealthData) {
|
|
83
|
+
// Always SHA256 the concatenation — even for empty arrays.
|
|
84
|
+
// On-chain: sha256(&data[stealth_start..stealth_end]) — empty slice → sha256("")
|
|
85
|
+
const totalLen = stealthData.reduce((sum, sd) => sum + sd.length, 0);
|
|
86
|
+
const concat = new Uint8Array(totalLen);
|
|
87
|
+
let offset = 0;
|
|
88
|
+
for (const sd of stealthData) {
|
|
89
|
+
concat.set(sd, offset);
|
|
90
|
+
offset += sd.length;
|
|
91
|
+
}
|
|
92
|
+
return sha256(concat);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compute the operation-specific bound parameters hash.
|
|
96
|
+
*
|
|
97
|
+
* Solana callers must wrap this with
|
|
98
|
+
* `computeSolanaDomainBoundParamsHash()` to bind the program and pool.
|
|
99
|
+
*
|
|
100
|
+
* Deterministic serialization:
|
|
101
|
+
* - treeNumber: 4 bytes LE
|
|
102
|
+
* - flag: 1 byte (0=transfer, 1=unshield, 2=redeem)
|
|
103
|
+
* - unshieldAddress: 32 bytes (zeros if null)
|
|
104
|
+
* - chainId: 8 bytes LE
|
|
105
|
+
* - stealthDataHash: 32 bytes (SHA256 of concatenated stealth data)
|
|
106
|
+
* - requester: 32 bytes (redeem only — appended, extending the buffer to 109 bytes)
|
|
107
|
+
*
|
|
108
|
+
* Total: 77 bytes (transfer/unshield) or 109 bytes (redeem) → SHA256 → mod BN254
|
|
109
|
+
*/
|
|
110
|
+
export function computeBoundParamsHash(params) {
|
|
111
|
+
const isRedeem = params.mode === 'redeem';
|
|
112
|
+
// Redeem binds the requester pubkey, extending the preimage by 32 bytes (must match the
|
|
113
|
+
// on-chain compute_bound_params_hash_redeem layout).
|
|
114
|
+
const buf = new Uint8Array(isRedeem ? 109 : 77);
|
|
115
|
+
const view = new DataView(buf.buffer);
|
|
116
|
+
// treeNumber (4 bytes LE)
|
|
117
|
+
view.setUint32(0, params.treeNumber, true);
|
|
118
|
+
// flag byte: transfer=0, unshield=1, redeem=2
|
|
119
|
+
if (isRedeem) {
|
|
120
|
+
buf[4] = 2;
|
|
121
|
+
}
|
|
122
|
+
else if (params.mode === 'unshield' || params.unshieldAddress) {
|
|
123
|
+
buf[4] = 1;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
buf[4] = 0;
|
|
127
|
+
}
|
|
128
|
+
// unshieldAddress (32 bytes, zeros if null)
|
|
129
|
+
if (params.unshieldAddress) {
|
|
130
|
+
buf.set(params.unshieldAddress.slice(0, 32), 5);
|
|
131
|
+
}
|
|
132
|
+
// chainId (8 bytes LE)
|
|
133
|
+
const chainIdBuf = new Uint8Array(8);
|
|
134
|
+
let chainId = params.chainId;
|
|
135
|
+
for (let i = 0; i < 8; i++) {
|
|
136
|
+
chainIdBuf[i] = Number(chainId & 0xffn);
|
|
137
|
+
chainId >>= 8n;
|
|
138
|
+
}
|
|
139
|
+
buf.set(chainIdBuf, 37);
|
|
140
|
+
// stealthDataHash (32 bytes)
|
|
141
|
+
buf.set(params.stealthDataHash.slice(0, 32), 45);
|
|
142
|
+
// requester (32 bytes) — redeem only
|
|
143
|
+
if (isRedeem) {
|
|
144
|
+
if (!params.requester || params.requester.length !== 32) {
|
|
145
|
+
throw new Error("redeem bound params require a 32-byte requester pubkey");
|
|
146
|
+
}
|
|
147
|
+
buf.set(params.requester.slice(0, 32), 77);
|
|
148
|
+
}
|
|
149
|
+
// SHA256 → mod BN254
|
|
150
|
+
const hash = sha256(buf);
|
|
151
|
+
return bytesToBigint(hash) % BN254_FIELD_PRIME;
|
|
152
|
+
}
|
|
153
|
+
/** Canonical chain ids folded into bound-params hashes (must match on-chain). */
|
|
154
|
+
export const SOLANA_MAINNET_BOUND_CHAIN_ID = 101n;
|
|
155
|
+
export const SOLANA_DEVNET_BOUND_CHAIN_ID = 103n;
|
|
156
|
+
/** Backward-compatible devnet alias. Prefer the network-specific constants. */
|
|
157
|
+
export const SOLANA_BOUND_CHAIN_ID = SOLANA_DEVNET_BOUND_CHAIN_ID;
|
|
158
|
+
/**
|
|
159
|
+
* Default bound params for Solana devnet (private transfer)
|
|
160
|
+
*/
|
|
161
|
+
export function createTransferBoundParams(stealthDataHash, chainId = 103n, treeNumber = 0) {
|
|
162
|
+
return {
|
|
163
|
+
treeNumber,
|
|
164
|
+
unshieldAddress: null,
|
|
165
|
+
chainId,
|
|
166
|
+
stealthDataHash,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** @deprecated Use createTransferBoundParams instead */
|
|
170
|
+
export const DEFAULT_BOUND_PARAMS = {
|
|
171
|
+
treeNumber: 0,
|
|
172
|
+
unshieldAddress: null,
|
|
173
|
+
chainId: 103n,
|
|
174
|
+
stealthDataHash: new Uint8Array(32),
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Create bound params for a redeem (JoinSplit → BTC withdrawal, multi-output)
|
|
178
|
+
*
|
|
179
|
+
* The BTC scriptPubKeys are concatenated and SHA-256 hashed into the address field
|
|
180
|
+
* so the proof cryptographically binds ALL withdrawal destinations.
|
|
181
|
+
*
|
|
182
|
+
* For single output: SHA256(script_1) — no special case.
|
|
183
|
+
* For multi-output: SHA256(script_1 || script_2 || ...)
|
|
184
|
+
*
|
|
185
|
+
* `requester` is the 32-byte pubkey of the signer that will submit the redeem (becomes
|
|
186
|
+
* RedemptionRequest.requester); it is bound into the hash so the proof cannot be replayed
|
|
187
|
+
* under a different signer.
|
|
188
|
+
*/
|
|
189
|
+
function u32le(n) {
|
|
190
|
+
const b = new Uint8Array(4);
|
|
191
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, true);
|
|
192
|
+
return b;
|
|
193
|
+
}
|
|
194
|
+
export function createRedeemBoundParams(btcScripts, stealthDataHash, requester, chainId = 103n, treeNumber = 0) {
|
|
195
|
+
if (!requester || requester.length !== 32) {
|
|
196
|
+
throw new Error("createRedeemBoundParams requires a 32-byte requester pubkey");
|
|
197
|
+
}
|
|
198
|
+
// Normalize to array
|
|
199
|
+
const scripts = btcScripts instanceof Uint8Array ? [btcScripts] : btcScripts;
|
|
200
|
+
// Length-prefixed scripts hash (audit #4): sha256(u32le(count) || per-script
|
|
201
|
+
// [u32le(len) || bytes]). Binds the script boundaries so a redeem proof cannot be
|
|
202
|
+
// replayed with the scripts re-partitioned to the same concatenation. Must match the
|
|
203
|
+
// on-chain Solana `length_prefixed_hash` in compute_bound_params_hash_redeem.
|
|
204
|
+
const lpParts = [u32le(scripts.length)];
|
|
205
|
+
for (const s of scripts) {
|
|
206
|
+
lpParts.push(u32le(s.length), s);
|
|
207
|
+
}
|
|
208
|
+
const lpTotal = lpParts.reduce((sum, p) => sum + p.length, 0);
|
|
209
|
+
const lp = new Uint8Array(lpTotal);
|
|
210
|
+
let off = 0;
|
|
211
|
+
for (const p of lpParts) {
|
|
212
|
+
lp.set(p, off);
|
|
213
|
+
off += p.length;
|
|
214
|
+
}
|
|
215
|
+
const scriptHash = sha256(lp);
|
|
216
|
+
return {
|
|
217
|
+
treeNumber,
|
|
218
|
+
unshieldAddress: scriptHash,
|
|
219
|
+
chainId,
|
|
220
|
+
mode: 'redeem',
|
|
221
|
+
stealthDataHash,
|
|
222
|
+
requester,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create bound params for an unshield (public withdrawal, multi-output)
|
|
227
|
+
*
|
|
228
|
+
* For multi-output: destinations_hash = SHA256(owner_1 || owner_2 || ...)
|
|
229
|
+
* For single output: SHA256(owner_1) — no special case.
|
|
230
|
+
*/
|
|
231
|
+
export function createUnshieldBoundParams(recipientAddresses, stealthDataHash, chainId = 103n, treeNumber = 0) {
|
|
232
|
+
// Normalize to array
|
|
233
|
+
const addrs = recipientAddresses instanceof Uint8Array ? [recipientAddresses] : recipientAddresses;
|
|
234
|
+
// Concatenate all addresses
|
|
235
|
+
const totalLen = addrs.reduce((sum, a) => sum + a.length, 0);
|
|
236
|
+
const concat = new Uint8Array(totalLen);
|
|
237
|
+
let off = 0;
|
|
238
|
+
for (const a of addrs) {
|
|
239
|
+
concat.set(a, off);
|
|
240
|
+
off += a.length;
|
|
241
|
+
}
|
|
242
|
+
const addressHash = sha256(concat);
|
|
243
|
+
return {
|
|
244
|
+
treeNumber,
|
|
245
|
+
unshieldAddress: addressHash,
|
|
246
|
+
chainId,
|
|
247
|
+
stealthDataHash,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChadBuffer Client
|
|
3
|
+
*
|
|
4
|
+
* Helper functions to upload Bitcoin transaction data to ChadBuffer
|
|
5
|
+
* for SPV verification on Solana.
|
|
6
|
+
*
|
|
7
|
+
* Networks: Bitcoin Testnet4, Solana Devnet
|
|
8
|
+
*
|
|
9
|
+
* Reference: https://github.com/deanmlittle/chadbuffer
|
|
10
|
+
*/
|
|
11
|
+
import { type Address, type KeyPairSigner, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
|
|
12
|
+
export declare const CHADBUFFER_PROGRAM_ID: Address;
|
|
13
|
+
export declare const AUTHORITY_SIZE = 32;
|
|
14
|
+
export declare const SOLANA_TX_SIZE_LIMIT = 1232;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum data bytes per ChadBuffer Write transaction
|
|
17
|
+
* Dynamically calculated from TX limit minus overhead
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_DATA_PER_WRITE: number;
|
|
20
|
+
/**
|
|
21
|
+
* Upload raw Bitcoin transaction to ChadBuffer
|
|
22
|
+
*
|
|
23
|
+
* @param rpc - Solana RPC client
|
|
24
|
+
* @param rpcSubscriptions - Solana RPC subscriptions client
|
|
25
|
+
* @param payer - Transaction fee payer (KeyPairSigner)
|
|
26
|
+
* @param rawTx - Raw Bitcoin transaction bytes
|
|
27
|
+
* @param seed - Optional seed for buffer keypair derivation
|
|
28
|
+
* @returns Buffer address
|
|
29
|
+
*/
|
|
30
|
+
export declare function uploadTransactionToBuffer(rpc: Rpc<SolanaRpcApi>, rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>, payer: KeyPairSigner, rawTx: Uint8Array, seed?: Uint8Array): Promise<Address>;
|
|
31
|
+
/**
|
|
32
|
+
* Close buffer and reclaim rent
|
|
33
|
+
*/
|
|
34
|
+
export declare function closeBuffer(rpc: Rpc<SolanaRpcApi>, rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>, payer: KeyPairSigner, bufferAddress: Address, recipient?: Address): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Read buffer data
|
|
37
|
+
*/
|
|
38
|
+
export declare function readBufferData(rpc: Rpc<SolanaRpcApi>, bufferAddress: Address): Promise<{
|
|
39
|
+
authority: Address;
|
|
40
|
+
data: Uint8Array;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch raw Bitcoin transaction from Esplora/Blockstream API
|
|
44
|
+
*/
|
|
45
|
+
export declare function fetchRawTransaction(txid: string, network?: "mainnet" | "testnet" | "testnet4"): Promise<Uint8Array>;
|
|
46
|
+
/**
|
|
47
|
+
* Fetch merkle proof from Esplora/Blockstream API
|
|
48
|
+
*/
|
|
49
|
+
export declare function fetchMerkleProof(txid: string, network?: "mainnet" | "testnet" | "testnet4"): Promise<{
|
|
50
|
+
blockHeight: number;
|
|
51
|
+
merkleProof: Uint8Array[];
|
|
52
|
+
txIndex: number;
|
|
53
|
+
}>;
|
|
54
|
+
export { bytesToHex } from "./crypto";
|
|
55
|
+
/**
|
|
56
|
+
* Build merkle proof data for on-chain verification
|
|
57
|
+
*
|
|
58
|
+
* Layout:
|
|
59
|
+
* - txid: [u8; 32]
|
|
60
|
+
* - num_siblings: u8
|
|
61
|
+
* - siblings: [[u8; 32]; num_siblings]
|
|
62
|
+
* - tx_index: u32 (little-endian)
|
|
63
|
+
*
|
|
64
|
+
* @param txidBytes - 32-byte txid (already reversed for internal byte order)
|
|
65
|
+
* @param merkleProof - Array of 32-byte sibling hashes
|
|
66
|
+
* @param txIndex - Transaction index in block
|
|
67
|
+
* @returns Merkle proof data as Uint8Array
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildMerkleProof(txidBytes: Uint8Array, merkleProof: Uint8Array[], txIndex: number): Uint8Array;
|
|
70
|
+
/**
|
|
71
|
+
* Complete flow: Fetch tx, upload to buffer, return verification data
|
|
72
|
+
*/
|
|
73
|
+
export declare function prepareVerifyDeposit(rpc: Rpc<SolanaRpcApi>, rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>, payer: KeyPairSigner, txid: string, network?: "mainnet" | "testnet" | "testnet4"): Promise<{
|
|
74
|
+
bufferAddress: Address;
|
|
75
|
+
transactionSize: number;
|
|
76
|
+
merkleProof: Uint8Array[];
|
|
77
|
+
blockHeight: number;
|
|
78
|
+
txIndex: number;
|
|
79
|
+
txidBytes: Uint8Array;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Check if a proof needs buffer mode (too large for inline)
|
|
83
|
+
*
|
|
84
|
+
* @param proofBytes - Proof data
|
|
85
|
+
* @param availableSpace - Available space in transaction (default: use buffer for any proof > 900 bytes)
|
|
86
|
+
*/
|
|
87
|
+
export declare function needsBuffer(proofBytes: Uint8Array, availableSpace?: number): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Result of uploading a proof to buffer
|
|
90
|
+
*/
|
|
91
|
+
export interface ProofUploadResult {
|
|
92
|
+
/** Buffer account address */
|
|
93
|
+
bufferAddress: Address;
|
|
94
|
+
/** Whether buffer was used (false = inline) */
|
|
95
|
+
usedBuffer: boolean;
|
|
96
|
+
/** Number of chunks uploaded */
|
|
97
|
+
chunksUploaded: number;
|
|
98
|
+
/** Total proof size */
|
|
99
|
+
proofSize: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Upload proof to ChadBuffer if needed
|
|
103
|
+
*
|
|
104
|
+
* Automatically determines if buffer mode is needed based on proof size.
|
|
105
|
+
* Returns buffer address for buffer mode, or null for inline mode.
|
|
106
|
+
*
|
|
107
|
+
* @param rpc - Solana RPC client
|
|
108
|
+
* @param rpcSubscriptions - Solana RPC subscriptions client
|
|
109
|
+
* @param payer - Transaction fee payer
|
|
110
|
+
* @param proofBytes - Raw proof bytes
|
|
111
|
+
* @returns Upload result with buffer address (or null if inline)
|
|
112
|
+
*/
|
|
113
|
+
export declare function uploadProofToBuffer(rpc: Rpc<SolanaRpcApi>, rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>, payer: KeyPairSigner, proofBytes: Uint8Array): Promise<ProofUploadResult>;
|
|
114
|
+
/**
|
|
115
|
+
* Helper to determine proof source mode
|
|
116
|
+
*/
|
|
117
|
+
export declare function getProofSource(proofBytes: Uint8Array): "inline" | "buffer";
|
|
118
|
+
/**
|
|
119
|
+
* Calculate required number of transactions for a proof upload
|
|
120
|
+
*
|
|
121
|
+
* @param proofSize - Size of proof in bytes
|
|
122
|
+
* @param useBuffer - Force buffer mode (default: true for proofs > MAX_DATA_PER_WRITE)
|
|
123
|
+
*/
|
|
124
|
+
export declare function calculateUploadTransactions(proofSize: number, useBuffer?: boolean): number;
|