@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 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
  [![npm](https://img.shields.io/npm/v/@frontiercompute/zcash-ika)](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
6
+ [![downloads](https://img.shields.io/npm/dw/@frontiercompute/zcash-ika)](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
7
+ [![license](https://img.shields.io/npm/l/@frontiercompute/zcash-ika)](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
@@ -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 {};
@@ -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
  }
@@ -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
+ };
@@ -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.6.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",