@frontiercompute/zcash-ika 0.5.0 → 0.6.1

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,10 @@
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)
7
+ ![license](https://img.shields.io/npm/l/@frontiercompute/zcash-ika)
8
+ [![downloads](https://img.shields.io/npm/dw/@frontiercompute/zcash-ika)](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
9
+ [![license](https://img.shields.io/npm/l/@frontiercompute/zcash-ika)](https://github.com/Frontier-Compute/zcash-ika/blob/main/LICENSE)
6
10
 
7
11
  ## What this does
8
12
 
@@ -183,6 +187,22 @@ SUI_PRIVATE_KEY=... node dist/test-e2e.js
183
187
  - [Zebra](https://github.com/ZcashFoundation/zebra) - Zcash node
184
188
  - [Sui Move](https://docs.sui.io/concepts/sui-move-concepts) - policy enforcement
185
189
 
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
+ ## Related Packages
198
+
199
+ | Package | What it does |
200
+ |---------|-------------|
201
+ | [@frontiercompute/zcash-mcp](https://www.npmjs.com/package/@frontiercompute/zcash-mcp) | MCP server for Zcash (22 tools) |
202
+ | [@frontiercompute/openclaw-zap1](https://www.npmjs.com/package/@frontiercompute/openclaw-zap1) | OpenClaw skill for ZAP1 attestation |
203
+ | [@frontiercompute/zap1](https://www.npmjs.com/package/@frontiercompute/zap1) | ZAP1 attestation client |
204
+ | [@frontiercompute/silo-zap1](https://www.npmjs.com/package/@frontiercompute/silo-zap1) | Silo agent attestation via ZAP1 |
205
+
186
206
  ## License
187
207
 
188
208
  MIT
@@ -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++) {
package/dist/index.d.ts CHANGED
@@ -216,6 +216,65 @@ export declare function setPolicy(config: ZcashIkaConfig, walletId: string, poli
216
216
  export declare function checkPolicy(config: ZcashIkaConfig, policyId: string, amount?: number, recipient?: string): Promise<PolicyState & {
217
217
  allowed: boolean;
218
218
  }>;
219
+ export interface VaultResult {
220
+ /** CustodyVault shared object ID on Sui */
221
+ vaultId: string;
222
+ /** AdminCap object ID (vault creator holds this) */
223
+ adminCapId: string;
224
+ /** Sui transaction digest */
225
+ txDigest: string;
226
+ }
227
+ export interface AgentResult {
228
+ /** AgentCap object ID (issued to the registered agent) */
229
+ agentCapId: string;
230
+ /** Sui transaction digest */
231
+ txDigest: string;
232
+ }
233
+ export interface VaultState {
234
+ vaultId: string;
235
+ dwalletId: string;
236
+ maxPerTx: number;
237
+ maxDaily: number;
238
+ dailySpent: number;
239
+ totalSpent: number;
240
+ totalTxCount: number;
241
+ agentCount: number;
242
+ frozen: boolean;
243
+ }
244
+ /**
245
+ * Create a CustodyVault for a dWallet.
246
+ * The vault enforces spend policy on-chain and tracks agent access.
247
+ * Returns the vault ID and AdminCap (transferred to caller).
248
+ */
249
+ export declare function createVault(config: ZcashIkaConfig, dwalletId: string, maxPerTx: number, maxDaily: number): Promise<VaultResult>;
250
+ /**
251
+ * Register an agent on a CustodyVault.
252
+ * Only the admin (holder of AdminCap) can do this.
253
+ * Returns the AgentCap which should be transferred to the agent.
254
+ */
255
+ export declare function registerAgent(config: ZcashIkaConfig, vaultId: string, adminCapId: string, agentAddress: string, agentName: string): Promise<AgentResult>;
256
+ /**
257
+ * Request a spend through the CustodyVault.
258
+ * The Move contract checks all policy constraints on-chain.
259
+ * Aborts if the spend violates any limit.
260
+ */
261
+ export declare function requestSpend(config: ZcashIkaConfig, vaultId: string, agentCapId: string, amount: number, recipient: string, chain: string): Promise<{
262
+ approved: boolean;
263
+ txDigest: string;
264
+ error?: string;
265
+ }>;
266
+ /**
267
+ * Read the on-chain state of a CustodyVault.
268
+ */
269
+ export declare function getVaultState(config: ZcashIkaConfig, vaultId: string): Promise<VaultState>;
270
+ /**
271
+ * Freeze a CustodyVault. All spend requests will be rejected until unfrozen.
272
+ */
273
+ export declare function freezeVault(config: ZcashIkaConfig, vaultId: string, adminCapId: string): Promise<string>;
274
+ /**
275
+ * Unfreeze a CustodyVault. Resumes normal spend processing.
276
+ */
277
+ export declare function unfreezeVault(config: ZcashIkaConfig, vaultId: string, adminCapId: string): Promise<string>;
219
278
  /**
220
279
  * Spend from a Zcash transparent wallet.
221
280
  *
package/dist/index.js CHANGED
@@ -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,
@@ -517,9 +522,9 @@ export async function sign(config, request) {
517
522
  signTxDigest: signResult.digest,
518
523
  };
519
524
  }
520
- // Published package ID - set after sui client publish
521
- // Override via POLICY_PACKAGE_ID env var or pass directly
522
- const DEFAULT_POLICY_PACKAGE_ID = "0x0";
525
+ // Sui testnet deployment (zap1_policy package)
526
+ // Override via POLICY_PACKAGE_ID env var for mainnet or redeployments
527
+ const DEFAULT_POLICY_PACKAGE_ID = "0xb0468033d854e95ad89de4b6fec8f6d8e8187778c9d8337a6aa30a5c24775a77";
523
528
  function getPolicyPackageId() {
524
529
  return process.env.POLICY_PACKAGE_ID || DEFAULT_POLICY_PACKAGE_ID;
525
530
  }
@@ -530,10 +535,6 @@ function getPolicyPackageId() {
530
535
  */
531
536
  export async function setPolicy(config, walletId, policy) {
532
537
  const packageId = getPolicyPackageId();
533
- if (packageId === "0x0") {
534
- throw new Error("Policy Move module not deployed. Set POLICY_PACKAGE_ID env var " +
535
- "after running: sui client publish --path move/");
536
- }
537
538
  const { suiClient, keypair } = await initClients(config);
538
539
  const tx = new Transaction();
539
540
  // 0x6 is the shared Clock object on Sui
@@ -655,6 +656,211 @@ export async function checkPolicy(config, policyId, amount, recipient) {
655
656
  }
656
657
  return { ...state, allowed };
657
658
  }
659
+ /**
660
+ * Create a CustodyVault for a dWallet.
661
+ * The vault enforces spend policy on-chain and tracks agent access.
662
+ * Returns the vault ID and AdminCap (transferred to caller).
663
+ */
664
+ export async function createVault(config, dwalletId, maxPerTx, maxDaily) {
665
+ const packageId = getPolicyPackageId();
666
+ const { suiClient, keypair } = await initClients(config);
667
+ const sender = keypair.getPublicKey().toSuiAddress();
668
+ const tx = new Transaction();
669
+ const adminCap = tx.moveCall({
670
+ target: `${packageId}::custody::create_vault`,
671
+ arguments: [
672
+ tx.pure.address(dwalletId),
673
+ tx.pure.u64(maxPerTx),
674
+ tx.pure.u64(maxDaily),
675
+ tx.object("0x6"), // Clock
676
+ ],
677
+ });
678
+ tx.transferObjects([adminCap], sender);
679
+ const result = await suiClient.signAndExecuteTransaction({
680
+ transaction: tx,
681
+ signer: keypair,
682
+ options: { showEffects: true, showObjectChanges: true },
683
+ });
684
+ if (result.effects?.status?.status !== "success") {
685
+ throw new Error(`createVault TX failed: ${result.effects?.status?.error}`);
686
+ }
687
+ let vaultId = "";
688
+ let adminCapId = "";
689
+ const changes = result.objectChanges || [];
690
+ for (const change of changes) {
691
+ if (change.type !== "created")
692
+ continue;
693
+ const objType = change.objectType || "";
694
+ if (objType.includes("::custody::CustodyVault")) {
695
+ vaultId = change.objectId;
696
+ }
697
+ else if (objType.includes("::custody::AdminCap")) {
698
+ adminCapId = change.objectId;
699
+ }
700
+ }
701
+ if (!vaultId || !adminCapId) {
702
+ const created = result.effects?.created || [];
703
+ for (const obj of created) {
704
+ const id = obj.reference?.objectId || obj.objectId;
705
+ if (id && !vaultId)
706
+ vaultId = id;
707
+ else if (id && !adminCapId)
708
+ adminCapId = id;
709
+ }
710
+ }
711
+ return { vaultId, adminCapId, txDigest: result.digest };
712
+ }
713
+ /**
714
+ * Register an agent on a CustodyVault.
715
+ * Only the admin (holder of AdminCap) can do this.
716
+ * Returns the AgentCap which should be transferred to the agent.
717
+ */
718
+ export async function registerAgent(config, vaultId, adminCapId, agentAddress, agentName) {
719
+ const packageId = getPolicyPackageId();
720
+ const { suiClient, keypair } = await initClients(config);
721
+ const sender = keypair.getPublicKey().toSuiAddress();
722
+ const tx = new Transaction();
723
+ const nameBytes = Array.from(new TextEncoder().encode(agentName));
724
+ const agentCap = tx.moveCall({
725
+ target: `${packageId}::custody::register_agent`,
726
+ arguments: [
727
+ tx.object(vaultId),
728
+ tx.object(adminCapId),
729
+ tx.pure.address(agentAddress),
730
+ tx.pure.vector("u8", nameBytes),
731
+ tx.object("0x6"), // Clock
732
+ ],
733
+ });
734
+ // Transfer AgentCap to the agent address
735
+ tx.transferObjects([agentCap], agentAddress);
736
+ const result = await suiClient.signAndExecuteTransaction({
737
+ transaction: tx,
738
+ signer: keypair,
739
+ options: { showEffects: true, showObjectChanges: true },
740
+ });
741
+ if (result.effects?.status?.status !== "success") {
742
+ throw new Error(`registerAgent TX failed: ${result.effects?.status?.error}`);
743
+ }
744
+ let agentCapId = "";
745
+ const changes = result.objectChanges || [];
746
+ for (const change of changes) {
747
+ if (change.type !== "created")
748
+ continue;
749
+ const objType = change.objectType || "";
750
+ if (objType.includes("::custody::AgentCap")) {
751
+ agentCapId = change.objectId;
752
+ }
753
+ }
754
+ return { agentCapId, txDigest: result.digest };
755
+ }
756
+ /**
757
+ * Request a spend through the CustodyVault.
758
+ * The Move contract checks all policy constraints on-chain.
759
+ * Aborts if the spend violates any limit.
760
+ */
761
+ export async function requestSpend(config, vaultId, agentCapId, amount, recipient, chain) {
762
+ const packageId = getPolicyPackageId();
763
+ const { suiClient, keypair } = await initClients(config);
764
+ const tx = new Transaction();
765
+ const recipientBytes = Array.from(new TextEncoder().encode(recipient));
766
+ const chainBytes = Array.from(new TextEncoder().encode(chain));
767
+ tx.moveCall({
768
+ target: `${packageId}::custody::request_spend_entry`,
769
+ arguments: [
770
+ tx.object(vaultId),
771
+ tx.object(agentCapId),
772
+ tx.pure.u64(amount),
773
+ tx.pure.vector("u8", recipientBytes),
774
+ tx.pure.vector("u8", chainBytes),
775
+ tx.object("0x6"), // Clock
776
+ ],
777
+ });
778
+ const result = await suiClient.signAndExecuteTransaction({
779
+ transaction: tx,
780
+ signer: keypair,
781
+ options: { showEffects: true },
782
+ });
783
+ if (result.effects?.status?.status !== "success") {
784
+ return { approved: false, txDigest: result.digest, error: result.effects?.status?.error || "Policy violation" };
785
+ }
786
+ return { approved: true, txDigest: result.digest };
787
+ }
788
+ /**
789
+ * Read the on-chain state of a CustodyVault.
790
+ */
791
+ export async function getVaultState(config, vaultId) {
792
+ const { suiClient } = await initClients(config);
793
+ const obj = await suiClient.getObject({
794
+ id: vaultId,
795
+ options: { showContent: true },
796
+ });
797
+ const content = obj.data?.content;
798
+ if (!content || content.dataType !== "moveObject") {
799
+ throw new Error(`Vault object ${vaultId} not found or not a Move object`);
800
+ }
801
+ const fields = content.fields;
802
+ return {
803
+ vaultId,
804
+ dwalletId: fields.dwallet_id,
805
+ maxPerTx: Number(fields.max_per_tx),
806
+ maxDaily: Number(fields.max_daily),
807
+ dailySpent: Number(fields.daily_spent),
808
+ totalSpent: Number(fields.total_spent),
809
+ totalTxCount: Number(fields.total_tx_count),
810
+ agentCount: Number(fields.agent_count),
811
+ frozen: fields.frozen,
812
+ };
813
+ }
814
+ /**
815
+ * Freeze a CustodyVault. All spend requests will be rejected until unfrozen.
816
+ */
817
+ export async function freezeVault(config, vaultId, adminCapId) {
818
+ const packageId = getPolicyPackageId();
819
+ const { suiClient, keypair } = await initClients(config);
820
+ const tx = new Transaction();
821
+ tx.moveCall({
822
+ target: `${packageId}::custody::freeze_vault`,
823
+ arguments: [
824
+ tx.object(vaultId),
825
+ tx.object(adminCapId),
826
+ tx.object("0x6"), // Clock
827
+ ],
828
+ });
829
+ const result = await suiClient.signAndExecuteTransaction({
830
+ transaction: tx,
831
+ signer: keypair,
832
+ options: { showEffects: true },
833
+ });
834
+ if (result.effects?.status?.status !== "success") {
835
+ throw new Error(`freezeVault TX failed: ${result.effects?.status?.error}`);
836
+ }
837
+ return result.digest;
838
+ }
839
+ /**
840
+ * Unfreeze a CustodyVault. Resumes normal spend processing.
841
+ */
842
+ export async function unfreezeVault(config, vaultId, adminCapId) {
843
+ const packageId = getPolicyPackageId();
844
+ const { suiClient, keypair } = await initClients(config);
845
+ const tx = new Transaction();
846
+ tx.moveCall({
847
+ target: `${packageId}::custody::unfreeze_vault`,
848
+ arguments: [
849
+ tx.object(vaultId),
850
+ tx.object(adminCapId),
851
+ tx.object("0x6"), // Clock
852
+ ],
853
+ });
854
+ const result = await suiClient.signAndExecuteTransaction({
855
+ transaction: tx,
856
+ signer: keypair,
857
+ options: { showEffects: true },
858
+ });
859
+ if (result.effects?.status?.status !== "success") {
860
+ throw new Error(`unfreezeVault TX failed: ${result.effects?.status?.error}`);
861
+ }
862
+ return result.digest;
863
+ }
658
864
  /**
659
865
  * Spend from a Zcash transparent wallet.
660
866
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontiercompute/zcash-ika",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",