@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/README.md +67 -85
- package/dist/hybrid.d.ts +119 -0
- package/dist/hybrid.js +148 -0
- package/dist/index.d.ts +83 -60
- package/dist/index.js +456 -81
- package/package.json +11 -3
- package/dist/test-dkg.d.ts +0 -17
- package/dist/test-dkg.js +0 -150
- package/src/index.ts +0 -338
- package/src/test-dkg.ts +0 -199
- package/tsconfig.json +0 -13
package/dist/index.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @frontiercompute/zcash-ika
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Split-key custody for Zcash transparent, Bitcoin, and EVM chains.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
39
|
-
* Same operator controls both via Ika split-key.
|
|
86
|
+
* Derive a Zcash transparent address from a compressed secp256k1 public key.
|
|
40
87
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
*
|
|
127
|
+
* Initialize Ika + Sui clients from config.
|
|
61
128
|
*/
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
84
|
-
* 2.
|
|
85
|
-
* 3. Submit to Ika
|
|
86
|
-
* 4. Poll
|
|
87
|
-
* 5. Extract
|
|
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,
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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
|
|
485
|
+
* Spend from a Zcash transparent wallet.
|
|
113
486
|
*
|
|
114
|
-
* 1. Build Zcash
|
|
115
|
-
* 2.
|
|
116
|
-
* 3. Sign via Ika 2PC-MPC (
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
136
|
-
throw new Error("spendBitcoin requires
|
|
137
|
-
"
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
}
|
package/dist/test-dkg.d.ts
DELETED
|
@@ -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 {};
|