@frontiercompute/zcash-ika 0.3.0 → 0.4.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.d.ts CHANGED
@@ -71,6 +71,19 @@ export declare const CHAIN_PARAMS: {
71
71
  * 4. Base58 encode (version + hash + checksum)
72
72
  */
73
73
  export declare function deriveZcashAddress(publicKey: Uint8Array, network?: "mainnet" | "testnet"): string;
74
+ /**
75
+ * Derive a Bitcoin P2PKH address from a compressed secp256k1 public key.
76
+ *
77
+ * Same as Zcash transparent but with a 1-byte version prefix:
78
+ * mainnet 0x00 (1...), testnet 0x6f (m.../n...)
79
+ *
80
+ * Steps:
81
+ * 1. SHA256(pubkey) then RIPEMD160 = 20-byte hash
82
+ * 2. Prepend 1-byte version
83
+ * 3. Double-SHA256 checksum (first 4 bytes)
84
+ * 4. Base58 encode (version + hash + checksum)
85
+ */
86
+ export declare function deriveBitcoinAddress(publicKey: Uint8Array, network?: "mainnet" | "testnet"): string;
74
87
  export interface DWalletHandle {
75
88
  /** dWallet object ID on Sui */
76
89
  id: string;
package/dist/index.js CHANGED
@@ -118,6 +118,45 @@ export function deriveZcashAddress(publicKey, network = "mainnet") {
118
118
  full.set(checksum, 22);
119
119
  return base58Encode(full);
120
120
  }
121
+ // Bitcoin P2PKH version bytes (1 byte each)
122
+ const BITCOIN_VERSION_BYTE = {
123
+ mainnet: 0x00, // 1...
124
+ testnet: 0x6f, // m... or n...
125
+ };
126
+ /**
127
+ * Derive a Bitcoin P2PKH address from a compressed secp256k1 public key.
128
+ *
129
+ * Same as Zcash transparent but with a 1-byte version prefix:
130
+ * mainnet 0x00 (1...), testnet 0x6f (m.../n...)
131
+ *
132
+ * Steps:
133
+ * 1. SHA256(pubkey) then RIPEMD160 = 20-byte hash
134
+ * 2. Prepend 1-byte version
135
+ * 3. Double-SHA256 checksum (first 4 bytes)
136
+ * 4. Base58 encode (version + hash + checksum)
137
+ */
138
+ export function deriveBitcoinAddress(publicKey, network = "mainnet") {
139
+ if (publicKey.length !== 33) {
140
+ throw new Error(`Expected 33-byte compressed secp256k1 pubkey, got ${publicKey.length} bytes`);
141
+ }
142
+ const prefix = publicKey[0];
143
+ if (prefix !== 0x02 && prefix !== 0x03) {
144
+ throw new Error(`Invalid compressed pubkey prefix 0x${prefix.toString(16)}, expected 0x02 or 0x03`);
145
+ }
146
+ const pubkeyHash = hash160(publicKey); // 20 bytes
147
+ const version = BITCOIN_VERSION_BYTE[network];
148
+ // version (1) + hash160 (20) = 21 bytes
149
+ const payload = new Uint8Array(21);
150
+ payload[0] = version;
151
+ payload.set(pubkeyHash, 1);
152
+ // checksum: first 4 bytes of SHA256(SHA256(payload))
153
+ const checksum = sha256(sha256(payload)).subarray(0, 4);
154
+ // final: payload (21) + checksum (4) = 25 bytes
155
+ const full = new Uint8Array(25);
156
+ full.set(payload, 0);
157
+ full.set(checksum, 21);
158
+ return base58Encode(full);
159
+ }
121
160
  // Default poll settings for testnet (epochs can be slow)
122
161
  const POLL_OPTS = {
123
162
  timeout: 300_000,
@@ -276,6 +315,9 @@ export async function createWallet(config, chain, _operatorSeed) {
276
315
  if (pubkey && pubkey.length === 33 && chain === "zcash-transparent") {
277
316
  derivedAddress = deriveZcashAddress(pubkey, config.network);
278
317
  }
318
+ else if (pubkey && pubkey.length === 33 && chain === "bitcoin") {
319
+ derivedAddress = deriveBitcoinAddress(pubkey, config.network);
320
+ }
279
321
  return {
280
322
  id: dwalletId,
281
323
  publicKey: pubkey || new Uint8Array(0),
@@ -9,6 +9,7 @@
9
9
  export declare const BRANCH_ID: {
10
10
  readonly NU5: 3268858036;
11
11
  readonly NU6: 3370586197;
12
+ readonly NU61: 1307332080;
12
13
  };
13
14
  export interface UTXO {
14
15
  txid: string;
@@ -16,6 +16,7 @@ const TX_VERSION_GROUP_ID = 0x26a7270a;
16
16
  export const BRANCH_ID = {
17
17
  NU5: 0xc2d6d0b4,
18
18
  NU6: 0xc8e71055,
19
+ NU61: 0x4dec4df0,
19
20
  };
20
21
  // SIGHASH flags
21
22
  const SIGHASH_ALL = 0x01;
@@ -159,7 +160,7 @@ function hashPrevouts(inputs, branchId) {
159
160
  parts.push(outpoint);
160
161
  }
161
162
  const data = Buffer.concat(parts);
162
- return blake2b256(data, personalization("ZTxIdPrevoutHash", branchIdBytes(branchId)));
163
+ return blake2b256(data, personalization("ZTxIdPrevoutHash"));
163
164
  }
164
165
  // Hash of all input amounts
165
166
  function hashAmounts(inputs, branchId) {
@@ -167,7 +168,7 @@ function hashAmounts(inputs, branchId) {
167
168
  for (let i = 0; i < inputs.length; i++) {
168
169
  writeI64LE(data, inputs[i].value, i * 8);
169
170
  }
170
- return blake2b256(data, personalization("ZTxTrAmountsHash", branchIdBytes(branchId)));
171
+ return blake2b256(data, personalization("ZTxTrAmountsHash"));
171
172
  }
172
173
  // Hash of all input scriptPubKeys
173
174
  function hashScriptPubKeys(inputs, branchId) {
@@ -177,7 +178,7 @@ function hashScriptPubKeys(inputs, branchId) {
177
178
  parts.push(inp.script);
178
179
  }
179
180
  const data = Buffer.concat(parts);
180
- return blake2b256(data, personalization("ZTxTrScriptsHash", branchIdBytes(branchId)));
181
+ return blake2b256(data, personalization("ZTxTrScriptsHash"));
181
182
  }
182
183
  // Hash of all sequences
183
184
  function hashSequences(inputs, branchId) {
@@ -185,7 +186,7 @@ function hashSequences(inputs, branchId) {
185
186
  for (let i = 0; i < inputs.length; i++) {
186
187
  writeU32LE(data, inputs[i].sequence, i * 4);
187
188
  }
188
- return blake2b256(data, personalization("ZTxIdSequencHash", branchIdBytes(branchId)));
189
+ return blake2b256(data, personalization("ZTxIdSequencHash"));
189
190
  }
190
191
  // Hash of all transparent outputs
191
192
  function hashOutputs(outputs, branchId) {
@@ -198,70 +199,66 @@ function hashOutputs(outputs, branchId) {
198
199
  parts.push(out.script);
199
200
  }
200
201
  const data = Buffer.concat(parts);
201
- return blake2b256(data, personalization("ZTxIdOutputsHash", branchIdBytes(branchId)));
202
+ return blake2b256(data, personalization("ZTxIdOutputsHash"));
202
203
  }
203
- // Transparent inputs digest (ZIP 244 section T.3a)
204
- function transparentInputsDigest(inputs, branchId) {
205
- const prevoutsHash = hashPrevouts(inputs, branchId);
206
- const amountsHash = hashAmounts(inputs, branchId);
207
- const scriptPubKeysHash = hashScriptPubKeys(inputs, branchId);
208
- const sequencesHash = hashSequences(inputs, branchId);
209
- const data = Buffer.concat([prevoutsHash, amountsHash, scriptPubKeysHash, sequencesHash]);
210
- return blake2b256(data, personalization("ZTxIdTrInHash__", branchIdBytes(branchId)));
211
- }
212
- // Transparent outputs digest (ZIP 244 section T.3b)
213
- function transparentOutputsDigest(outputs, branchId) {
214
- const outputsHash = hashOutputs(outputs, branchId);
215
- return blake2b256(outputsHash, personalization("ZTxIdTrOutHash_", branchIdBytes(branchId)));
216
- }
217
- // Full transparent digest for txid (ZIP 244 T.3)
204
+ // Full transparent digest for txid (ZIP 244 T.2)
205
+ // transparent_digest = BLAKE2b("ZTxIdTranspaHash", prevouts || sequences || outputs)
218
206
  function transparentDigest(inputs, outputs, branchId) {
219
207
  if (inputs.length === 0 && outputs.length === 0) {
220
- return blake2b256(Buffer.alloc(0), personalization("ZTxIdTranspaHash", branchIdBytes(branchId)));
208
+ return blake2b256(Buffer.alloc(0), personalization("ZTxIdTranspaHash"));
221
209
  }
222
- const inDigest = transparentInputsDigest(inputs, branchId);
223
- const outDigest = transparentOutputsDigest(outputs, branchId);
224
- return blake2b256(Buffer.concat([inDigest, outDigest]), personalization("ZTxIdTranspaHash", branchIdBytes(branchId)));
210
+ const prevoutsDigest = hashPrevouts(inputs, branchId);
211
+ const sequenceDigest = hashSequences(inputs, branchId);
212
+ const outputsDigest = hashOutputs(outputs, branchId);
213
+ return blake2b256(Buffer.concat([prevoutsDigest, sequenceDigest, outputsDigest]), personalization("ZTxIdTranspaHash"));
225
214
  }
226
215
  // Sapling digest (empty bundle)
227
- function emptyBundleDigest(tag, branchId) {
228
- return blake2b256(Buffer.alloc(0), personalization(tag, branchIdBytes(branchId)));
216
+ function emptyBundleDigest(tag) {
217
+ return blake2b256(Buffer.alloc(0), personalization(tag));
229
218
  }
230
219
  // Header digest (ZIP 244 T.1)
231
220
  function headerDigest(version, versionGroupId, branchId, lockTime, expiryHeight) {
232
221
  const data = Buffer.alloc(4 + 4 + 4 + 4 + 4);
233
- writeU32LE(data, version, 0);
222
+ writeU32LE(data, (version | (1 << 31)) >>> 0, 0);
234
223
  writeU32LE(data, versionGroupId, 4);
235
224
  writeU32LE(data, branchId, 8);
236
225
  writeU32LE(data, lockTime, 12);
237
226
  writeU32LE(data, expiryHeight, 16);
238
- return blake2b256(data, personalization("ZTxIdHeadersHash", branchIdBytes(branchId)));
227
+ return blake2b256(data, personalization("ZTxIdHeadersHash"));
239
228
  }
240
229
  // Transaction digest for txid (ZIP 244 T)
241
230
  function txidDigest(inputs, outputs, branchId, lockTime, expiryHeight) {
242
231
  const hdrDigest = headerDigest(TX_VERSION, TX_VERSION_GROUP_ID, branchId, lockTime, expiryHeight);
243
232
  const txpDigest = transparentDigest(inputs, outputs, branchId);
244
- const sapDigest = emptyBundleDigest("ZTxIdSaplingHash", branchId);
245
- const orchDigest = emptyBundleDigest("ZTxIdOrchardHash", branchId);
233
+ const sapDigest = emptyBundleDigest("ZTxIdSaplingHash");
234
+ const orchDigest = emptyBundleDigest("ZTxIdOrchardHash");
246
235
  return blake2b256(Buffer.concat([hdrDigest, txpDigest, sapDigest, orchDigest]), personalization("ZcashTxHash__", branchIdBytes(branchId)));
247
236
  }
248
- // Per-input sighash for signing (ZIP 244 S.2 - transparent)
249
- // This is the hash that actually gets signed by ECDSA
237
+ // Per-input sighash for signing (ZIP 244 signature_digest)
238
+ // Structure: BLAKE2b("ZcashTxHash_" || BRANCH_ID,
239
+ // S.1: header_digest
240
+ // S.2: transparent_sig_digest (NOT the txid transparent digest)
241
+ // S.3: sapling_digest
242
+ // S.4: orchard_digest
243
+ // )
250
244
  function transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, inputIndex, hashType) {
251
- // T.1: header digest
245
+ // S.1: header digest (same as T.1)
252
246
  const hdrDigest = headerDigest(TX_VERSION, TX_VERSION_GROUP_ID, branchId, lockTime, expiryHeight);
253
- // T.3: transparent digest (full, for the txid computation)
254
- const txpDigest = transparentDigest(inputs, outputs, branchId);
255
- // T.4: sapling digest (empty)
256
- const sapDigest = emptyBundleDigest("ZTxIdSaplingHash", branchId);
257
- // T.5: orchard digest (empty)
258
- const orchDigest = emptyBundleDigest("ZTxIdOrchardHash", branchId);
259
- // S.2: per-input transparent sighash data
260
- // hash_type (1 byte)
261
- // prevout (32 + 4 bytes)
262
- // value (8 bytes)
263
- // scriptPubKey (compact size + script bytes)
264
- // sequence (4 bytes)
247
+ // S.2: transparent_sig_digest
248
+ // For SIGHASH_ALL without ANYONECANPAY:
249
+ // S.2a: hash_type (1 byte)
250
+ // S.2b: prevouts_sig_digest = prevouts_digest (same as T.2a)
251
+ // S.2c: amounts_sig_digest
252
+ // S.2d: scriptpubkeys_sig_digest
253
+ // S.2e: sequence_sig_digest = sequence_digest (same as T.2b)
254
+ // S.2f: outputs_sig_digest = outputs_digest (same as T.2c)
255
+ // S.2g: txin_sig_digest (per-input)
256
+ const prevoutsSigDigest = hashPrevouts(inputs, branchId);
257
+ const amountsSigDigest = hashAmounts(inputs, branchId);
258
+ const scriptpubkeysSigDigest = hashScriptPubKeys(inputs, branchId);
259
+ const sequenceSigDigest = hashSequences(inputs, branchId);
260
+ const outputsSigDigest = hashOutputs(outputs, branchId);
261
+ // S.2g: txin_sig_digest for the input being signed
265
262
  const inp = inputs[inputIndex];
266
263
  const prevout = Buffer.alloc(36);
267
264
  inp.prevTxid.copy(prevout, 0);
@@ -270,17 +267,23 @@ function transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, i
270
267
  writeI64LE(valueBuf, inp.value, 0);
271
268
  const seqBuf = Buffer.alloc(4);
272
269
  writeU32LE(seqBuf, inp.sequence, 0);
273
- const txinSigDigestData = Buffer.concat([
270
+ const txinSigDigest = blake2b256(Buffer.concat([prevout, valueBuf, compactSize(inp.script.length), inp.script, seqBuf]), personalization("Zcash___TxInHash"));
271
+ // S.2: transparent_sig_digest
272
+ const transparentSigDigest = blake2b256(Buffer.concat([
274
273
  Buffer.from([hashType]),
275
- prevout,
276
- valueBuf,
277
- compactSize(inp.script.length),
278
- inp.script,
279
- seqBuf,
280
- ]);
281
- const txinSigDigest = blake2b256(txinSigDigestData, personalization("Zcash___TxInHash", branchIdBytes(branchId)));
282
- // Final sighash: BLAKE2b of all digests
283
- return blake2b256(Buffer.concat([hdrDigest, txpDigest, sapDigest, orchDigest, txinSigDigest]), personalization("ZcashTxHash__", branchIdBytes(branchId)));
274
+ prevoutsSigDigest,
275
+ amountsSigDigest,
276
+ scriptpubkeysSigDigest,
277
+ sequenceSigDigest,
278
+ outputsSigDigest,
279
+ txinSigDigest,
280
+ ]), personalization("ZTxIdTranspaHash"));
281
+ // S.3: sapling digest (empty)
282
+ const sapDigest = emptyBundleDigest("ZTxIdSaplingHash");
283
+ // S.4: orchard digest (empty)
284
+ const orchDigest = emptyBundleDigest("ZTxIdOrchardHash");
285
+ // Final signature_digest
286
+ return blake2b256(Buffer.concat([hdrDigest, transparentSigDigest, sapDigest, orchDigest]), personalization("ZcashTxHash_", branchIdBytes(branchId)));
284
287
  }
285
288
  // Serialize a v5 transparent-only transaction to raw bytes.
286
289
  // If scriptSigs is provided, inputs get signed scriptSigs.
@@ -402,7 +405,7 @@ export function selectUTXOs(utxos, targetAmount, fee) {
402
405
  * Returns the unsigned serialized TX and per-input sighashes
403
406
  * that need to be signed via MPC.
404
407
  */
405
- export function buildUnsignedTx(utxos, recipient, amount, fee = 10000, changeAddress, branchId = BRANCH_ID.NU5) {
408
+ export function buildUnsignedTx(utxos, recipient, amount, fee = 10000, changeAddress, branchId = BRANCH_ID.NU61) {
406
409
  if (utxos.length === 0)
407
410
  throw new Error("No UTXOs provided");
408
411
  if (amount <= 0)
@@ -467,7 +470,7 @@ function buildScriptSig(derSig, pubkey) {
467
470
  * DER-encoded signatures from MPC, and the compressed pubkey.
468
471
  * Returns hex-encoded signed transaction ready for broadcast.
469
472
  */
470
- export function attachSignatures(utxos, recipient, amount, fee, changeAddress, signatures, pubkey, branchId = BRANCH_ID.NU5) {
473
+ export function attachSignatures(utxos, recipient, amount, fee, changeAddress, signatures, pubkey, branchId = BRANCH_ID.NU61) {
471
474
  if (signatures.length !== utxos.length) {
472
475
  throw new Error(`Expected ${utxos.length} signatures, got ${signatures.length}`);
473
476
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontiercompute/zcash-ika",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Split-key custody for Zcash, Bitcoin, and EVM. 2PC-MPC signing, on-chain spend policy, transparent TX builder, ZAP1 attestation.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,8 @@
12
12
  "dependencies": {
13
13
  "@ika.xyz/sdk": "^0.3.1",
14
14
  "@mysten/sui": "^2.5.0",
15
- "blakejs": "^1.2.1"
15
+ "blakejs": "^1.2.1",
16
+ "elliptic": "^6.6.1"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/node": "^25.5.2",