@frontiercompute/zcash-ika 0.6.1 → 0.7.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/README.md +0 -9
- package/dist/btc-tx-builder.d.ts +67 -0
- package/dist/btc-tx-builder.js +274 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/tx-builder.d.ts +36 -0
- package/dist/tx-builder.js +106 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
Split-key custody for Zcash, Bitcoin, and EVM chains. The private key never exists whole. Spend policy enforced on-chain. Every action attested to Zcash.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
|
|
6
|
-

|
|
7
|
-

|
|
8
6
|
[](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
|
|
9
7
|
[](https://github.com/Frontier-Compute/zcash-ika/blob/main/LICENSE)
|
|
10
8
|
|
|
@@ -187,13 +185,6 @@ SUI_PRIVATE_KEY=... node dist/test-e2e.js
|
|
|
187
185
|
- [Zebra](https://github.com/ZcashFoundation/zebra) - Zcash node
|
|
188
186
|
- [Sui Move](https://docs.sui.io/concepts/sui-move-concepts) - policy enforcement
|
|
189
187
|
|
|
190
|
-
## See also
|
|
191
|
-
|
|
192
|
-
- [@frontiercompute/zcash-mcp](https://www.npmjs.com/package/@frontiercompute/zcash-mcp) - MCP server for Zcash wallets and nodes
|
|
193
|
-
- [@frontiercompute/openclaw-zap1](https://www.npmjs.com/package/@frontiercompute/openclaw-zap1) - OpenClaw attestation client for ZAP1
|
|
194
|
-
- [@frontiercompute/zap1](https://www.npmjs.com/package/@frontiercompute/zap1) - ZAP1 on-chain attestation SDK
|
|
195
|
-
- [@frontiercompute/zcash-ika](https://www.npmjs.com/package/@frontiercompute/zcash-ika) - Split-key custody for Zcash, Bitcoin, and EVM (this package)
|
|
196
|
-
|
|
197
188
|
## Related Packages
|
|
198
189
|
|
|
199
190
|
| Package | What it does |
|
package/dist/btc-tx-builder.d.ts
CHANGED
|
@@ -84,4 +84,71 @@ export declare function serializeBtcTx(inputs: BtcInput[], outputs: BtcOutput[],
|
|
|
84
84
|
* Returns the txid on success.
|
|
85
85
|
*/
|
|
86
86
|
export declare function broadcastBtcTx(rawHex: string, network?: BtcNetwork): Promise<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Derive a P2TR (Taproot) bech32m address from an x-only public key.
|
|
89
|
+
*
|
|
90
|
+
* BIP 341 defines P2TR output as: OP_1 <32-byte-x-only-pubkey>
|
|
91
|
+
* The address is bech32m encoded with witness version 1.
|
|
92
|
+
*
|
|
93
|
+
* For key-path-only spending (no script tree), the output key equals
|
|
94
|
+
* the internal key tweaked with an empty merkle root:
|
|
95
|
+
* Q = P + H("TapTweak", P) * G
|
|
96
|
+
*
|
|
97
|
+
* This function takes the already-tweaked x-only pubkey (32 bytes).
|
|
98
|
+
* If using an MPC-derived key, perform the taptweak externally before
|
|
99
|
+
* calling this function.
|
|
100
|
+
*/
|
|
101
|
+
export declare function deriveTaprootAddress(xOnlyPubKey: Uint8Array, network?: BtcNetwork): string;
|
|
102
|
+
/**
|
|
103
|
+
* Build a P2TR scriptPubKey from a 32-byte x-only public key.
|
|
104
|
+
* Format: OP_1 <0x20> <32-byte-x-only-pubkey>
|
|
105
|
+
*/
|
|
106
|
+
export declare function p2trScript(xOnlyPubKey: Uint8Array): Buffer;
|
|
107
|
+
export interface TaprootInput {
|
|
108
|
+
prevTxid: string;
|
|
109
|
+
prevIndex: number;
|
|
110
|
+
value: number;
|
|
111
|
+
scriptPubKey: string;
|
|
112
|
+
}
|
|
113
|
+
export interface TaprootTxParams {
|
|
114
|
+
inputs: TaprootInput[];
|
|
115
|
+
outputs: BtcTxOutput[];
|
|
116
|
+
changeAddress?: string;
|
|
117
|
+
fee: number;
|
|
118
|
+
/** x-only pubkey for change output P2TR script (32 bytes) */
|
|
119
|
+
changeXOnlyPubKey?: Uint8Array;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build an unsigned P2TR (Taproot) key-path spend transaction.
|
|
123
|
+
*
|
|
124
|
+
* Returns per-input sighashes for BIP 340 Schnorr signing.
|
|
125
|
+
* The witness for key-path spend is a single 64-byte Schnorr signature
|
|
126
|
+
* (no sighash type byte appended for SIGHASH_DEFAULT).
|
|
127
|
+
*
|
|
128
|
+
* Transaction version is 2 (segwit). Uses witness serialization.
|
|
129
|
+
*/
|
|
130
|
+
export declare function buildTaprootTx(params: TaprootTxParams): {
|
|
131
|
+
sighashes: Buffer[];
|
|
132
|
+
inputs: {
|
|
133
|
+
prevTxid: Buffer;
|
|
134
|
+
prevIndex: number;
|
|
135
|
+
value: number;
|
|
136
|
+
scriptPubKey: Buffer;
|
|
137
|
+
sequence: number;
|
|
138
|
+
}[];
|
|
139
|
+
outputs: BtcOutput[];
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Serialize a signed Taproot transaction (segwit v1 with witness).
|
|
143
|
+
*
|
|
144
|
+
* Each input witness is a single stack item: the 64-byte Schnorr signature
|
|
145
|
+
* (for SIGHASH_DEFAULT, no sighash byte is appended).
|
|
146
|
+
*/
|
|
147
|
+
export declare function serializeTaprootTx(inputs: {
|
|
148
|
+
prevTxid: Buffer;
|
|
149
|
+
prevIndex: number;
|
|
150
|
+
value: number;
|
|
151
|
+
scriptPubKey: Buffer;
|
|
152
|
+
sequence: number;
|
|
153
|
+
}[], outputs: BtcOutput[], schnorrSigs: Buffer[]): Buffer;
|
|
87
154
|
export {};
|
package/dist/btc-tx-builder.js
CHANGED
|
@@ -379,3 +379,277 @@ export async function broadcastBtcTx(rawHex, network = "mainnet") {
|
|
|
379
379
|
// Blockstream returns the txid as plain text
|
|
380
380
|
return (await resp.text()).trim();
|
|
381
381
|
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// P2TR (Taproot) key-path spend support (BIP 340/341/350)
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Bech32m charset
|
|
386
|
+
const BECH32M_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
387
|
+
const BECH32M_CONST = 0x2bc830a3;
|
|
388
|
+
function bech32mPolymod(values) {
|
|
389
|
+
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
390
|
+
let chk = 1;
|
|
391
|
+
for (const v of values) {
|
|
392
|
+
const b = chk >> 25;
|
|
393
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
394
|
+
for (let i = 0; i < 5; i++) {
|
|
395
|
+
if ((b >> i) & 1)
|
|
396
|
+
chk ^= GEN[i];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return chk;
|
|
400
|
+
}
|
|
401
|
+
function bech32mHrpExpand(hrp) {
|
|
402
|
+
const ret = [];
|
|
403
|
+
for (const c of hrp)
|
|
404
|
+
ret.push(c.charCodeAt(0) >> 5);
|
|
405
|
+
ret.push(0);
|
|
406
|
+
for (const c of hrp)
|
|
407
|
+
ret.push(c.charCodeAt(0) & 31);
|
|
408
|
+
return ret;
|
|
409
|
+
}
|
|
410
|
+
function bech32mCreateChecksum(hrp, data) {
|
|
411
|
+
const values = bech32mHrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
|
412
|
+
const polymod = bech32mPolymod(values) ^ BECH32M_CONST;
|
|
413
|
+
const ret = [];
|
|
414
|
+
for (let i = 0; i < 6; i++) {
|
|
415
|
+
ret.push((polymod >> (5 * (5 - i))) & 31);
|
|
416
|
+
}
|
|
417
|
+
return ret;
|
|
418
|
+
}
|
|
419
|
+
function bech32mEncode(hrp, data) {
|
|
420
|
+
const checksum = bech32mCreateChecksum(hrp, data);
|
|
421
|
+
const combined = data.concat(checksum);
|
|
422
|
+
let ret = hrp + "1";
|
|
423
|
+
for (const d of combined)
|
|
424
|
+
ret += BECH32M_CHARSET[d];
|
|
425
|
+
return ret;
|
|
426
|
+
}
|
|
427
|
+
function convertBits(data, fromBits, toBits, pad) {
|
|
428
|
+
let acc = 0;
|
|
429
|
+
let bits = 0;
|
|
430
|
+
const ret = [];
|
|
431
|
+
const maxv = (1 << toBits) - 1;
|
|
432
|
+
for (const value of data) {
|
|
433
|
+
acc = (acc << fromBits) | value;
|
|
434
|
+
bits += fromBits;
|
|
435
|
+
while (bits >= toBits) {
|
|
436
|
+
bits -= toBits;
|
|
437
|
+
ret.push((acc >> bits) & maxv);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (pad) {
|
|
441
|
+
if (bits > 0)
|
|
442
|
+
ret.push((acc << (toBits - bits)) & maxv);
|
|
443
|
+
}
|
|
444
|
+
else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
|
|
445
|
+
throw new Error("Invalid bit conversion");
|
|
446
|
+
}
|
|
447
|
+
return ret;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Derive a P2TR (Taproot) bech32m address from an x-only public key.
|
|
451
|
+
*
|
|
452
|
+
* BIP 341 defines P2TR output as: OP_1 <32-byte-x-only-pubkey>
|
|
453
|
+
* The address is bech32m encoded with witness version 1.
|
|
454
|
+
*
|
|
455
|
+
* For key-path-only spending (no script tree), the output key equals
|
|
456
|
+
* the internal key tweaked with an empty merkle root:
|
|
457
|
+
* Q = P + H("TapTweak", P) * G
|
|
458
|
+
*
|
|
459
|
+
* This function takes the already-tweaked x-only pubkey (32 bytes).
|
|
460
|
+
* If using an MPC-derived key, perform the taptweak externally before
|
|
461
|
+
* calling this function.
|
|
462
|
+
*/
|
|
463
|
+
export function deriveTaprootAddress(xOnlyPubKey, network = "mainnet") {
|
|
464
|
+
if (xOnlyPubKey.length !== 32) {
|
|
465
|
+
throw new Error("Expected 32-byte x-only pubkey, got " + xOnlyPubKey.length);
|
|
466
|
+
}
|
|
467
|
+
const hrp = network === "mainnet" ? "bc" : "tb";
|
|
468
|
+
const witnessVersion = 1;
|
|
469
|
+
const data5bit = convertBits(xOnlyPubKey, 8, 5, true);
|
|
470
|
+
return bech32mEncode(hrp, [witnessVersion].concat(data5bit));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Build a P2TR scriptPubKey from a 32-byte x-only public key.
|
|
474
|
+
* Format: OP_1 <0x20> <32-byte-x-only-pubkey>
|
|
475
|
+
*/
|
|
476
|
+
export function p2trScript(xOnlyPubKey) {
|
|
477
|
+
if (xOnlyPubKey.length !== 32) {
|
|
478
|
+
throw new Error("Expected 32-byte x-only pubkey, got " + xOnlyPubKey.length);
|
|
479
|
+
}
|
|
480
|
+
const script = Buffer.alloc(34);
|
|
481
|
+
script[0] = 0x51; // OP_1 (witness v1)
|
|
482
|
+
script[1] = 0x20; // push 32 bytes
|
|
483
|
+
Buffer.from(xOnlyPubKey).copy(script, 2);
|
|
484
|
+
return script;
|
|
485
|
+
}
|
|
486
|
+
// BIP 340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg)
|
|
487
|
+
function taggedHash(tag, ...msgs) {
|
|
488
|
+
const tagHash = sha256(Buffer.from(tag, "utf8"));
|
|
489
|
+
const parts = [tagHash, tagHash, ...msgs];
|
|
490
|
+
return sha256(Buffer.concat(parts));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Compute BIP 341 taproot sighash for key-path spending.
|
|
494
|
+
* Uses SIGHASH_DEFAULT (0x00) which commits to all inputs and outputs.
|
|
495
|
+
* The epoch byte (0x00) is prepended per BIP 341.
|
|
496
|
+
*/
|
|
497
|
+
function computeTaprootSighash(inputs, outputs, inputIndex, hashType = 0x00) {
|
|
498
|
+
const parts = [];
|
|
499
|
+
// Epoch
|
|
500
|
+
parts.push(Buffer.from([0x00]));
|
|
501
|
+
// Hash type
|
|
502
|
+
parts.push(Buffer.from([hashType]));
|
|
503
|
+
// Transaction version: 2
|
|
504
|
+
const ver = Buffer.alloc(4);
|
|
505
|
+
writeU32LE(ver, 2, 0);
|
|
506
|
+
parts.push(ver);
|
|
507
|
+
// nLockTime
|
|
508
|
+
const lt = Buffer.alloc(4);
|
|
509
|
+
writeU32LE(lt, 0, 0);
|
|
510
|
+
parts.push(lt);
|
|
511
|
+
// sha_prevouts
|
|
512
|
+
const prevoutsData = [];
|
|
513
|
+
for (const inp of inputs) {
|
|
514
|
+
const outpoint = Buffer.alloc(36);
|
|
515
|
+
inp.prevTxid.copy(outpoint, 0);
|
|
516
|
+
writeU32LE(outpoint, inp.prevIndex, 32);
|
|
517
|
+
prevoutsData.push(outpoint);
|
|
518
|
+
}
|
|
519
|
+
parts.push(sha256(Buffer.concat(prevoutsData)));
|
|
520
|
+
// sha_amounts
|
|
521
|
+
const amountsData = Buffer.alloc(inputs.length * 8);
|
|
522
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
523
|
+
writeI64LE(amountsData, inputs[i].value, i * 8);
|
|
524
|
+
}
|
|
525
|
+
parts.push(sha256(amountsData));
|
|
526
|
+
// sha_scriptpubkeys
|
|
527
|
+
const scriptsData = [];
|
|
528
|
+
for (const inp of inputs) {
|
|
529
|
+
scriptsData.push(compactSize(inp.scriptPubKey.length));
|
|
530
|
+
scriptsData.push(inp.scriptPubKey);
|
|
531
|
+
}
|
|
532
|
+
parts.push(sha256(Buffer.concat(scriptsData)));
|
|
533
|
+
// sha_sequences
|
|
534
|
+
const seqData = Buffer.alloc(inputs.length * 4);
|
|
535
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
536
|
+
writeU32LE(seqData, inputs[i].sequence, i * 4);
|
|
537
|
+
}
|
|
538
|
+
parts.push(sha256(seqData));
|
|
539
|
+
// sha_outputs
|
|
540
|
+
const outsData = [];
|
|
541
|
+
for (const out of outputs) {
|
|
542
|
+
const valueBuf = Buffer.alloc(8);
|
|
543
|
+
writeI64LE(valueBuf, out.value, 0);
|
|
544
|
+
outsData.push(valueBuf);
|
|
545
|
+
outsData.push(compactSize(out.script.length));
|
|
546
|
+
outsData.push(out.script);
|
|
547
|
+
}
|
|
548
|
+
parts.push(sha256(Buffer.concat(outsData)));
|
|
549
|
+
// spend_type: 0x00 (key-path, no annex)
|
|
550
|
+
parts.push(Buffer.from([0x00]));
|
|
551
|
+
// Input index
|
|
552
|
+
const idxBuf = Buffer.alloc(4);
|
|
553
|
+
writeU32LE(idxBuf, inputIndex, 0);
|
|
554
|
+
parts.push(idxBuf);
|
|
555
|
+
return taggedHash("TapSighash", Buffer.concat(parts));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Build an unsigned P2TR (Taproot) key-path spend transaction.
|
|
559
|
+
*
|
|
560
|
+
* Returns per-input sighashes for BIP 340 Schnorr signing.
|
|
561
|
+
* The witness for key-path spend is a single 64-byte Schnorr signature
|
|
562
|
+
* (no sighash type byte appended for SIGHASH_DEFAULT).
|
|
563
|
+
*
|
|
564
|
+
* Transaction version is 2 (segwit). Uses witness serialization.
|
|
565
|
+
*/
|
|
566
|
+
export function buildTaprootTx(params) {
|
|
567
|
+
const { inputs: rawInputs, outputs: txOutputs, fee, changeAddress, changeXOnlyPubKey } = params;
|
|
568
|
+
if (rawInputs.length === 0)
|
|
569
|
+
throw new Error("No inputs provided");
|
|
570
|
+
const inputs = rawInputs.map((inp) => ({
|
|
571
|
+
prevTxid: reverseTxid(inp.prevTxid),
|
|
572
|
+
prevIndex: inp.prevIndex,
|
|
573
|
+
value: inp.value,
|
|
574
|
+
scriptPubKey: Buffer.from(inp.scriptPubKey, "hex"),
|
|
575
|
+
sequence: 0xfffffffd, // RBF-enabled default
|
|
576
|
+
}));
|
|
577
|
+
const totalInput = rawInputs.reduce((s, u) => s + u.value, 0);
|
|
578
|
+
const totalOutput = txOutputs.reduce((s, o) => s + o.value, 0);
|
|
579
|
+
const change = totalInput - totalOutput - fee;
|
|
580
|
+
const outputs = txOutputs.map((o) => ({
|
|
581
|
+
value: o.value,
|
|
582
|
+
script: scriptFromAddress(o.address),
|
|
583
|
+
}));
|
|
584
|
+
if (change > 0 && change >= 546) {
|
|
585
|
+
if (changeXOnlyPubKey) {
|
|
586
|
+
outputs.push({ value: change, script: p2trScript(changeXOnlyPubKey) });
|
|
587
|
+
}
|
|
588
|
+
else if (changeAddress) {
|
|
589
|
+
outputs.push({ value: change, script: scriptFromAddress(changeAddress) });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else if (change < 0) {
|
|
593
|
+
throw new Error("Inputs total " + totalInput + " < outputs " + totalOutput + " + fee " + fee);
|
|
594
|
+
}
|
|
595
|
+
const sighashes = [];
|
|
596
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
597
|
+
sighashes.push(computeTaprootSighash(inputs, outputs, i));
|
|
598
|
+
}
|
|
599
|
+
return { sighashes, inputs, outputs };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Serialize a signed Taproot transaction (segwit v1 with witness).
|
|
603
|
+
*
|
|
604
|
+
* Each input witness is a single stack item: the 64-byte Schnorr signature
|
|
605
|
+
* (for SIGHASH_DEFAULT, no sighash byte is appended).
|
|
606
|
+
*/
|
|
607
|
+
export function serializeTaprootTx(inputs, outputs, schnorrSigs) {
|
|
608
|
+
if (schnorrSigs.length !== inputs.length) {
|
|
609
|
+
throw new Error("Expected " + inputs.length + " signatures, got " + schnorrSigs.length);
|
|
610
|
+
}
|
|
611
|
+
const parts = [];
|
|
612
|
+
// Version: 2
|
|
613
|
+
const ver = Buffer.alloc(4);
|
|
614
|
+
writeU32LE(ver, 2, 0);
|
|
615
|
+
parts.push(ver);
|
|
616
|
+
// Segwit marker + flag
|
|
617
|
+
parts.push(Buffer.from([0x00, 0x01]));
|
|
618
|
+
// Input count
|
|
619
|
+
parts.push(compactSize(inputs.length));
|
|
620
|
+
// Inputs (empty scriptSig for segwit)
|
|
621
|
+
for (const inp of inputs) {
|
|
622
|
+
const outpoint = Buffer.alloc(36);
|
|
623
|
+
inp.prevTxid.copy(outpoint, 0);
|
|
624
|
+
writeU32LE(outpoint, inp.prevIndex, 32);
|
|
625
|
+
parts.push(outpoint);
|
|
626
|
+
parts.push(compactSize(0));
|
|
627
|
+
const seq = Buffer.alloc(4);
|
|
628
|
+
writeU32LE(seq, inp.sequence, 0);
|
|
629
|
+
parts.push(seq);
|
|
630
|
+
}
|
|
631
|
+
// Output count
|
|
632
|
+
parts.push(compactSize(outputs.length));
|
|
633
|
+
// Outputs
|
|
634
|
+
for (const out of outputs) {
|
|
635
|
+
const valueBuf = Buffer.alloc(8);
|
|
636
|
+
writeI64LE(valueBuf, out.value, 0);
|
|
637
|
+
parts.push(valueBuf);
|
|
638
|
+
parts.push(compactSize(out.script.length));
|
|
639
|
+
parts.push(out.script);
|
|
640
|
+
}
|
|
641
|
+
// Witness data
|
|
642
|
+
for (const sig of schnorrSigs) {
|
|
643
|
+
if (sig.length !== 64) {
|
|
644
|
+
throw new Error("Schnorr signature must be 64 bytes, got " + sig.length);
|
|
645
|
+
}
|
|
646
|
+
parts.push(Buffer.from([0x01])); // 1 witness item
|
|
647
|
+
parts.push(compactSize(sig.length));
|
|
648
|
+
parts.push(sig);
|
|
649
|
+
}
|
|
650
|
+
// Locktime
|
|
651
|
+
const lt = Buffer.alloc(4);
|
|
652
|
+
writeU32LE(lt, 0, 0);
|
|
653
|
+
parts.push(lt);
|
|
654
|
+
return Buffer.concat(parts);
|
|
655
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* is viable through this package today.
|
|
15
15
|
*/
|
|
16
16
|
export { Curve, Hash, SignatureAlgorithm, IkaClient, IkaTransaction, UserShareEncryptionKeys, getNetworkConfig, createClassGroupsKeypair, createRandomSessionIdentifier, prepareDKG, prepareDKGAsync, prepareDKGSecondRound, prepareDKGSecondRoundAsync, createDKGUserOutput, publicKeyFromDWalletOutput, parseSignatureFromSignOutput, } from "@ika.xyz/sdk";
|
|
17
|
-
export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, } from "./tx-builder.js";
|
|
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";
|
|
17
|
+
export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, computeSighashV6, } from "./tx-builder.js";
|
|
18
|
+
export type { UTXO, V6SighashParams } from "./tx-builder.js";
|
|
19
|
+
export { fetchBtcUTXOs, selectBtcUTXOs, buildUnsignedBtcTx, attachBtcSignatures, serializeBtcTx, broadcastBtcTx, estimateBtcFee, computeBtcSighash, deriveTaprootAddress, p2trScript, buildTaprootTx, serializeTaprootTx, } from "./btc-tx-builder.js";
|
|
20
|
+
export type { BtcUTXO, BtcTxOutput, BtcNetwork, TaprootInput, TaprootTxParams } from "./btc-tx-builder.js";
|
|
21
21
|
export type Chain = "zcash-transparent" | "bitcoin" | "ethereum";
|
|
22
22
|
export interface ZcashIkaConfig {
|
|
23
23
|
/** Ika network: mainnet or testnet */
|
package/dist/index.js
CHANGED
|
@@ -20,9 +20,9 @@ import { Transaction } from "@mysten/sui/transactions";
|
|
|
20
20
|
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
|
-
export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, } from "./tx-builder.js";
|
|
23
|
+
export { fetchUTXOs, selectUTXOs, buildUnsignedTx, attachSignatures, broadcastTx, estimateFee, BRANCH_ID, computeSighashV6, } from "./tx-builder.js";
|
|
24
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";
|
|
25
|
+
export { fetchBtcUTXOs, selectBtcUTXOs, buildUnsignedBtcTx, attachBtcSignatures, serializeBtcTx, broadcastBtcTx, estimateBtcFee, computeBtcSighash, deriveTaprootAddress, p2trScript, buildTaprootTx, serializeTaprootTx, } from "./btc-tx-builder.js";
|
|
26
26
|
const IKA_COIN_TYPE = "0x1f26bb2f711ff82dcda4d02c77d5123089cb7f8418751474b9fb744ce031526a::ika::IKA";
|
|
27
27
|
/** Parameters for dWallet creation per chain.
|
|
28
28
|
*
|
package/dist/tx-builder.d.ts
CHANGED
|
@@ -66,3 +66,39 @@ export declare function broadcastTx(zebraRpcUrl: string, txHex: string): Promise
|
|
|
66
66
|
* Each additional input adds 1 logical action = +5000 zatoshis
|
|
67
67
|
*/
|
|
68
68
|
export declare function estimateFee(numInputs: number, numOutputs: number): number;
|
|
69
|
+
export interface V6SighashParams {
|
|
70
|
+
/** UTXOs being spent */
|
|
71
|
+
utxos: UTXO[];
|
|
72
|
+
/** Recipient address */
|
|
73
|
+
recipient: string;
|
|
74
|
+
/** Send amount in zatoshis */
|
|
75
|
+
amount: number;
|
|
76
|
+
/** Explicit fee in zatoshis (ZIP 2002) */
|
|
77
|
+
fee: number;
|
|
78
|
+
/** Change address */
|
|
79
|
+
changeAddress: string;
|
|
80
|
+
/** Consensus branch ID (defaults to NU6.1 until NU7 is defined) */
|
|
81
|
+
branchId?: number;
|
|
82
|
+
/** NSM burn amount in zatoshis (ZIP 233), defaults to 0 */
|
|
83
|
+
zip233Amount?: number;
|
|
84
|
+
/** Lock time, defaults to 0 */
|
|
85
|
+
lockTime?: number;
|
|
86
|
+
/** Expiry height, defaults to 0 */
|
|
87
|
+
expiryHeight?: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Compute per-input sighash for a v6 transparent transaction (ZIP 246).
|
|
91
|
+
*
|
|
92
|
+
* Structure mirrors ZIP 244 signature_digest but uses headerDigestV6
|
|
93
|
+
* which includes the explicit fee and NSM burn amount.
|
|
94
|
+
*
|
|
95
|
+
* Returns per-input sighashes ready for MPC signing.
|
|
96
|
+
*
|
|
97
|
+
* NOTE: NU7 is not yet activated. The v6 version group ID and branch ID
|
|
98
|
+
* are placeholders. This function will produce structurally correct
|
|
99
|
+
* sighashes once the constants are finalized.
|
|
100
|
+
*/
|
|
101
|
+
export declare function computeSighashV6(params: V6SighashParams): {
|
|
102
|
+
sighashes: Buffer[];
|
|
103
|
+
txid: Buffer;
|
|
104
|
+
};
|
package/dist/tx-builder.js
CHANGED
|
@@ -535,3 +535,109 @@ export function estimateFee(numInputs, numOutputs) {
|
|
|
535
535
|
const graceActions = 2;
|
|
536
536
|
return Math.max(graceActions, logicalActions) * 5000;
|
|
537
537
|
}
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
// ZIP 246: v6 transaction sighash (NU7)
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Zcash v6 constants. Version group ID and NU7 branch ID are placeholders
|
|
542
|
+
// until the protocol spec finalizes them. The sighash structure follows
|
|
543
|
+
// ZIP 246 which extends ZIP 244 with two new header digest fields.
|
|
544
|
+
const TX_VERSION_V6 = 6;
|
|
545
|
+
const TX_VERSION_GROUP_ID_V6 = 0x26a7270a; // TODO: update when NU7 assigns new vgid
|
|
546
|
+
/**
|
|
547
|
+
* Header digest for v6 transactions (ZIP 246 T.1).
|
|
548
|
+
*
|
|
549
|
+
* Extends the v5 header digest with two new fields appended:
|
|
550
|
+
* T.1f: fee (8-byte LE uint64) per ZIP 2002
|
|
551
|
+
* T.1g: zip233Amount (8-byte LE uint64) per ZIP 233 (NSM burn)
|
|
552
|
+
*
|
|
553
|
+
* Personalization: "ZTxIdHeadersHash" (same as v5)
|
|
554
|
+
*/
|
|
555
|
+
function headerDigestV6(version, versionGroupId, branchId, lockTime, expiryHeight, fee, zip233Amount) {
|
|
556
|
+
const data = Buffer.alloc(36);
|
|
557
|
+
writeU32LE(data, (version | (1 << 31)) >>> 0, 0);
|
|
558
|
+
writeU32LE(data, versionGroupId, 4);
|
|
559
|
+
writeU32LE(data, branchId, 8);
|
|
560
|
+
writeU32LE(data, lockTime, 12);
|
|
561
|
+
writeU32LE(data, expiryHeight, 16);
|
|
562
|
+
writeI64LE(data, fee, 20);
|
|
563
|
+
writeI64LE(data, zip233Amount, 28);
|
|
564
|
+
return blake2b256(data, personalization("ZTxIdHeadersHash"));
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Compute per-input sighash for a v6 transparent transaction (ZIP 246).
|
|
568
|
+
*
|
|
569
|
+
* Structure mirrors ZIP 244 signature_digest but uses headerDigestV6
|
|
570
|
+
* which includes the explicit fee and NSM burn amount.
|
|
571
|
+
*
|
|
572
|
+
* Returns per-input sighashes ready for MPC signing.
|
|
573
|
+
*
|
|
574
|
+
* NOTE: NU7 is not yet activated. The v6 version group ID and branch ID
|
|
575
|
+
* are placeholders. This function will produce structurally correct
|
|
576
|
+
* sighashes once the constants are finalized.
|
|
577
|
+
*/
|
|
578
|
+
export function computeSighashV6(params) {
|
|
579
|
+
const { utxos, recipient, amount, fee, changeAddress, branchId = BRANCH_ID.NU61, // TODO: replace with NU7 branch ID when defined
|
|
580
|
+
zip233Amount = 0, lockTime = 0, expiryHeight = 0, } = params;
|
|
581
|
+
if (utxos.length === 0)
|
|
582
|
+
throw new Error("No UTXOs provided");
|
|
583
|
+
if (amount <= 0)
|
|
584
|
+
throw new Error("Amount must be positive");
|
|
585
|
+
if (fee < 0)
|
|
586
|
+
throw new Error("Fee must be non-negative");
|
|
587
|
+
const inputs = utxos.map((u) => ({
|
|
588
|
+
prevTxid: reverseTxid(u.txid),
|
|
589
|
+
prevIndex: u.outputIndex,
|
|
590
|
+
script: Buffer.from(u.script, "hex"),
|
|
591
|
+
value: u.satoshis,
|
|
592
|
+
sequence: 0xffffffff,
|
|
593
|
+
}));
|
|
594
|
+
const totalInput = utxos.reduce((s, u) => s + u.satoshis, 0);
|
|
595
|
+
const change = totalInput - amount - fee;
|
|
596
|
+
const outputs = [
|
|
597
|
+
{ value: amount, script: scriptFromAddress(recipient) },
|
|
598
|
+
];
|
|
599
|
+
if (change > 0 && change >= 546) {
|
|
600
|
+
outputs.push({ value: change, script: scriptFromAddress(changeAddress) });
|
|
601
|
+
}
|
|
602
|
+
else if (change < 0) {
|
|
603
|
+
throw new Error("UTXOs total " + totalInput + " < amount " + amount + " + fee " + fee);
|
|
604
|
+
}
|
|
605
|
+
// V6 header digest with fee and zip233Amount
|
|
606
|
+
const hdrDigest = headerDigestV6(TX_VERSION_V6, TX_VERSION_GROUP_ID_V6, branchId, lockTime, expiryHeight, fee, zip233Amount);
|
|
607
|
+
// Transparent digest components (same as v5 / ZIP 244)
|
|
608
|
+
const prevoutsSigDigest = hashPrevouts(inputs, branchId);
|
|
609
|
+
const amountsSigDigest = hashAmounts(inputs, branchId);
|
|
610
|
+
const scriptpubkeysSigDigest = hashScriptPubKeys(inputs, branchId);
|
|
611
|
+
const sequenceSigDigest = hashSequences(inputs, branchId);
|
|
612
|
+
const outputsSigDigest = hashOutputs(outputs, branchId);
|
|
613
|
+
// Empty bundle digests (transparent-only tx)
|
|
614
|
+
const sapDigest = emptyBundleDigest("ZTxIdSaplingHash");
|
|
615
|
+
const orchDigest = emptyBundleDigest("ZTxIdOrchardHash");
|
|
616
|
+
const sighashes = [];
|
|
617
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
618
|
+
const inp = inputs[i];
|
|
619
|
+
const prevout = Buffer.alloc(36);
|
|
620
|
+
inp.prevTxid.copy(prevout, 0);
|
|
621
|
+
writeU32LE(prevout, inp.prevIndex, 32);
|
|
622
|
+
const valueBuf = Buffer.alloc(8);
|
|
623
|
+
writeI64LE(valueBuf, inp.value, 0);
|
|
624
|
+
const seqBuf = Buffer.alloc(4);
|
|
625
|
+
writeU32LE(seqBuf, inp.sequence, 0);
|
|
626
|
+
const txinSigDigest = blake2b256(Buffer.concat([prevout, valueBuf, compactSize(inp.script.length), inp.script, seqBuf]), personalization("Zcash___TxInHash"));
|
|
627
|
+
const transparentSigDig = blake2b256(Buffer.concat([
|
|
628
|
+
Buffer.from([SIGHASH_ALL]),
|
|
629
|
+
prevoutsSigDigest,
|
|
630
|
+
amountsSigDigest,
|
|
631
|
+
scriptpubkeysSigDigest,
|
|
632
|
+
sequenceSigDigest,
|
|
633
|
+
outputsSigDigest,
|
|
634
|
+
txinSigDigest,
|
|
635
|
+
]), personalization("ZTxIdTranspaHash"));
|
|
636
|
+
const sighash = blake2b256(Buffer.concat([hdrDigest, transparentSigDig, sapDigest, orchDigest]), personalization("ZcashTxHash_", branchIdBytes(branchId)));
|
|
637
|
+
sighashes.push(sighash);
|
|
638
|
+
}
|
|
639
|
+
// Compute txid digest using v6 header
|
|
640
|
+
const txpDigest = transparentDigest(inputs, outputs, branchId);
|
|
641
|
+
const txid = blake2b256(Buffer.concat([hdrDigest, txpDigest, sapDigest, orchDigest]), personalization("ZcashTxHash__", branchIdBytes(branchId)));
|
|
642
|
+
return { sighashes, txid };
|
|
643
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontiercompute/zcash-ika",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|