@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 +20 -0
- package/dist/btc-tx-builder.js +8 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +214 -8
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](https://www.npmjs.com/package/@frontiercompute/zcash-ika)
|
|
9
|
+
[](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
|
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++) {
|
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
|
-
//
|
|
521
|
-
// Override via POLICY_PACKAGE_ID env var or
|
|
522
|
-
const DEFAULT_POLICY_PACKAGE_ID = "
|
|
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.
|
|
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",
|