@leofcoin/launch-chain 0.4.26 → 0.5.1
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 +38 -0
- package/exports/batch.d.ts +30 -0
- package/exports/batch.js +140 -0
- package/exports/cli-options.d.ts +1 -1
- package/exports/cli-options.js +1 -1
- package/exports/cli.d.ts +1 -1
- package/exports/cli.js +47 -3
- package/exports/node.d.ts +3 -0
- package/exports/node.js +55 -9
- package/exports/validator-cli.js +4 -1
- package/exports/validator.d.ts +2 -0
- package/exports/validator.js +9 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -102,8 +102,46 @@ the process:
|
|
|
102
102
|
leofcoin> account
|
|
103
103
|
leofcoin> balance
|
|
104
104
|
leofcoin> transfer YTq... 1000
|
|
105
|
+
leofcoin> batch
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
### interactive testnet batch
|
|
109
|
+
|
|
110
|
+
Type `batch` in the existing node or validator wallet shell. It uses the already
|
|
111
|
+
unlocked wallet and selected account in that process: **no wallet export, password
|
|
112
|
+
file, extra endpoint, or second node is required**.
|
|
113
|
+
When the node is stopped, `leofcoin batch [options]` opens the existing local
|
|
114
|
+
wallet in a normal non-participating node and starts the same wizard. It requires
|
|
115
|
+
a terminal. Do not run it alongside a node using the same root.
|
|
116
|
+
|
|
117
|
+
The wizard asks for transaction count (default 100,000), recipient (default self),
|
|
118
|
+
LFC per transaction (default one atom), and concurrency (default 32). Smaller and
|
|
119
|
+
larger positive safe-integer counts are supported. Enter `yes` to prepare; after
|
|
120
|
+
signing and exact fee/balance checks, enter `SEND` to actually enqueue.
|
|
121
|
+
Anything else at either confirmation cancels without sending.
|
|
122
|
+
|
|
123
|
+
Only a synced, connected Peach node is accepted. Local validator heartbeats are
|
|
124
|
+
paused while the wizard owns the wallet. A transfer already awaiting finalization
|
|
125
|
+
must finish first. Do not use this account from another node, wallet UI or API
|
|
126
|
+
during the batch; the wizard rechecks its nonce before sending but cannot reserve
|
|
127
|
+
the key across other processes.
|
|
128
|
+
|
|
129
|
+
All transactions are signed before submission; concurrency bounds simultaneous
|
|
130
|
+
admission calls, not total pool size. Submissions do not wait for blocks, and
|
|
131
|
+
`admitted` is not finality. Self-transfers recycle principal but still need the
|
|
132
|
+
transfer amount plus all fees. Preparing very large batches uses time and memory.
|
|
133
|
+
Only load testnets you own or have permission to test.
|
|
134
|
+
|
|
135
|
+
Receipts are automatically saved under `~/.leofcoin/batch-receipts/` in a unique
|
|
136
|
+
mode-0600 JSONL file. Each attempted transaction records its signed public fields
|
|
137
|
+
before submission, then hash/status after admission. No private keys or passwords
|
|
138
|
+
are stored. Errors stop new calls and drain in-flight work; unknown transactions
|
|
139
|
+
may still finalize. Reconcile the journal and pending/canonical state before
|
|
140
|
+
retrying; no automatic retry or cancellation of admitted transactions occurs.
|
|
141
|
+
|
|
142
|
+
The standalone [batch script](../../docs/testnet-batch.md) remains available for
|
|
143
|
+
automation with explicit encrypted signer files.
|
|
144
|
+
|
|
107
145
|
When the node is stopped, a one-shot transfer can start the same local identity,
|
|
108
146
|
finalize the transfer, and exit. It does not register or participate as a validator:
|
|
109
147
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { signTransaction } from '@leofcoin/lib';
|
|
2
|
+
type SignedTransaction = Awaited<ReturnType<typeof signTransaction>>;
|
|
3
|
+
type JournalEntry = Record<string, string | number | boolean>;
|
|
4
|
+
export type BatchJournal = {
|
|
5
|
+
path: string;
|
|
6
|
+
append: (entry: JournalEntry) => Promise<void>;
|
|
7
|
+
close: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type BatchWallet = {
|
|
10
|
+
account: string;
|
|
11
|
+
identity: () => Promise<unknown>;
|
|
12
|
+
connected: () => boolean;
|
|
13
|
+
balance: () => Promise<bigint>;
|
|
14
|
+
nonce: () => Promise<number>;
|
|
15
|
+
parseAmount: (value: string) => bigint;
|
|
16
|
+
formatAmount: (value: bigint) => string;
|
|
17
|
+
sign: (nonce: number, to: string, amount: bigint) => Promise<SignedTransaction>;
|
|
18
|
+
submit: (transaction: SignedTransaction) => Promise<{
|
|
19
|
+
hash: string;
|
|
20
|
+
wait: Promise<unknown>;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export type BatchInteraction = {
|
|
24
|
+
question: (prompt: string) => Promise<string>;
|
|
25
|
+
log: (message: string) => void;
|
|
26
|
+
createJournal?: () => Promise<BatchJournal>;
|
|
27
|
+
};
|
|
28
|
+
/** Uses the running node's signer; never starts another node or opens wallet stores. */
|
|
29
|
+
export declare function interactiveBatch(wallet: BatchWallet, interaction: BatchInteraction): Promise<void>;
|
|
30
|
+
export {};
|
package/exports/batch.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, open } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { calculateFee } from '@leofcoin/lib';
|
|
6
|
+
import { verifyChainIdentity, createChainIdentity } from '@leofcoin/lib/chain-identity';
|
|
7
|
+
import addresses from '@leofcoin/addresses';
|
|
8
|
+
|
|
9
|
+
const createJournal = async () => {
|
|
10
|
+
const directory = join(homedir(), '.leofcoin', 'batch-receipts');
|
|
11
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
12
|
+
const path = join(directory, `${Date.now()}-${randomUUID()}.jsonl`);
|
|
13
|
+
const file = await open(path, 'wx', 0o600);
|
|
14
|
+
let tail = Promise.resolve();
|
|
15
|
+
return {
|
|
16
|
+
path,
|
|
17
|
+
append: entry => {
|
|
18
|
+
tail = tail.then(async () => { await file.writeFile(JSON.stringify(entry) + '\n'); await file.sync(); });
|
|
19
|
+
return tail;
|
|
20
|
+
},
|
|
21
|
+
close: () => file.close()
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const positiveInteger = (value, label, maximum = Number.MAX_SAFE_INTEGER) => {
|
|
25
|
+
if (!/^[1-9]\d*$/.test(value))
|
|
26
|
+
throw new Error(`${label} must be a positive integer`);
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isSafeInteger(parsed) || parsed > maximum)
|
|
29
|
+
throw new Error(`${label} exceeds ${maximum}`);
|
|
30
|
+
return parsed;
|
|
31
|
+
};
|
|
32
|
+
/** Uses the running node's signer; never starts another node or opens wallet stores. */
|
|
33
|
+
async function interactiveBatch(wallet, interaction) {
|
|
34
|
+
const { question, log } = interaction;
|
|
35
|
+
const identity = await verifyChainIdentity(await wallet.identity());
|
|
36
|
+
const expected = await createChainIdentity('leofcoin:peach', addresses);
|
|
37
|
+
if (identity.network !== 'leofcoin:peach' || identity.chainId !== expected.chainId)
|
|
38
|
+
throw new Error('Batch sending is only available on the expected Peach testnet');
|
|
39
|
+
if (!wallet.connected())
|
|
40
|
+
throw new Error('Node must be synced and connected before batch sending');
|
|
41
|
+
log(`Testnet batch wallet: ${wallet.account}`);
|
|
42
|
+
log(`Balance: ${wallet.formatAmount(await wallet.balance())} LFC`);
|
|
43
|
+
const count = positiveInteger((await question('Number of transactions [100000]: ')).trim() || '100000', 'count');
|
|
44
|
+
const to = (await question(`Recipient [self: ${wallet.account}]: `)).trim() || wallet.account;
|
|
45
|
+
const defaultAmount = wallet.formatAmount(1n);
|
|
46
|
+
const amount = wallet.parseAmount((await question(`LFC per transaction [${defaultAmount}]: `)).trim() || defaultAmount);
|
|
47
|
+
if (amount <= 0n)
|
|
48
|
+
throw new Error('Amount must be positive');
|
|
49
|
+
const concurrency = positiveInteger((await question('Concurrent submissions [32]: ')).trim() || '32', 'concurrency', 1024);
|
|
50
|
+
log(`${count} transactions to ${to}, ${wallet.formatAmount(amount)} LFC each; concurrency ${concurrency}.`);
|
|
51
|
+
log('Uses your unlocked wallet. Fees apply; admission is not finalization. Only load networks you are permitted to test.');
|
|
52
|
+
if ((await question('Prepare this batch? Type yes: ')).trim().toLowerCase() !== 'yes') {
|
|
53
|
+
log('Batch cancelled; no transactions sent.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const startNonce = await wallet.nonce();
|
|
57
|
+
if (!Number.isSafeInteger(startNonce) || startNonce < 0 || !Number.isSafeInteger(startNonce + count))
|
|
58
|
+
throw new Error('Invalid or unsafe nonce range');
|
|
59
|
+
const transactions = [];
|
|
60
|
+
let fees = 0n;
|
|
61
|
+
for (let index = 0; index < count; index++) {
|
|
62
|
+
const signed = await wallet.sign(startNonce + index + 1, to, amount);
|
|
63
|
+
fees += BigInt(await calculateFee(signed));
|
|
64
|
+
transactions.push(signed);
|
|
65
|
+
if ((index + 1) % 1000 === 0)
|
|
66
|
+
log(`Signed ${index + 1}/${count}`);
|
|
67
|
+
}
|
|
68
|
+
// Self-transfers recycle principal, but still need enough for a transfer plus
|
|
69
|
+
// every fee. Fees are not assumed to return as validator rewards.
|
|
70
|
+
const principal = to === wallet.account ? amount : amount * BigInt(count);
|
|
71
|
+
const required = principal + fees;
|
|
72
|
+
const balance = await wallet.balance();
|
|
73
|
+
log(`Total fees: ${wallet.formatAmount(fees)} LFC; required balance: ${wallet.formatAmount(required)} LFC.`);
|
|
74
|
+
if (balance < required)
|
|
75
|
+
throw new Error('Insufficient wallet balance for this batch including fees');
|
|
76
|
+
if ((await question(`Send ${count} testnet transactions now? Type SEND: `)).trim() !== 'SEND') {
|
|
77
|
+
log('Batch cancelled; no transactions sent.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!wallet.connected())
|
|
81
|
+
throw new Error('Node is no longer synced and connected; no transactions sent');
|
|
82
|
+
if (await wallet.nonce() !== startNonce)
|
|
83
|
+
throw new Error('Wallet nonce changed; no transactions sent. Stop other senders using this wallet');
|
|
84
|
+
if (await wallet.balance() < required)
|
|
85
|
+
throw new Error('Wallet balance changed; no transactions sent');
|
|
86
|
+
const journal = await (interaction.createJournal ?? createJournal)();
|
|
87
|
+
log(`Receipts: ${journal.path}`);
|
|
88
|
+
let next = 0;
|
|
89
|
+
let attempted = 0;
|
|
90
|
+
let admitted = 0;
|
|
91
|
+
let failed = false;
|
|
92
|
+
let failure;
|
|
93
|
+
try {
|
|
94
|
+
await journal.append({ status: 'prepared', account: wallet.account, to, count, amountAtoms: String(amount),
|
|
95
|
+
feeAtoms: String(fees), chainId: identity.chainId, firstNonce: startNonce + 1, lastNonce: startNonce + count });
|
|
96
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(count, concurrency) }, async () => {
|
|
97
|
+
while (!failed && next < count) {
|
|
98
|
+
const index = next++;
|
|
99
|
+
const nonce = startNonce + index + 1;
|
|
100
|
+
let dispatched = false;
|
|
101
|
+
try {
|
|
102
|
+
await journal.append({ status: 'attempting', nonce, from: wallet.account, contract: addresses.nativeToken,
|
|
103
|
+
method: 'transfer', to, amountAtoms: String(amount), timestamp: transactions[index].timestamp,
|
|
104
|
+
signature: String(transactions[index].signature) });
|
|
105
|
+
dispatched = true;
|
|
106
|
+
attempted++;
|
|
107
|
+
const pending = await wallet.submit(transactions[index]);
|
|
108
|
+
if (!pending?.hash || !pending.wait || typeof pending.wait.then !== 'function')
|
|
109
|
+
throw new Error('Node returned an invalid admission handle');
|
|
110
|
+
void pending.wait.catch(error => log(`Transaction ${pending.hash} failed after admission: ${String(error)}`));
|
|
111
|
+
await journal.append({ status: 'admitted', nonce, hash: pending.hash });
|
|
112
|
+
admitted++;
|
|
113
|
+
if (admitted % 1000 === 0)
|
|
114
|
+
log(`Admitted ${admitted}/${count}`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
failed = true;
|
|
118
|
+
failure ??= error;
|
|
119
|
+
await journal.append({ status: dispatched ? 'unknown' : 'not-sent', nonce,
|
|
120
|
+
error: error instanceof Error ? error.message : String(error) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
const rejected = workers.find(result => result.status === 'rejected');
|
|
125
|
+
if (rejected?.status === 'rejected') {
|
|
126
|
+
failed = true;
|
|
127
|
+
failure ??= rejected.reason;
|
|
128
|
+
}
|
|
129
|
+
await journal.append({ status: failed ? 'interrupted' : 'admitted', requested: count, attempted,
|
|
130
|
+
admitted, unsent: count - attempted, finalized: false });
|
|
131
|
+
log(`Batch: ${admitted}/${count} admitted; ${count - attempted} unsent. Not a finalization result.`);
|
|
132
|
+
if (failed)
|
|
133
|
+
throw new Error(`Batch stopped; reconcile ${journal.path} before retrying. Some transactions may still finalize`, { cause: failure });
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await journal.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { interactiveBatch };
|
package/exports/cli-options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CliCommand = 'node' | 'validator' | 'account' | 'balance' | 'status' | 'transfer' | 'help' | 'version';
|
|
1
|
+
export type CliCommand = 'node' | 'validator' | 'account' | 'balance' | 'status' | 'transfer' | 'batch' | 'help' | 'version';
|
|
2
2
|
export type CliOptions = {
|
|
3
3
|
command: CliCommand;
|
|
4
4
|
positionals: string[];
|
package/exports/cli-options.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const commands = new Set(['node', 'validator', 'account', 'balance', 'status', 'transfer', 'help', 'version']);
|
|
1
|
+
const commands = new Set(['node', 'validator', 'account', 'balance', 'status', 'transfer', 'batch', 'help', 'version']);
|
|
2
2
|
const port = (name, value) => {
|
|
3
3
|
const parsed = Number(value);
|
|
4
4
|
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65_535)
|
package/exports/cli.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const help = "Leofcoin command line\n\nUsage:\n leofcoin node [options] Run a normal full node\n leofcoin validator [options] Run a validator node\n leofcoin account [options] Show the local account\n leofcoin balance [address] [options] Show an LFC balance\n leofcoin status [options] Show local chain status\n leofcoin transfer <address> <LFC> [options] Send LFC and await finalization\n\nOptions:\n --network <name> Network (default: leofcoin:peach)\n --root <path> Persistent data directory\n --password-file <path> Read the identity password from a private file\n --star <url> Discovery star; repeat for multiple stars\n --no-stars Disable discovery stars\n --http-port <port> HTTP API port (default: 8080)\n --ws-port <port> WebSocket API port (default: 4040)\n --no-endpoints Disable HTTP and WebSocket endpoints\n --no-shell Disable the interactive wallet shell\n --interval <minutes> Validator heartbeat interval (default: 5)\n -h, --help Show help\n -v, --version Show version\n\nPasswords are never accepted as command-line values. Use --password-file,\nLEOFCOIN_PASSWORD_FILE, LEOFCOIN_PASSWORD, or the interactive password prompt.\n";
|
|
2
|
+
export declare const help = "Leofcoin command line\n\nUsage:\n leofcoin node [options] Run a normal full node\n leofcoin validator [options] Run a validator node\n leofcoin account [options] Show the local account\n leofcoin balance [address] [options] Show an LFC balance\n leofcoin status [options] Show local chain status\n leofcoin transfer <address> <LFC> [options] Send LFC and await finalization\n leofcoin batch [options] Interactive testnet burst using the local wallet\n\nOptions:\n --network <name> Network (default: leofcoin:peach)\n --root <path> Persistent data directory\n --password-file <path> Read the identity password from a private file\n --star <url> Discovery star; repeat for multiple stars\n --no-stars Disable discovery stars\n --http-port <port> HTTP API port (default: 8080)\n --ws-port <port> WebSocket API port (default: 4040)\n --no-endpoints Disable HTTP and WebSocket endpoints\n --no-shell Disable the interactive wallet shell\n --interval <minutes> Validator heartbeat interval (default: 5)\n -h, --help Show help\n -v, --version Show version\n\nPasswords are never accepted as command-line values. Use --password-file,\nLEOFCOIN_PASSWORD_FILE, LEOFCOIN_PASSWORD, or the interactive password prompt.\nIn the wallet shell, type batch for an interactive Peach transaction burst\nusing the already unlocked wallet (no separate wallet export or endpoint needed).\n";
|
|
3
3
|
export declare const runCli: (args?: string[]) => Promise<undefined>;
|
package/exports/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { resolve, isAbsolute, relative } from 'node:path';
|
|
5
|
+
import { resolve, join, isAbsolute, relative } from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { parseCliOptions } from './cli-options.js';
|
|
@@ -16,6 +16,9 @@ import '@leofcoin/chain/chain';
|
|
|
16
16
|
import '@leofcoin/lib/node-config';
|
|
17
17
|
import '@leofcoin/endpoint-clients/ws';
|
|
18
18
|
import '@leofcoin/endpoint-clients/http';
|
|
19
|
+
import './batch.js';
|
|
20
|
+
import 'node:crypto';
|
|
21
|
+
import '@leofcoin/lib/chain-identity';
|
|
19
22
|
import '@leofcoin/addresses';
|
|
20
23
|
|
|
21
24
|
const help = `Leofcoin command line
|
|
@@ -27,6 +30,7 @@ Usage:
|
|
|
27
30
|
leofcoin balance [address] [options] Show an LFC balance
|
|
28
31
|
leofcoin status [options] Show local chain status
|
|
29
32
|
leofcoin transfer <address> <LFC> [options] Send LFC and await finalization
|
|
33
|
+
leofcoin batch [options] Interactive testnet burst using the local wallet
|
|
30
34
|
|
|
31
35
|
Options:
|
|
32
36
|
--network <name> Network (default: leofcoin:peach)
|
|
@@ -44,6 +48,8 @@ Options:
|
|
|
44
48
|
|
|
45
49
|
Passwords are never accepted as command-line values. Use --password-file,
|
|
46
50
|
LEOFCOIN_PASSWORD_FILE, LEOFCOIN_PASSWORD, or the interactive password prompt.
|
|
51
|
+
In the wallet shell, type batch for an interactive Peach transaction burst
|
|
52
|
+
using the already unlocked wallet (no separate wallet export or endpoint needed).
|
|
47
53
|
`;
|
|
48
54
|
const packageVersion = async () => {
|
|
49
55
|
const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -105,12 +111,17 @@ const runShell = async (handle) => {
|
|
|
105
111
|
}
|
|
106
112
|
else if (command === 'status')
|
|
107
113
|
await showStatus(handle);
|
|
114
|
+
else if (command === 'batch')
|
|
115
|
+
await handle.batch({
|
|
116
|
+
question: prompt => shell.question(prompt),
|
|
117
|
+
log: message => process.stdout.write(`${message}\n`)
|
|
118
|
+
});
|
|
108
119
|
else if (command === 'transfer') {
|
|
109
120
|
const hash = await handle.transfer(values[0], amountFor(chain, values[1]));
|
|
110
121
|
process.stdout.write(`transfer finalized: ${hash}\n`);
|
|
111
122
|
}
|
|
112
123
|
else if (command === 'help') {
|
|
113
|
-
process.stdout.write('account | balance [address] | status | transfer <address> <LFC> | exit\n');
|
|
124
|
+
process.stdout.write('account | balance [address] | status | transfer <address> <LFC> | batch | exit\n');
|
|
114
125
|
}
|
|
115
126
|
else if (command === 'exit' || command === 'quit') {
|
|
116
127
|
shell.close();
|
|
@@ -130,6 +141,8 @@ const runCli = async (args = process.argv.slice(2)) => {
|
|
|
130
141
|
return void process.stdout.write(help);
|
|
131
142
|
if (options.command === 'version')
|
|
132
143
|
return void process.stdout.write(`${await packageVersion()}\n`);
|
|
144
|
+
if (options.command === 'batch' && !process.stdin.isTTY)
|
|
145
|
+
throw new Error('Interactive batch requires a terminal; use the standalone script with explicit confirmation for automation');
|
|
133
146
|
const common = {
|
|
134
147
|
network: options.network ?? process.env.LEOFCOIN_NETWORK,
|
|
135
148
|
root: dataRootFor(options.root ?? process.env.LEOFCOIN_DATA_ROOT),
|
|
@@ -138,6 +151,21 @@ const runCli = async (args = process.argv.slice(2)) => {
|
|
|
138
151
|
httpPort: options.httpPort,
|
|
139
152
|
wsPort: options.wsPort
|
|
140
153
|
};
|
|
154
|
+
if (options.command === 'batch') {
|
|
155
|
+
if ((common.network ?? 'leofcoin:peach') !== 'leofcoin:peach')
|
|
156
|
+
throw new Error('Interactive batch is only available on Peach testnet');
|
|
157
|
+
const root = resolve(homedir(), common.root || '.leofcoin/peach');
|
|
158
|
+
for (const name of ['account', 'wallet']) {
|
|
159
|
+
const marker = join(root, name, 'CURRENT');
|
|
160
|
+
try {
|
|
161
|
+
if (!(await lstat(marker)).isFile())
|
|
162
|
+
throw new Error(`Invalid existing ${name} store`);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw new Error(`Batch requires an existing local wallet at ${root}; it will not create a new wallet`, { cause: error });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
141
169
|
const handle = options.command === 'validator'
|
|
142
170
|
? await startValidator({ ...common, intervalMinutes: options.intervalMinutes })
|
|
143
171
|
: await startNode(common);
|
|
@@ -165,6 +193,22 @@ const runCli = async (args = process.argv.slice(2)) => {
|
|
|
165
193
|
const hash = await nodeHandle.transfer(to, amountFor(chain, options.positionals[1]));
|
|
166
194
|
process.stdout.write(`transfer finalized: ${hash}\n`);
|
|
167
195
|
}
|
|
196
|
+
else if (options.command === 'batch') {
|
|
197
|
+
const shell = createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
try {
|
|
199
|
+
await nodeHandle.batch({ question: prompt => shell.question(prompt),
|
|
200
|
+
log: message => process.stdout.write(`${message}\n`) });
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
shell.close();
|
|
208
|
+
nodeHandle.stop();
|
|
209
|
+
setTimeout(() => process.exit(process.exitCode ?? 0), 100);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
168
212
|
else {
|
|
169
213
|
process.stdout.write(`${options.command} account: ${nodeHandle.account}\n`);
|
|
170
214
|
if (options.shell && process.stdin.isTTY)
|
package/exports/node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Chain from '@leofcoin/chain/chain';
|
|
2
|
+
import { type BatchInteraction } from './batch.js';
|
|
2
3
|
export type LocalNodeOptions = {
|
|
3
4
|
network?: string;
|
|
4
5
|
networkVersion?: string;
|
|
@@ -17,6 +18,8 @@ export type LocalNodeHandle = {
|
|
|
17
18
|
ws: string[];
|
|
18
19
|
};
|
|
19
20
|
transfer: (to: string, amount: bigint) => Promise<string>;
|
|
21
|
+
batch: (interaction: BatchInteraction) => Promise<void>;
|
|
22
|
+
walletBusy: boolean;
|
|
20
23
|
stop: () => void;
|
|
21
24
|
};
|
|
22
25
|
export declare const startNode: (options?: LocalNodeOptions) => Promise<LocalNodeHandle>;
|
package/exports/node.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { signTransaction } from '@leofcoin/lib';
|
|
2
2
|
import networks from '@leofcoin/networks';
|
|
3
3
|
import launch from './index.js';
|
|
4
|
+
import { interactiveBatch } from './batch.js';
|
|
4
5
|
import '@leofcoin/chain/node';
|
|
5
6
|
import '@leofcoin/chain/chain';
|
|
6
7
|
import '@leofcoin/lib/node-config';
|
|
7
8
|
import '@leofcoin/endpoint-clients/ws';
|
|
8
9
|
import '@leofcoin/endpoint-clients/http';
|
|
10
|
+
import 'node:fs/promises';
|
|
11
|
+
import 'node:os';
|
|
12
|
+
import 'node:path';
|
|
13
|
+
import 'node:crypto';
|
|
14
|
+
import '@leofcoin/lib/chain-identity';
|
|
15
|
+
import '@leofcoin/addresses';
|
|
9
16
|
|
|
10
17
|
const startNode = async (options = {}) => {
|
|
11
18
|
const network = options.network ?? 'leofcoin:peach';
|
|
@@ -26,26 +33,65 @@ const startNode = async (options = {}) => {
|
|
|
26
33
|
const account = peernet?.selectedAccount;
|
|
27
34
|
if (!account)
|
|
28
35
|
throw new Error('node identity has no selected account');
|
|
36
|
+
let batchActive = false;
|
|
37
|
+
let activeTransfers = 0;
|
|
29
38
|
const transfer = async (to, amount) => {
|
|
39
|
+
if (batchActive)
|
|
40
|
+
throw new Error('Wallet is reserved for an interactive batch');
|
|
30
41
|
if (!to)
|
|
31
42
|
throw new Error('transfer recipient is required');
|
|
32
43
|
if (amount <= 0n)
|
|
33
44
|
throw new Error('transfer amount must be greater than zero');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
activeTransfers++;
|
|
46
|
+
try {
|
|
47
|
+
const balance = BigInt(await chainRuntime.balanceOf(account));
|
|
48
|
+
if (balance < amount)
|
|
49
|
+
throw new Error(`insufficient balance: have ${balance}, need at least ${amount}`);
|
|
50
|
+
const raw = await chainRuntime.createTransaction({
|
|
51
|
+
from: account, to: chain.nativeToken, method: 'transfer', params: [to, amount]
|
|
52
|
+
});
|
|
53
|
+
const signed = await signTransaction(raw, peernet.identity);
|
|
54
|
+
const pending = await chainRuntime.sendTransaction(signed);
|
|
55
|
+
return await pending.wait;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
activeTransfers--;
|
|
59
|
+
}
|
|
43
60
|
};
|
|
44
61
|
return {
|
|
45
62
|
account,
|
|
46
63
|
chain,
|
|
47
64
|
endpoints: launched.endpoints,
|
|
48
65
|
transfer,
|
|
66
|
+
get walletBusy() { return batchActive; },
|
|
67
|
+
batch: async (interaction) => {
|
|
68
|
+
if (batchActive)
|
|
69
|
+
throw new Error('A wallet batch is already active');
|
|
70
|
+
if (activeTransfers)
|
|
71
|
+
throw new Error('Wait for the active wallet transfer to finish before starting a batch');
|
|
72
|
+
batchActive = true;
|
|
73
|
+
try {
|
|
74
|
+
await interactiveBatch({
|
|
75
|
+
account,
|
|
76
|
+
identity: () => chainRuntime.getChainIdentity(),
|
|
77
|
+
connected: () => peernet.selectedAccount === account && chainRuntime.state?.sync === 'synced' &&
|
|
78
|
+
Object.values(peernet.connections || {}).some(peer => !!peer && typeof peer === 'object' && 'connected' in peer && peer.connected === true),
|
|
79
|
+
balance: async () => BigInt(await chainRuntime.balanceOf(account)),
|
|
80
|
+
nonce: () => chainRuntime.getNonce(account),
|
|
81
|
+
parseAmount: value => BigInt(chainRuntime.utils.parseUnits(value)),
|
|
82
|
+
formatAmount: value => String(chainRuntime.utils.formatUnits(value)),
|
|
83
|
+
sign: (nonce, to, amount) => {
|
|
84
|
+
const raw = { from: account, to: chain.nativeToken, method: 'transfer',
|
|
85
|
+
params: [to, amount], timestamp: Date.now(), nonce };
|
|
86
|
+
return signTransaction(raw, peernet.identity);
|
|
87
|
+
},
|
|
88
|
+
submit: transaction => chainRuntime.sendTransaction(transaction)
|
|
89
|
+
}, interaction);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
batchActive = false;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
49
95
|
stop: () => controller.abort(new Error('node stopped'))
|
|
50
96
|
};
|
|
51
97
|
};
|
package/exports/validator-cli.js
CHANGED
|
@@ -16,8 +16,11 @@ import '@leofcoin/chain/chain';
|
|
|
16
16
|
import '@leofcoin/lib/node-config';
|
|
17
17
|
import '@leofcoin/endpoint-clients/ws';
|
|
18
18
|
import '@leofcoin/endpoint-clients/http';
|
|
19
|
-
import './
|
|
19
|
+
import './batch.js';
|
|
20
|
+
import 'node:crypto';
|
|
21
|
+
import '@leofcoin/lib/chain-identity';
|
|
20
22
|
import '@leofcoin/addresses';
|
|
23
|
+
import './validator.js';
|
|
21
24
|
|
|
22
25
|
runCli(['validator', ...process.argv.slice(2)]).catch((error) => {
|
|
23
26
|
process.stderr.write(`leofcoin-validator: ${error?.message ?? error}\n`);
|
package/exports/validator.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type ValidatorHandle = {
|
|
|
21
21
|
ws: string[];
|
|
22
22
|
};
|
|
23
23
|
transfer: (to: string, amount: bigint) => Promise<string>;
|
|
24
|
+
batch: import('./node.js').LocalNodeHandle['batch'];
|
|
25
|
+
walletBusy: boolean;
|
|
24
26
|
stop: () => void;
|
|
25
27
|
};
|
|
26
28
|
export declare const validateIntervalMinutes: (value: number) => number;
|
package/exports/validator.js
CHANGED
|
@@ -8,6 +8,12 @@ import '@leofcoin/chain/chain';
|
|
|
8
8
|
import '@leofcoin/lib/node-config';
|
|
9
9
|
import '@leofcoin/endpoint-clients/ws';
|
|
10
10
|
import '@leofcoin/endpoint-clients/http';
|
|
11
|
+
import './batch.js';
|
|
12
|
+
import 'node:fs/promises';
|
|
13
|
+
import 'node:os';
|
|
14
|
+
import 'node:path';
|
|
15
|
+
import 'node:crypto';
|
|
16
|
+
import '@leofcoin/lib/chain-identity';
|
|
11
17
|
|
|
12
18
|
const delay = (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
13
19
|
if (signal?.aborted)
|
|
@@ -81,7 +87,7 @@ const startValidator = async (options = {}) => {
|
|
|
81
87
|
const transfer = node.transfer;
|
|
82
88
|
let sending = false;
|
|
83
89
|
const heartbeat = async () => {
|
|
84
|
-
if (controller.signal.aborted || sending)
|
|
90
|
+
if (controller.signal.aborted || sending || node.walletBusy)
|
|
85
91
|
return;
|
|
86
92
|
sending = true;
|
|
87
93
|
try {
|
|
@@ -115,7 +121,8 @@ const startValidator = async (options = {}) => {
|
|
|
115
121
|
clearInterval(interval);
|
|
116
122
|
};
|
|
117
123
|
controller.signal.addEventListener('abort', () => interval && clearInterval(interval), { once: true });
|
|
118
|
-
return { account, chain, endpoints: node.endpoints, transfer,
|
|
124
|
+
return { account, chain, endpoints: node.endpoints, transfer,
|
|
125
|
+
batch: node.batch, get walletBusy() { return node.walletBusy; }, stop };
|
|
119
126
|
};
|
|
120
127
|
|
|
121
128
|
export { startValidator, validateIntervalMinutes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/launch-chain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/leofcoin/monorepo/tree/main/packages/launch-chain#readme",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@leofcoin/addresses": "^1.0
|
|
46
|
-
"@leofcoin/chain": "^1.
|
|
47
|
-
"@leofcoin/endpoint-clients": "^0.
|
|
48
|
-
"@leofcoin/endpoints": "^0.
|
|
49
|
-
"@leofcoin/lib": "^1.
|
|
45
|
+
"@leofcoin/addresses": "^1.1.0",
|
|
46
|
+
"@leofcoin/chain": "^1.11.0",
|
|
47
|
+
"@leofcoin/endpoint-clients": "^0.4.0",
|
|
48
|
+
"@leofcoin/endpoints": "^0.5.0",
|
|
49
|
+
"@leofcoin/lib": "^1.4.0",
|
|
50
50
|
"@leofcoin/networks": "^1.1.30",
|
|
51
51
|
"@vandeurenglenn/wrtc": "^0.7.2"
|
|
52
52
|
},
|