@frontiercompute/zcash-ika 0.3.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.
- package/dist/btc-tx-builder.d.ts +87 -0
- package/dist/btc-tx-builder.js +373 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +129 -3
- package/dist/tx-builder.d.ts +1 -0
- package/dist/tx-builder.js +60 -57
- package/package.json +6 -3
|
@@ -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 */
|
|
@@ -71,6 +73,19 @@ export declare const CHAIN_PARAMS: {
|
|
|
71
73
|
* 4. Base58 encode (version + hash + checksum)
|
|
72
74
|
*/
|
|
73
75
|
export declare function deriveZcashAddress(publicKey: Uint8Array, network?: "mainnet" | "testnet"): string;
|
|
76
|
+
/**
|
|
77
|
+
* Derive a Bitcoin P2PKH address from a compressed secp256k1 public key.
|
|
78
|
+
*
|
|
79
|
+
* Same as Zcash transparent but with a 1-byte version prefix:
|
|
80
|
+
* mainnet 0x00 (1...), testnet 0x6f (m.../n...)
|
|
81
|
+
*
|
|
82
|
+
* Steps:
|
|
83
|
+
* 1. SHA256(pubkey) then RIPEMD160 = 20-byte hash
|
|
84
|
+
* 2. Prepend 1-byte version
|
|
85
|
+
* 3. Double-SHA256 checksum (first 4 bytes)
|
|
86
|
+
* 4. Base58 encode (version + hash + checksum)
|
|
87
|
+
*/
|
|
88
|
+
export declare function deriveBitcoinAddress(publicKey: Uint8Array, network?: "mainnet" | "testnet"): string;
|
|
74
89
|
export interface DWalletHandle {
|
|
75
90
|
/** dWallet object ID on Sui */
|
|
76
91
|
id: string;
|
|
@@ -215,7 +230,14 @@ export declare function checkPolicy(config: ZcashIkaConfig, policyId: string, am
|
|
|
215
230
|
export declare function spendTransparent(config: ZcashIkaConfig, walletId: string, encryptionSeed: string, request: SpendRequest): Promise<SpendResult>;
|
|
216
231
|
/**
|
|
217
232
|
* Spend from a Bitcoin wallet.
|
|
218
|
-
*
|
|
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
|
|
219
241
|
*/
|
|
220
242
|
export declare function spendBitcoin(config: ZcashIkaConfig, walletId: string, encryptionSeed: string, request: SpendRequest): Promise<SpendResult>;
|
|
221
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
|
*
|
|
@@ -118,6 +120,45 @@ export function deriveZcashAddress(publicKey, network = "mainnet") {
|
|
|
118
120
|
full.set(checksum, 22);
|
|
119
121
|
return base58Encode(full);
|
|
120
122
|
}
|
|
123
|
+
// Bitcoin P2PKH version bytes (1 byte each)
|
|
124
|
+
const BITCOIN_VERSION_BYTE = {
|
|
125
|
+
mainnet: 0x00, // 1...
|
|
126
|
+
testnet: 0x6f, // m... or n...
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Derive a Bitcoin P2PKH address from a compressed secp256k1 public key.
|
|
130
|
+
*
|
|
131
|
+
* Same as Zcash transparent but with a 1-byte version prefix:
|
|
132
|
+
* mainnet 0x00 (1...), testnet 0x6f (m.../n...)
|
|
133
|
+
*
|
|
134
|
+
* Steps:
|
|
135
|
+
* 1. SHA256(pubkey) then RIPEMD160 = 20-byte hash
|
|
136
|
+
* 2. Prepend 1-byte version
|
|
137
|
+
* 3. Double-SHA256 checksum (first 4 bytes)
|
|
138
|
+
* 4. Base58 encode (version + hash + checksum)
|
|
139
|
+
*/
|
|
140
|
+
export function deriveBitcoinAddress(publicKey, network = "mainnet") {
|
|
141
|
+
if (publicKey.length !== 33) {
|
|
142
|
+
throw new Error(`Expected 33-byte compressed secp256k1 pubkey, got ${publicKey.length} bytes`);
|
|
143
|
+
}
|
|
144
|
+
const prefix = publicKey[0];
|
|
145
|
+
if (prefix !== 0x02 && prefix !== 0x03) {
|
|
146
|
+
throw new Error(`Invalid compressed pubkey prefix 0x${prefix.toString(16)}, expected 0x02 or 0x03`);
|
|
147
|
+
}
|
|
148
|
+
const pubkeyHash = hash160(publicKey); // 20 bytes
|
|
149
|
+
const version = BITCOIN_VERSION_BYTE[network];
|
|
150
|
+
// version (1) + hash160 (20) = 21 bytes
|
|
151
|
+
const payload = new Uint8Array(21);
|
|
152
|
+
payload[0] = version;
|
|
153
|
+
payload.set(pubkeyHash, 1);
|
|
154
|
+
// checksum: first 4 bytes of SHA256(SHA256(payload))
|
|
155
|
+
const checksum = sha256(sha256(payload)).subarray(0, 4);
|
|
156
|
+
// final: payload (21) + checksum (4) = 25 bytes
|
|
157
|
+
const full = new Uint8Array(25);
|
|
158
|
+
full.set(payload, 0);
|
|
159
|
+
full.set(checksum, 21);
|
|
160
|
+
return base58Encode(full);
|
|
161
|
+
}
|
|
121
162
|
// Default poll settings for testnet (epochs can be slow)
|
|
122
163
|
const POLL_OPTS = {
|
|
123
164
|
timeout: 300_000,
|
|
@@ -276,6 +317,9 @@ export async function createWallet(config, chain, _operatorSeed) {
|
|
|
276
317
|
if (pubkey && pubkey.length === 33 && chain === "zcash-transparent") {
|
|
277
318
|
derivedAddress = deriveZcashAddress(pubkey, config.network);
|
|
278
319
|
}
|
|
320
|
+
else if (pubkey && pubkey.length === 33 && chain === "bitcoin") {
|
|
321
|
+
derivedAddress = deriveBitcoinAddress(pubkey, config.network);
|
|
322
|
+
}
|
|
279
323
|
return {
|
|
280
324
|
id: dwalletId,
|
|
281
325
|
publicKey: pubkey || new Uint8Array(0),
|
|
@@ -709,11 +753,93 @@ export async function spendTransparent(config, walletId, encryptionSeed, request
|
|
|
709
753
|
}
|
|
710
754
|
/**
|
|
711
755
|
* Spend from a Bitcoin wallet.
|
|
712
|
-
*
|
|
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
|
|
713
764
|
*/
|
|
714
765
|
export async function spendBitcoin(config, walletId, encryptionSeed, request) {
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
};
|
|
717
843
|
}
|
|
718
844
|
/**
|
|
719
845
|
* Verify the wallet's attestation history via ZAP1.
|
package/dist/tx-builder.d.ts
CHANGED
package/dist/tx-builder.js
CHANGED
|
@@ -16,6 +16,7 @@ const TX_VERSION_GROUP_ID = 0x26a7270a;
|
|
|
16
16
|
export const BRANCH_ID = {
|
|
17
17
|
NU5: 0xc2d6d0b4,
|
|
18
18
|
NU6: 0xc8e71055,
|
|
19
|
+
NU61: 0x4dec4df0,
|
|
19
20
|
};
|
|
20
21
|
// SIGHASH flags
|
|
21
22
|
const SIGHASH_ALL = 0x01;
|
|
@@ -159,7 +160,7 @@ function hashPrevouts(inputs, branchId) {
|
|
|
159
160
|
parts.push(outpoint);
|
|
160
161
|
}
|
|
161
162
|
const data = Buffer.concat(parts);
|
|
162
|
-
return blake2b256(data, personalization("ZTxIdPrevoutHash"
|
|
163
|
+
return blake2b256(data, personalization("ZTxIdPrevoutHash"));
|
|
163
164
|
}
|
|
164
165
|
// Hash of all input amounts
|
|
165
166
|
function hashAmounts(inputs, branchId) {
|
|
@@ -167,7 +168,7 @@ function hashAmounts(inputs, branchId) {
|
|
|
167
168
|
for (let i = 0; i < inputs.length; i++) {
|
|
168
169
|
writeI64LE(data, inputs[i].value, i * 8);
|
|
169
170
|
}
|
|
170
|
-
return blake2b256(data, personalization("ZTxTrAmountsHash"
|
|
171
|
+
return blake2b256(data, personalization("ZTxTrAmountsHash"));
|
|
171
172
|
}
|
|
172
173
|
// Hash of all input scriptPubKeys
|
|
173
174
|
function hashScriptPubKeys(inputs, branchId) {
|
|
@@ -177,7 +178,7 @@ function hashScriptPubKeys(inputs, branchId) {
|
|
|
177
178
|
parts.push(inp.script);
|
|
178
179
|
}
|
|
179
180
|
const data = Buffer.concat(parts);
|
|
180
|
-
return blake2b256(data, personalization("ZTxTrScriptsHash"
|
|
181
|
+
return blake2b256(data, personalization("ZTxTrScriptsHash"));
|
|
181
182
|
}
|
|
182
183
|
// Hash of all sequences
|
|
183
184
|
function hashSequences(inputs, branchId) {
|
|
@@ -185,7 +186,7 @@ function hashSequences(inputs, branchId) {
|
|
|
185
186
|
for (let i = 0; i < inputs.length; i++) {
|
|
186
187
|
writeU32LE(data, inputs[i].sequence, i * 4);
|
|
187
188
|
}
|
|
188
|
-
return blake2b256(data, personalization("ZTxIdSequencHash"
|
|
189
|
+
return blake2b256(data, personalization("ZTxIdSequencHash"));
|
|
189
190
|
}
|
|
190
191
|
// Hash of all transparent outputs
|
|
191
192
|
function hashOutputs(outputs, branchId) {
|
|
@@ -198,70 +199,66 @@ function hashOutputs(outputs, branchId) {
|
|
|
198
199
|
parts.push(out.script);
|
|
199
200
|
}
|
|
200
201
|
const data = Buffer.concat(parts);
|
|
201
|
-
return blake2b256(data, personalization("ZTxIdOutputsHash"
|
|
202
|
+
return blake2b256(data, personalization("ZTxIdOutputsHash"));
|
|
202
203
|
}
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
const prevoutsHash = hashPrevouts(inputs, branchId);
|
|
206
|
-
const amountsHash = hashAmounts(inputs, branchId);
|
|
207
|
-
const scriptPubKeysHash = hashScriptPubKeys(inputs, branchId);
|
|
208
|
-
const sequencesHash = hashSequences(inputs, branchId);
|
|
209
|
-
const data = Buffer.concat([prevoutsHash, amountsHash, scriptPubKeysHash, sequencesHash]);
|
|
210
|
-
return blake2b256(data, personalization("ZTxIdTrInHash__", branchIdBytes(branchId)));
|
|
211
|
-
}
|
|
212
|
-
// Transparent outputs digest (ZIP 244 section T.3b)
|
|
213
|
-
function transparentOutputsDigest(outputs, branchId) {
|
|
214
|
-
const outputsHash = hashOutputs(outputs, branchId);
|
|
215
|
-
return blake2b256(outputsHash, personalization("ZTxIdTrOutHash_", branchIdBytes(branchId)));
|
|
216
|
-
}
|
|
217
|
-
// Full transparent digest for txid (ZIP 244 T.3)
|
|
204
|
+
// Full transparent digest for txid (ZIP 244 T.2)
|
|
205
|
+
// transparent_digest = BLAKE2b("ZTxIdTranspaHash", prevouts || sequences || outputs)
|
|
218
206
|
function transparentDigest(inputs, outputs, branchId) {
|
|
219
207
|
if (inputs.length === 0 && outputs.length === 0) {
|
|
220
|
-
return blake2b256(Buffer.alloc(0), personalization("ZTxIdTranspaHash"
|
|
208
|
+
return blake2b256(Buffer.alloc(0), personalization("ZTxIdTranspaHash"));
|
|
221
209
|
}
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
210
|
+
const prevoutsDigest = hashPrevouts(inputs, branchId);
|
|
211
|
+
const sequenceDigest = hashSequences(inputs, branchId);
|
|
212
|
+
const outputsDigest = hashOutputs(outputs, branchId);
|
|
213
|
+
return blake2b256(Buffer.concat([prevoutsDigest, sequenceDigest, outputsDigest]), personalization("ZTxIdTranspaHash"));
|
|
225
214
|
}
|
|
226
215
|
// Sapling digest (empty bundle)
|
|
227
|
-
function emptyBundleDigest(tag
|
|
228
|
-
return blake2b256(Buffer.alloc(0), personalization(tag
|
|
216
|
+
function emptyBundleDigest(tag) {
|
|
217
|
+
return blake2b256(Buffer.alloc(0), personalization(tag));
|
|
229
218
|
}
|
|
230
219
|
// Header digest (ZIP 244 T.1)
|
|
231
220
|
function headerDigest(version, versionGroupId, branchId, lockTime, expiryHeight) {
|
|
232
221
|
const data = Buffer.alloc(4 + 4 + 4 + 4 + 4);
|
|
233
|
-
writeU32LE(data, version, 0);
|
|
222
|
+
writeU32LE(data, (version | (1 << 31)) >>> 0, 0);
|
|
234
223
|
writeU32LE(data, versionGroupId, 4);
|
|
235
224
|
writeU32LE(data, branchId, 8);
|
|
236
225
|
writeU32LE(data, lockTime, 12);
|
|
237
226
|
writeU32LE(data, expiryHeight, 16);
|
|
238
|
-
return blake2b256(data, personalization("ZTxIdHeadersHash"
|
|
227
|
+
return blake2b256(data, personalization("ZTxIdHeadersHash"));
|
|
239
228
|
}
|
|
240
229
|
// Transaction digest for txid (ZIP 244 T)
|
|
241
230
|
function txidDigest(inputs, outputs, branchId, lockTime, expiryHeight) {
|
|
242
231
|
const hdrDigest = headerDigest(TX_VERSION, TX_VERSION_GROUP_ID, branchId, lockTime, expiryHeight);
|
|
243
232
|
const txpDigest = transparentDigest(inputs, outputs, branchId);
|
|
244
|
-
const sapDigest = emptyBundleDigest("ZTxIdSaplingHash"
|
|
245
|
-
const orchDigest = emptyBundleDigest("ZTxIdOrchardHash"
|
|
233
|
+
const sapDigest = emptyBundleDigest("ZTxIdSaplingHash");
|
|
234
|
+
const orchDigest = emptyBundleDigest("ZTxIdOrchardHash");
|
|
246
235
|
return blake2b256(Buffer.concat([hdrDigest, txpDigest, sapDigest, orchDigest]), personalization("ZcashTxHash__", branchIdBytes(branchId)));
|
|
247
236
|
}
|
|
248
|
-
// Per-input sighash for signing (ZIP 244
|
|
249
|
-
//
|
|
237
|
+
// Per-input sighash for signing (ZIP 244 signature_digest)
|
|
238
|
+
// Structure: BLAKE2b("ZcashTxHash_" || BRANCH_ID,
|
|
239
|
+
// S.1: header_digest
|
|
240
|
+
// S.2: transparent_sig_digest (NOT the txid transparent digest)
|
|
241
|
+
// S.3: sapling_digest
|
|
242
|
+
// S.4: orchard_digest
|
|
243
|
+
// )
|
|
250
244
|
function transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, inputIndex, hashType) {
|
|
251
|
-
//
|
|
245
|
+
// S.1: header digest (same as T.1)
|
|
252
246
|
const hdrDigest = headerDigest(TX_VERSION, TX_VERSION_GROUP_ID, branchId, lockTime, expiryHeight);
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
// S.
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
247
|
+
// S.2: transparent_sig_digest
|
|
248
|
+
// For SIGHASH_ALL without ANYONECANPAY:
|
|
249
|
+
// S.2a: hash_type (1 byte)
|
|
250
|
+
// S.2b: prevouts_sig_digest = prevouts_digest (same as T.2a)
|
|
251
|
+
// S.2c: amounts_sig_digest
|
|
252
|
+
// S.2d: scriptpubkeys_sig_digest
|
|
253
|
+
// S.2e: sequence_sig_digest = sequence_digest (same as T.2b)
|
|
254
|
+
// S.2f: outputs_sig_digest = outputs_digest (same as T.2c)
|
|
255
|
+
// S.2g: txin_sig_digest (per-input)
|
|
256
|
+
const prevoutsSigDigest = hashPrevouts(inputs, branchId);
|
|
257
|
+
const amountsSigDigest = hashAmounts(inputs, branchId);
|
|
258
|
+
const scriptpubkeysSigDigest = hashScriptPubKeys(inputs, branchId);
|
|
259
|
+
const sequenceSigDigest = hashSequences(inputs, branchId);
|
|
260
|
+
const outputsSigDigest = hashOutputs(outputs, branchId);
|
|
261
|
+
// S.2g: txin_sig_digest for the input being signed
|
|
265
262
|
const inp = inputs[inputIndex];
|
|
266
263
|
const prevout = Buffer.alloc(36);
|
|
267
264
|
inp.prevTxid.copy(prevout, 0);
|
|
@@ -270,17 +267,23 @@ function transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, i
|
|
|
270
267
|
writeI64LE(valueBuf, inp.value, 0);
|
|
271
268
|
const seqBuf = Buffer.alloc(4);
|
|
272
269
|
writeU32LE(seqBuf, inp.sequence, 0);
|
|
273
|
-
const
|
|
270
|
+
const txinSigDigest = blake2b256(Buffer.concat([prevout, valueBuf, compactSize(inp.script.length), inp.script, seqBuf]), personalization("Zcash___TxInHash"));
|
|
271
|
+
// S.2: transparent_sig_digest
|
|
272
|
+
const transparentSigDigest = blake2b256(Buffer.concat([
|
|
274
273
|
Buffer.from([hashType]),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
|
|
274
|
+
prevoutsSigDigest,
|
|
275
|
+
amountsSigDigest,
|
|
276
|
+
scriptpubkeysSigDigest,
|
|
277
|
+
sequenceSigDigest,
|
|
278
|
+
outputsSigDigest,
|
|
279
|
+
txinSigDigest,
|
|
280
|
+
]), personalization("ZTxIdTranspaHash"));
|
|
281
|
+
// S.3: sapling digest (empty)
|
|
282
|
+
const sapDigest = emptyBundleDigest("ZTxIdSaplingHash");
|
|
283
|
+
// S.4: orchard digest (empty)
|
|
284
|
+
const orchDigest = emptyBundleDigest("ZTxIdOrchardHash");
|
|
285
|
+
// Final signature_digest
|
|
286
|
+
return blake2b256(Buffer.concat([hdrDigest, transparentSigDigest, sapDigest, orchDigest]), personalization("ZcashTxHash_", branchIdBytes(branchId)));
|
|
284
287
|
}
|
|
285
288
|
// Serialize a v5 transparent-only transaction to raw bytes.
|
|
286
289
|
// If scriptSigs is provided, inputs get signed scriptSigs.
|
|
@@ -402,7 +405,7 @@ export function selectUTXOs(utxos, targetAmount, fee) {
|
|
|
402
405
|
* Returns the unsigned serialized TX and per-input sighashes
|
|
403
406
|
* that need to be signed via MPC.
|
|
404
407
|
*/
|
|
405
|
-
export function buildUnsignedTx(utxos, recipient, amount, fee = 10000, changeAddress, branchId = BRANCH_ID.
|
|
408
|
+
export function buildUnsignedTx(utxos, recipient, amount, fee = 10000, changeAddress, branchId = BRANCH_ID.NU61) {
|
|
406
409
|
if (utxos.length === 0)
|
|
407
410
|
throw new Error("No UTXOs provided");
|
|
408
411
|
if (amount <= 0)
|
|
@@ -467,7 +470,7 @@ function buildScriptSig(derSig, pubkey) {
|
|
|
467
470
|
* DER-encoded signatures from MPC, and the compressed pubkey.
|
|
468
471
|
* Returns hex-encoded signed transaction ready for broadcast.
|
|
469
472
|
*/
|
|
470
|
-
export function attachSignatures(utxos, recipient, amount, fee, changeAddress, signatures, pubkey, branchId = BRANCH_ID.
|
|
473
|
+
export function attachSignatures(utxos, recipient, amount, fee, changeAddress, signatures, pubkey, branchId = BRANCH_ID.NU61) {
|
|
471
474
|
if (signatures.length !== utxos.length) {
|
|
472
475
|
throw new Error(`Expected ${utxos.length} signatures, got ${signatures.length}`);
|
|
473
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontiercompute/zcash-ika",
|
|
3
|
-
"version": "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",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@ika.xyz/sdk": "^0.3.1",
|
|
14
14
|
"@mysten/sui": "^2.5.0",
|
|
15
|
-
"blakejs": "^1.2.1"
|
|
15
|
+
"blakejs": "^1.2.1",
|
|
16
|
+
"elliptic": "^6.6.1"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/node": "^25.5.2",
|
|
@@ -24,7 +25,9 @@
|
|
|
24
25
|
"dist/tx-builder.js",
|
|
25
26
|
"dist/tx-builder.d.ts",
|
|
26
27
|
"dist/hybrid.js",
|
|
27
|
-
"dist/hybrid.d.ts"
|
|
28
|
+
"dist/hybrid.d.ts",
|
|
29
|
+
"dist/btc-tx-builder.js",
|
|
30
|
+
"dist/btc-tx-builder.d.ts"
|
|
28
31
|
],
|
|
29
32
|
"repository": {
|
|
30
33
|
"type": "git",
|