@frontiercompute/zcash-ika 0.4.0 → 0.5.0

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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Bitcoin P2PKH transaction builder.
3
+ *
4
+ * Builds legacy (non-segwit) P2PKH transactions for MPC-signed spends.
5
+ * The signing itself happens via Ika dWallet (secp256k1 ECDSA).
6
+ * This module handles UTXO fetch, TX structure, sighash computation,
7
+ * signature attachment, and broadcast via Blockstream API.
8
+ */
9
+ export type BtcNetwork = "mainnet" | "testnet";
10
+ export interface BtcUTXO {
11
+ txid: string;
12
+ vout: number;
13
+ scriptPubKey: string;
14
+ value: number;
15
+ }
16
+ export interface BtcTxOutput {
17
+ address: string;
18
+ value: number;
19
+ }
20
+ interface BtcInput {
21
+ prevTxid: Buffer;
22
+ prevIndex: number;
23
+ scriptPubKey: Buffer;
24
+ value: number;
25
+ sequence: number;
26
+ }
27
+ interface BtcOutput {
28
+ value: number;
29
+ script: Buffer;
30
+ }
31
+ /**
32
+ * Fetch UTXOs for a Bitcoin P2PKH address from Blockstream API.
33
+ */
34
+ export declare function fetchBtcUTXOs(address: string, network?: BtcNetwork): Promise<BtcUTXO[]>;
35
+ /**
36
+ * Estimate transaction size in bytes for P2PKH.
37
+ * ~148 bytes per input, ~34 bytes per output, 10 bytes overhead.
38
+ */
39
+ export declare function estimateBtcFee(numInputs: number, numOutputs: number, feeRate: number): number;
40
+ /**
41
+ * Select UTXOs to cover the target amount + estimated fee.
42
+ * Greedy largest-first selection. Re-estimates fee after selection.
43
+ */
44
+ export declare function selectBtcUTXOs(utxos: BtcUTXO[], targetAmount: number, feeRate: number): {
45
+ selected: BtcUTXO[];
46
+ fee: number;
47
+ totalInput: number;
48
+ };
49
+ /**
50
+ * Compute the legacy P2PKH sighash for a specific input.
51
+ *
52
+ * For each input being signed:
53
+ * 1. Copy the transaction
54
+ * 2. Set all input scriptSigs to empty
55
+ * 3. Set the current input's scriptSig to the previous output's scriptPubKey
56
+ * 4. Append SIGHASH_ALL (0x01000000) as 4 bytes LE
57
+ * 5. Double-SHA256 the result
58
+ */
59
+ export declare function computeBtcSighash(inputs: BtcInput[], outputs: BtcOutput[], inputIndex: number, hashType?: number): Buffer;
60
+ /**
61
+ * Build an unsigned Bitcoin P2PKH transaction.
62
+ *
63
+ * Returns the per-input sighashes that need to be signed via MPC,
64
+ * plus the internal tx structure needed for signature attachment.
65
+ */
66
+ export declare function buildUnsignedBtcTx(utxos: BtcUTXO[], txOutputs: BtcTxOutput[], changeAddress: string, fee: number): {
67
+ sighashes: Buffer[];
68
+ inputs: BtcInput[];
69
+ outputs: BtcOutput[];
70
+ };
71
+ /**
72
+ * Attach DER signatures to an unsigned transaction.
73
+ * Returns the fully serialized signed transaction as a Buffer.
74
+ */
75
+ export declare function attachBtcSignatures(inputs: BtcInput[], outputs: BtcOutput[], signatures: Buffer[], pubkey: Buffer): Buffer;
76
+ /**
77
+ * Serialize a Bitcoin transaction (version 1, no witness).
78
+ * If scriptSigs is provided, inputs get signed scriptSigs.
79
+ * Otherwise inputs get empty scriptSigs (unsigned).
80
+ */
81
+ export declare function serializeBtcTx(inputs: BtcInput[], outputs: BtcOutput[], scriptSigs?: Buffer[]): Buffer;
82
+ /**
83
+ * Broadcast a signed transaction via Blockstream API.
84
+ * Returns the txid on success.
85
+ */
86
+ export declare function broadcastBtcTx(rawHex: string, network?: BtcNetwork): Promise<string>;
87
+ export {};
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Bitcoin P2PKH transaction builder.
3
+ *
4
+ * Builds legacy (non-segwit) P2PKH transactions for MPC-signed spends.
5
+ * The signing itself happens via Ika dWallet (secp256k1 ECDSA).
6
+ * This module handles UTXO fetch, TX structure, sighash computation,
7
+ * signature attachment, and broadcast via Blockstream API.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ // SIGHASH flags
11
+ const SIGHASH_ALL = 0x01;
12
+ // Script opcodes for P2PKH
13
+ const OP_DUP = 0x76;
14
+ const OP_HASH160 = 0xa9;
15
+ const OP_EQUALVERIFY = 0x88;
16
+ const OP_CHECKSIG = 0xac;
17
+ // Bitcoin address version bytes
18
+ const BTC_ADDR_VERSION = {
19
+ mainnet: 0x00,
20
+ testnet: 0x6f,
21
+ };
22
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
23
+ function sha256(data) {
24
+ return createHash("sha256").update(data).digest();
25
+ }
26
+ function doubleSha256(data) {
27
+ return sha256(sha256(data));
28
+ }
29
+ function hash160(data) {
30
+ return createHash("ripemd160").update(sha256(data)).digest();
31
+ }
32
+ // Write uint32 little-endian
33
+ function writeU32LE(buf, value, offset) {
34
+ buf.writeUInt32LE(value >>> 0, offset);
35
+ }
36
+ // Write int64 little-endian (as two uint32s, safe for values < 2^53)
37
+ function writeI64LE(buf, value, offset) {
38
+ buf.writeUInt32LE(value & 0xffffffff, offset);
39
+ buf.writeUInt32LE(Math.floor(value / 0x100000000) & 0xffffffff, offset + 4);
40
+ }
41
+ // Compact size encoding (Bitcoin varint)
42
+ function compactSize(n) {
43
+ if (n < 0xfd) {
44
+ return Buffer.from([n]);
45
+ }
46
+ else if (n <= 0xffff) {
47
+ const buf = Buffer.alloc(3);
48
+ buf[0] = 0xfd;
49
+ buf.writeUInt16LE(n, 1);
50
+ return buf;
51
+ }
52
+ else {
53
+ const buf = Buffer.alloc(5);
54
+ buf[0] = 0xfe;
55
+ buf.writeUInt32LE(n, 1);
56
+ return buf;
57
+ }
58
+ }
59
+ // Reverse a hex-encoded txid (Bitcoin internal byte order is reversed)
60
+ function reverseTxid(txid) {
61
+ const buf = Buffer.from(txid, "hex");
62
+ if (buf.length !== 32)
63
+ throw new Error(`Invalid txid length: ${buf.length}`);
64
+ return Buffer.from(buf.reverse());
65
+ }
66
+ // Decode a Bitcoin base58check address to its 20-byte pubkey hash
67
+ function decodeBtcAddress(addr) {
68
+ let num = BigInt(0);
69
+ for (const c of addr) {
70
+ const idx = BASE58_ALPHABET.indexOf(c);
71
+ if (idx < 0)
72
+ throw new Error(`Invalid base58 character: ${c}`);
73
+ num = num * 58n + BigInt(idx);
74
+ }
75
+ // 25 bytes: 1 version + 20 hash + 4 checksum
76
+ const bytes = new Uint8Array(25);
77
+ for (let i = 24; i >= 0; i--) {
78
+ bytes[i] = Number(num & 0xffn);
79
+ num = num >> 8n;
80
+ }
81
+ // Verify checksum
82
+ const payload = bytes.subarray(0, 21);
83
+ const checksum = doubleSha256(payload).subarray(0, 4);
84
+ for (let i = 0; i < 4; i++) {
85
+ if (bytes[21 + i] !== checksum[i]) {
86
+ throw new Error(`Invalid address checksum: ${addr}`);
87
+ }
88
+ }
89
+ const version = bytes[0];
90
+ let network;
91
+ if (version === BTC_ADDR_VERSION.mainnet) {
92
+ network = "mainnet";
93
+ }
94
+ else if (version === BTC_ADDR_VERSION.testnet) {
95
+ network = "testnet";
96
+ }
97
+ else {
98
+ throw new Error(`Unknown address version byte: 0x${version.toString(16)}`);
99
+ }
100
+ return {
101
+ pubkeyHash: Buffer.from(bytes.subarray(1, 21)),
102
+ network,
103
+ };
104
+ }
105
+ // Build a P2PKH scriptPubKey from a 20-byte pubkey hash
106
+ function p2pkhScript(pubkeyHash) {
107
+ const script = Buffer.alloc(25);
108
+ script[0] = OP_DUP;
109
+ script[1] = OP_HASH160;
110
+ script[2] = 0x14; // push 20 bytes
111
+ pubkeyHash.copy(script, 3);
112
+ script[23] = OP_EQUALVERIFY;
113
+ script[24] = OP_CHECKSIG;
114
+ return script;
115
+ }
116
+ // Build P2PKH scriptPubKey from a Bitcoin address string
117
+ function scriptFromAddress(addr) {
118
+ const { pubkeyHash } = decodeBtcAddress(addr);
119
+ return p2pkhScript(pubkeyHash);
120
+ }
121
+ // Blockstream API base URL
122
+ function apiBase(network) {
123
+ return network === "mainnet"
124
+ ? "https://blockstream.info/api"
125
+ : "https://blockstream.info/testnet/api";
126
+ }
127
+ /**
128
+ * Fetch UTXOs for a Bitcoin P2PKH address from Blockstream API.
129
+ */
130
+ export async function fetchBtcUTXOs(address, network = "mainnet") {
131
+ const base = apiBase(network);
132
+ const resp = await fetch(`${base}/address/${address}/utxo`);
133
+ if (!resp.ok) {
134
+ throw new Error(`Blockstream API error: ${resp.status} ${resp.statusText}`);
135
+ }
136
+ const data = (await resp.json());
137
+ // Blockstream returns {txid, vout, status, value} but no scriptPubKey.
138
+ // For P2PKH we can derive scriptPubKey from the address.
139
+ const { pubkeyHash } = decodeBtcAddress(address);
140
+ const script = p2pkhScript(pubkeyHash).toString("hex");
141
+ return data.map((u) => ({
142
+ txid: u.txid,
143
+ vout: u.vout,
144
+ scriptPubKey: script,
145
+ value: u.value,
146
+ }));
147
+ }
148
+ /**
149
+ * Estimate transaction size in bytes for P2PKH.
150
+ * ~148 bytes per input, ~34 bytes per output, 10 bytes overhead.
151
+ */
152
+ export function estimateBtcFee(numInputs, numOutputs, feeRate // sat/vbyte
153
+ ) {
154
+ const size = 10 + numInputs * 148 + numOutputs * 34;
155
+ return Math.ceil(size * feeRate);
156
+ }
157
+ /**
158
+ * Select UTXOs to cover the target amount + estimated fee.
159
+ * Greedy largest-first selection. Re-estimates fee after selection.
160
+ */
161
+ export function selectBtcUTXOs(utxos, targetAmount, feeRate // sat/vbyte
162
+ ) {
163
+ const sorted = [...utxos].sort((a, b) => b.value - a.value);
164
+ const selected = [];
165
+ let total = 0;
166
+ // Initial estimate: 1 output + 1 change output
167
+ for (const u of sorted) {
168
+ selected.push(u);
169
+ total += u.value;
170
+ const fee = estimateBtcFee(selected.length, 2, feeRate);
171
+ if (total >= targetAmount + fee)
172
+ break;
173
+ }
174
+ const fee = estimateBtcFee(selected.length, 2, feeRate);
175
+ if (total < targetAmount + fee) {
176
+ throw new Error(`Insufficient funds: have ${total} sats, need ${targetAmount + fee} (${targetAmount} + ${fee} fee)`);
177
+ }
178
+ return { selected, fee, totalInput: total };
179
+ }
180
+ /**
181
+ * Compute the legacy P2PKH sighash for a specific input.
182
+ *
183
+ * For each input being signed:
184
+ * 1. Copy the transaction
185
+ * 2. Set all input scriptSigs to empty
186
+ * 3. Set the current input's scriptSig to the previous output's scriptPubKey
187
+ * 4. Append SIGHASH_ALL (0x01000000) as 4 bytes LE
188
+ * 5. Double-SHA256 the result
189
+ */
190
+ export function computeBtcSighash(inputs, outputs, inputIndex, hashType = SIGHASH_ALL) {
191
+ const parts = [];
192
+ // version
193
+ const ver = Buffer.alloc(4);
194
+ writeU32LE(ver, 1, 0);
195
+ parts.push(ver);
196
+ // input count
197
+ parts.push(compactSize(inputs.length));
198
+ // inputs
199
+ for (let i = 0; i < inputs.length; i++) {
200
+ const inp = inputs[i];
201
+ // prevout (txid + vout)
202
+ const outpoint = Buffer.alloc(36);
203
+ inp.prevTxid.copy(outpoint, 0);
204
+ writeU32LE(outpoint, inp.prevIndex, 32);
205
+ parts.push(outpoint);
206
+ // scriptSig: empty for all inputs except the one being signed
207
+ if (i === inputIndex) {
208
+ parts.push(compactSize(inp.scriptPubKey.length));
209
+ parts.push(inp.scriptPubKey);
210
+ }
211
+ else {
212
+ parts.push(compactSize(0));
213
+ }
214
+ // sequence
215
+ const seq = Buffer.alloc(4);
216
+ writeU32LE(seq, inp.sequence, 0);
217
+ parts.push(seq);
218
+ }
219
+ // output count
220
+ parts.push(compactSize(outputs.length));
221
+ // outputs
222
+ for (const out of outputs) {
223
+ const valueBuf = Buffer.alloc(8);
224
+ writeI64LE(valueBuf, out.value, 0);
225
+ parts.push(valueBuf);
226
+ parts.push(compactSize(out.script.length));
227
+ parts.push(out.script);
228
+ }
229
+ // locktime
230
+ const lt = Buffer.alloc(4);
231
+ writeU32LE(lt, 0, 0);
232
+ parts.push(lt);
233
+ // Append hash type as 4 bytes LE
234
+ const ht = Buffer.alloc(4);
235
+ writeU32LE(ht, hashType, 0);
236
+ parts.push(ht);
237
+ return doubleSha256(Buffer.concat(parts));
238
+ }
239
+ /**
240
+ * Build an unsigned Bitcoin P2PKH transaction.
241
+ *
242
+ * Returns the per-input sighashes that need to be signed via MPC,
243
+ * plus the internal tx structure needed for signature attachment.
244
+ */
245
+ export function buildUnsignedBtcTx(utxos, txOutputs, changeAddress, fee) {
246
+ if (utxos.length === 0)
247
+ throw new Error("No UTXOs provided");
248
+ // Build inputs
249
+ const inputs = utxos.map((u) => ({
250
+ prevTxid: reverseTxid(u.txid),
251
+ prevIndex: u.vout,
252
+ scriptPubKey: Buffer.from(u.scriptPubKey, "hex"),
253
+ value: u.value,
254
+ sequence: 0xffffffff,
255
+ }));
256
+ // Build outputs
257
+ const totalInput = utxos.reduce((s, u) => s + u.value, 0);
258
+ const totalOutput = txOutputs.reduce((s, o) => s + o.value, 0);
259
+ const change = totalInput - totalOutput - fee;
260
+ const outputs = txOutputs.map((o) => ({
261
+ value: o.value,
262
+ script: scriptFromAddress(o.address),
263
+ }));
264
+ if (change > 0) {
265
+ // Dust threshold: skip change if below 546 satoshis
266
+ if (change >= 546) {
267
+ outputs.push({ value: change, script: scriptFromAddress(changeAddress) });
268
+ }
269
+ }
270
+ else if (change < 0) {
271
+ throw new Error(`UTXOs total ${totalInput} < outputs ${totalOutput} + fee ${fee}`);
272
+ }
273
+ // Compute per-input sighashes
274
+ const sighashes = [];
275
+ for (let i = 0; i < inputs.length; i++) {
276
+ sighashes.push(computeBtcSighash(inputs, outputs, i, SIGHASH_ALL));
277
+ }
278
+ return { sighashes, inputs, outputs };
279
+ }
280
+ /**
281
+ * Build a P2PKH scriptSig from a DER signature and compressed pubkey.
282
+ *
283
+ * Format: <sig_length> <DER_sig + SIGHASH_ALL_byte> <pubkey_length> <compressed_pubkey>
284
+ */
285
+ function buildScriptSig(derSig, pubkey) {
286
+ const sigWithHashType = Buffer.concat([derSig, Buffer.from([SIGHASH_ALL])]);
287
+ const parts = [
288
+ Buffer.from([sigWithHashType.length]),
289
+ sigWithHashType,
290
+ Buffer.from([pubkey.length]),
291
+ pubkey,
292
+ ];
293
+ return Buffer.concat(parts);
294
+ }
295
+ /**
296
+ * Attach DER signatures to an unsigned transaction.
297
+ * Returns the fully serialized signed transaction as a Buffer.
298
+ */
299
+ export function attachBtcSignatures(inputs, outputs, signatures, pubkey) {
300
+ if (signatures.length !== inputs.length) {
301
+ throw new Error(`Expected ${inputs.length} signatures, got ${signatures.length}`);
302
+ }
303
+ if (pubkey.length !== 33) {
304
+ throw new Error(`Expected 33-byte compressed pubkey, got ${pubkey.length}`);
305
+ }
306
+ const scriptSigs = signatures.map((sig) => buildScriptSig(sig, pubkey));
307
+ return serializeBtcTx(inputs, outputs, scriptSigs);
308
+ }
309
+ /**
310
+ * Serialize a Bitcoin transaction (version 1, no witness).
311
+ * If scriptSigs is provided, inputs get signed scriptSigs.
312
+ * Otherwise inputs get empty scriptSigs (unsigned).
313
+ */
314
+ export function serializeBtcTx(inputs, outputs, scriptSigs) {
315
+ const parts = [];
316
+ // version: 4 bytes LE
317
+ const ver = Buffer.alloc(4);
318
+ writeU32LE(ver, 1, 0);
319
+ parts.push(ver);
320
+ // input count
321
+ parts.push(compactSize(inputs.length));
322
+ // inputs
323
+ for (let i = 0; i < inputs.length; i++) {
324
+ const inp = inputs[i];
325
+ // prevout
326
+ const outpoint = Buffer.alloc(36);
327
+ inp.prevTxid.copy(outpoint, 0);
328
+ writeU32LE(outpoint, inp.prevIndex, 32);
329
+ parts.push(outpoint);
330
+ // scriptSig
331
+ const sig = scriptSigs ? scriptSigs[i] : Buffer.alloc(0);
332
+ parts.push(compactSize(sig.length));
333
+ if (sig.length > 0)
334
+ parts.push(sig);
335
+ // sequence
336
+ const seq = Buffer.alloc(4);
337
+ writeU32LE(seq, inp.sequence, 0);
338
+ parts.push(seq);
339
+ }
340
+ // output count
341
+ parts.push(compactSize(outputs.length));
342
+ // outputs
343
+ for (const out of outputs) {
344
+ const valueBuf = Buffer.alloc(8);
345
+ writeI64LE(valueBuf, out.value, 0);
346
+ parts.push(valueBuf);
347
+ parts.push(compactSize(out.script.length));
348
+ parts.push(out.script);
349
+ }
350
+ // locktime: 4 bytes LE
351
+ const lt = Buffer.alloc(4);
352
+ writeU32LE(lt, 0, 0);
353
+ parts.push(lt);
354
+ return Buffer.concat(parts);
355
+ }
356
+ /**
357
+ * Broadcast a signed transaction via Blockstream API.
358
+ * Returns the txid on success.
359
+ */
360
+ export async function broadcastBtcTx(rawHex, network = "mainnet") {
361
+ const base = apiBase(network);
362
+ const resp = await fetch(`${base}/tx`, {
363
+ method: "POST",
364
+ headers: { "Content-Type": "text/plain" },
365
+ body: rawHex,
366
+ });
367
+ if (!resp.ok) {
368
+ const body = await resp.text();
369
+ throw new Error(`Broadcast failed: ${resp.status} ${body}`);
370
+ }
371
+ // Blockstream returns the txid as plain text
372
+ return (await resp.text()).trim();
373
+ }
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@
16
16
  export { Curve, Hash, SignatureAlgorithm, IkaClient, IkaTransaction, UserShareEncryptionKeys, getNetworkConfig, createClassGroupsKeypair, createRandomSessionIdentifier, prepareDKG, prepareDKGAsync, prepareDKGSecondRound, prepareDKGSecondRoundAsync, createDKGUserOutput, publicKeyFromDWalletOutput, parseSignatureFromSignOutput, } from "@ika.xyz/sdk";
17
17
  export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, } from "./tx-builder.js";
18
18
  export type { UTXO } from "./tx-builder.js";
19
+ export { fetchBtcUTXOs, selectBtcUTXOs, buildUnsignedBtcTx, attachBtcSignatures, serializeBtcTx, broadcastBtcTx, estimateBtcFee, computeBtcSighash, } from "./btc-tx-builder.js";
20
+ export type { BtcUTXO, BtcTxOutput, BtcNetwork } from "./btc-tx-builder.js";
19
21
  export type Chain = "zcash-transparent" | "bitcoin" | "ethereum";
20
22
  export interface ZcashIkaConfig {
21
23
  /** Ika network: mainnet or testnet */
@@ -228,7 +230,14 @@ export declare function checkPolicy(config: ZcashIkaConfig, policyId: string, am
228
230
  export declare function spendTransparent(config: ZcashIkaConfig, walletId: string, encryptionSeed: string, request: SpendRequest): Promise<SpendResult>;
229
231
  /**
230
232
  * Spend from a Bitcoin wallet.
231
- * Same MPC flow as Zcash transparent - DoubleSHA256 sighash, ECDSA signature.
233
+ *
234
+ * Full pipeline:
235
+ * 1. Fetch UTXOs from Blockstream API
236
+ * 2. Build unsigned TX, compute legacy P2PKH sighashes
237
+ * 3. Sign each sighash via Ika 2PC-MPC
238
+ * 4. Attach signatures, serialize signed TX
239
+ * 5. Broadcast via Blockstream API
240
+ * 6. Attest to ZAP1 as AGENT_ACTION
232
241
  */
233
242
  export declare function spendBitcoin(config: ZcashIkaConfig, walletId: string, encryptionSeed: string, request: SpendRequest): Promise<SpendResult>;
234
243
  /**
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
21
21
  import { createHash } from "node:crypto";
22
22
  import { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, } from "./tx-builder.js";
23
23
  export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, } from "./tx-builder.js";
24
+ import { fetchBtcUTXOs, selectBtcUTXOs, buildUnsignedBtcTx, attachBtcSignatures, broadcastBtcTx, } from "./btc-tx-builder.js";
25
+ export { fetchBtcUTXOs, selectBtcUTXOs, buildUnsignedBtcTx, attachBtcSignatures, serializeBtcTx, broadcastBtcTx, estimateBtcFee, computeBtcSighash, } from "./btc-tx-builder.js";
24
26
  const IKA_COIN_TYPE = "0x1f26bb2f711ff82dcda4d02c77d5123089cb7f8418751474b9fb744ce031526a::ika::IKA";
25
27
  /** Parameters for dWallet creation per chain.
26
28
  *
@@ -751,11 +753,93 @@ export async function spendTransparent(config, walletId, encryptionSeed, request
751
753
  }
752
754
  /**
753
755
  * Spend from a Bitcoin wallet.
754
- * Same MPC flow as Zcash transparent - DoubleSHA256 sighash, ECDSA signature.
756
+ *
757
+ * Full pipeline:
758
+ * 1. Fetch UTXOs from Blockstream API
759
+ * 2. Build unsigned TX, compute legacy P2PKH sighashes
760
+ * 3. Sign each sighash via Ika 2PC-MPC
761
+ * 4. Attach signatures, serialize signed TX
762
+ * 5. Broadcast via Blockstream API
763
+ * 6. Attest to ZAP1 as AGENT_ACTION
755
764
  */
756
765
  export async function spendBitcoin(config, walletId, encryptionSeed, request) {
757
- throw new Error("spendBitcoin requires Bitcoin tx builder. " +
758
- "Use sign() with chain='bitcoin' and a pre-computed sighash for now.");
766
+ // Fetch the dWallet to get the public key
767
+ const { ikaClient } = await initClients(config);
768
+ const dWallet = await ikaClient.getDWallet(walletId);
769
+ if (!dWallet?.state?.Active) {
770
+ throw new Error(`dWallet ${walletId} not Active`);
771
+ }
772
+ const rawOutput = dWallet.state.Active.public_output;
773
+ const outputBytes = new Uint8Array(Array.isArray(rawOutput) ? rawOutput : Array.from(rawOutput));
774
+ const pubkey = await publicKeyFromDWalletOutput(Curve.SECP256K1, outputBytes);
775
+ if (!pubkey || pubkey.length !== 33) {
776
+ throw new Error("Could not extract 33-byte compressed pubkey from dWallet");
777
+ }
778
+ const btcNetwork = config.network === "mainnet" ? "mainnet" : "testnet";
779
+ // Derive our BTC address from the pubkey
780
+ const ourAddress = deriveBitcoinAddress(pubkey, btcNetwork);
781
+ // Step 1: Fetch UTXOs
782
+ const allUtxos = await fetchBtcUTXOs(ourAddress, btcNetwork);
783
+ if (allUtxos.length === 0) {
784
+ throw new Error(`No UTXOs found for ${ourAddress}`);
785
+ }
786
+ // Step 2: Select UTXOs and build unsigned TX
787
+ const feeRate = 10; // sat/vbyte, conservative default
788
+ const { selected, fee } = selectBtcUTXOs(allUtxos, request.amount, feeRate);
789
+ const { sighashes, inputs, outputs } = buildUnsignedBtcTx(selected, [{ address: request.to, value: request.amount }], ourAddress, // change back to our address
790
+ fee);
791
+ // Step 3: Sign each sighash via MPC
792
+ const signatures = [];
793
+ for (const sighash of sighashes) {
794
+ const signResult = await sign(config, {
795
+ messageHash: new Uint8Array(sighash),
796
+ walletId,
797
+ chain: "bitcoin",
798
+ encryptionSeed,
799
+ });
800
+ signatures.push(Buffer.from(signResult.signature));
801
+ }
802
+ // Step 4: Attach signatures and serialize
803
+ const signedTx = attachBtcSignatures(inputs, outputs, signatures, Buffer.from(pubkey));
804
+ const txHex = signedTx.toString("hex");
805
+ // Step 5: Broadcast
806
+ const broadcastTxid = await broadcastBtcTx(txHex, btcNetwork);
807
+ // Step 6: Attest to ZAP1
808
+ let leafHash = "";
809
+ if (config.zap1ApiUrl && config.zap1ApiKey) {
810
+ try {
811
+ const attestResp = await fetch(`${config.zap1ApiUrl}/attest`, {
812
+ method: "POST",
813
+ headers: {
814
+ "Content-Type": "application/json",
815
+ "Authorization": `Bearer ${config.zap1ApiKey}`,
816
+ },
817
+ body: JSON.stringify({
818
+ event_type: "AGENT_ACTION",
819
+ agent_id: walletId,
820
+ action: "bitcoin_spend",
821
+ chain_txid: broadcastTxid,
822
+ recipient: request.to,
823
+ amount: request.amount,
824
+ fee,
825
+ memo: request.memo || "",
826
+ }),
827
+ });
828
+ if (attestResp.ok) {
829
+ const attestData = (await attestResp.json());
830
+ leafHash = attestData.leaf_hash || "";
831
+ }
832
+ }
833
+ catch {
834
+ // Attestation failure is non-fatal - tx already broadcast
835
+ }
836
+ }
837
+ return {
838
+ txid: broadcastTxid,
839
+ leafHash,
840
+ chain: "bitcoin",
841
+ policyChecked: false,
842
+ };
759
843
  }
760
844
  /**
761
845
  * Verify the wallet's attestation history via ZAP1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontiercompute/zcash-ika",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Split-key custody for Zcash, Bitcoin, and EVM. 2PC-MPC signing, on-chain spend policy, transparent TX builder, ZAP1 attestation.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,9 @@
25
25
  "dist/tx-builder.js",
26
26
  "dist/tx-builder.d.ts",
27
27
  "dist/hybrid.js",
28
- "dist/hybrid.d.ts"
28
+ "dist/hybrid.d.ts",
29
+ "dist/btc-tx-builder.js",
30
+ "dist/btc-tx-builder.d.ts"
29
31
  ],
30
32
  "repository": {
31
33
  "type": "git",