@frontiercompute/zcash-ika 0.1.0 → 0.3.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 +126 -92
- package/dist/hybrid.d.ts +119 -0
- package/dist/hybrid.js +148 -0
- package/dist/index.d.ts +117 -65
- package/dist/index.js +671 -88
- package/dist/tx-builder.d.ts +67 -0
- package/dist/tx-builder.js +534 -0
- package/package.json +32 -4
- 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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zcash v5 transparent transaction builder with ZIP 244 sighash.
|
|
3
|
+
*
|
|
4
|
+
* Builds P2PKH transactions for MPC-signed transparent spends.
|
|
5
|
+
* The signing itself happens via Ika dWallet (secp256k1 ECDSA).
|
|
6
|
+
* This module handles everything else: UTXO fetch, TX structure,
|
|
7
|
+
* sighash computation, signature attachment, and broadcast.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BRANCH_ID: {
|
|
10
|
+
readonly NU5: 3268858036;
|
|
11
|
+
readonly NU6: 3370586197;
|
|
12
|
+
};
|
|
13
|
+
export interface UTXO {
|
|
14
|
+
txid: string;
|
|
15
|
+
outputIndex: number;
|
|
16
|
+
script: string;
|
|
17
|
+
satoshis: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TxOutput {
|
|
20
|
+
address: string;
|
|
21
|
+
amount: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch UTXOs for a transparent address from Zebra RPC.
|
|
25
|
+
* Uses getaddressutxos (requires Zebra with -indexer flag).
|
|
26
|
+
*/
|
|
27
|
+
export declare function fetchUTXOs(zebraRpcUrl: string, tAddress: string): Promise<UTXO[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Select UTXOs to cover the target amount + fee.
|
|
30
|
+
* Simple largest-first selection. Returns selected UTXOs and total value.
|
|
31
|
+
*/
|
|
32
|
+
export declare function selectUTXOs(utxos: UTXO[], targetAmount: number, fee: number): {
|
|
33
|
+
selected: UTXO[];
|
|
34
|
+
totalInput: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Build an unsigned Zcash v5 transparent transaction.
|
|
38
|
+
*
|
|
39
|
+
* Returns the unsigned serialized TX and per-input sighashes
|
|
40
|
+
* that need to be signed via MPC.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildUnsignedTx(utxos: UTXO[], recipient: string, amount: number, fee: number | undefined, changeAddress: string, branchId?: number): {
|
|
43
|
+
unsignedTx: Buffer;
|
|
44
|
+
sighashes: Buffer[];
|
|
45
|
+
txid: Buffer;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Attach MPC signatures to an unsigned transaction.
|
|
49
|
+
*
|
|
50
|
+
* Takes the original UTXO list (to reconstruct inputs/outputs),
|
|
51
|
+
* DER-encoded signatures from MPC, and the compressed pubkey.
|
|
52
|
+
* Returns hex-encoded signed transaction ready for broadcast.
|
|
53
|
+
*/
|
|
54
|
+
export declare function attachSignatures(utxos: UTXO[], recipient: string, amount: number, fee: number, changeAddress: string, signatures: Buffer[], pubkey: Buffer, branchId?: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* Broadcast a signed transaction via Zebra RPC.
|
|
57
|
+
* Returns the txid on success.
|
|
58
|
+
*/
|
|
59
|
+
export declare function broadcastTx(zebraRpcUrl: string, txHex: string): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Estimate fee for a transparent P2PKH transaction.
|
|
62
|
+
*
|
|
63
|
+
* ZIP 317 marginal fee: max(grace_actions, logical_actions) * 5000
|
|
64
|
+
* For simple P2PKH: 1 input + 2 outputs = 2 logical actions = 10000 zatoshis
|
|
65
|
+
* Each additional input adds 1 logical action = +5000 zatoshis
|
|
66
|
+
*/
|
|
67
|
+
export declare function estimateFee(numInputs: number, numOutputs: number): number;
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zcash v5 transparent transaction builder with ZIP 244 sighash.
|
|
3
|
+
*
|
|
4
|
+
* Builds P2PKH transactions for MPC-signed transparent spends.
|
|
5
|
+
* The signing itself happens via Ika dWallet (secp256k1 ECDSA).
|
|
6
|
+
* This module handles everything else: UTXO fetch, TX structure,
|
|
7
|
+
* sighash computation, signature attachment, and broadcast.
|
|
8
|
+
*/
|
|
9
|
+
import blakejs from "blakejs";
|
|
10
|
+
const { blake2bInit, blake2bUpdate, blake2bFinal } = blakejs;
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
// Zcash v5 transaction constants
|
|
13
|
+
const TX_VERSION = 5;
|
|
14
|
+
const TX_VERSION_GROUP_ID = 0x26a7270a;
|
|
15
|
+
// Consensus branch IDs
|
|
16
|
+
export const BRANCH_ID = {
|
|
17
|
+
NU5: 0xc2d6d0b4,
|
|
18
|
+
NU6: 0xc8e71055,
|
|
19
|
+
};
|
|
20
|
+
// SIGHASH flags
|
|
21
|
+
const SIGHASH_ALL = 0x01;
|
|
22
|
+
// Script opcodes for P2PKH
|
|
23
|
+
const OP_DUP = 0x76;
|
|
24
|
+
const OP_HASH160 = 0xa9;
|
|
25
|
+
const OP_EQUALVERIFY = 0x88;
|
|
26
|
+
const OP_CHECKSIG = 0xac;
|
|
27
|
+
// Zcash t-address version prefixes (for decoding)
|
|
28
|
+
const T_ADDR_VERSIONS = {
|
|
29
|
+
"1cb8": { mainnet: true }, // t1...
|
|
30
|
+
"1d25": { mainnet: false }, // tm...
|
|
31
|
+
};
|
|
32
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
33
|
+
function sha256(data) {
|
|
34
|
+
return createHash("sha256").update(data).digest();
|
|
35
|
+
}
|
|
36
|
+
function hash160(data) {
|
|
37
|
+
return createHash("ripemd160").update(sha256(data)).digest();
|
|
38
|
+
}
|
|
39
|
+
// BLAKE2b-256 with personalization
|
|
40
|
+
// blakejs types don't expose the personal param on blake2bInit, but the JS does
|
|
41
|
+
function blake2b256(data, personal) {
|
|
42
|
+
const ctx = blake2bInit(32, undefined, undefined, personal);
|
|
43
|
+
blake2bUpdate(ctx, data);
|
|
44
|
+
return Buffer.from(blake2bFinal(ctx));
|
|
45
|
+
}
|
|
46
|
+
// Write uint32 little-endian into buffer
|
|
47
|
+
function writeU32LE(buf, value, offset) {
|
|
48
|
+
buf.writeUInt32LE(value >>> 0, offset);
|
|
49
|
+
}
|
|
50
|
+
// Write int64 little-endian (as two uint32s, safe for values < 2^53)
|
|
51
|
+
function writeI64LE(buf, value, offset) {
|
|
52
|
+
buf.writeUInt32LE(value & 0xffffffff, offset);
|
|
53
|
+
buf.writeUInt32LE(Math.floor(value / 0x100000000) & 0xffffffff, offset + 4);
|
|
54
|
+
}
|
|
55
|
+
// Compact size encoding (Bitcoin varint)
|
|
56
|
+
function compactSize(n) {
|
|
57
|
+
if (n < 0xfd) {
|
|
58
|
+
return Buffer.from([n]);
|
|
59
|
+
}
|
|
60
|
+
else if (n <= 0xffff) {
|
|
61
|
+
const buf = Buffer.alloc(3);
|
|
62
|
+
buf[0] = 0xfd;
|
|
63
|
+
buf.writeUInt16LE(n, 1);
|
|
64
|
+
return buf;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const buf = Buffer.alloc(5);
|
|
68
|
+
buf[0] = 0xfe;
|
|
69
|
+
buf.writeUInt32LE(n, 1);
|
|
70
|
+
return buf;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Decode a Zcash t-address to its 20-byte pubkey hash
|
|
74
|
+
function decodeTAddress(addr) {
|
|
75
|
+
// Base58 decode
|
|
76
|
+
let num = BigInt(0);
|
|
77
|
+
for (const c of addr) {
|
|
78
|
+
const idx = BASE58_ALPHABET.indexOf(c);
|
|
79
|
+
if (idx < 0)
|
|
80
|
+
throw new Error(`Invalid base58 character: ${c}`);
|
|
81
|
+
num = num * 58n + BigInt(idx);
|
|
82
|
+
}
|
|
83
|
+
// Convert to bytes (26 bytes: 2 version + 20 hash + 4 checksum)
|
|
84
|
+
const bytes = new Uint8Array(26);
|
|
85
|
+
for (let i = 25; i >= 0; i--) {
|
|
86
|
+
bytes[i] = Number(num & 0xffn);
|
|
87
|
+
num = num >> 8n;
|
|
88
|
+
}
|
|
89
|
+
// Verify checksum
|
|
90
|
+
const payload = bytes.subarray(0, 22);
|
|
91
|
+
const checksum = sha256(sha256(payload)).subarray(0, 4);
|
|
92
|
+
for (let i = 0; i < 4; i++) {
|
|
93
|
+
if (bytes[22 + i] !== checksum[i]) {
|
|
94
|
+
throw new Error(`Invalid t-address checksum: ${addr}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const versionHex = Buffer.from(bytes.subarray(0, 2)).toString("hex");
|
|
98
|
+
const info = T_ADDR_VERSIONS[versionHex];
|
|
99
|
+
if (!info) {
|
|
100
|
+
throw new Error(`Unknown t-address version: 0x${versionHex}`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
pubkeyHash: Buffer.from(bytes.subarray(2, 22)),
|
|
104
|
+
mainnet: info.mainnet,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Build a P2PKH scriptPubKey from a 20-byte pubkey hash
|
|
108
|
+
function p2pkhScript(pubkeyHash) {
|
|
109
|
+
// OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
|
|
110
|
+
const script = Buffer.alloc(25);
|
|
111
|
+
script[0] = OP_DUP;
|
|
112
|
+
script[1] = OP_HASH160;
|
|
113
|
+
script[2] = 0x14; // push 20 bytes
|
|
114
|
+
pubkeyHash.copy(script, 3);
|
|
115
|
+
script[23] = OP_EQUALVERIFY;
|
|
116
|
+
script[24] = OP_CHECKSIG;
|
|
117
|
+
return script;
|
|
118
|
+
}
|
|
119
|
+
// Build P2PKH scriptPubKey from a t-address string
|
|
120
|
+
function scriptFromAddress(addr) {
|
|
121
|
+
const { pubkeyHash } = decodeTAddress(addr);
|
|
122
|
+
return p2pkhScript(pubkeyHash);
|
|
123
|
+
}
|
|
124
|
+
// Reverse a hex-encoded txid (internal byte order is reversed)
|
|
125
|
+
function reverseTxid(txid) {
|
|
126
|
+
const buf = Buffer.from(txid, "hex");
|
|
127
|
+
if (buf.length !== 32)
|
|
128
|
+
throw new Error(`Invalid txid length: ${buf.length}`);
|
|
129
|
+
return Buffer.from(buf.reverse());
|
|
130
|
+
}
|
|
131
|
+
// Consensus branch ID as 4-byte LE buffer
|
|
132
|
+
function branchIdBytes(branchId) {
|
|
133
|
+
const buf = Buffer.alloc(4);
|
|
134
|
+
writeU32LE(buf, branchId, 0);
|
|
135
|
+
return buf;
|
|
136
|
+
}
|
|
137
|
+
// ZIP 244 sighash computation for v5 transparent transactions
|
|
138
|
+
// Personalization string as bytes, padded/truncated to 16 bytes
|
|
139
|
+
function personalization(tag, suffix) {
|
|
140
|
+
const tagBytes = Buffer.from(tag, "ascii");
|
|
141
|
+
if (suffix) {
|
|
142
|
+
const result = Buffer.alloc(16);
|
|
143
|
+
tagBytes.copy(result, 0, 0, Math.min(tagBytes.length, 12));
|
|
144
|
+
suffix.copy(result, 12, 0, 4);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// Pad to 16 bytes with zeros
|
|
148
|
+
const result = Buffer.alloc(16);
|
|
149
|
+
tagBytes.copy(result, 0, 0, Math.min(tagBytes.length, 16));
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
// Hash of all prevouts (txid + index) for transparent inputs
|
|
153
|
+
function hashPrevouts(inputs, branchId) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
for (const inp of inputs) {
|
|
156
|
+
const outpoint = Buffer.alloc(36);
|
|
157
|
+
inp.prevTxid.copy(outpoint, 0);
|
|
158
|
+
writeU32LE(outpoint, inp.prevIndex, 32);
|
|
159
|
+
parts.push(outpoint);
|
|
160
|
+
}
|
|
161
|
+
const data = Buffer.concat(parts);
|
|
162
|
+
return blake2b256(data, personalization("ZTxIdPrevoutHash", branchIdBytes(branchId)));
|
|
163
|
+
}
|
|
164
|
+
// Hash of all input amounts
|
|
165
|
+
function hashAmounts(inputs, branchId) {
|
|
166
|
+
const data = Buffer.alloc(inputs.length * 8);
|
|
167
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
168
|
+
writeI64LE(data, inputs[i].value, i * 8);
|
|
169
|
+
}
|
|
170
|
+
return blake2b256(data, personalization("ZTxTrAmountsHash", branchIdBytes(branchId)));
|
|
171
|
+
}
|
|
172
|
+
// Hash of all input scriptPubKeys
|
|
173
|
+
function hashScriptPubKeys(inputs, branchId) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
for (const inp of inputs) {
|
|
176
|
+
parts.push(compactSize(inp.script.length));
|
|
177
|
+
parts.push(inp.script);
|
|
178
|
+
}
|
|
179
|
+
const data = Buffer.concat(parts);
|
|
180
|
+
return blake2b256(data, personalization("ZTxTrScriptsHash", branchIdBytes(branchId)));
|
|
181
|
+
}
|
|
182
|
+
// Hash of all sequences
|
|
183
|
+
function hashSequences(inputs, branchId) {
|
|
184
|
+
const data = Buffer.alloc(inputs.length * 4);
|
|
185
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
186
|
+
writeU32LE(data, inputs[i].sequence, i * 4);
|
|
187
|
+
}
|
|
188
|
+
return blake2b256(data, personalization("ZTxIdSequencHash", branchIdBytes(branchId)));
|
|
189
|
+
}
|
|
190
|
+
// Hash of all transparent outputs
|
|
191
|
+
function hashOutputs(outputs, branchId) {
|
|
192
|
+
const parts = [];
|
|
193
|
+
for (const out of outputs) {
|
|
194
|
+
const valueBuf = Buffer.alloc(8);
|
|
195
|
+
writeI64LE(valueBuf, out.value, 0);
|
|
196
|
+
parts.push(valueBuf);
|
|
197
|
+
parts.push(compactSize(out.script.length));
|
|
198
|
+
parts.push(out.script);
|
|
199
|
+
}
|
|
200
|
+
const data = Buffer.concat(parts);
|
|
201
|
+
return blake2b256(data, personalization("ZTxIdOutputsHash", branchIdBytes(branchId)));
|
|
202
|
+
}
|
|
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)
|
|
218
|
+
function transparentDigest(inputs, outputs, branchId) {
|
|
219
|
+
if (inputs.length === 0 && outputs.length === 0) {
|
|
220
|
+
return blake2b256(Buffer.alloc(0), personalization("ZTxIdTranspaHash", branchIdBytes(branchId)));
|
|
221
|
+
}
|
|
222
|
+
const inDigest = transparentInputsDigest(inputs, branchId);
|
|
223
|
+
const outDigest = transparentOutputsDigest(outputs, branchId);
|
|
224
|
+
return blake2b256(Buffer.concat([inDigest, outDigest]), personalization("ZTxIdTranspaHash", branchIdBytes(branchId)));
|
|
225
|
+
}
|
|
226
|
+
// Sapling digest (empty bundle)
|
|
227
|
+
function emptyBundleDigest(tag, branchId) {
|
|
228
|
+
return blake2b256(Buffer.alloc(0), personalization(tag, branchIdBytes(branchId)));
|
|
229
|
+
}
|
|
230
|
+
// Header digest (ZIP 244 T.1)
|
|
231
|
+
function headerDigest(version, versionGroupId, branchId, lockTime, expiryHeight) {
|
|
232
|
+
const data = Buffer.alloc(4 + 4 + 4 + 4 + 4);
|
|
233
|
+
writeU32LE(data, version, 0);
|
|
234
|
+
writeU32LE(data, versionGroupId, 4);
|
|
235
|
+
writeU32LE(data, branchId, 8);
|
|
236
|
+
writeU32LE(data, lockTime, 12);
|
|
237
|
+
writeU32LE(data, expiryHeight, 16);
|
|
238
|
+
return blake2b256(data, personalization("ZTxIdHeadersHash", branchIdBytes(branchId)));
|
|
239
|
+
}
|
|
240
|
+
// Transaction digest for txid (ZIP 244 T)
|
|
241
|
+
function txidDigest(inputs, outputs, branchId, lockTime, expiryHeight) {
|
|
242
|
+
const hdrDigest = headerDigest(TX_VERSION, TX_VERSION_GROUP_ID, branchId, lockTime, expiryHeight);
|
|
243
|
+
const txpDigest = transparentDigest(inputs, outputs, branchId);
|
|
244
|
+
const sapDigest = emptyBundleDigest("ZTxIdSaplingHash", branchId);
|
|
245
|
+
const orchDigest = emptyBundleDigest("ZTxIdOrchardHash", branchId);
|
|
246
|
+
return blake2b256(Buffer.concat([hdrDigest, txpDigest, sapDigest, orchDigest]), personalization("ZcashTxHash__", branchIdBytes(branchId)));
|
|
247
|
+
}
|
|
248
|
+
// Per-input sighash for signing (ZIP 244 S.2 - transparent)
|
|
249
|
+
// This is the hash that actually gets signed by ECDSA
|
|
250
|
+
function transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, inputIndex, hashType) {
|
|
251
|
+
// T.1: header digest
|
|
252
|
+
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)
|
|
265
|
+
const inp = inputs[inputIndex];
|
|
266
|
+
const prevout = Buffer.alloc(36);
|
|
267
|
+
inp.prevTxid.copy(prevout, 0);
|
|
268
|
+
writeU32LE(prevout, inp.prevIndex, 32);
|
|
269
|
+
const valueBuf = Buffer.alloc(8);
|
|
270
|
+
writeI64LE(valueBuf, inp.value, 0);
|
|
271
|
+
const seqBuf = Buffer.alloc(4);
|
|
272
|
+
writeU32LE(seqBuf, inp.sequence, 0);
|
|
273
|
+
const txinSigDigestData = Buffer.concat([
|
|
274
|
+
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)));
|
|
284
|
+
}
|
|
285
|
+
// Serialize a v5 transparent-only transaction to raw bytes.
|
|
286
|
+
// If scriptSigs is provided, inputs get signed scriptSigs.
|
|
287
|
+
// Otherwise inputs get empty scriptSigs (unsigned).
|
|
288
|
+
function serializeTx(inputs, outputs, branchId, lockTime, expiryHeight, scriptSigs) {
|
|
289
|
+
const parts = [];
|
|
290
|
+
// Header
|
|
291
|
+
const header = Buffer.alloc(4);
|
|
292
|
+
// v5: version field encodes (version | fOverwintered flag)
|
|
293
|
+
// fOverwintered = 1 << 31
|
|
294
|
+
writeU32LE(header, (TX_VERSION | (1 << 31)) >>> 0, 0);
|
|
295
|
+
parts.push(header);
|
|
296
|
+
// nVersionGroupId
|
|
297
|
+
const vgid = Buffer.alloc(4);
|
|
298
|
+
writeU32LE(vgid, TX_VERSION_GROUP_ID, 0);
|
|
299
|
+
parts.push(vgid);
|
|
300
|
+
// nConsensusBranchId
|
|
301
|
+
parts.push(branchIdBytes(branchId));
|
|
302
|
+
// nLockTime
|
|
303
|
+
const lt = Buffer.alloc(4);
|
|
304
|
+
writeU32LE(lt, lockTime, 0);
|
|
305
|
+
parts.push(lt);
|
|
306
|
+
// nExpiryHeight
|
|
307
|
+
const eh = Buffer.alloc(4);
|
|
308
|
+
writeU32LE(eh, expiryHeight, 0);
|
|
309
|
+
parts.push(eh);
|
|
310
|
+
// Transparent bundle
|
|
311
|
+
// tx_in_count
|
|
312
|
+
parts.push(compactSize(inputs.length));
|
|
313
|
+
// tx_in
|
|
314
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
315
|
+
const inp = inputs[i];
|
|
316
|
+
// prevout
|
|
317
|
+
const outpoint = Buffer.alloc(36);
|
|
318
|
+
inp.prevTxid.copy(outpoint, 0);
|
|
319
|
+
writeU32LE(outpoint, inp.prevIndex, 32);
|
|
320
|
+
parts.push(outpoint);
|
|
321
|
+
// scriptSig
|
|
322
|
+
const sig = scriptSigs ? scriptSigs[i] : Buffer.alloc(0);
|
|
323
|
+
parts.push(compactSize(sig.length));
|
|
324
|
+
if (sig.length > 0)
|
|
325
|
+
parts.push(sig);
|
|
326
|
+
// sequence
|
|
327
|
+
const seq = Buffer.alloc(4);
|
|
328
|
+
writeU32LE(seq, inp.sequence, 0);
|
|
329
|
+
parts.push(seq);
|
|
330
|
+
}
|
|
331
|
+
// tx_out_count
|
|
332
|
+
parts.push(compactSize(outputs.length));
|
|
333
|
+
// tx_out
|
|
334
|
+
for (const out of outputs) {
|
|
335
|
+
const valueBuf = Buffer.alloc(8);
|
|
336
|
+
writeI64LE(valueBuf, out.value, 0);
|
|
337
|
+
parts.push(valueBuf);
|
|
338
|
+
parts.push(compactSize(out.script.length));
|
|
339
|
+
parts.push(out.script);
|
|
340
|
+
}
|
|
341
|
+
// Sapling bundle (empty)
|
|
342
|
+
parts.push(compactSize(0)); // nSpendsSapling
|
|
343
|
+
parts.push(compactSize(0)); // nOutputsSapling
|
|
344
|
+
// Orchard bundle (empty)
|
|
345
|
+
parts.push(Buffer.from([0x00])); // nActionsOrchard = 0
|
|
346
|
+
return Buffer.concat(parts);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Fetch UTXOs for a transparent address from Zebra RPC.
|
|
350
|
+
* Uses getaddressutxos (requires Zebra with -indexer flag).
|
|
351
|
+
*/
|
|
352
|
+
export async function fetchUTXOs(zebraRpcUrl, tAddress) {
|
|
353
|
+
const resp = await fetch(zebraRpcUrl, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: { "Content-Type": "application/json" },
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
jsonrpc: "2.0",
|
|
358
|
+
id: 1,
|
|
359
|
+
method: "getaddressutxos",
|
|
360
|
+
params: [{ addresses: [tAddress] }],
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
if (!resp.ok) {
|
|
364
|
+
throw new Error(`Zebra RPC error: ${resp.status} ${resp.statusText}`);
|
|
365
|
+
}
|
|
366
|
+
const data = (await resp.json());
|
|
367
|
+
if (data.error) {
|
|
368
|
+
throw new Error(`Zebra RPC: ${data.error.message || JSON.stringify(data.error)}`);
|
|
369
|
+
}
|
|
370
|
+
const utxos = (data.result || []).map((u) => ({
|
|
371
|
+
txid: u.txid,
|
|
372
|
+
outputIndex: u.outputIndex ?? u.vout ?? u.index,
|
|
373
|
+
script: u.script ?? u.scriptPubKey,
|
|
374
|
+
satoshis: u.satoshis ?? u.value ?? u.amount,
|
|
375
|
+
}));
|
|
376
|
+
return utxos;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Select UTXOs to cover the target amount + fee.
|
|
380
|
+
* Simple largest-first selection. Returns selected UTXOs and total value.
|
|
381
|
+
*/
|
|
382
|
+
export function selectUTXOs(utxos, targetAmount, fee) {
|
|
383
|
+
const needed = targetAmount + fee;
|
|
384
|
+
// Sort descending by value
|
|
385
|
+
const sorted = [...utxos].sort((a, b) => b.satoshis - a.satoshis);
|
|
386
|
+
const selected = [];
|
|
387
|
+
let total = 0;
|
|
388
|
+
for (const u of sorted) {
|
|
389
|
+
selected.push(u);
|
|
390
|
+
total += u.satoshis;
|
|
391
|
+
if (total >= needed)
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
if (total < needed) {
|
|
395
|
+
throw new Error(`Insufficient funds: have ${total} zatoshis, need ${needed} (${targetAmount} + ${fee} fee)`);
|
|
396
|
+
}
|
|
397
|
+
return { selected, totalInput: total };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Build an unsigned Zcash v5 transparent transaction.
|
|
401
|
+
*
|
|
402
|
+
* Returns the unsigned serialized TX and per-input sighashes
|
|
403
|
+
* that need to be signed via MPC.
|
|
404
|
+
*/
|
|
405
|
+
export function buildUnsignedTx(utxos, recipient, amount, fee = 10000, changeAddress, branchId = BRANCH_ID.NU5) {
|
|
406
|
+
if (utxos.length === 0)
|
|
407
|
+
throw new Error("No UTXOs provided");
|
|
408
|
+
if (amount <= 0)
|
|
409
|
+
throw new Error("Amount must be positive");
|
|
410
|
+
// Build inputs
|
|
411
|
+
const inputs = utxos.map((u) => ({
|
|
412
|
+
prevTxid: reverseTxid(u.txid),
|
|
413
|
+
prevIndex: u.outputIndex,
|
|
414
|
+
script: Buffer.from(u.script, "hex"),
|
|
415
|
+
value: u.satoshis,
|
|
416
|
+
sequence: 0xffffffff,
|
|
417
|
+
}));
|
|
418
|
+
// Build outputs
|
|
419
|
+
const totalInput = utxos.reduce((s, u) => s + u.satoshis, 0);
|
|
420
|
+
const change = totalInput - amount - fee;
|
|
421
|
+
const outputs = [
|
|
422
|
+
{ value: amount, script: scriptFromAddress(recipient) },
|
|
423
|
+
];
|
|
424
|
+
if (change > 0) {
|
|
425
|
+
// Dust threshold: skip change if below 546 zatoshis
|
|
426
|
+
if (change >= 546) {
|
|
427
|
+
outputs.push({ value: change, script: scriptFromAddress(changeAddress) });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (change < 0) {
|
|
431
|
+
throw new Error(`UTXOs total ${totalInput} < amount ${amount} + fee ${fee}`);
|
|
432
|
+
}
|
|
433
|
+
const lockTime = 0;
|
|
434
|
+
const expiryHeight = 0;
|
|
435
|
+
// Serialize unsigned TX
|
|
436
|
+
const unsignedTx = serializeTx(inputs, outputs, branchId, lockTime, expiryHeight);
|
|
437
|
+
// Compute per-input sighashes
|
|
438
|
+
const sighashes = [];
|
|
439
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
440
|
+
const sh = transparentSighash(inputs, outputs, branchId, lockTime, expiryHeight, i, SIGHASH_ALL);
|
|
441
|
+
sighashes.push(sh);
|
|
442
|
+
}
|
|
443
|
+
// Compute txid (hash of unsigned TX structure per ZIP 244)
|
|
444
|
+
const txid = txidDigest(inputs, outputs, branchId, lockTime, expiryHeight);
|
|
445
|
+
return { unsignedTx, sighashes, txid };
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Build a P2PKH scriptSig from a DER signature and compressed pubkey.
|
|
449
|
+
*
|
|
450
|
+
* Format: <sig_length> <DER_sig + SIGHASH_ALL_byte> <pubkey_length> <compressed_pubkey>
|
|
451
|
+
*/
|
|
452
|
+
function buildScriptSig(derSig, pubkey) {
|
|
453
|
+
// Append SIGHASH_ALL byte to signature
|
|
454
|
+
const sigWithHashType = Buffer.concat([derSig, Buffer.from([SIGHASH_ALL])]);
|
|
455
|
+
const parts = [
|
|
456
|
+
Buffer.from([sigWithHashType.length]),
|
|
457
|
+
sigWithHashType,
|
|
458
|
+
Buffer.from([pubkey.length]),
|
|
459
|
+
pubkey,
|
|
460
|
+
];
|
|
461
|
+
return Buffer.concat(parts);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Attach MPC signatures to an unsigned transaction.
|
|
465
|
+
*
|
|
466
|
+
* Takes the original UTXO list (to reconstruct inputs/outputs),
|
|
467
|
+
* DER-encoded signatures from MPC, and the compressed pubkey.
|
|
468
|
+
* Returns hex-encoded signed transaction ready for broadcast.
|
|
469
|
+
*/
|
|
470
|
+
export function attachSignatures(utxos, recipient, amount, fee, changeAddress, signatures, pubkey, branchId = BRANCH_ID.NU5) {
|
|
471
|
+
if (signatures.length !== utxos.length) {
|
|
472
|
+
throw new Error(`Expected ${utxos.length} signatures, got ${signatures.length}`);
|
|
473
|
+
}
|
|
474
|
+
if (pubkey.length !== 33) {
|
|
475
|
+
throw new Error(`Expected 33-byte compressed pubkey, got ${pubkey.length}`);
|
|
476
|
+
}
|
|
477
|
+
// Rebuild inputs/outputs (same as buildUnsignedTx)
|
|
478
|
+
const inputs = utxos.map((u) => ({
|
|
479
|
+
prevTxid: reverseTxid(u.txid),
|
|
480
|
+
prevIndex: u.outputIndex,
|
|
481
|
+
script: Buffer.from(u.script, "hex"),
|
|
482
|
+
value: u.satoshis,
|
|
483
|
+
sequence: 0xffffffff,
|
|
484
|
+
}));
|
|
485
|
+
const totalInput = utxos.reduce((s, u) => s + u.satoshis, 0);
|
|
486
|
+
const change = totalInput - amount - fee;
|
|
487
|
+
const outputs = [
|
|
488
|
+
{ value: amount, script: scriptFromAddress(recipient) },
|
|
489
|
+
];
|
|
490
|
+
if (change >= 546) {
|
|
491
|
+
outputs.push({ value: change, script: scriptFromAddress(changeAddress) });
|
|
492
|
+
}
|
|
493
|
+
// Build scriptSigs
|
|
494
|
+
const scriptSigs = signatures.map((sig) => buildScriptSig(sig, pubkey));
|
|
495
|
+
// Serialize signed TX
|
|
496
|
+
const signedTx = serializeTx(inputs, outputs, branchId, 0, 0, scriptSigs);
|
|
497
|
+
return signedTx.toString("hex");
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Broadcast a signed transaction via Zebra RPC.
|
|
501
|
+
* Returns the txid on success.
|
|
502
|
+
*/
|
|
503
|
+
export async function broadcastTx(zebraRpcUrl, txHex) {
|
|
504
|
+
const resp = await fetch(zebraRpcUrl, {
|
|
505
|
+
method: "POST",
|
|
506
|
+
headers: { "Content-Type": "application/json" },
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
jsonrpc: "2.0",
|
|
509
|
+
id: 1,
|
|
510
|
+
method: "sendrawtransaction",
|
|
511
|
+
params: [txHex],
|
|
512
|
+
}),
|
|
513
|
+
});
|
|
514
|
+
if (!resp.ok) {
|
|
515
|
+
throw new Error(`Zebra RPC error: ${resp.status} ${resp.statusText}`);
|
|
516
|
+
}
|
|
517
|
+
const data = (await resp.json());
|
|
518
|
+
if (data.error) {
|
|
519
|
+
throw new Error(`Broadcast failed: ${data.error.message || JSON.stringify(data.error)}`);
|
|
520
|
+
}
|
|
521
|
+
return data.result;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Estimate fee for a transparent P2PKH transaction.
|
|
525
|
+
*
|
|
526
|
+
* ZIP 317 marginal fee: max(grace_actions, logical_actions) * 5000
|
|
527
|
+
* For simple P2PKH: 1 input + 2 outputs = 2 logical actions = 10000 zatoshis
|
|
528
|
+
* Each additional input adds 1 logical action = +5000 zatoshis
|
|
529
|
+
*/
|
|
530
|
+
export function estimateFee(numInputs, numOutputs) {
|
|
531
|
+
const logicalActions = Math.max(numInputs, numOutputs);
|
|
532
|
+
const graceActions = 2;
|
|
533
|
+
return Math.max(graceActions, logicalActions) * 5000;
|
|
534
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontiercompute/zcash-ika",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
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",
|
|
13
|
-
"@mysten/sui": "^2.5.0"
|
|
14
|
+
"@mysten/sui": "^2.5.0",
|
|
15
|
+
"blakejs": "^1.2.1"
|
|
14
16
|
},
|
|
15
17
|
"devDependencies": {
|
|
16
18
|
"@types/node": "^25.5.2",
|
|
17
19
|
"typescript": "^5.4.0"
|
|
18
20
|
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/index.js",
|
|
23
|
+
"dist/index.d.ts",
|
|
24
|
+
"dist/tx-builder.js",
|
|
25
|
+
"dist/tx-builder.d.ts",
|
|
26
|
+
"dist/hybrid.js",
|
|
27
|
+
"dist/hybrid.d.ts"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/Frontier-Compute/zcash-ika.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "zk_nd3r <zk_nd3r@frontiercompute.io>",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"zcash",
|
|
36
|
+
"ika",
|
|
37
|
+
"dwallet",
|
|
38
|
+
"mpc",
|
|
39
|
+
"custody",
|
|
40
|
+
"bitcoin",
|
|
41
|
+
"split-key",
|
|
42
|
+
"policy",
|
|
43
|
+
"sui-move",
|
|
44
|
+
"transparent-tx",
|
|
45
|
+
"attestation"
|
|
46
|
+
],
|
|
19
47
|
"license": "MIT"
|
|
20
48
|
}
|
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 {};
|