@xchainjs/zcash-js 1.0.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/LICENSE +21 -0
- package/README.md +71 -0
- package/lib/addr.d.ts +5 -0
- package/lib/builder.d.ts +4 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.esm.js +517 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +537 -0
- package/lib/index.js.map +1 -0
- package/lib/rpc.d.ts +4 -0
- package/lib/script.d.ts +3 -0
- package/lib/types.d.ts +30 -0
- package/lib/writer.d.ts +2 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 THORChain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @xchainjs/zcash-js
|
|
2
|
+
|
|
3
|
+
Zcash JavaScript library for XChainJS - a fork of @mayaprotocol/zcash-js with critical fixes.
|
|
4
|
+
|
|
5
|
+
## Key Fixes
|
|
6
|
+
|
|
7
|
+
### NU6.1 Consensus Branch ID (CRITICAL)
|
|
8
|
+
|
|
9
|
+
This package includes the updated NU6.1 consensus branch ID (`0x4DEC4DF0`) which activated on November 24, 2025 at block height 3,146,400.
|
|
10
|
+
|
|
11
|
+
The original @mayaprotocol/zcash-js package uses the outdated NU5 branch ID (`0xc8e71055`), causing all transactions to be rejected by the network with "bad-txns-wrong-version" errors.
|
|
12
|
+
|
|
13
|
+
### Browser Compatibility
|
|
14
|
+
|
|
15
|
+
- Replaced `blake2b-wasm` with `@noble/hashes/blake2b` - pure JavaScript implementation that works in browsers
|
|
16
|
+
- Uses synchronous hashing instead of async WASM loading
|
|
17
|
+
- No Node.js-specific APIs required
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { buildTx, signAndFinalize, getFee } from '@xchainjs/zcash-js'
|
|
23
|
+
|
|
24
|
+
// Build an unsigned transaction
|
|
25
|
+
const tx = await buildTx(
|
|
26
|
+
currentHeight,
|
|
27
|
+
senderAddress,
|
|
28
|
+
recipientAddress,
|
|
29
|
+
amount, // in satoshis
|
|
30
|
+
utxos,
|
|
31
|
+
true, // isMainnet
|
|
32
|
+
'optional memo'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Sign and finalize
|
|
36
|
+
const signedTx = await signAndFinalize(
|
|
37
|
+
currentHeight,
|
|
38
|
+
privateKeyHex,
|
|
39
|
+
tx.inputs,
|
|
40
|
+
tx.outputs
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Broadcast the hex-encoded transaction
|
|
44
|
+
const txHex = signedTx.toString('hex')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `buildTx(height, from, to, amount, utxos, isMainnet, memo?)`
|
|
50
|
+
|
|
51
|
+
Creates an unsigned transaction with automatic UTXO selection.
|
|
52
|
+
|
|
53
|
+
### `signAndFinalize(height, privateKey, utxos, outputs)`
|
|
54
|
+
|
|
55
|
+
Signs the transaction and returns the serialized transaction buffer.
|
|
56
|
+
|
|
57
|
+
### `getFee(inputCount, outputCount, memo?)`
|
|
58
|
+
|
|
59
|
+
Calculates the transaction fee using Zcash's ZIP 317 fee algorithm.
|
|
60
|
+
|
|
61
|
+
## Network Upgrade History
|
|
62
|
+
|
|
63
|
+
| Version | Branch ID | Activation |
|
|
64
|
+
|---------|-----------|------------|
|
|
65
|
+
| NU5 | 0xc8e71055 | May 2022 |
|
|
66
|
+
| NU6 | 0xc8e71055 | Nov 2024 |
|
|
67
|
+
| NU6.1 | 0x4DEC4DF0 | Nov 24, 2025 |
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/lib/addr.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const testnetPrefix: number[];
|
|
2
|
+
export declare const mainnetPrefix: number[];
|
|
3
|
+
export declare function isValidAddr(address: string, prefix: number[] | Buffer | Uint8Array): boolean;
|
|
4
|
+
export declare function pkToAddr(pk: Uint8Array, prefix: number[] | Uint8Array): string;
|
|
5
|
+
export declare function skToAddr(sk: Uint8Array, prefix: number[] | Uint8Array): string;
|
package/lib/builder.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Output, Tx, UTXO } from './types';
|
|
2
|
+
export declare function getFee(inCount: number, outCount: number, memo?: string): number;
|
|
3
|
+
export declare function buildTx(height: number, from: string, to: string, amount: number, utxos: UTXO[], isMainnet: boolean, memo?: string): Promise<Tx>;
|
|
4
|
+
export declare function signAndFinalize(height: number, skb: string, utxos: UTXO[], outputs: Output[]): Promise<Buffer>;
|
package/lib/index.d.ts
ADDED
package/lib/index.esm.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
2
|
+
import { ripemd160 } from '@noble/hashes/ripemd160';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
4
|
+
import bs58check from 'bs58check';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { JSONRPCClient } from 'json-rpc-2.0';
|
|
7
|
+
import { blake2b } from '@noble/hashes/blake2b';
|
|
8
|
+
import { sumBy, min } from 'lodash';
|
|
9
|
+
|
|
10
|
+
const testnetPrefix = [0x1d, 0x25];
|
|
11
|
+
const mainnetPrefix = [0x1c, 0xb8];
|
|
12
|
+
function isValidAddr(address, prefix) {
|
|
13
|
+
try {
|
|
14
|
+
const addrb = bs58check.decode(address);
|
|
15
|
+
if (Buffer.from(addrb.slice(0, 2)).compare(Buffer.from(prefix)) != 0) {
|
|
16
|
+
throw new Error('Invalid prefix');
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (_a) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function pkToAddr(pk, prefix) {
|
|
25
|
+
const hash = sha256(pk);
|
|
26
|
+
const pkh = ripemd160(hash);
|
|
27
|
+
const addrb = Buffer.alloc(22);
|
|
28
|
+
Buffer.from(prefix).copy(addrb);
|
|
29
|
+
Buffer.from(pkh).copy(addrb, 2);
|
|
30
|
+
const addr = bs58check.encode(addrb);
|
|
31
|
+
return addr;
|
|
32
|
+
}
|
|
33
|
+
function skToAddr(sk, prefix) {
|
|
34
|
+
const pk = secp256k1.getPublicKey(sk, true);
|
|
35
|
+
return pkToAddr(pk, prefix);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/******************************************************************************
|
|
39
|
+
Copyright (c) Microsoft Corporation.
|
|
40
|
+
|
|
41
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
42
|
+
purpose with or without fee is hereby granted.
|
|
43
|
+
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
45
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
46
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
47
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
48
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
49
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
50
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
51
|
+
***************************************************************************** */
|
|
52
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
56
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
57
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
58
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
59
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
60
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
61
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
66
|
+
var e = new Error(message);
|
|
67
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const RPC_TIMEOUT = 30000; // 30 seconds
|
|
71
|
+
function makeClient(config) {
|
|
72
|
+
const client = new JSONRPCClient((jsonRPCRequest) => __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
const response = yield axios.post(config.server.host, jsonRPCRequest, {
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
auth: {
|
|
76
|
+
username: config.server.user,
|
|
77
|
+
password: config.server.password,
|
|
78
|
+
},
|
|
79
|
+
timeout: RPC_TIMEOUT,
|
|
80
|
+
});
|
|
81
|
+
client.receive(response.data);
|
|
82
|
+
}));
|
|
83
|
+
return client;
|
|
84
|
+
}
|
|
85
|
+
function getUTXOS(from, config) {
|
|
86
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
const client = makeClient(config);
|
|
88
|
+
const utxos = yield client.request('getaddressutxos', [from]);
|
|
89
|
+
return utxos;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function waitForTransaction(txid_1, config_1) {
|
|
93
|
+
return __awaiter(this, arguments, void 0, function* (txid, config, maxAttempts = 30) {
|
|
94
|
+
const client = makeClient(config);
|
|
95
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
96
|
+
try {
|
|
97
|
+
const tx = yield client.request('gettransaction', [txid]);
|
|
98
|
+
if (tx && tx.confirmations > 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (_a) {
|
|
103
|
+
// Transaction might not be in wallet
|
|
104
|
+
}
|
|
105
|
+
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Transaction ${txid} not confirmed after ${maxAttempts} attempts`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function sendRawTransaction(txb, config) {
|
|
111
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
try {
|
|
114
|
+
// Direct axios call to get better error details
|
|
115
|
+
const response = yield axios.post(config.server.host, {
|
|
116
|
+
jsonrpc: '2.0',
|
|
117
|
+
method: 'sendrawtransaction',
|
|
118
|
+
params: [txb.toString('hex')],
|
|
119
|
+
id: 1,
|
|
120
|
+
}, {
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
auth: {
|
|
123
|
+
username: config.server.user,
|
|
124
|
+
password: config.server.password,
|
|
125
|
+
},
|
|
126
|
+
timeout: RPC_TIMEOUT,
|
|
127
|
+
});
|
|
128
|
+
if (response.data.error) {
|
|
129
|
+
console.error('RPC Error:', response.data.error);
|
|
130
|
+
throw new Error(response.data.error.message);
|
|
131
|
+
}
|
|
132
|
+
return response.data.result;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const axiosError = error;
|
|
136
|
+
if ((_b = (_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) {
|
|
137
|
+
throw new Error(axiosError.response.data.error.message);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeCompactInt(value) {
|
|
145
|
+
if (value < 0xfd) {
|
|
146
|
+
return Buffer.from([value]);
|
|
147
|
+
}
|
|
148
|
+
else if (value <= 0xffff) {
|
|
149
|
+
// 0xFD followed by 16-bit integer
|
|
150
|
+
const buffer = Buffer.alloc(3);
|
|
151
|
+
buffer[0] = 0xfd;
|
|
152
|
+
buffer.writeUInt16LE(value, 1);
|
|
153
|
+
return buffer;
|
|
154
|
+
}
|
|
155
|
+
else if (value <= 0xffffffff) {
|
|
156
|
+
// 0xFE followed by 32-bit integer
|
|
157
|
+
const buffer = Buffer.alloc(5);
|
|
158
|
+
buffer[0] = 0xfe;
|
|
159
|
+
buffer.writeUInt32LE(value, 1);
|
|
160
|
+
return buffer;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// 0xFF followed by 64-bit integer
|
|
164
|
+
const buffer = Buffer.alloc(9);
|
|
165
|
+
buffer[0] = 0xff;
|
|
166
|
+
const bigValue = BigInt(value);
|
|
167
|
+
buffer.writeBigUInt64LE(bigValue, 1);
|
|
168
|
+
return buffer;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function pushData(length) {
|
|
172
|
+
const buf = Buffer.alloc(2);
|
|
173
|
+
let offset = 0;
|
|
174
|
+
if (length < 0x4c) {
|
|
175
|
+
buf[0] = length;
|
|
176
|
+
offset += 1;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
buf[0] = 0x4c;
|
|
180
|
+
buf[1] = length;
|
|
181
|
+
offset += 2;
|
|
182
|
+
}
|
|
183
|
+
return buf.subarray(0, offset);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function memoToScript(memo) {
|
|
187
|
+
const opr = Buffer.alloc(memo.length + 4);
|
|
188
|
+
opr[1] = 0x6a;
|
|
189
|
+
let offset = 2;
|
|
190
|
+
const pml = pushData(memo.length);
|
|
191
|
+
pml.copy(opr, offset);
|
|
192
|
+
offset += pml.length;
|
|
193
|
+
Buffer.from(memo).copy(opr, offset);
|
|
194
|
+
offset += memo.length;
|
|
195
|
+
opr[0] = offset - 1;
|
|
196
|
+
const script = opr.subarray(0, offset);
|
|
197
|
+
return script;
|
|
198
|
+
}
|
|
199
|
+
function addressToScript(address) {
|
|
200
|
+
const addrb = bs58check.decode(address);
|
|
201
|
+
const pkh = Buffer.alloc(20);
|
|
202
|
+
Buffer.from(addrb).copy(pkh, 0, 2);
|
|
203
|
+
const script = Buffer.alloc(26);
|
|
204
|
+
Buffer.from('1976a914', 'hex').copy(script);
|
|
205
|
+
Buffer.from(pkh).copy(script, 4);
|
|
206
|
+
Buffer.from('88ac', 'hex').copy(script, 24);
|
|
207
|
+
return script;
|
|
208
|
+
}
|
|
209
|
+
function writeSigScript(signature, pk) {
|
|
210
|
+
const buf = Buffer.alloc(5 + signature.length + pk.length);
|
|
211
|
+
let offset = 0;
|
|
212
|
+
const psl = pushData(signature.length + 1);
|
|
213
|
+
psl.copy(buf, offset);
|
|
214
|
+
offset += psl.length;
|
|
215
|
+
Buffer.from(signature).copy(buf, offset);
|
|
216
|
+
offset += signature.length;
|
|
217
|
+
buf[offset] = 1;
|
|
218
|
+
offset += 1;
|
|
219
|
+
const pkl = pushData(pk.length);
|
|
220
|
+
pkl.copy(buf, offset);
|
|
221
|
+
offset += pkl.length;
|
|
222
|
+
Buffer.from(pk).copy(buf, offset);
|
|
223
|
+
offset += pk.length;
|
|
224
|
+
return buf.subarray(0, offset);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// NU6.1 Consensus Branch ID - activated November 24, 2025 at block height 3146400
|
|
228
|
+
// See ZIP 255: https://zips.z.cash/zip-0255
|
|
229
|
+
const NU6_1_CONSENSUS_BRANCH_ID = 0x4dec4df0;
|
|
230
|
+
// Version Group ID for transaction version 5
|
|
231
|
+
const TX_VERSION_GROUP_ID = 0x26a7270a;
|
|
232
|
+
// Transaction version 5 with overwinter flag
|
|
233
|
+
const TX_VERSION = 0x80000005;
|
|
234
|
+
const PKH_OUTPUT_SIZE = 34;
|
|
235
|
+
const MARGINAL_FEE = 5000;
|
|
236
|
+
const GRACE_ACTIONS = 2;
|
|
237
|
+
function calculateFee(inCount, outCount) {
|
|
238
|
+
const logicalActions = inCount + outCount;
|
|
239
|
+
return MARGINAL_FEE * Math.max(GRACE_ACTIONS, logicalActions);
|
|
240
|
+
}
|
|
241
|
+
function getFee(inCount, outCount, memo) {
|
|
242
|
+
if (memo && memo.length > 0) {
|
|
243
|
+
const memoLenWithOverhead = memo.length + 2;
|
|
244
|
+
const memoOutputSlots = Math.floor((memoLenWithOverhead + PKH_OUTPUT_SIZE - 1) / PKH_OUTPUT_SIZE);
|
|
245
|
+
outCount += memoOutputSlots;
|
|
246
|
+
}
|
|
247
|
+
return calculateFee(inCount, outCount);
|
|
248
|
+
}
|
|
249
|
+
function selectUTXOS(utxos, amount, memo) {
|
|
250
|
+
let currentFee = 0;
|
|
251
|
+
const selected = [];
|
|
252
|
+
let remaining = amount;
|
|
253
|
+
for (const utxo of utxos) {
|
|
254
|
+
if (remaining == 0)
|
|
255
|
+
break;
|
|
256
|
+
selected.push(utxo);
|
|
257
|
+
const fee = getFee(selected.length, 2, memo);
|
|
258
|
+
const deltaFee = fee - currentFee;
|
|
259
|
+
currentFee = fee;
|
|
260
|
+
remaining += deltaFee;
|
|
261
|
+
const used = min([utxo.satoshis, remaining]);
|
|
262
|
+
remaining -= used;
|
|
263
|
+
}
|
|
264
|
+
return selected;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Create a blake2b hash with personalization string
|
|
268
|
+
*/
|
|
269
|
+
function blake2bWithPersonal(data, personal) {
|
|
270
|
+
const personalBytes = typeof personal === 'string' ? new TextEncoder().encode(personal) : personal;
|
|
271
|
+
// Pad personal to 16 bytes
|
|
272
|
+
const paddedPersonal = new Uint8Array(16);
|
|
273
|
+
paddedPersonal.set(personalBytes.slice(0, 16));
|
|
274
|
+
return blake2b(data, { dkLen: 32, personalization: paddedPersonal });
|
|
275
|
+
}
|
|
276
|
+
function buildTx(height, from, to, amount, utxos, isMainnet, memo) {
|
|
277
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
const prefixb = isMainnet ? mainnetPrefix : testnetPrefix;
|
|
279
|
+
const prefix = Buffer.from(prefixb);
|
|
280
|
+
if (!isValidAddr(from, prefix))
|
|
281
|
+
throw new Error('Invalid "from" address');
|
|
282
|
+
if (!isValidAddr(to, prefix))
|
|
283
|
+
throw new Error('Invalid "to" address');
|
|
284
|
+
if (amount > 1e14)
|
|
285
|
+
throw new Error('Amount too large');
|
|
286
|
+
if (memo && memo.length > 80)
|
|
287
|
+
throw new Error('Memo too long');
|
|
288
|
+
const inputs = selectUTXOS(utxos, amount, memo);
|
|
289
|
+
const outputCount = memo ? 3 : 2; // change + to + memo (if exists)
|
|
290
|
+
const fee = getFee(inputs.length, outputCount, memo);
|
|
291
|
+
const change = sumBy(inputs, (u) => u.satoshis) - amount - fee;
|
|
292
|
+
if (change < 0)
|
|
293
|
+
throw new Error('Not enough funds');
|
|
294
|
+
const outputs = [];
|
|
295
|
+
outputs.push({
|
|
296
|
+
type: 'pkh',
|
|
297
|
+
address: from,
|
|
298
|
+
amount: change,
|
|
299
|
+
});
|
|
300
|
+
outputs.push({
|
|
301
|
+
type: 'pkh',
|
|
302
|
+
address: to,
|
|
303
|
+
amount: amount,
|
|
304
|
+
});
|
|
305
|
+
if (memo) {
|
|
306
|
+
outputs.push({
|
|
307
|
+
type: 'op_return',
|
|
308
|
+
memo: memo,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
height: height,
|
|
313
|
+
inputs: inputs,
|
|
314
|
+
outputs: outputs,
|
|
315
|
+
fee: fee,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function signAndFinalize(height, skb, utxos, outputs) {
|
|
320
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
321
|
+
const sk = new Uint8Array(Buffer.from(skb, 'hex'));
|
|
322
|
+
const pk = secp256k1.getPublicKey(sk, true);
|
|
323
|
+
let offset = 0;
|
|
324
|
+
// HEADER with NU6.1 consensus branch ID
|
|
325
|
+
let buf = Buffer.alloc(20);
|
|
326
|
+
buf.writeUInt32LE(TX_VERSION, 0); // Transaction version 5 with overwinter flag
|
|
327
|
+
buf.writeUInt32LE(TX_VERSION_GROUP_ID, 4); // Version group ID
|
|
328
|
+
buf.writeUInt32LE(NU6_1_CONSENSUS_BRANCH_ID, 8); // NU6.1 consensus branch ID (FIXED!)
|
|
329
|
+
buf.writeUInt32LE(0x00000000, 12); // Lock time
|
|
330
|
+
buf.writeUInt32LE(height, 16); // Expiry height
|
|
331
|
+
const headerHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxIdHeadersHash')).toString('hex');
|
|
332
|
+
buf = Buffer.alloc(36 * utxos.length);
|
|
333
|
+
for (const [i, utxo] of utxos.entries()) {
|
|
334
|
+
const txid = Buffer.from(utxo.txid, 'hex');
|
|
335
|
+
txid.reverse();
|
|
336
|
+
txid.copy(buf, 36 * i);
|
|
337
|
+
buf.writeUInt32LE(utxo.outputIndex, 36 * i + 32);
|
|
338
|
+
}
|
|
339
|
+
const prevoutputsHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxIdPrevoutHash')).toString('hex');
|
|
340
|
+
buf = Buffer.alloc(4 * utxos.length);
|
|
341
|
+
for (const [i] of utxos.entries()) {
|
|
342
|
+
buf.writeInt32LE(-1, 4 * i);
|
|
343
|
+
}
|
|
344
|
+
const sequencesHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxIdSequencHash')).toString('hex');
|
|
345
|
+
// Calculate the actual buffer size needed for outputs
|
|
346
|
+
let outputsBufferSize = 0;
|
|
347
|
+
for (const output of outputs) {
|
|
348
|
+
if (output.type === 'pkh') {
|
|
349
|
+
outputsBufferSize += 34; // 8 bytes amount + 26 bytes script
|
|
350
|
+
}
|
|
351
|
+
else if (output.type === 'op_return') {
|
|
352
|
+
outputsBufferSize += 8 + memoToScript(output.memo).length;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
buf = Buffer.alloc(outputsBufferSize);
|
|
356
|
+
offset = 0;
|
|
357
|
+
for (const output of outputs) {
|
|
358
|
+
switch (output.type) {
|
|
359
|
+
case 'pkh': {
|
|
360
|
+
buf.writeUIntLE(output.amount, offset, 6); // 6 is the max
|
|
361
|
+
offset += 8;
|
|
362
|
+
const pkhscript = addressToScript(output.address);
|
|
363
|
+
pkhscript.copy(buf, offset);
|
|
364
|
+
offset += 26;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'op_return': {
|
|
368
|
+
offset += 8;
|
|
369
|
+
const oprscript = memoToScript(output.memo);
|
|
370
|
+
oprscript.copy(buf, offset);
|
|
371
|
+
offset += oprscript.length;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const outputsHash = Buffer.from(blake2bWithPersonal(buf.subarray(0, offset), 'ZTxIdOutputsHash')).toString('hex');
|
|
377
|
+
buf = Buffer.alloc(8 * utxos.length);
|
|
378
|
+
for (const [i, utxo] of utxos.entries()) {
|
|
379
|
+
buf.writeUIntLE(utxo.satoshis, 8 * i, 6);
|
|
380
|
+
}
|
|
381
|
+
const amountsHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxTrAmountsHash')).toString('hex');
|
|
382
|
+
buf = Buffer.alloc(26 * utxos.length);
|
|
383
|
+
for (const [i, utxo] of utxos.entries()) {
|
|
384
|
+
const script = addressToScript(utxo.address);
|
|
385
|
+
script.copy(buf, 26 * i);
|
|
386
|
+
}
|
|
387
|
+
const scriptsHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxTrScriptsHash')).toString('hex');
|
|
388
|
+
const signatures = [];
|
|
389
|
+
for (const utxo of utxos) {
|
|
390
|
+
buf = Buffer.alloc(32 + 4 + 8 + 26 + 4);
|
|
391
|
+
offset = 0;
|
|
392
|
+
const txid = Buffer.from(utxo.txid, 'hex');
|
|
393
|
+
txid.reverse();
|
|
394
|
+
txid.copy(buf, offset);
|
|
395
|
+
offset += 32;
|
|
396
|
+
buf.writeUInt32LE(utxo.outputIndex, offset);
|
|
397
|
+
offset += 4;
|
|
398
|
+
buf.writeUIntLE(utxo.satoshis, offset, 6);
|
|
399
|
+
offset += 8;
|
|
400
|
+
const script = addressToScript(utxo.address);
|
|
401
|
+
script.copy(buf, offset);
|
|
402
|
+
offset += 26;
|
|
403
|
+
buf.writeInt32LE(-1, offset);
|
|
404
|
+
const txInHash = Buffer.from(blake2bWithPersonal(buf, 'Zcash___TxInHash')).toString('hex');
|
|
405
|
+
buf = Buffer.alloc(1 + 32 * 6);
|
|
406
|
+
offset = 1;
|
|
407
|
+
buf[0] = 1;
|
|
408
|
+
Buffer.from(prevoutputsHash, 'hex').copy(buf, offset);
|
|
409
|
+
offset += 32;
|
|
410
|
+
Buffer.from(amountsHash, 'hex').copy(buf, offset);
|
|
411
|
+
offset += 32;
|
|
412
|
+
Buffer.from(scriptsHash, 'hex').copy(buf, offset);
|
|
413
|
+
offset += 32;
|
|
414
|
+
Buffer.from(sequencesHash, 'hex').copy(buf, offset);
|
|
415
|
+
offset += 32;
|
|
416
|
+
Buffer.from(outputsHash, 'hex').copy(buf, offset);
|
|
417
|
+
offset += 32;
|
|
418
|
+
Buffer.from(txInHash, 'hex').copy(buf, offset);
|
|
419
|
+
offset += 32;
|
|
420
|
+
const transparentHash = Buffer.from(blake2bWithPersonal(buf, 'ZTxIdTranspaHash')).toString('hex');
|
|
421
|
+
buf = Buffer.alloc(32 * 4);
|
|
422
|
+
offset = 0;
|
|
423
|
+
Buffer.from(headerHash, 'hex').copy(buf, offset);
|
|
424
|
+
offset += 32;
|
|
425
|
+
Buffer.from(transparentHash, 'hex').copy(buf, offset);
|
|
426
|
+
offset += 32;
|
|
427
|
+
Buffer.from('6f2fc8f98feafd94e74a0df4bed74391ee0b5a69945e4ced8ca8a095206f00ae', 'hex').copy(buf, offset);
|
|
428
|
+
offset += 32;
|
|
429
|
+
Buffer.from('9fbe4ed13b0c08e671c11a3407d84e1117cd45028a2eee1b9feae78b48a6e2c1', 'hex').copy(buf, offset);
|
|
430
|
+
offset += 32;
|
|
431
|
+
// Create personalization with NU6.1 branch ID
|
|
432
|
+
const personal = Buffer.alloc(16);
|
|
433
|
+
Buffer.from('ZcashTxHash_').copy(personal);
|
|
434
|
+
personal.writeUInt32LE(NU6_1_CONSENSUS_BRANCH_ID, 12); // NU6.1 consensus branch ID (FIXED!)
|
|
435
|
+
const sigHash = blake2bWithPersonal(buf, personal);
|
|
436
|
+
const signature = secp256k1.sign(sigHash, sk, { lowS: true, prehash: false });
|
|
437
|
+
const signatureDER = signature.toDERRawBytes();
|
|
438
|
+
signatures.push(signatureDER);
|
|
439
|
+
}
|
|
440
|
+
// Build final transaction - calculate required buffer size dynamically
|
|
441
|
+
// Header: 20 bytes (version + versionGroupId + consensusBranchId + lockTime + expiryHeight)
|
|
442
|
+
// Per input: 32 (txid) + 4 (vout) + ~1 (compactInt) + ~110 (sigScript: 5 + ~72 sig + 33 pubkey) + 4 (sequence) = ~151 bytes
|
|
443
|
+
// Per output: 8 (amount) + ~26-80 (script) = ~34-88 bytes
|
|
444
|
+
// Trailing: 3 bytes (empty sapling/orchard bundles)
|
|
445
|
+
const maxSigScriptSize = 5 + 73 + 33; // overhead + max DER sig + compressed pubkey
|
|
446
|
+
const perInputSize = 32 + 4 + 3 + maxSigScriptSize + 4; // txid + vout + compactInt + sigScript + sequence
|
|
447
|
+
const perOutputSize = 8 + 80; // amount + max script size (memo can be up to 80 chars)
|
|
448
|
+
const headerSize = 20;
|
|
449
|
+
const trailingSize = 3;
|
|
450
|
+
const compactIntSize = 3; // max for reasonable counts
|
|
451
|
+
const txBufferSize = headerSize +
|
|
452
|
+
compactIntSize +
|
|
453
|
+
utxos.length * perInputSize +
|
|
454
|
+
compactIntSize +
|
|
455
|
+
outputs.length * perOutputSize +
|
|
456
|
+
trailingSize;
|
|
457
|
+
buf = Buffer.alloc(txBufferSize);
|
|
458
|
+
offset = 0;
|
|
459
|
+
buf.writeUInt32LE(TX_VERSION, offset); // Transaction version
|
|
460
|
+
offset += 4;
|
|
461
|
+
buf.writeUInt32LE(TX_VERSION_GROUP_ID, offset); // Version group ID
|
|
462
|
+
offset += 4;
|
|
463
|
+
buf.writeUInt32LE(NU6_1_CONSENSUS_BRANCH_ID, offset); // NU6.1 consensus branch ID (FIXED!)
|
|
464
|
+
offset += 4;
|
|
465
|
+
buf.writeUInt32LE(0x00000000, offset); // Lock time
|
|
466
|
+
offset += 4;
|
|
467
|
+
buf.writeUInt32LE(height, offset); // Expiry height
|
|
468
|
+
offset += 4;
|
|
469
|
+
const txinc = writeCompactInt(utxos.length);
|
|
470
|
+
txinc.copy(buf, offset);
|
|
471
|
+
offset += txinc.length;
|
|
472
|
+
for (const [i, utxo] of utxos.entries()) {
|
|
473
|
+
const txid = Buffer.from(utxo.txid, 'hex');
|
|
474
|
+
txid.reverse();
|
|
475
|
+
txid.copy(buf, offset);
|
|
476
|
+
offset += 32;
|
|
477
|
+
buf.writeUInt32LE(utxo.outputIndex, offset);
|
|
478
|
+
offset += 4;
|
|
479
|
+
const ss = writeSigScript(signatures[i], pk);
|
|
480
|
+
const ssl = writeCompactInt(ss.length);
|
|
481
|
+
ssl.copy(buf, offset);
|
|
482
|
+
offset += ssl.length;
|
|
483
|
+
ss.copy(buf, offset);
|
|
484
|
+
offset += ss.length;
|
|
485
|
+
buf.writeInt32LE(-1, offset);
|
|
486
|
+
offset += 4;
|
|
487
|
+
}
|
|
488
|
+
const txoutc = writeCompactInt(outputs.length);
|
|
489
|
+
txoutc.copy(buf, offset);
|
|
490
|
+
offset += txoutc.length;
|
|
491
|
+
for (const out of outputs) {
|
|
492
|
+
switch (out.type) {
|
|
493
|
+
case 'pkh': {
|
|
494
|
+
buf.writeBigInt64LE(BigInt(out.amount), offset);
|
|
495
|
+
offset += 8;
|
|
496
|
+
const pkhscript = addressToScript(out.address);
|
|
497
|
+
pkhscript.copy(buf, offset);
|
|
498
|
+
offset += pkhscript.length;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case 'op_return': {
|
|
502
|
+
offset += 8;
|
|
503
|
+
const memoscript = memoToScript(out.memo);
|
|
504
|
+
memoscript.copy(buf, offset);
|
|
505
|
+
offset += memoscript.length;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Add 000000 (empty sapling/orchard bundles)
|
|
511
|
+
offset += 3;
|
|
512
|
+
return buf.subarray(0, offset);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export { addressToScript, buildTx, getFee, getUTXOS, isValidAddr, mainnetPrefix, memoToScript, pkToAddr, sendRawTransaction, signAndFinalize, skToAddr, testnetPrefix, waitForTransaction, writeSigScript };
|
|
517
|
+
//# sourceMappingURL=index.esm.js.map
|