@frontiercompute/zcash-ika 0.6.0 → 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 +11 -0
- package/dist/btc-tx-builder.d.ts +67 -0
- package/dist/btc-tx-builder.js +282 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +9 -4
- 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,6 +3,8 @@
|
|
|
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
|
+
[](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
|
|
7
|
+
[](https://github.com/Frontier-Compute/zcash-ika/blob/main/LICENSE)
|
|
6
8
|
|
|
7
9
|
## What this does
|
|
8
10
|
|
|
@@ -183,6 +185,15 @@ SUI_PRIVATE_KEY=... node dist/test-e2e.js
|
|
|
183
185
|
- [Zebra](https://github.com/ZcashFoundation/zebra) - Zcash node
|
|
184
186
|
- [Sui Move](https://docs.sui.io/concepts/sui-move-concepts) - policy enforcement
|
|
185
187
|
|
|
188
|
+
## Related Packages
|
|
189
|
+
|
|
190
|
+
| Package | What it does |
|
|
191
|
+
|---------|-------------|
|
|
192
|
+
| [@frontiercompute/zcash-mcp](https://www.npmjs.com/package/@frontiercompute/zcash-mcp) | MCP server for Zcash (22 tools) |
|
|
193
|
+
| [@frontiercompute/openclaw-zap1](https://www.npmjs.com/package/@frontiercompute/openclaw-zap1) | OpenClaw skill for ZAP1 attestation |
|
|
194
|
+
| [@frontiercompute/zap1](https://www.npmjs.com/package/@frontiercompute/zap1) | ZAP1 attestation client |
|
|
195
|
+
| [@frontiercompute/silo-zap1](https://www.npmjs.com/package/@frontiercompute/silo-zap1) | Silo agent attestation via ZAP1 |
|
|
196
|
+
|
|
186
197
|
## License
|
|
187
198
|
|
|
188
199
|
MIT
|
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
|
@@ -245,6 +245,8 @@ export function computeBtcSighash(inputs, outputs, inputIndex, hashType = SIGHAS
|
|
|
245
245
|
export function buildUnsignedBtcTx(utxos, txOutputs, changeAddress, fee) {
|
|
246
246
|
if (utxos.length === 0)
|
|
247
247
|
throw new Error("No UTXOs provided");
|
|
248
|
+
if (fee < 0)
|
|
249
|
+
throw new Error("Fee must be non-negative");
|
|
248
250
|
// Build inputs
|
|
249
251
|
const inputs = utxos.map((u) => ({
|
|
250
252
|
prevTxid: reverseTxid(u.txid),
|
|
@@ -270,6 +272,12 @@ export function buildUnsignedBtcTx(utxos, txOutputs, changeAddress, fee) {
|
|
|
270
272
|
else if (change < 0) {
|
|
271
273
|
throw new Error(`UTXOs total ${totalInput} < outputs ${totalOutput} + fee ${fee}`);
|
|
272
274
|
}
|
|
275
|
+
// Warn on dust outputs (let the network reject them)
|
|
276
|
+
for (const out of outputs) {
|
|
277
|
+
if (out.value < 546) {
|
|
278
|
+
console.warn(`Warning: output value ${out.value} sats is below dust threshold (546)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
273
281
|
// Compute per-input sighashes
|
|
274
282
|
const sighashes = [];
|
|
275
283
|
for (let i = 0; i < inputs.length; i++) {
|
|
@@ -371,3 +379,277 @@ export async function broadcastBtcTx(rawHex, network = "mainnet") {
|
|
|
371
379
|
// Blockstream returns the txid as plain text
|
|
372
380
|
return (await resp.text()).trim();
|
|
373
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 */
|
|
@@ -261,6 +261,7 @@ export declare function registerAgent(config: ZcashIkaConfig, vaultId: string, a
|
|
|
261
261
|
export declare function requestSpend(config: ZcashIkaConfig, vaultId: string, agentCapId: string, amount: number, recipient: string, chain: string): Promise<{
|
|
262
262
|
approved: boolean;
|
|
263
263
|
txDigest: string;
|
|
264
|
+
error?: string;
|
|
264
265
|
}>;
|
|
265
266
|
/**
|
|
266
267
|
* Read the on-chain state of a CustodyVault.
|
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
|
*
|
|
@@ -373,13 +373,18 @@ export async function sign(config, request) {
|
|
|
373
373
|
});
|
|
374
374
|
const presignIkaCoin = presignTx.object(ikaCoinId);
|
|
375
375
|
const presignSuiCoin = presignTx.splitCoins(presignTx.gas, [50_000_000]);
|
|
376
|
-
presignIkaTx.requestGlobalPresign({
|
|
376
|
+
const presignReturn = presignIkaTx.requestGlobalPresign({
|
|
377
377
|
dwalletNetworkEncryptionKeyId: dWallet.dwallet_network_encryption_key_id,
|
|
378
378
|
curve: Curve.SECP256K1,
|
|
379
379
|
signatureAlgorithm: SignatureAlgorithm.ECDSASecp256k1,
|
|
380
380
|
ikaCoin: presignIkaCoin,
|
|
381
381
|
suiCoin: presignSuiCoin,
|
|
382
382
|
});
|
|
383
|
+
// Transfer split SUI coin back and presign cap to ourselves
|
|
384
|
+
presignTx.transferObjects([presignSuiCoin], address);
|
|
385
|
+
if (presignReturn) {
|
|
386
|
+
presignTx.transferObjects([presignReturn], address);
|
|
387
|
+
}
|
|
383
388
|
const presignResult = await suiClient.signAndExecuteTransaction({
|
|
384
389
|
transaction: presignTx,
|
|
385
390
|
signer: keypair,
|
|
@@ -776,7 +781,7 @@ export async function requestSpend(config, vaultId, agentCapId, amount, recipien
|
|
|
776
781
|
options: { showEffects: true },
|
|
777
782
|
});
|
|
778
783
|
if (result.effects?.status?.status !== "success") {
|
|
779
|
-
return { approved: false, txDigest: result.digest };
|
|
784
|
+
return { approved: false, txDigest: result.digest, error: result.effects?.status?.error || "Policy violation" };
|
|
780
785
|
}
|
|
781
786
|
return { approved: true, txDigest: result.digest };
|
|
782
787
|
}
|
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",
|