@frontiercompute/zcash-ika 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,26 +1,31 @@
1
1
  /**
2
2
  * @frontiercompute/zcash-ika
3
3
  *
4
- * Zero-trust custody for Zcash and Bitcoin. Born shielded, stay shielded.
4
+ * Split-key custody for Zcash transparent, Bitcoin, and EVM chains.
5
5
  *
6
- * Two dWallets, one operator:
7
- * - Ed25519 dWallet -> Zcash Orchard (shielded ZEC)
8
- * - secp256k1 dWallet -> Bitcoin (BTC) + Zcash transparent (t-addr)
9
- *
10
- * Neither key ever exists whole. Both chains signed through Ika 2PC-MPC.
6
+ * One secp256k1 dWallet signs for all three chain families.
7
+ * Neither key half can sign alone. Policy enforced by Sui Move contract.
11
8
  * Every operation attested to Zcash via ZAP1.
12
9
  *
13
10
  * Built on Ika's 2PC-MPC network (Sui).
11
+ *
12
+ * NOTE: Zcash shielded (Orchard) uses RedPallas on the Pallas curve,
13
+ * which Ika does not currently support. Only transparent ZEC (secp256k1)
14
+ * is viable through this package today.
14
15
  */
15
16
  export { Curve, Hash, SignatureAlgorithm, IkaClient, IkaTransaction, UserShareEncryptionKeys, getNetworkConfig, createClassGroupsKeypair, createRandomSessionIdentifier, prepareDKG, prepareDKGAsync, prepareDKGSecondRound, prepareDKGSecondRoundAsync, createDKGUserOutput, publicKeyFromDWalletOutput, parseSignatureFromSignOutput, } from "@ika.xyz/sdk";
16
- /** Parameters for dWallet creation per chain */
17
+ import { Curve, Hash, SignatureAlgorithm, IkaClient, IkaTransaction, UserShareEncryptionKeys, getNetworkConfig, createRandomSessionIdentifier, prepareDKGAsync, publicKeyFromDWalletOutput, } from "@ika.xyz/sdk";
18
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
19
+ import { Transaction } from "@mysten/sui/transactions";
20
+ import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
21
+ import { createHash } from "node:crypto";
22
+ const IKA_COIN_TYPE = "0x1f26bb2f711ff82dcda4d02c77d5123089cb7f8418751474b9fb744ce031526a::ika::IKA";
23
+ /** Parameters for dWallet creation per chain.
24
+ *
25
+ * All chains use secp256k1 - one dWallet signs for all of them.
26
+ * Zcash shielded (Orchard) requires RedPallas on the Pallas curve,
27
+ * which is not available in Ika's current MPC. Transparent ZEC works. */
17
28
  export const CHAIN_PARAMS = {
18
- "zcash-shielded": {
19
- curve: "ED25519",
20
- algorithm: "EdDSA",
21
- hash: "SHA512",
22
- description: "Zcash Orchard shielded pool (Ed25519/EdDSA)",
23
- },
24
29
  "zcash-transparent": {
25
30
  curve: "SECP256K1",
26
31
  algorithm: "ECDSASecp256k1",
@@ -33,114 +38,482 @@ export const CHAIN_PARAMS = {
33
38
  hash: "DoubleSHA256",
34
39
  description: "Bitcoin (secp256k1/ECDSA, DoubleSHA256)",
35
40
  },
41
+ ethereum: {
42
+ curve: "SECP256K1",
43
+ algorithm: "ECDSASecp256k1",
44
+ hash: "KECCAK256",
45
+ description: "Ethereum/EVM (secp256k1/ECDSA, KECCAK256)",
46
+ },
47
+ };
48
+ // Zcash t-address version bytes (2 bytes each)
49
+ const ZCASH_VERSION_BYTES = {
50
+ mainnet: Uint8Array.from([0x1c, 0xb8]), // t1...
51
+ testnet: Uint8Array.from([0x1d, 0x25]), // tm...
36
52
  };
53
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
54
+ function base58Encode(data) {
55
+ // Count leading zeros
56
+ let leadingZeros = 0;
57
+ for (const b of data) {
58
+ if (b !== 0)
59
+ break;
60
+ leadingZeros++;
61
+ }
62
+ // Convert to bigint for division
63
+ let num = BigInt(0);
64
+ for (const b of data) {
65
+ num = num * 256n + BigInt(b);
66
+ }
67
+ const chars = [];
68
+ while (num > 0n) {
69
+ const rem = Number(num % 58n);
70
+ num = num / 58n;
71
+ chars.push(BASE58_ALPHABET[rem]);
72
+ }
73
+ // Prepend '1' for each leading zero byte
74
+ for (let i = 0; i < leadingZeros; i++) {
75
+ chars.push("1");
76
+ }
77
+ return chars.reverse().join("");
78
+ }
79
+ function sha256(data) {
80
+ return createHash("sha256").update(data).digest();
81
+ }
82
+ function hash160(data) {
83
+ return createHash("ripemd160").update(sha256(data)).digest();
84
+ }
37
85
  /**
38
- * Create a dual-custody setup: one shielded ZEC wallet + one BTC wallet.
39
- * Same operator controls both via Ika split-key.
86
+ * Derive a Zcash transparent address from a compressed secp256k1 public key.
40
87
  *
41
- * Flow per wallet:
42
- * 1. Generate UserShareEncryptionKeys from operator seed
43
- * 2. Run DKG on Ika (2PC-MPC key generation)
44
- * 3. Extract public key, derive chain-specific address
45
- * 4. Attest wallet creation via ZAP1
88
+ * Same as Bitcoin P2PKH but with Zcash 2-byte version prefix:
89
+ * mainnet 0x1cb8 (t1...), testnet 0x1d25 (tm...)
90
+ *
91
+ * Steps:
92
+ * 1. SHA256(pubkey) then RIPEMD160 = 20-byte hash
93
+ * 2. Prepend 2-byte version
94
+ * 3. Double-SHA256 checksum (first 4 bytes)
95
+ * 4. Base58 encode (version + hash + checksum)
46
96
  */
47
- export async function createDualCustody(config, operatorSeed) {
48
- // Both wallets share one operator seed.
49
- // Each gets its own dWallet with different curve params.
50
- //
51
- // Phase 1 (current): throw with setup instructions
52
- // Phase 2: actual DKG on Ika testnet
53
- throw new Error("createDualCustody requires Ika network access. " +
54
- "Shielded: Ed25519/EdDSA/SHA512 dWallet -> Orchard address. " +
55
- "Bitcoin: secp256k1/ECDSA/DoubleSHA256 dWallet -> BTC address. " +
56
- "npm install @ika.xyz/sdk @mysten/sui && configure Sui wallet. " +
57
- "See https://docs.ika.xyz for DKG walkthrough.");
97
+ export function deriveZcashAddress(publicKey, network = "mainnet") {
98
+ if (publicKey.length !== 33) {
99
+ throw new Error(`Expected 33-byte compressed secp256k1 pubkey, got ${publicKey.length} bytes`);
100
+ }
101
+ const prefix = publicKey[0];
102
+ if (prefix !== 0x02 && prefix !== 0x03) {
103
+ throw new Error(`Invalid compressed pubkey prefix 0x${prefix.toString(16)}, expected 0x02 or 0x03`);
104
+ }
105
+ const pubkeyHash = hash160(publicKey); // 20 bytes
106
+ const version = ZCASH_VERSION_BYTES[network];
107
+ // version (2) + hash160 (20) = 22 bytes
108
+ const payload = new Uint8Array(22);
109
+ payload.set(version, 0);
110
+ payload.set(pubkeyHash, 2);
111
+ // checksum: first 4 bytes of SHA256(SHA256(payload))
112
+ const checksum = sha256(sha256(payload)).subarray(0, 4);
113
+ // final: payload (22) + checksum (4) = 26 bytes
114
+ const full = new Uint8Array(26);
115
+ full.set(payload, 0);
116
+ full.set(checksum, 22);
117
+ return base58Encode(full);
58
118
  }
119
+ // Default poll settings for testnet (epochs can be slow)
120
+ const POLL_OPTS = {
121
+ timeout: 300_000,
122
+ interval: 3_000,
123
+ maxInterval: 10_000,
124
+ backoffMultiplier: 1.5,
125
+ };
59
126
  /**
60
- * Create a single dWallet for a specific chain.
127
+ * Initialize Ika + Sui clients from config.
61
128
  */
62
- export async function createWallet(config, chain, operatorSeed) {
63
- const params = CHAIN_PARAMS[chain];
64
- // The DKG flow:
65
- // 1. IkaClient.init({ network: config.network })
66
- // 2. UserShareEncryptionKeys from operatorSeed
67
- // 3. prepareDKG(curve, signatureAlgorithm, hash)
68
- // 4. Submit DKG round 1 to Ika via IkaTransaction
69
- // 5. prepareDKGSecondRound with network output
70
- // 6. Submit round 2 -> get dWallet object ID + public key
71
- // 7. Derive chain address from public key
72
- throw new Error(`createWallet(${chain}) requires Ika ${config.network} access. ` +
73
- `Params: ${params.curve}/${params.algorithm}/${params.hash}. ` +
74
- `${params.description}.`);
129
+ async function initClients(config) {
130
+ const decoded = decodeSuiPrivateKey(config.suiPrivateKey);
131
+ const keypair = Ed25519Keypair.fromSecretKey(decoded.secretKey);
132
+ const address = keypair.getPublicKey().toSuiAddress();
133
+ const { SuiJsonRpcClient } = await import("@mysten/sui/jsonRpc");
134
+ const rpcUrl = config.suiRpcUrl || (config.network === "testnet"
135
+ ? "https://sui-testnet-rpc.publicnode.com"
136
+ : "https://sui-mainnet-rpc.publicnode.com");
137
+ const suiClient = new SuiJsonRpcClient({
138
+ url: rpcUrl,
139
+ network: config.network,
140
+ });
141
+ const ikaConfig = getNetworkConfig(config.network);
142
+ if (!ikaConfig)
143
+ throw new Error(`No Ika ${config.network} config`);
144
+ const ikaClient = new IkaClient({
145
+ suiClient,
146
+ config: ikaConfig,
147
+ cache: true,
148
+ encryptionKeyOptions: { autoDetect: true },
149
+ });
150
+ await ikaClient.initialize();
151
+ return { ikaClient, suiClient, keypair, address };
75
152
  }
76
153
  /**
77
- * Sign a message hash through Ika 2PC-MPC.
154
+ * Find the IKA coin object ID for an address.
155
+ * IKA is a separate token from SUI - needed for Ika transaction fees.
156
+ */
157
+ async function findIkaCoin(rpcUrl, address) {
158
+ const resp = await fetch(rpcUrl, {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({
162
+ jsonrpc: "2.0", id: 1,
163
+ method: "suix_getCoins",
164
+ params: [address, IKA_COIN_TYPE, null, 5],
165
+ }),
166
+ });
167
+ const data = (await resp.json());
168
+ const coins = data.result?.data || [];
169
+ if (coins.length === 0) {
170
+ throw new Error("No IKA tokens found. Get them from https://faucet.ika.xyz");
171
+ }
172
+ return coins[0].coinObjectId;
173
+ }
174
+ /**
175
+ * Create a split-key custody wallet.
176
+ * One secp256k1 dWallet signs for Zcash transparent, Bitcoin, and EVM.
78
177
  *
79
- * The operator provides their seed, Ika provides the network share.
80
- * Neither party ever sees the full private key.
178
+ * Returns the dWallet handle with ID, public key, and encryption seed.
179
+ * Save the encryption seed - you need it for signing.
180
+ */
181
+ export async function createDualCustody(config, _operatorSeed) {
182
+ const wallet = await createWallet(config, "zcash-transparent");
183
+ const { address } = await initClients(config);
184
+ return {
185
+ primary: wallet,
186
+ operatorAddress: address,
187
+ };
188
+ }
189
+ /**
190
+ * Create a single secp256k1 dWallet on Ika.
81
191
  *
82
192
  * Flow:
83
- * 1. Create presign session on Ika
84
- * 2. Compute partial user signature locally
85
- * 3. Submit to Ika coordinator
86
- * 4. Poll for completion
87
- * 5. Extract full signature from sign output
193
+ * 1. Generate encryption keys from random seed
194
+ * 2. Prepare DKG locally (WASM crypto)
195
+ * 3. Submit DKG request to Ika network
196
+ * 4. Poll until dWallet reaches Active state
197
+ * 5. Extract compressed public key
198
+ */
199
+ export async function createWallet(config, chain, _operatorSeed) {
200
+ const { ikaClient, suiClient, keypair, address } = await initClients(config);
201
+ // Generate encryption keys
202
+ const seed = new Uint8Array(32);
203
+ crypto.getRandomValues(seed);
204
+ const encKeys = await UserShareEncryptionKeys.fromRootSeedKey(seed, Curve.SECP256K1);
205
+ // Prepare DKG
206
+ const bytesToHash = createRandomSessionIdentifier();
207
+ const dkgInput = await prepareDKGAsync(ikaClient, Curve.SECP256K1, encKeys, bytesToHash, address);
208
+ // Build and submit DKG transaction
209
+ const tx = new Transaction();
210
+ const ikaTx = new IkaTransaction({
211
+ ikaClient,
212
+ transaction: tx,
213
+ userShareEncryptionKeys: encKeys,
214
+ });
215
+ const sessionId = ikaTx.registerSessionIdentifier(bytesToHash);
216
+ const networkEncKey = await ikaClient.getLatestNetworkEncryptionKey?.()
217
+ || await ikaClient.getConfiguredNetworkEncryptionKey?.();
218
+ // IKA coin (separate token type) required for Ika fees
219
+ const rpcUrl = config.suiRpcUrl || (config.network === "testnet"
220
+ ? "https://sui-testnet-rpc.publicnode.com"
221
+ : "https://sui-mainnet-rpc.publicnode.com");
222
+ const ikaCoinId = config.ikaCoinId || await findIkaCoin(rpcUrl, address);
223
+ const ikaCoinObj = tx.object(ikaCoinId);
224
+ const dkgReturn = await ikaTx.requestDWalletDKG({
225
+ dkgRequestInput: dkgInput,
226
+ sessionIdentifier: sessionId,
227
+ dwalletNetworkEncryptionKeyId: networkEncKey?.id,
228
+ curve: Curve.SECP256K1,
229
+ ikaCoin: tx.splitCoins(ikaCoinObj, [50_000_000]),
230
+ suiCoin: tx.splitCoins(tx.gas, [50_000_000]),
231
+ });
232
+ if (dkgReturn) {
233
+ tx.transferObjects([dkgReturn], address);
234
+ }
235
+ const result = await suiClient.signAndExecuteTransaction({
236
+ transaction: tx,
237
+ signer: keypair,
238
+ options: { showEffects: true },
239
+ });
240
+ if (result.effects?.status?.status !== "success") {
241
+ throw new Error(`DKG TX failed: ${result.effects?.status?.error}`);
242
+ }
243
+ // Find and poll the dWallet object
244
+ const created = result.effects?.created || [];
245
+ let dwalletId = null;
246
+ let pubkey = null;
247
+ for (const obj of created) {
248
+ const id = obj.reference?.objectId || obj.objectId;
249
+ if (!id)
250
+ continue;
251
+ try {
252
+ const dw = await ikaClient.getDWalletInParticularState(id, "Active", POLL_OPTS);
253
+ if (dw) {
254
+ dwalletId = id;
255
+ try {
256
+ const rawOut = dw.state?.Active?.public_output || dw.publicOutput;
257
+ const outBytes = new Uint8Array(Array.isArray(rawOut) ? rawOut : Array.from(rawOut));
258
+ pubkey = await publicKeyFromDWalletOutput(Curve.SECP256K1, outBytes);
259
+ }
260
+ catch { /* extract later if needed */ }
261
+ break;
262
+ }
263
+ }
264
+ catch {
265
+ // Not a dWallet object or timeout - skip
266
+ }
267
+ }
268
+ if (!dwalletId) {
269
+ throw new Error("DKG completed but could not find Active dWallet in created objects");
270
+ }
271
+ const seedHex = Buffer.from(seed).toString("hex");
272
+ // Derive chain-specific address from compressed pubkey
273
+ let derivedAddress = "";
274
+ if (pubkey && pubkey.length === 33 && chain === "zcash-transparent") {
275
+ derivedAddress = deriveZcashAddress(pubkey, config.network);
276
+ }
277
+ return {
278
+ id: dwalletId,
279
+ publicKey: pubkey || new Uint8Array(0),
280
+ chain,
281
+ address: derivedAddress,
282
+ network: config.network,
283
+ encryptionSeed: seedHex,
284
+ };
285
+ }
286
+ /**
287
+ * Sign a message hash through Ika 2PC-MPC.
288
+ *
289
+ * Two on-chain transactions:
290
+ * 1. Request presign (pre-compute MPC ephemeral key share)
291
+ * 2. Approve message + request signature
292
+ *
293
+ * The operator provides their encryption seed, Ika provides the network share.
294
+ * Neither party ever sees the full private key.
88
295
  */
89
- export async function sign(config, operatorSeed, request) {
296
+ export async function sign(config, request) {
297
+ const { ikaClient, suiClient, keypair, address } = await initClients(config);
90
298
  const params = CHAIN_PARAMS[request.chain];
91
- // The sign flow:
92
- // 1. IkaClient.init({ network: config.network })
93
- // 2. RequestGlobalPresign for the dWallet
94
- // 3. createUserSignMessageWithCentralizedOutput(messageHash, userShare, ...)
95
- // 4. ApproveMessage on Sui (this is where Move policy gates)
96
- // 5. RequestSign -> poll SessionsManager for Completed status
97
- // 6. parseSignatureFromSignOutput(signOutput)
98
- throw new Error(`sign requires active dWallet + Ika ${config.network}. ` +
99
- `Chain: ${request.chain}, params: ${params.curve}/${params.algorithm}/${params.hash}.`);
299
+ // Reconstruct encryption keys
300
+ const encSeed = Buffer.from(request.encryptionSeed, "hex");
301
+ const encKeys = await UserShareEncryptionKeys.fromRootSeedKey(new Uint8Array(encSeed), Curve.SECP256K1);
302
+ // Fetch dWallet (must be Active)
303
+ const dWallet = await ikaClient.getDWallet(request.walletId);
304
+ if (!dWallet?.state?.Active) {
305
+ throw new Error(`dWallet ${request.walletId} not Active`);
306
+ }
307
+ // Find dWalletCap
308
+ let capId = request.dWalletCapId;
309
+ if (!capId) {
310
+ const capsResult = await ikaClient.getOwnedDWalletCaps(address);
311
+ const cap = (capsResult.dWalletCaps || []).find((c) => c.dwallet_id === request.walletId);
312
+ if (!cap)
313
+ throw new Error(`No dWalletCap found for ${request.walletId}`);
314
+ capId = cap.id;
315
+ }
316
+ // Find IKA coin for fees
317
+ const rpcUrl = config.suiRpcUrl || (config.network === "testnet"
318
+ ? "https://sui-testnet-rpc.publicnode.com"
319
+ : "https://sui-mainnet-rpc.publicnode.com");
320
+ const ikaCoinId = config.ikaCoinId || await findIkaCoin(rpcUrl, address);
321
+ // TX 1: Request presign
322
+ const presignTx = new Transaction();
323
+ const presignIkaTx = new IkaTransaction({
324
+ ikaClient,
325
+ transaction: presignTx,
326
+ userShareEncryptionKeys: encKeys,
327
+ });
328
+ const presignIkaCoin = presignTx.object(ikaCoinId);
329
+ const presignSuiCoin = presignTx.splitCoins(presignTx.gas, [50_000_000]);
330
+ presignIkaTx.requestGlobalPresign({
331
+ dwalletNetworkEncryptionKeyId: dWallet.dwallet_network_encryption_key_id,
332
+ curve: Curve.SECP256K1,
333
+ signatureAlgorithm: SignatureAlgorithm.ECDSASecp256k1,
334
+ ikaCoin: presignIkaCoin,
335
+ suiCoin: presignSuiCoin,
336
+ });
337
+ const presignResult = await suiClient.signAndExecuteTransaction({
338
+ transaction: presignTx,
339
+ signer: keypair,
340
+ options: { showEffects: true },
341
+ });
342
+ if (presignResult.effects?.status?.status !== "success") {
343
+ throw new Error(`Presign TX failed: ${presignResult.effects?.status?.error}`);
344
+ }
345
+ // Find presign session and poll for completion.
346
+ // Poll manually instead of using getPresignInParticularState so we can
347
+ // detect NetworkRejected early rather than burning the full timeout.
348
+ const presignCreated = presignResult.effects?.created || [];
349
+ let completedPresign = null;
350
+ for (const obj of presignCreated) {
351
+ const id = obj.reference?.objectId || obj.objectId;
352
+ if (!id)
353
+ continue;
354
+ try {
355
+ const startTime = Date.now();
356
+ let interval = POLL_OPTS.interval || 3_000;
357
+ while (Date.now() - startTime < (POLL_OPTS.timeout || 300_000)) {
358
+ const presign = await ikaClient.getPresign(id);
359
+ const kind = presign?.state?.$kind;
360
+ if (kind === "Completed") {
361
+ completedPresign = presign;
362
+ break;
363
+ }
364
+ if (kind === "NetworkRejected") {
365
+ throw new Error(`Presign ${id} rejected by network (state: NetworkRejected). ` +
366
+ `This usually means the MPC round was aborted by validators. ` +
367
+ `Retry or check Ika network status.`);
368
+ }
369
+ await new Promise(r => setTimeout(r, interval));
370
+ interval = Math.min(interval * (POLL_OPTS.backoffMultiplier || 1.5), POLL_OPTS.maxInterval || 10_000);
371
+ }
372
+ if (completedPresign)
373
+ break;
374
+ }
375
+ catch (e) {
376
+ if (e.message?.includes("NetworkRejected"))
377
+ throw e;
378
+ // Not a presign object or fetch error, try next created object
379
+ }
380
+ }
381
+ if (!completedPresign) {
382
+ throw new Error("Presign TX succeeded but timed out waiting for completion. Check Ika network status.");
383
+ }
384
+ // TX 2: Approve message + sign
385
+ const hashEnum = Hash[params.hash];
386
+ const signTx = new Transaction();
387
+ const signIkaTx = new IkaTransaction({
388
+ ikaClient,
389
+ transaction: signTx,
390
+ userShareEncryptionKeys: encKeys,
391
+ });
392
+ const verifiedPresignCap = signIkaTx.verifyPresignCap({
393
+ presign: completedPresign,
394
+ });
395
+ const messageApproval = signIkaTx.approveMessage({
396
+ dWalletCap: capId,
397
+ curve: Curve.SECP256K1,
398
+ signatureAlgorithm: SignatureAlgorithm.ECDSASecp256k1,
399
+ hashScheme: hashEnum,
400
+ message: request.messageHash,
401
+ });
402
+ await signIkaTx.requestSign({
403
+ dWallet: dWallet,
404
+ messageApproval,
405
+ hashScheme: hashEnum,
406
+ verifiedPresignCap,
407
+ presign: completedPresign,
408
+ message: request.messageHash,
409
+ signatureScheme: SignatureAlgorithm.ECDSASecp256k1,
410
+ ikaCoin: signTx.splitCoins(signTx.object(ikaCoinId), [50_000_000]),
411
+ suiCoin: signTx.splitCoins(signTx.gas, [50_000_000]),
412
+ });
413
+ const signResult = await suiClient.signAndExecuteTransaction({
414
+ transaction: signTx,
415
+ signer: keypair,
416
+ options: { showEffects: true },
417
+ });
418
+ if (signResult.effects?.status?.status !== "success") {
419
+ throw new Error(`Sign TX failed: ${signResult.effects?.status?.error}`);
420
+ }
421
+ // Find sign session and poll for signature.
422
+ // Same manual polling as presign to detect NetworkRejected early.
423
+ const signCreated = signResult.effects?.created || [];
424
+ let completedSign = null;
425
+ for (const obj of signCreated) {
426
+ const id = obj.reference?.objectId || obj.objectId;
427
+ if (!id)
428
+ continue;
429
+ try {
430
+ const startTime = Date.now();
431
+ let interval = POLL_OPTS.interval || 3_000;
432
+ while (Date.now() - startTime < (POLL_OPTS.timeout || 300_000)) {
433
+ const sign = await ikaClient.getSign(id, Curve.SECP256K1, SignatureAlgorithm.ECDSASecp256k1);
434
+ const kind = sign?.state?.$kind;
435
+ if (kind === "Completed") {
436
+ completedSign = sign;
437
+ break;
438
+ }
439
+ if (kind === "NetworkRejected") {
440
+ throw new Error(`Sign ${id} rejected by network (state: NetworkRejected). ` +
441
+ `MPC signing round aborted. Retry or check Ika network status.`);
442
+ }
443
+ await new Promise(r => setTimeout(r, interval));
444
+ interval = Math.min(interval * (POLL_OPTS.backoffMultiplier || 1.5), POLL_OPTS.maxInterval || 10_000);
445
+ }
446
+ if (completedSign)
447
+ break;
448
+ }
449
+ catch (e) {
450
+ if (e.message?.includes("NetworkRejected"))
451
+ throw e;
452
+ // Not a sign object or fetch error, try next created object
453
+ }
454
+ }
455
+ if (!completedSign?.state?.Completed?.signature) {
456
+ throw new Error("Sign TX succeeded but timed out waiting for signature. Check Ika network status.");
457
+ }
458
+ const rawSig = completedSign.state.Completed.signature;
459
+ const sigBytes = new Uint8Array(Array.isArray(rawSig) ? rawSig : Array.from(rawSig));
460
+ // Extract public key from dWallet
461
+ let pubkey = new Uint8Array(0);
462
+ try {
463
+ const rawOutput = dWallet.state.Active.public_output;
464
+ const outputBytes = new Uint8Array(Array.isArray(rawOutput) ? rawOutput : Array.from(rawOutput));
465
+ pubkey = await publicKeyFromDWalletOutput(Curve.SECP256K1, outputBytes);
466
+ }
467
+ catch { /* non-fatal */ }
468
+ return {
469
+ signature: sigBytes,
470
+ publicKey: pubkey,
471
+ signTxDigest: signResult.digest,
472
+ };
100
473
  }
101
474
  /**
102
475
  * Set spending policy on the dWallet.
103
476
  * Policy enforced at Sui Move contract level.
104
477
  * The agent cannot bypass it - the contract holds the DWalletCap.
105
478
  */
106
- export async function setPolicy(config, walletId, policy) {
479
+ export async function setPolicy(_config, _walletId, _policy) {
107
480
  throw new Error("setPolicy requires a deployed Move module on Sui. " +
108
481
  "The module gates approve_message() with spending constraints. " +
109
482
  "See docs/move-policy-template.move for the template.");
110
483
  }
111
484
  /**
112
- * Spend from a shielded ZEC wallet.
485
+ * Spend from a Zcash transparent wallet.
113
486
  *
114
- * 1. Build Zcash Orchard transaction (zcash_primitives)
115
- * 2. Extract sighash
116
- * 3. Sign via Ika 2PC-MPC (Ed25519/EdDSA)
487
+ * 1. Build Zcash transparent transaction (requires Zebra)
488
+ * 2. Compute sighash (DoubleSHA256)
489
+ * 3. Sign via Ika 2PC-MPC (secp256k1/ECDSA)
117
490
  * 4. Attach signature to transaction
118
491
  * 5. Broadcast via Zebra sendrawtransaction
119
492
  * 6. Attest via ZAP1 as AGENT_ACTION
120
493
  */
121
- export async function spendShielded(config, walletId, operatorSeed, request) {
122
- throw new Error("spendShielded requires active Ed25519 dWallet + Zebra node. " +
123
- "Integration in progress - need Ed25519 -> Orchard spending key derivation bridge.");
494
+ export async function spendTransparent(config, walletId, encryptionSeed, request) {
495
+ // Build transaction, extract sighash
496
+ // For now: the caller provides the sighash directly via sign()
497
+ // This function will be the full pipeline once we have tx building
498
+ throw new Error("spendTransparent requires Zcash transparent tx builder. " +
499
+ "Use sign() directly with a pre-computed sighash for now. " +
500
+ "Full pipeline: build tx -> sighash -> sign() -> attach sig -> broadcast.");
124
501
  }
125
502
  /**
126
503
  * Spend from a Bitcoin wallet.
127
- *
128
- * 1. Build Bitcoin transaction
129
- * 2. Compute sighash (DoubleSHA256)
130
- * 3. Sign via Ika 2PC-MPC (secp256k1/ECDSA)
131
- * 4. Attach signature
132
- * 5. Broadcast to Bitcoin network
133
- * 6. Attest via ZAP1 as AGENT_ACTION
504
+ * Same MPC flow as Zcash transparent - DoubleSHA256 sighash, ECDSA signature.
134
505
  */
135
- export async function spendBitcoin(config, walletId, operatorSeed, request) {
136
- throw new Error("spendBitcoin requires active secp256k1 dWallet + Bitcoin node. " +
137
- "Same MPC flow as Zcash transparent - DoubleSHA256 sighash, ECDSA signature.");
506
+ export async function spendBitcoin(config, walletId, encryptionSeed, request) {
507
+ throw new Error("spendBitcoin requires Bitcoin tx builder. " +
508
+ "Use sign() with chain='bitcoin' and a pre-computed sighash for now.");
138
509
  }
139
510
  /**
140
511
  * Verify the wallet's attestation history via ZAP1.
141
512
  * Works today against the live API.
142
513
  */
143
514
  export async function getHistory(config, walletId) {
515
+ if (!config.zap1ApiUrl)
516
+ return [];
144
517
  const resp = await fetch(`${config.zap1ApiUrl}/lifecycle/${walletId}`);
145
518
  if (!resp.ok)
146
519
  return [];
@@ -156,6 +529,8 @@ export async function getHistory(config, walletId) {
156
529
  * Works today against the live API.
157
530
  */
158
531
  export async function checkCompliance(config, walletId) {
532
+ if (!config.zap1ApiUrl)
533
+ return { compliant: false, violations: -1, bondDeposits: 0 };
159
534
  const resp = await fetch(`${config.zap1ApiUrl}/agent/${walletId}/policy/verify`);
160
535
  if (!resp.ok)
161
536
  return { compliant: false, violations: -1, bondDeposits: 0 };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@frontiercompute/zcash-ika",
3
- "version": "0.1.0",
4
- "description": "Zero-trust Zcash agent custody via Ika dWallet. Born shielded, stay shielded.",
3
+ "version": "0.2.0",
4
+ "description": "Split-key Zcash custody via Ika dWallet. secp256k1 MPC for transparent, hybrid model for shielded.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "test": "node --experimental-vm-modules dist/test.js"
9
+ "test:dkg": "node dist/test-dkg.js",
10
+ "test:sign": "node dist/test-sign.js"
10
11
  },
11
12
  "dependencies": {
12
13
  "@ika.xyz/sdk": "^0.3.1",
@@ -16,5 +17,12 @@
16
17
  "@types/node": "^25.5.2",
17
18
  "typescript": "^5.4.0"
18
19
  },
20
+ "files": ["dist/index.js", "dist/index.d.ts", "dist/hybrid.js", "dist/hybrid.d.ts"],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Frontier-Compute/zcash-ika.git"
24
+ },
25
+ "author": "zk_nd3r <zk_nd3r@frontiercompute.io>",
26
+ "keywords": ["zcash", "ika", "dwallet", "mpc", "custody", "bitcoin", "split-key"],
19
27
  "license": "MIT"
20
28
  }
@@ -1,17 +0,0 @@
1
- /**
2
- * Ika DKG test - create dWallets on testnet.
3
- *
4
- * Creates:
5
- * 1. Ed25519 dWallet (Zcash Orchard shielded)
6
- * 2. secp256k1 dWallet (Bitcoin + USDC + USDT + any EVM)
7
- *
8
- * One operator, split-key custody across all chains.
9
- * Swiss bank in your pocket. Jailbroken but legal tender.
10
- *
11
- * Requires: SUI_PRIVATE_KEY env var (base64 Sui keypair)
12
- * Get testnet SUI: https://faucet.sui.io
13
- *
14
- * Usage:
15
- * SUI_PRIVATE_KEY=... node dist/test-dkg.js
16
- */
17
- export {};