@leofcoin/launch-chain 0.5.0 → 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 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 {};
@@ -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 };
@@ -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[];
@@ -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
- const balance = BigInt(await chainRuntime.balanceOf(account));
35
- if (balance < amount)
36
- throw new Error(`insufficient balance: have ${balance}, need at least ${amount}`);
37
- const raw = await chainRuntime.createTransaction({
38
- from: account, to: chain.nativeToken, method: 'transfer', params: [to, amount]
39
- });
40
- const signed = await signTransaction(raw, peernet.identity);
41
- const pending = await chainRuntime.sendTransaction(signed);
42
- return pending.wait;
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
  };
@@ -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 './validator.js';
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`);
@@ -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;
@@ -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, stop };
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.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {