zcash-dev 0.1.0-alpha.0 → 0.1.0-alpha.2
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/cli.d.ts +51 -0
- package/dist/cli.js +500 -33
- package/dist/env.d.ts +5 -0
- package/dist/env.js +86 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +48 -0
- package/docker/regtest/regtest-init.sh +28 -4
- package/docker/regtest-nu7/regtest-init.sh +28 -4
- package/package.json +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { send, syncWallet, WalletHandle } from '@zcash-app/sdk';
|
|
2
4
|
import { readZebraCookie, buildTestnetLightwalletdArgs, redactLightwalletdArgs, TestnetLightwalletdOptions } from './testnet-lightwalletd';
|
|
3
5
|
export { readZebraCookie, buildTestnetLightwalletdArgs, redactLightwalletdArgs, TestnetLightwalletdOptions, };
|
|
4
6
|
/** Reads this package's own version so `zcash-dev --version` cannot drift from package.json. */
|
|
5
7
|
export declare function readOwnVersion(): string;
|
|
6
8
|
export declare function getRegtestAccount0MinerAddress(network?: string): string;
|
|
9
|
+
declare const program: Command;
|
|
10
|
+
export interface SendActionOptions {
|
|
11
|
+
to: string;
|
|
12
|
+
amount: string;
|
|
13
|
+
memo?: string;
|
|
14
|
+
yes?: boolean;
|
|
15
|
+
mainnet?: boolean;
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SendDependencies {
|
|
19
|
+
client?: any;
|
|
20
|
+
sendFn?: typeof send;
|
|
21
|
+
syncWalletFn?: typeof syncWallet;
|
|
22
|
+
openWalletFn?: typeof WalletHandle.open;
|
|
23
|
+
}
|
|
24
|
+
export interface SendResult {
|
|
25
|
+
isDryRun: boolean;
|
|
26
|
+
network: string;
|
|
27
|
+
recipient: string;
|
|
28
|
+
amountZatoshis: bigint;
|
|
29
|
+
feeZatoshis: bigint;
|
|
30
|
+
endpoint: string;
|
|
31
|
+
memo?: string;
|
|
32
|
+
txid?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function executeSend(options: SendActionOptions, config?: Record<string, string>, deps?: SendDependencies): Promise<SendResult>;
|
|
35
|
+
export interface FundActionOptions {
|
|
36
|
+
address: string;
|
|
37
|
+
amount: string;
|
|
38
|
+
endpoint?: string;
|
|
39
|
+
stack?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface FundDependencies {
|
|
42
|
+
client?: any;
|
|
43
|
+
sendFn?: typeof send;
|
|
44
|
+
syncWalletFn?: typeof syncWallet;
|
|
45
|
+
openWalletFn?: typeof WalletHandle.open;
|
|
46
|
+
fetchFn?: typeof fetch;
|
|
47
|
+
}
|
|
48
|
+
export interface FundResult {
|
|
49
|
+
txid: string;
|
|
50
|
+
stack: 'nu7' | 'z3';
|
|
51
|
+
recipient: string;
|
|
52
|
+
amountZec: string;
|
|
53
|
+
zatoshis: bigint;
|
|
54
|
+
endpoint: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function executeFund(options: FundActionOptions, config?: Record<string, string>, deps?: FundDependencies): Promise<FundResult>;
|
|
7
57
|
export declare function resolveRegtestDir(explicitPath?: string, stack?: string): string;
|
|
58
|
+
export { program };
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.redactLightwalletdArgs = exports.buildTestnetLightwalletdArgs = exports.readZebraCookie = void 0;
|
|
7
|
+
exports.program = exports.redactLightwalletdArgs = exports.buildTestnetLightwalletdArgs = exports.readZebraCookie = void 0;
|
|
8
8
|
exports.readOwnVersion = readOwnVersion;
|
|
9
9
|
exports.getRegtestAccount0MinerAddress = getRegtestAccount0MinerAddress;
|
|
10
|
+
exports.executeSend = executeSend;
|
|
11
|
+
exports.executeFund = executeFund;
|
|
10
12
|
exports.resolveRegtestDir = resolveRegtestDir;
|
|
11
13
|
const commander_1 = require("commander");
|
|
12
14
|
const prompts_1 = __importDefault(require("prompts"));
|
|
@@ -15,6 +17,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
15
17
|
const os_1 = __importDefault(require("os"));
|
|
16
18
|
const child_process_1 = require("child_process");
|
|
17
19
|
const sdk_1 = require("@zcash-app/sdk");
|
|
20
|
+
const env_1 = require("./env");
|
|
21
|
+
const redact_1 = require("./redact");
|
|
18
22
|
const testnet_lightwalletd_1 = require("./testnet-lightwalletd");
|
|
19
23
|
Object.defineProperty(exports, "readZebraCookie", { enumerable: true, get: function () { return testnet_lightwalletd_1.readZebraCookie; } });
|
|
20
24
|
Object.defineProperty(exports, "buildTestnetLightwalletdArgs", { enumerable: true, get: function () { return testnet_lightwalletd_1.buildTestnetLightwalletdArgs; } });
|
|
@@ -41,6 +45,7 @@ const REMOTE_ENDPOINTS = {
|
|
|
41
45
|
mainnet: 'zec.rocks:443',
|
|
42
46
|
};
|
|
43
47
|
const program = new commander_1.Command();
|
|
48
|
+
exports.program = program;
|
|
44
49
|
program
|
|
45
50
|
.name('zcash-dev')
|
|
46
51
|
.description('Developer harness & regtest helper CLI for shielded Zcash apps')
|
|
@@ -75,54 +80,492 @@ program
|
|
|
75
80
|
await client.close();
|
|
76
81
|
}
|
|
77
82
|
});
|
|
83
|
+
async function executeSend(options, config, deps) {
|
|
84
|
+
const env = config ?? (0, env_1.loadProjectEnv)();
|
|
85
|
+
const network = (env.ZCASH_NETWORK ?? 'regtest').trim().toLowerCase();
|
|
86
|
+
const regtestStack = (env.ZCASH_REGTEST_STACK ?? '').trim().toLowerCase();
|
|
87
|
+
const endpoint = options.endpoint || env.ZCASH_ENDPOINT || REMOTE_ENDPOINTS[network] || '127.0.0.1:39067';
|
|
88
|
+
const isNu7 = network === 'regtest' && (regtestStack === 'nu7' || endpoint.includes('39067') || (!regtestStack && !endpoint.includes('28137')));
|
|
89
|
+
const effectiveNetwork = isNu7 ? 'regtest-nu7' : network;
|
|
90
|
+
const mnemonic = (env.ZCASH_MNEMONIC ?? '').trim();
|
|
91
|
+
const ufvk = (env.ZCASH_UFVK ?? '').trim();
|
|
92
|
+
const accountIndex = parseInt(env.ZCASH_ACCOUNT_INDEX ?? '0', 10) || 0;
|
|
93
|
+
const birthdayRaw = (env.ZCASH_BIRTHDAY ?? '').trim();
|
|
94
|
+
const birthday = /^\d+$/.test(birthdayRaw) ? parseInt(birthdayRaw, 10) : undefined;
|
|
95
|
+
// Reject UFVK-only configs
|
|
96
|
+
if (ufvk && !mnemonic) {
|
|
97
|
+
throw new Error('Cannot send from a watch-only account (UFVK). A spending key (mnemonic) is required.');
|
|
98
|
+
}
|
|
99
|
+
if (!mnemonic && network !== 'regtest') {
|
|
100
|
+
throw new Error(`No spending key (ZCASH_MNEMONIC) configured for ${network}. Cannot send without a spending key.`);
|
|
101
|
+
}
|
|
102
|
+
const amountZatoshis = (0, env_1.parseZecToZatoshis)(options.amount);
|
|
103
|
+
const feeZatoshis = (0, sdk_1.quoteFee)(2);
|
|
104
|
+
// Dry run mode (default when --yes is not set)
|
|
105
|
+
if (!options.yes) {
|
|
106
|
+
return {
|
|
107
|
+
isDryRun: true,
|
|
108
|
+
network,
|
|
109
|
+
recipient: options.to,
|
|
110
|
+
amountZatoshis,
|
|
111
|
+
feeZatoshis,
|
|
112
|
+
endpoint: (0, redact_1.redactSecretUrl)(endpoint),
|
|
113
|
+
memo: options.memo,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Broadcast mode (--yes passed)
|
|
117
|
+
if (network === 'mainnet' && !options.mainnet) {
|
|
118
|
+
throw new Error('Sending on mainnet requires the explicit --mainnet flag.');
|
|
119
|
+
}
|
|
120
|
+
const activeMnemonic = mnemonic || (network === 'regtest' ? sdk_1.KNOWN_TEST_MNEMONICS[accountIndex] || sdk_1.KNOWN_TEST_MNEMONICS[0] : '');
|
|
121
|
+
const account = (0, sdk_1.accountFromMnemonic)(activeMnemonic, accountIndex, effectiveNetwork);
|
|
122
|
+
const client = deps?.client ?? (0, sdk_1.createClient)({ endpoint, network: effectiveNetwork });
|
|
123
|
+
const openFn = deps?.openWalletFn ?? ((...args) => sdk_1.WalletHandle.open(...args));
|
|
124
|
+
const syncFn = deps?.syncWalletFn ?? sdk_1.syncWallet;
|
|
125
|
+
const sendFn = deps?.sendFn ?? sdk_1.send;
|
|
126
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'zcash-dev-wallet-'));
|
|
127
|
+
const dbPath = path_1.default.join(tempDir, 'wallet.db');
|
|
128
|
+
let result;
|
|
129
|
+
try {
|
|
130
|
+
const wallet = openFn(dbPath, effectiveNetwork, account.getUnifiedFullViewingKey(), `acct${accountIndex}`, birthday);
|
|
131
|
+
await syncFn(client, wallet);
|
|
132
|
+
result = await sendFn({
|
|
133
|
+
client,
|
|
134
|
+
wallet,
|
|
135
|
+
from: account,
|
|
136
|
+
to: options.to,
|
|
137
|
+
amount: amountZatoshis,
|
|
138
|
+
memo: options.memo,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (!deps?.client) {
|
|
143
|
+
await client.close().catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
isDryRun: false,
|
|
152
|
+
network,
|
|
153
|
+
recipient: options.to,
|
|
154
|
+
amountZatoshis,
|
|
155
|
+
feeZatoshis: result.fee ? BigInt(result.fee) : feeZatoshis,
|
|
156
|
+
endpoint: (0, redact_1.redactSecretUrl)(endpoint),
|
|
157
|
+
memo: options.memo,
|
|
158
|
+
txid: result.txid,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
program
|
|
162
|
+
.command('address')
|
|
163
|
+
.description('Display the derived address for the configured account')
|
|
164
|
+
.option('-i, --index <n>', 'Account index')
|
|
165
|
+
.action((options) => {
|
|
166
|
+
try {
|
|
167
|
+
(0, env_1.applyProjectEnv)();
|
|
168
|
+
const network = (process.env.ZCASH_NETWORK ?? 'regtest').trim().toLowerCase();
|
|
169
|
+
const regtestStack = (process.env.ZCASH_REGTEST_STACK ?? '').trim().toLowerCase();
|
|
170
|
+
const endpoint = process.env.ZCASH_ENDPOINT ?? '';
|
|
171
|
+
const isNu7 = network === 'regtest' && (regtestStack === 'nu7' || endpoint.includes('39067') || (!regtestStack && !endpoint.includes('28137')));
|
|
172
|
+
const effectiveNetwork = isNu7 ? 'regtest-nu7' : network;
|
|
173
|
+
const rawIndex = options.index ?? process.env.ZCASH_ACCOUNT_INDEX ?? '0';
|
|
174
|
+
const accountIndex = parseInt(rawIndex, 10) || 0;
|
|
175
|
+
const mnemonic = (process.env.ZCASH_MNEMONIC ?? '').trim();
|
|
176
|
+
const ufvk = (process.env.ZCASH_UFVK ?? '').trim();
|
|
177
|
+
if (!mnemonic && !ufvk) {
|
|
178
|
+
if (network === 'regtest') {
|
|
179
|
+
const acct = sdk_1.dev.fixtures.account(accountIndex, effectiveNetwork);
|
|
180
|
+
console.log(`\n--- Configured Account Address ---`);
|
|
181
|
+
console.log(`Network: ${network}`);
|
|
182
|
+
console.log(`Account Index: ${accountIndex} (regtest default fixture)`);
|
|
183
|
+
console.log(`Unified Address: ${acct.getUnifiedAddress()}`);
|
|
184
|
+
const tAddr = acct.getTransparentAddress?.();
|
|
185
|
+
if (tAddr)
|
|
186
|
+
console.log(`Transparent: ${tAddr}`);
|
|
187
|
+
console.log('');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Neither ZCASH_MNEMONIC nor ZCASH_UFVK is configured for ${network}. Set ZCASH_MNEMONIC or ZCASH_UFVK in .env.local.`);
|
|
191
|
+
}
|
|
192
|
+
if (mnemonic) {
|
|
193
|
+
const acct = (0, sdk_1.accountFromMnemonic)(mnemonic, accountIndex, effectiveNetwork);
|
|
194
|
+
console.log(`\n--- Configured Account Address ---`);
|
|
195
|
+
console.log(`Network: ${network}`);
|
|
196
|
+
console.log(`Account Index: ${accountIndex}`);
|
|
197
|
+
console.log(`Unified Address: ${acct.getUnifiedAddress()}`);
|
|
198
|
+
const tAddr = acct.getTransparentAddress?.();
|
|
199
|
+
if (tAddr)
|
|
200
|
+
console.log(`Transparent: ${tAddr}`);
|
|
201
|
+
console.log('');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (ufvk) {
|
|
205
|
+
const watchAcct = (0, sdk_1.accountFromUfvk)(ufvk, effectiveNetwork);
|
|
206
|
+
console.log(`\n--- Configured Account Address (Watch-Only) ---`);
|
|
207
|
+
console.log(`Network: ${network}`);
|
|
208
|
+
console.log(`Account Index: ${accountIndex} (UFVK)`);
|
|
209
|
+
console.log(`Unified Address: ${watchAcct.getUnifiedAddress()}`);
|
|
210
|
+
const tAddr = watchAcct.getTransparentAddress?.();
|
|
211
|
+
if (tAddr)
|
|
212
|
+
console.log(`Transparent: ${tAddr}`);
|
|
213
|
+
console.log('');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const sanitized = (0, redact_1.sanitizeError)(err, process.env.ZCASH_MNEMONIC);
|
|
219
|
+
console.error('Address derivation failed:', sanitized.message);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
program
|
|
224
|
+
.command('balance')
|
|
225
|
+
.description('Check balance for the configured account')
|
|
226
|
+
.option('-e, --endpoint <endpoint>', 'Zaino / lightwalletd gRPC endpoint')
|
|
227
|
+
.action(async (options) => {
|
|
228
|
+
let client;
|
|
229
|
+
try {
|
|
230
|
+
(0, env_1.applyProjectEnv)();
|
|
231
|
+
const network = (process.env.ZCASH_NETWORK ?? 'regtest').trim().toLowerCase();
|
|
232
|
+
const regtestStack = (process.env.ZCASH_REGTEST_STACK ?? '').trim().toLowerCase();
|
|
233
|
+
const endpoint = options.endpoint || process.env.ZCASH_ENDPOINT || REMOTE_ENDPOINTS[network] || '127.0.0.1:39067';
|
|
234
|
+
const isNu7 = network === 'regtest' && (regtestStack === 'nu7' || endpoint.includes('39067') || (!regtestStack && !endpoint.includes('28137')));
|
|
235
|
+
const effectiveNetwork = isNu7 ? 'regtest-nu7' : network;
|
|
236
|
+
const birthdayRaw = (process.env.ZCASH_BIRTHDAY ?? '').trim();
|
|
237
|
+
const birthday = /^\d+$/.test(birthdayRaw) ? parseInt(birthdayRaw, 10) : undefined;
|
|
238
|
+
const accountIndex = parseInt(process.env.ZCASH_ACCOUNT_INDEX ?? '0', 10) || 0;
|
|
239
|
+
const mnemonic = (process.env.ZCASH_MNEMONIC ?? '').trim();
|
|
240
|
+
const ufvk = (process.env.ZCASH_UFVK ?? '').trim();
|
|
241
|
+
let account;
|
|
242
|
+
if (mnemonic) {
|
|
243
|
+
account = (0, sdk_1.accountFromMnemonic)(mnemonic, accountIndex, effectiveNetwork);
|
|
244
|
+
}
|
|
245
|
+
else if (ufvk) {
|
|
246
|
+
account = (0, sdk_1.accountFromUfvk)(ufvk, effectiveNetwork);
|
|
247
|
+
}
|
|
248
|
+
else if (network === 'regtest') {
|
|
249
|
+
account = sdk_1.dev.fixtures.account(accountIndex, effectiveNetwork);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw new Error(`Neither ZCASH_MNEMONIC nor ZCASH_UFVK is configured for ${network}. Set ZCASH_MNEMONIC or ZCASH_UFVK in .env.local.`);
|
|
253
|
+
}
|
|
254
|
+
console.log(`Querying balance on ${network} via ${(0, redact_1.redactSecretUrl)(endpoint)}...`);
|
|
255
|
+
client = (0, sdk_1.createClient)({ endpoint, network: effectiveNetwork });
|
|
256
|
+
const b = await (0, sdk_1.getBalance)(client, account, { sinceHeight: birthday });
|
|
257
|
+
console.log('\n--- Account Balance ---');
|
|
258
|
+
console.log(`Network: ${network}`);
|
|
259
|
+
console.log(`Total: ${(0, env_1.formatZec)(b.total)} ZEC (${b.total} zatoshis)`);
|
|
260
|
+
console.log(`Ironwood: ${(0, env_1.formatZec)(b.ironwood)} ZEC`);
|
|
261
|
+
console.log(`Orchard: ${(0, env_1.formatZec)(b.orchard)} ZEC`);
|
|
262
|
+
console.log(`Sapling: ${(0, env_1.formatZec)(b.sapling)} ZEC`);
|
|
263
|
+
console.log(`Transparent: ${(0, env_1.formatZec)(b.transparent)} ZEC`);
|
|
264
|
+
if (b.confirmed && b.pending) {
|
|
265
|
+
console.log(`Confirmed: ${(0, env_1.formatZec)(b.confirmed.total)} ZEC`);
|
|
266
|
+
console.log(`Pending: ${(0, env_1.formatZec)(b.pending.total)} ZEC`);
|
|
267
|
+
}
|
|
268
|
+
console.log('');
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const sanitized = (0, redact_1.sanitizeError)(err, process.env.ZCASH_MNEMONIC);
|
|
272
|
+
console.error('Balance check failed:', sanitized.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
if (client) {
|
|
277
|
+
await client.close().catch(() => { });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
program
|
|
282
|
+
.command('send')
|
|
283
|
+
.description('Send ZEC or perform a transaction dry run')
|
|
284
|
+
.requiredOption('--to <address>', 'Recipient address (unified, sapling, or transparent)')
|
|
285
|
+
.requiredOption('--amount <zec>', 'Amount to send in ZEC')
|
|
286
|
+
.option('--memo <memo>', 'Optional memo for shielded recipient')
|
|
287
|
+
.option('--yes', 'Broadcast the transaction (without --yes, runs in dry-run mode)')
|
|
288
|
+
.option('--mainnet', 'Safety confirmation flag required when broadcasting on mainnet')
|
|
289
|
+
.option('-e, --endpoint <endpoint>', 'Zaino / lightwalletd gRPC endpoint')
|
|
290
|
+
.action(async (options) => {
|
|
291
|
+
try {
|
|
292
|
+
(0, env_1.applyProjectEnv)();
|
|
293
|
+
const config = (0, env_1.loadProjectEnv)();
|
|
294
|
+
const res = await executeSend(options, config);
|
|
295
|
+
if (res.isDryRun) {
|
|
296
|
+
console.log('\n=== Transaction Dry Run ===');
|
|
297
|
+
console.log(`Network: ${res.network}`);
|
|
298
|
+
console.log(`Recipient: ${res.recipient}`);
|
|
299
|
+
console.log(`Amount: ${options.amount} ZEC (${res.amountZatoshis} zatoshis)`);
|
|
300
|
+
console.log(`Fee: ${(0, env_1.formatZec)(res.feeZatoshis)} ZEC (${res.feeZatoshis} zatoshis) [estimated (ZIP-317, 2 outputs assumed)]`);
|
|
301
|
+
console.log(`Endpoint: ${res.endpoint}`);
|
|
302
|
+
if (res.memo) {
|
|
303
|
+
console.log(`Memo: ${res.memo}`);
|
|
304
|
+
}
|
|
305
|
+
console.log('Status: DRY RUN ONLY — transaction was NOT broadcast.');
|
|
306
|
+
console.log(`Pass --yes to sign and broadcast this transaction${res.network === 'mainnet' ? ' (requires --mainnet)' : ''}.\n`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log('\n✅ Transaction broadcast successfully!');
|
|
310
|
+
console.log(`Txid: ${res.txid}`);
|
|
311
|
+
console.log(`Fee: ${(0, env_1.formatZec)(res.feeZatoshis)} ZEC (${res.feeZatoshis} zatoshis)\n`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const sanitized = (0, redact_1.sanitizeError)(err, process.env.ZCASH_MNEMONIC);
|
|
316
|
+
console.error('Send failed:', sanitized.message);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
async function executeFund(options, config, deps) {
|
|
321
|
+
const env = config ?? (0, env_1.loadProjectEnv)();
|
|
322
|
+
const network = (env.ZCASH_NETWORK ?? 'regtest').trim().toLowerCase();
|
|
323
|
+
if (network !== 'regtest' && network !== 'regtest-nu7') {
|
|
324
|
+
throw new Error(`Fund is only available on regtest (current network: ${network}).`);
|
|
325
|
+
}
|
|
326
|
+
let stack = (options.stack || env.ZCASH_REGTEST_STACK || '').trim().toLowerCase();
|
|
327
|
+
const endpointOpt = options.endpoint || env.ZCASH_ENDPOINT;
|
|
328
|
+
if (!stack) {
|
|
329
|
+
if (endpointOpt && (endpointOpt.includes('39067') || endpointOpt.includes('39232'))) {
|
|
330
|
+
stack = 'nu7';
|
|
331
|
+
}
|
|
332
|
+
else if (endpointOpt && (endpointOpt.includes('28137') || endpointOpt.includes('8181'))) {
|
|
333
|
+
stack = 'z3';
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
stack = 'nu7';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (stack !== 'nu7' && stack !== 'z3') {
|
|
340
|
+
throw new Error(`Invalid regtest stack "${stack}". Must be "nu7" or "z3".`);
|
|
341
|
+
}
|
|
342
|
+
const zatoshis = (0, env_1.parseZecToZatoshis)(options.amount);
|
|
343
|
+
const effectiveFetch = deps?.fetchFn ?? fetch;
|
|
344
|
+
if (stack === 'nu7') {
|
|
345
|
+
const effectiveEndpoint = options.endpoint || env.ZCASH_ENDPOINT || '127.0.0.1:39067';
|
|
346
|
+
const minerAccount = (0, sdk_1.accountFromMnemonic)(sdk_1.KNOWN_TEST_MNEMONICS[0], 0, 'regtest-nu7');
|
|
347
|
+
const client = deps?.client ?? (0, sdk_1.createClient)({ endpoint: effectiveEndpoint, network: 'regtest-nu7' });
|
|
348
|
+
const openFn = deps?.openWalletFn ?? ((...args) => sdk_1.WalletHandle.open(...args));
|
|
349
|
+
const syncFn = deps?.syncWalletFn ?? sdk_1.syncWallet;
|
|
350
|
+
const sendFn = deps?.sendFn ?? sdk_1.send;
|
|
351
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'zcash-dev-fund-'));
|
|
352
|
+
const dbPath = path_1.default.join(tempDir, 'wallet.db');
|
|
353
|
+
let result;
|
|
354
|
+
try {
|
|
355
|
+
const wallet = openFn(dbPath, 'regtest-nu7', minerAccount.getUnifiedFullViewingKey(), 'miner0', 0);
|
|
356
|
+
await syncFn(client, wallet);
|
|
357
|
+
result = await sendFn({
|
|
358
|
+
client,
|
|
359
|
+
wallet,
|
|
360
|
+
from: minerAccount,
|
|
361
|
+
to: options.address,
|
|
362
|
+
amount: zatoshis,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
const msg = err.message || String(err);
|
|
367
|
+
if (msg.includes('ECONNREFUSED') || msg.includes('UNAVAILABLE') || msg.includes('connect')) {
|
|
368
|
+
throw new Error(`Could not connect to NU7 lightwalletd indexer at ${(0, redact_1.redactSecretUrl)(effectiveEndpoint)}. Is the NU7 stack running? Run: zcash-dev regtest up --stack nu7 (${msg})`);
|
|
369
|
+
}
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
if (!deps?.client) {
|
|
374
|
+
await client.close().catch(() => { });
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
catch { }
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
txid: result.txid,
|
|
383
|
+
stack: 'nu7',
|
|
384
|
+
recipient: options.address,
|
|
385
|
+
amountZec: options.amount,
|
|
386
|
+
zatoshis,
|
|
387
|
+
endpoint: (0, redact_1.redactSecretUrl)(effectiveEndpoint),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const rpcRouterUrl = 'http://127.0.0.1:8181';
|
|
392
|
+
let addrData;
|
|
393
|
+
try {
|
|
394
|
+
const addrRes = await effectiveFetch(rpcRouterUrl, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: {
|
|
397
|
+
'Content-Type': 'application/json',
|
|
398
|
+
Authorization: 'Basic ' + Buffer.from('zebra:zebra').toString('base64'),
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
jsonrpc: '2.0',
|
|
402
|
+
id: 'fund-listaddresses',
|
|
403
|
+
method: 'listaddresses',
|
|
404
|
+
params: [],
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
if (!addrRes.ok) {
|
|
408
|
+
throw new Error(`Zallet HTTP ${addrRes.status}: ${addrRes.statusText}`);
|
|
409
|
+
}
|
|
410
|
+
addrData = await addrRes.json();
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
throw new Error(`Could not connect to Z3 RPC router / Zallet at ${rpcRouterUrl}. Is the Z3 stack running? Run: zcash-dev regtest up --stack nu6 (${err.message || err})`);
|
|
414
|
+
}
|
|
415
|
+
const accounts = addrData?.result?.[0]?.unified;
|
|
416
|
+
const senderUa = accounts?.[0]?.addresses?.[0]?.address;
|
|
417
|
+
if (!senderUa) {
|
|
418
|
+
throw new Error('No sender address found in Zallet regtest wallet');
|
|
419
|
+
}
|
|
420
|
+
const isNegative = zatoshis < 0n;
|
|
421
|
+
const abs = isNegative ? -zatoshis : zatoshis;
|
|
422
|
+
const whole = abs / 100000000n;
|
|
423
|
+
const frac = abs % 100000000n;
|
|
424
|
+
const amountZec = `${isNegative ? '-' : ''}${whole}.${frac.toString().padStart(8, '0')}`;
|
|
425
|
+
const sendRes = await effectiveFetch(rpcRouterUrl, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: {
|
|
428
|
+
'Content-Type': 'application/json',
|
|
429
|
+
Authorization: 'Basic ' + Buffer.from('zebra:zebra').toString('base64'),
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
jsonrpc: '2.0',
|
|
433
|
+
id: 'fund-sendmany',
|
|
434
|
+
method: 'z_sendmany',
|
|
435
|
+
params: [senderUa, [{ address: options.address, amount: amountZec }]],
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
if (!sendRes.ok) {
|
|
439
|
+
throw new Error(`Zallet z_sendmany HTTP ${sendRes.status}: ${sendRes.statusText}`);
|
|
440
|
+
}
|
|
441
|
+
const sendData = await sendRes.json();
|
|
442
|
+
if (sendData.error) {
|
|
443
|
+
throw new Error(`Zallet z_sendmany RPC error: ${sendData.error.message || JSON.stringify(sendData.error)}`);
|
|
444
|
+
}
|
|
445
|
+
const txid = sendData.result;
|
|
446
|
+
return {
|
|
447
|
+
txid,
|
|
448
|
+
stack: 'z3',
|
|
449
|
+
recipient: options.address,
|
|
450
|
+
amountZec: options.amount,
|
|
451
|
+
zatoshis,
|
|
452
|
+
endpoint: rpcRouterUrl,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
78
456
|
program
|
|
79
457
|
.command('mine')
|
|
80
458
|
.description('Mine blocks on regtest')
|
|
81
459
|
.argument('[blocks]', 'Number of blocks to mine', '1')
|
|
82
|
-
.option('-e, --endpoint <endpoint>', 'Zaino
|
|
460
|
+
.option('-e, --endpoint <endpoint>', 'Zebra RPC or Zaino endpoint')
|
|
461
|
+
.option('--stack <stack>', 'Regtest stack (nu7 | z3)')
|
|
83
462
|
.action(async (blocksStr, options) => {
|
|
84
|
-
const blocks = parseInt(blocksStr, 10);
|
|
85
|
-
const client = (0, sdk_1.createClient)({
|
|
86
|
-
endpoint: options.endpoint,
|
|
87
|
-
network: 'regtest',
|
|
88
|
-
});
|
|
89
463
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
464
|
+
(0, env_1.applyProjectEnv)();
|
|
465
|
+
const env = (0, env_1.loadProjectEnv)();
|
|
466
|
+
const blocks = parseInt(blocksStr, 10);
|
|
467
|
+
let stack = (options.stack || env.ZCASH_REGTEST_STACK || '').trim().toLowerCase();
|
|
468
|
+
const endpointOpt = options.endpoint || env.ZCASH_ENDPOINT;
|
|
469
|
+
const zebraRpcOpt = env.ZCASH_ZEBRA_RPC;
|
|
470
|
+
if (!stack) {
|
|
471
|
+
if ((endpointOpt && (endpointOpt.includes('39067') || endpointOpt.includes('39232'))) || (zebraRpcOpt && zebraRpcOpt.includes('39232'))) {
|
|
472
|
+
stack = 'nu7';
|
|
473
|
+
}
|
|
474
|
+
else if ((endpointOpt && (endpointOpt.includes('28137') || endpointOpt.includes('8181'))) || (zebraRpcOpt && zebraRpcOpt.includes('18232'))) {
|
|
475
|
+
stack = 'z3';
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
stack = 'nu7';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
console.log(`Mining ${blocks} block(s) on ${stack.toUpperCase()} regtest stack...`);
|
|
482
|
+
if (stack === 'nu7') {
|
|
483
|
+
const zebraUrl = (options.endpoint && options.endpoint.startsWith('http'))
|
|
484
|
+
? options.endpoint
|
|
485
|
+
: (zebraRpcOpt || 'http://127.0.0.1:39232');
|
|
486
|
+
let res;
|
|
487
|
+
try {
|
|
488
|
+
res = await fetch(zebraUrl, {
|
|
489
|
+
method: 'POST',
|
|
490
|
+
headers: { 'Content-Type': 'application/json' },
|
|
491
|
+
body: JSON.stringify({
|
|
492
|
+
jsonrpc: '2.0',
|
|
493
|
+
id: 'zcash-dev-mine',
|
|
494
|
+
method: 'generate',
|
|
495
|
+
params: [blocks],
|
|
496
|
+
}),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
throw new Error(`Could not connect to Zebra JSON-RPC at ${zebraUrl}. Is the NU7 stack running? Run: zcash-dev regtest up --stack nu7 (${err.message})`);
|
|
501
|
+
}
|
|
502
|
+
if (!res.ok) {
|
|
503
|
+
throw new Error(`Zebra mining HTTP ${res.status}: ${res.statusText}`);
|
|
504
|
+
}
|
|
505
|
+
const data = await res.json();
|
|
506
|
+
if (data.error) {
|
|
507
|
+
throw new Error(`Zebra mining JSON-RPC error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
508
|
+
}
|
|
509
|
+
console.log(`✅ Successfully mined ${blocks} block(s) on Zebra.`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
const rpcRouterUrl = 'http://127.0.0.1:8181';
|
|
513
|
+
let res;
|
|
514
|
+
try {
|
|
515
|
+
res = await fetch(rpcRouterUrl, {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers: {
|
|
518
|
+
'Content-Type': 'application/json',
|
|
519
|
+
Authorization: 'Basic ' + Buffer.from('zebra:zebra').toString('base64'),
|
|
520
|
+
},
|
|
521
|
+
body: JSON.stringify({
|
|
522
|
+
jsonrpc: '2.0',
|
|
523
|
+
id: 'zcash-dev-mine',
|
|
524
|
+
method: 'generate',
|
|
525
|
+
params: [blocks],
|
|
526
|
+
}),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
throw new Error(`Could not connect to Z3 RPC router at ${rpcRouterUrl}. Is the Z3 stack running? Run: zcash-dev regtest up --stack nu6 (${err.message})`);
|
|
531
|
+
}
|
|
532
|
+
if (!res.ok) {
|
|
533
|
+
throw new Error(`Z3 mining HTTP ${res.status}: ${res.statusText}`);
|
|
534
|
+
}
|
|
535
|
+
const data = await res.json();
|
|
536
|
+
if (data.error) {
|
|
537
|
+
throw new Error(`Z3 mining JSON-RPC error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
538
|
+
}
|
|
539
|
+
console.log(`✅ Successfully mined ${blocks} block(s) on Z3 stack.`);
|
|
540
|
+
}
|
|
93
541
|
}
|
|
94
542
|
catch (err) {
|
|
95
543
|
console.error('Mining failed:', err.message || err);
|
|
96
544
|
process.exit(1);
|
|
97
545
|
}
|
|
98
|
-
finally {
|
|
99
|
-
await client.close();
|
|
100
|
-
}
|
|
101
546
|
});
|
|
102
547
|
program
|
|
103
548
|
.command('fund')
|
|
104
549
|
.description('Fund a Unified Address on regtest')
|
|
105
550
|
.argument('<address>', 'Recipient Unified Address')
|
|
106
551
|
.argument('[amount]', 'Amount in ZEC', '1.0')
|
|
107
|
-
.option('-e, --endpoint <endpoint>', 'Zaino gRPC endpoint'
|
|
552
|
+
.option('-e, --endpoint <endpoint>', 'Zaino / lightwalletd gRPC endpoint')
|
|
553
|
+
.option('--stack <stack>', 'Regtest stack (nu7 | z3)')
|
|
108
554
|
.action(async (address, amountStr, options) => {
|
|
109
|
-
const zatoshis = BigInt(Math.round(parseFloat(amountStr) * 100000000));
|
|
110
|
-
const client = (0, sdk_1.createClient)({
|
|
111
|
-
endpoint: options.endpoint,
|
|
112
|
-
network: 'regtest',
|
|
113
|
-
});
|
|
114
555
|
try {
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
556
|
+
(0, env_1.applyProjectEnv)();
|
|
557
|
+
const config = (0, env_1.loadProjectEnv)();
|
|
558
|
+
const res = await executeFund({ address, amount: amountStr, endpoint: options.endpoint, stack: options.stack }, config);
|
|
559
|
+
console.log(`\n✅ Funded successfully via ${res.stack.toUpperCase()} stack!`);
|
|
560
|
+
console.log(`Recipient: ${res.recipient}`);
|
|
561
|
+
console.log(`Amount: ${res.amountZec} ZEC (${res.zatoshis} zatoshis)`);
|
|
562
|
+
console.log(`Txid: ${res.txid}\n`);
|
|
118
563
|
}
|
|
119
564
|
catch (err) {
|
|
120
|
-
|
|
565
|
+
const sanitized = (0, redact_1.sanitizeError)(err, process.env.ZCASH_MNEMONIC);
|
|
566
|
+
console.error('Funding failed:', sanitized.message);
|
|
121
567
|
process.exit(1);
|
|
122
568
|
}
|
|
123
|
-
finally {
|
|
124
|
-
await client.close();
|
|
125
|
-
}
|
|
126
569
|
});
|
|
127
570
|
program
|
|
128
571
|
.command('fixtures')
|
|
@@ -377,7 +820,13 @@ regtestCmd
|
|
|
377
820
|
console.log('\n🐳 Starting NU7 Regtest containers...');
|
|
378
821
|
const initScript = path_1.default.join(regtestDir, 'regtest-init.sh');
|
|
379
822
|
if (fs_1.default.existsSync(initScript)) {
|
|
380
|
-
|
|
823
|
+
try {
|
|
824
|
+
(0, child_process_1.execSync)(`cd "${regtestDir}" && bash "${initScript}"`, { stdio: 'inherit' });
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
console.error('regtest-init.sh failed, see output above');
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
381
830
|
}
|
|
382
831
|
else {
|
|
383
832
|
(0, child_process_1.execSync)(`cd "${regtestDir}" && docker compose up -d`, { stdio: 'inherit' });
|
|
@@ -429,7 +878,13 @@ regtestCmd
|
|
|
429
878
|
console.log('\n🐳 Starting Regtest containers...');
|
|
430
879
|
const initScript = path_1.default.join(regtestDir, 'regtest-init.sh');
|
|
431
880
|
if (fs_1.default.existsSync(initScript)) {
|
|
432
|
-
|
|
881
|
+
try {
|
|
882
|
+
(0, child_process_1.execSync)(`cd "${regtestDir}" && bash "${initScript}"`, { stdio: 'inherit' });
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
console.error('regtest-init.sh failed, see output above');
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
433
888
|
}
|
|
434
889
|
else {
|
|
435
890
|
(0, child_process_1.execSync)(`cd "${regtestDir}" && docker compose up -d`, { stdio: 'inherit' });
|
|
@@ -637,7 +1092,13 @@ regtestCmd
|
|
|
637
1092
|
console.log('\n=== 🛠️ Reinitializing fresh NU7 chain via regtest-init.sh ===\n');
|
|
638
1093
|
const initScript = path_1.default.join(regtestDir, 'regtest-init.sh');
|
|
639
1094
|
if (fs_1.default.existsSync(initScript)) {
|
|
640
|
-
|
|
1095
|
+
try {
|
|
1096
|
+
(0, child_process_1.execSync)(`cd "${regtestDir}" && bash "${initScript}"`, { stdio: 'inherit' });
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
console.error('regtest-init.sh failed, see output above');
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
641
1102
|
}
|
|
642
1103
|
else {
|
|
643
1104
|
(0, child_process_1.execSync)(`cd "${regtestDir}" && docker compose up -d`, { stdio: 'inherit' });
|
|
@@ -669,11 +1130,17 @@ regtestCmd
|
|
|
669
1130
|
console.log('\n=== 🛠️ Reinitializing fresh chain via regtest-init.sh ===\n');
|
|
670
1131
|
const initScript = path_1.default.join(regtestDir, 'regtest-init.sh');
|
|
671
1132
|
const legacyInitScript = path_1.default.join(regtestDir, 'scripts/regtest-init.sh');
|
|
672
|
-
|
|
673
|
-
(
|
|
1133
|
+
try {
|
|
1134
|
+
if (fs_1.default.existsSync(initScript)) {
|
|
1135
|
+
(0, child_process_1.execSync)(`cd "${regtestDir}" && bash "${initScript}"`, { stdio: 'inherit' });
|
|
1136
|
+
}
|
|
1137
|
+
else if (fs_1.default.existsSync(legacyInitScript)) {
|
|
1138
|
+
(0, child_process_1.execSync)(`cd "${regtestDir}" && bash "${legacyInitScript}"`, { stdio: 'inherit' });
|
|
1139
|
+
}
|
|
674
1140
|
}
|
|
675
|
-
|
|
676
|
-
(
|
|
1141
|
+
catch {
|
|
1142
|
+
console.error('regtest-init.sh failed, see output above');
|
|
1143
|
+
process.exit(1);
|
|
677
1144
|
}
|
|
678
1145
|
(0, child_process_1.execSync)(`cd "${regtestDir}" && docker compose up -d && docker compose up -d zaino`, { stdio: 'inherit' });
|
|
679
1146
|
const waitForRpc = async (payload, maxAttempts = 45) => {
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function parseEnvContent(content: string): Record<string, string>;
|
|
2
|
+
export declare function loadProjectEnv(cwd?: string, processEnv?: NodeJS.ProcessEnv): Record<string, string>;
|
|
3
|
+
export declare function applyProjectEnv(cwd?: string): Record<string, string>;
|
|
4
|
+
export declare function formatZec(zatoshis: bigint | number): string;
|
|
5
|
+
export declare function parseZecToZatoshis(amountStr: string): bigint;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseEnvContent = parseEnvContent;
|
|
7
|
+
exports.loadProjectEnv = loadProjectEnv;
|
|
8
|
+
exports.applyProjectEnv = applyProjectEnv;
|
|
9
|
+
exports.formatZec = formatZec;
|
|
10
|
+
exports.parseZecToZatoshis = parseZecToZatoshis;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
function parseEnvContent(content) {
|
|
14
|
+
const result = {};
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
19
|
+
continue;
|
|
20
|
+
const eqIdx = trimmed.indexOf('=');
|
|
21
|
+
if (eqIdx === -1)
|
|
22
|
+
continue;
|
|
23
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
24
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
25
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
26
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
27
|
+
val = val.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
result[key] = val;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function loadProjectEnv(cwd = process.cwd(), processEnv = process.env) {
|
|
34
|
+
const envPath = path_1.default.join(cwd, '.env');
|
|
35
|
+
const envLocalPath = path_1.default.join(cwd, '.env.local');
|
|
36
|
+
const fromEnv = fs_1.default.existsSync(envPath)
|
|
37
|
+
? parseEnvContent(fs_1.default.readFileSync(envPath, 'utf8'))
|
|
38
|
+
: {};
|
|
39
|
+
const fromLocal = fs_1.default.existsSync(envLocalPath)
|
|
40
|
+
? parseEnvContent(fs_1.default.readFileSync(envLocalPath, 'utf8'))
|
|
41
|
+
: {};
|
|
42
|
+
// Precedence: processEnv wins > .env.local > .env
|
|
43
|
+
const merged = { ...fromEnv, ...fromLocal };
|
|
44
|
+
for (const [k, v] of Object.entries(processEnv)) {
|
|
45
|
+
if (v !== undefined) {
|
|
46
|
+
merged[k] = v;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
function applyProjectEnv(cwd = process.cwd()) {
|
|
52
|
+
const config = loadProjectEnv(cwd);
|
|
53
|
+
for (const [k, v] of Object.entries(config)) {
|
|
54
|
+
if (process.env[k] === undefined) {
|
|
55
|
+
process.env[k] = v;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
function formatZec(zatoshis) {
|
|
61
|
+
try {
|
|
62
|
+
const val = BigInt(zatoshis);
|
|
63
|
+
const neg = val < 0n;
|
|
64
|
+
const abs = neg ? -val : val;
|
|
65
|
+
const whole = abs / 100000000n;
|
|
66
|
+
const frac = (abs % 100000000n).toString().padStart(8, '0');
|
|
67
|
+
return `${neg ? '-' : ''}${whole}.${frac}`;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return String(zatoshis);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parseZecToZatoshis(amountStr) {
|
|
74
|
+
const trimmed = (amountStr ?? '').trim();
|
|
75
|
+
if (!trimmed || !/^\d+(\.\d{1,8})?$/.test(trimmed)) {
|
|
76
|
+
throw new Error(`Invalid ZEC amount: "${amountStr}". Expected decimal number with up to 8 decimal places.`);
|
|
77
|
+
}
|
|
78
|
+
const [whole, fraction = ''] = trimmed.split('.');
|
|
79
|
+
const wholeZat = BigInt(whole) * 100000000n;
|
|
80
|
+
const fractionZat = BigInt(fraction.padEnd(8, '0'));
|
|
81
|
+
const total = wholeZat + fractionZat;
|
|
82
|
+
if (total <= 0n) {
|
|
83
|
+
throw new Error(`Amount must be greater than zero, got ${amountStr} ZEC`);
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export * from '@zcash-app/sdk';
|
|
2
2
|
export * from './testnet-lightwalletd';
|
|
3
|
+
export * from './env';
|
|
4
|
+
export * from './redact';
|
|
5
|
+
export { program, executeSend, SendActionOptions, SendDependencies, SendResult, executeFund, FundActionOptions, FundDependencies, FundResult, } from './cli';
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.executeFund = exports.executeSend = exports.program = void 0;
|
|
17
18
|
__exportStar(require("@zcash-app/sdk"), exports);
|
|
18
19
|
__exportStar(require("./testnet-lightwalletd"), exports);
|
|
20
|
+
__exportStar(require("./env"), exports);
|
|
21
|
+
__exportStar(require("./redact"), exports);
|
|
22
|
+
var cli_1 = require("./cli");
|
|
23
|
+
Object.defineProperty(exports, "program", { enumerable: true, get: function () { return cli_1.program; } });
|
|
24
|
+
Object.defineProperty(exports, "executeSend", { enumerable: true, get: function () { return cli_1.executeSend; } });
|
|
25
|
+
Object.defineProperty(exports, "executeFund", { enumerable: true, get: function () { return cli_1.executeFund; } });
|
package/dist/redact.d.ts
ADDED
package/dist/redact.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactSecretUrl = redactSecretUrl;
|
|
4
|
+
exports.sanitizeError = sanitizeError;
|
|
5
|
+
function redactSecretUrl(rawUrl) {
|
|
6
|
+
if (!rawUrl || typeof rawUrl !== 'string')
|
|
7
|
+
return rawUrl;
|
|
8
|
+
const trimmed = rawUrl.trim();
|
|
9
|
+
if (!trimmed.includes('://')) {
|
|
10
|
+
if (trimmed.includes('@')) {
|
|
11
|
+
return trimmed.replace(/^[^@]+@/, '<redacted>@');
|
|
12
|
+
}
|
|
13
|
+
return trimmed;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(trimmed);
|
|
17
|
+
const hasAuth = Boolean(parsed.username || parsed.password);
|
|
18
|
+
const hasPathSecret = parsed.pathname.length > 1;
|
|
19
|
+
const hasQuerySecret = parsed.search.length > 1;
|
|
20
|
+
if (hasAuth || hasPathSecret || hasQuerySecret) {
|
|
21
|
+
const portPart = parsed.port ? `:${parsed.port}` : '';
|
|
22
|
+
return `${parsed.protocol}//${parsed.hostname}${portPart}/<redacted>`;
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return trimmed
|
|
28
|
+
.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@]+@([^\s/]+)/, '$1<redacted>@$2')
|
|
29
|
+
.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s/?]+)\/[^\s]+/, '$1/<redacted>')
|
|
30
|
+
.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s/?]+)\?[^\s]+/, '$1/<redacted>');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function sanitizeError(err, mnemonic) {
|
|
34
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
if (mnemonic && mnemonic.trim()) {
|
|
36
|
+
message = message.split(mnemonic.trim()).join('<redacted>');
|
|
37
|
+
const words = mnemonic.trim().split(/\s+/);
|
|
38
|
+
if (words.length >= 12) {
|
|
39
|
+
const joinedWords = words.join(' ');
|
|
40
|
+
message = message.split(joinedWords).join('<redacted>');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Redact URLs with secrets/paths
|
|
44
|
+
message = message.replace(/https?:\/\/[^\s"'<>]+/g, (url) => redactSecretUrl(url));
|
|
45
|
+
const clean = new Error(message);
|
|
46
|
+
clean.name = err instanceof Error ? err.name : 'Error';
|
|
47
|
+
return clean;
|
|
48
|
+
}
|
|
@@ -23,10 +23,34 @@ done
|
|
|
23
23
|
echo " Zebra is ready."
|
|
24
24
|
|
|
25
25
|
echo "==> Mining 2 blocks to activate NU6.3 at height 2..."
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
GEN_TIMEOUT="${GENERATE_TIMEOUT_SECS:-60}"
|
|
27
|
+
GEN_SUCCESS=0
|
|
28
|
+
LAST_GEN_RESPONSE=""
|
|
29
|
+
START_TIME=$(date +%s)
|
|
30
|
+
while true; do
|
|
31
|
+
LAST_GEN_RESPONSE="$(curl -s -X POST \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{"jsonrpc":"2.0","method":"generate","params":[2],"id":1}' \
|
|
34
|
+
"http://127.0.0.1:29232" 2>&1 || true)"
|
|
35
|
+
if echo "$LAST_GEN_RESPONSE" | grep -q '"result"' && ! echo "$LAST_GEN_RESPONSE" | grep -q '"error"'; then
|
|
36
|
+
GEN_SUCCESS=1
|
|
37
|
+
break
|
|
38
|
+
fi
|
|
39
|
+
NOW=$(date +%s)
|
|
40
|
+
if [ $((NOW - START_TIME)) -ge "$GEN_TIMEOUT" ]; then
|
|
41
|
+
break
|
|
42
|
+
fi
|
|
43
|
+
sleep 2
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [ "$GEN_SUCCESS" -ne 1 ]; then
|
|
47
|
+
echo "❌ Failed to mine initial blocks within ${GEN_TIMEOUT}s." >&2
|
|
48
|
+
echo "Last response body:" >&2
|
|
49
|
+
echo "$LAST_GEN_RESPONSE" >&2
|
|
50
|
+
echo "Last 20 lines of docker compose logs zebra:" >&2
|
|
51
|
+
$COMPOSE logs --tail 20 zebra >&2 || true
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
30
54
|
echo " Blocks mined."
|
|
31
55
|
|
|
32
56
|
echo "==> Preparing Zallet data volume..."
|
|
@@ -25,10 +25,34 @@ done
|
|
|
25
25
|
echo " Zebra is ready."
|
|
26
26
|
|
|
27
27
|
echo "==> Mining 2 initial blocks to activate NU7 at height 2..."
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
GEN_TIMEOUT="${GENERATE_TIMEOUT_SECS:-60}"
|
|
29
|
+
GEN_SUCCESS=0
|
|
30
|
+
LAST_GEN_RESPONSE=""
|
|
31
|
+
START_TIME=$(date +%s)
|
|
32
|
+
while true; do
|
|
33
|
+
LAST_GEN_RESPONSE="$(curl -s -X POST \
|
|
34
|
+
-H "Content-Type: application/json" \
|
|
35
|
+
-d '{"jsonrpc":"2.0","method":"generate","params":[2],"id":1}' \
|
|
36
|
+
"http://127.0.0.1:${ZEBRA_PORT}" 2>&1 || true)"
|
|
37
|
+
if echo "$LAST_GEN_RESPONSE" | grep -q '"result"' && ! echo "$LAST_GEN_RESPONSE" | grep -q '"error"'; then
|
|
38
|
+
GEN_SUCCESS=1
|
|
39
|
+
break
|
|
40
|
+
fi
|
|
41
|
+
NOW=$(date +%s)
|
|
42
|
+
if [ $((NOW - START_TIME)) -ge "$GEN_TIMEOUT" ]; then
|
|
43
|
+
break
|
|
44
|
+
fi
|
|
45
|
+
sleep 2
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
if [ "$GEN_SUCCESS" -ne 1 ]; then
|
|
49
|
+
echo "❌ Failed to mine initial blocks within ${GEN_TIMEOUT}s." >&2
|
|
50
|
+
echo "Last response body:" >&2
|
|
51
|
+
echo "$LAST_GEN_RESPONSE" >&2
|
|
52
|
+
echo "Last 20 lines of docker compose logs zebra:" >&2
|
|
53
|
+
$COMPOSE logs --tail 20 zebra >&2 || true
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
32
56
|
echo " Blocks mined. NU7 is now active."
|
|
33
57
|
|
|
34
58
|
echo "==> Starting lightwalletd service..."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zcash-dev",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Regtest developer helper CLI for shielded Zcash apps",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zcash-dev": "./dist/cli.js"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"commander": "^12.1.0",
|
|
36
36
|
"prompts": "^2.4.2",
|
|
37
|
-
"@zcash-app/sdk": "0.1.0-alpha.
|
|
37
|
+
"@zcash-app/sdk": "0.1.0-alpha.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|