finlayer-cli 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +361 -0
  2. package/package.json +34 -0
package/index.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { generateKeyPairSync, createHash } from 'crypto';
5
+ import { writeFileSync } from 'fs';
6
+ import { homedir } from 'os';
7
+ import { join } from 'path';
8
+
9
+ const program = new Command();
10
+ const API = 'https://api.devnet.zeromobile.site';
11
+
12
+ const logo = `
13
+ ██████╗ ███╗ ██╗ ██████╗ ███╗ ██╗
14
+ ██╔═══██╗████╗ ██║██╔════╝ ████╗ ██║
15
+ ██║ ██║██╔██╗ ██║██║ ███╗██╔██╗ ██║
16
+ ██║ ██║██║╚██╗██║██║ ██║██║╚██╗██║
17
+ ╚██████╔╝██║ ╚████║╚██████╔╝██║ ╚████║
18
+ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝`;
19
+
20
+ const divider = chalk.gray(' ─────────────────────────────────────────────────────');
21
+ const label = (k) => chalk.gray(` ${(k + ' ').padEnd(16, '·')} `);
22
+
23
+ const showHelp = () => {
24
+ console.log(chalk.green.bold(logo));
25
+ console.log(chalk.gray.bold(' FinLayer v1.0 · Nigerian Digital Currency System'));
26
+ console.log(chalk.gray(' Interact with the 0NGN ledger, query accounts, and broadcast transactions.\n'));
27
+ console.log(divider);
28
+ console.log(chalk.white.bold(' USAGE'));
29
+ console.log(divider);
30
+ console.log(chalk.yellow(' 0ngn <command> [options]\n'));
31
+ console.log(divider);
32
+ console.log(chalk.white.bold(' COMMANDS'));
33
+ console.log(divider);
34
+ const cmd = (name, args, desc) => {
35
+ console.log(chalk.cyan.bold(` ${name}`) + chalk.gray(` ${args}`));
36
+ console.log(chalk.gray(` ${desc}\n`));
37
+ };
38
+ cmd('info', '[--rpc <url>]',
39
+ 'Full network status — node health, ledger stats, token economics');
40
+ cmd('balance', '<account_id> [--rpc <url>]',
41
+ 'Account balance, public key, nonce, and transaction count');
42
+ cmd('tx', '<tx_hash> [--rpc <url>]',
43
+ 'Full transaction receipt — parties, amount, ed25519 sig, Solana L1 anchor');
44
+ cmd('transfer', '-s <id> -r <id> -a <kubo> -k <key> [-m <memo>] [--rpc <url>]',
45
+ 'Broadcast a signed transfer and stream Solana Devnet confirmation');
46
+ cmd('create-account', '[-o <file>]',
47
+ 'Generate a new Ed25519 keypair and save it to keypair.json');
48
+ console.log(divider);
49
+ console.log(chalk.white.bold(' OPTIONS'));
50
+ console.log(divider);
51
+ const opt = (flag, desc) => console.log(chalk.yellow(` ${flag.padEnd(24)} `) + chalk.gray(desc));
52
+ opt('--rpc <url>', `Override the RPC node (default: ${API})`);
53
+ opt('-s, --sender', 'Sender Account ID [transfer]');
54
+ opt('-r, --receiver', 'Receiver Account ID [transfer]');
55
+ opt('-a, --amount', 'Amount in Kubo (100 Kubo = ₦1 0NGN) [transfer]');
56
+ opt('-k, --keypair', '64-byte Ed25519 Private Key hex [transfer]');
57
+ opt('-m, --memo', 'Transaction memo / description [transfer]');
58
+ opt('-V, --version', 'Print CLI version');
59
+ opt('-h, --help', 'Show this help screen');
60
+ console.log();
61
+ console.log(divider);
62
+ console.log(chalk.white.bold(' EXAMPLES'));
63
+ console.log(divider);
64
+ console.log(chalk.gray(' # Check network status'));
65
+ console.log(chalk.cyan(' 0ngn info\n'));
66
+ console.log(chalk.gray(' # Query a balance'));
67
+ console.log(chalk.cyan(' 0ngn balance 4410520adbebcffa68899f7bcddeaf8f\n'));
68
+ console.log(chalk.gray(' # Look up a transaction'));
69
+ console.log(chalk.cyan(' 0ngn tx e27ff5a046f317a1c62e4f9a6d7c55620b7fd0602891cc7c45554b112c394b9f\n'));
70
+ console.log(chalk.gray(' # Send 1000 0NGN with a memo'));
71
+ console.log(chalk.cyan(' 0ngn transfer -s <from_id> -r <to_id> -a 100000 -k <priv_key> -m "Invoice #001"'));
72
+ console.log(divider);
73
+ console.log();
74
+ };
75
+
76
+
77
+ program
78
+ .name('0ngn')
79
+ .description('0NGN FinLayer Blockchain Command Line Interface')
80
+ .version('1.0.0');
81
+
82
+ // ── INFO ──────────────────────────────────────────────────────────────
83
+ program.command('info')
84
+ .description('Full network status — RPC health, ledger stats, supply info')
85
+ .option('--rpc <url>', 'Target RPC Node', API)
86
+ .action(async (opts) => {
87
+ try {
88
+ const res = await fetch(`${opts.rpc}/health`);
89
+ const j = await res.json();
90
+ if (!j.success) throw new Error(j.error);
91
+ const d = j.data;
92
+
93
+ console.log(chalk.green.bold(logo));
94
+ console.log(chalk.gray.bold(' FinLayer v1.0 · Nigerian Digital Currency System'));
95
+ console.log(chalk.gray(' 100 Kubo = 1 Naira · Fixed Supply: 21,000,000 0NGN'));
96
+ console.log();
97
+ console.log(divider);
98
+ console.log(chalk.green(' NODE STATUS'));
99
+ console.log(divider);
100
+ console.log(label('Node') + chalk.green.bold('ONLINE'));
101
+ console.log(label('RPC Endpoint') + chalk.cyan(opts.rpc));
102
+ console.log(label('Network') + chalk.yellow('Devnet (Solana-Anchored)'));
103
+ console.log();
104
+ console.log(divider);
105
+ console.log(chalk.green(' LEDGER STATE'));
106
+ console.log(divider);
107
+ console.log(label('Genesis ID') + chalk.cyan.bold(d.genesis_id));
108
+ console.log(label('Chain Tip') + chalk.cyan(d.chain_tip));
109
+ console.log(label('Accounts') + chalk.yellow.bold(d.accounts));
110
+ console.log(label('Total TXs') + chalk.yellow.bold(d.transactions));
111
+ console.log();
112
+ console.log(divider);
113
+ console.log(chalk.green(' TOKEN ECONOMICS'));
114
+ console.log(divider);
115
+ console.log(label('Unit') + chalk.white('0NGN (Nigerian Digital Currency)'));
116
+ console.log(label('Sub-unit') + chalk.white('Kubo (1 0NGN = 100 Kubo)'));
117
+ console.log(label('Max Supply') + chalk.white('21,000,000 0NGN'));
118
+ console.log(label('L1 Anchor') + chalk.magenta('Solana Devnet via SPL Memo Program'));
119
+ console.log(label('Checkpoint') + chalk.magenta('Arweave / Irys (daily state root)'));
120
+ console.log(divider);
121
+ console.log();
122
+ } catch (e) {
123
+ console.log(chalk.red(`\n [ERROR] Network unreachable — ${e.message}\n`));
124
+ }
125
+ });
126
+
127
+ // ── BALANCE ───────────────────────────────────────────────────────────
128
+ program.command('balance <id>')
129
+ .description('Query the balance and metadata of a registered account')
130
+ .option('--rpc <url>', 'Target RPC Node', API)
131
+ .action(async (id, opts) => {
132
+ try {
133
+ process.stdout.write(chalk.gray(`\n Fetching account ${id}...`));
134
+ const res = await fetch(`${opts.rpc}/explorer/account/${id}`);
135
+ const j = await res.json();
136
+
137
+ if (!j.success || !j.data) {
138
+ console.log(chalk.red('\n [ERROR] Account not found on the ledger.\n'));
139
+ return;
140
+ }
141
+ const a = j.data;
142
+ const bal = (a.balance_kubo / 100).toLocaleString('en-NG', { minimumFractionDigits: 2 });
143
+ const stk = (a.stake_amount / 100).toLocaleString('en-NG', { minimumFractionDigits: 2 });
144
+
145
+ console.log('\n');
146
+ console.log(divider);
147
+ console.log(chalk.green(' ACCOUNT RECORD'));
148
+ console.log(divider);
149
+ console.log(label('Account ID') + chalk.cyan.bold(a.account_id));
150
+ console.log(label('Public Key') + chalk.cyan(a.public_key));
151
+ console.log(label('Created') + chalk.white(a.created_at));
152
+ console.log();
153
+ console.log(divider);
154
+ console.log(chalk.green(' BALANCES'));
155
+ console.log(divider);
156
+ console.log(label('Balance') + chalk.yellow.bold(`₦${bal} 0NGN`));
157
+ console.log(label('Kubo') + chalk.yellow(`${Number(a.balance_kubo).toLocaleString()} Kubo`));
158
+ console.log(label('Staked') + chalk.white(`₦${stk} 0NGN`));
159
+ console.log(label('Nonce') + chalk.white(`#${a.nonce || 0}`));
160
+ console.log(label('Total TXs') + chalk.white(`${(a.transactions || []).length}`));
161
+ console.log(divider);
162
+ console.log();
163
+ } catch (e) {
164
+ console.log(chalk.red(`\n [ERROR] ${e.message}\n`));
165
+ }
166
+ });
167
+
168
+ // ── TX ────────────────────────────────────────────────────────────────
169
+ program.command('tx <hash>')
170
+ .description('Look up a transaction by its hash')
171
+ .option('--rpc <url>', 'Target RPC Node', API)
172
+ .action(async (hash, opts) => {
173
+ try {
174
+ process.stdout.write(chalk.gray(`\n Fetching transaction ${hash}...`));
175
+ const res = await fetch(`${opts.rpc}/explorer/tx/${hash}`);
176
+ const j = await res.json();
177
+
178
+ if (!j.success || !j.data) {
179
+ console.log(chalk.red('\n [ERROR] Transaction not found.\n'));
180
+ return;
181
+ }
182
+ const t = j.data;
183
+ const amount = (t.amount_kubo / 100).toLocaleString('en-NG', { minimumFractionDigits: 2 });
184
+
185
+ console.log('\n');
186
+ console.log(divider);
187
+ console.log(chalk.green(' TRANSACTION RECEIPT'));
188
+ console.log(divider);
189
+ console.log(label('Tx Hash') + chalk.cyan.bold(t.tx_hash));
190
+ console.log(label('Status') + chalk.green('CONFIRMED'));
191
+ console.log(label('Amount') + chalk.yellow.bold(`₦${amount} 0NGN (${Number(t.amount_kubo).toLocaleString()} Kubo)`));
192
+ console.log(label('Gas Fee') + chalk.white(`${t.gas_fee} Kubo`));
193
+ console.log(label('Nonce') + chalk.white(`#${t.nonce}`));
194
+ console.log(label('Memo') + chalk.white(t.description || '—'));
195
+ console.log(label('Timestamp') + chalk.white(t.timestamp));
196
+ console.log();
197
+ console.log(divider);
198
+ console.log(chalk.green(' PARTIES'));
199
+ console.log(divider);
200
+ console.log(label('From') + chalk.cyan(t.sender_id));
201
+ console.log(label('To') + chalk.cyan(t.receiver_id));
202
+ console.log(label('Parent Tx') + chalk.gray(t.prev_tx_hash || 'GENESIS ROOT'));
203
+ console.log(label('Ed25519 Sig') + chalk.gray(t.signature ? t.signature.slice(0, 32) + '...' : '—'));
204
+ console.log();
205
+ console.log(divider);
206
+ console.log(chalk.green(' L1 ANCHORS'));
207
+ console.log(divider);
208
+ if (t.solana_sig) {
209
+ console.log(label('Solana') + chalk.magenta.bold(t.solana_sig));
210
+ console.log(label('Explorer') + chalk.cyan(`https://explorer.solana.com/tx/${t.solana_sig}?cluster=devnet`));
211
+ } else {
212
+ console.log(label('Solana') + chalk.yellow('PENDING — relay awaiting confirmation'));
213
+ }
214
+ console.log(label('Arweave') + chalk.gray('Pending daily state checkpoint'));
215
+ console.log(divider);
216
+ console.log();
217
+ } catch (e) {
218
+ console.log(chalk.red(`\n [ERROR] ${e.message}\n`));
219
+ }
220
+ });
221
+
222
+ // ── TRANSFER ──────────────────────────────────────────────────────────
223
+ program.command('transfer')
224
+ .description('Transfer 0NGN to another account')
225
+ .requiredOption('-s, --sender <id>', 'Sender Account ID')
226
+ .requiredOption('-r, --receiver <id>', 'Receiver Account ID')
227
+ .requiredOption('-a, --amount <kubo>', 'Amount in Kubo (100 Kubo = 1 0NGN)')
228
+ .requiredOption('-k, --keypair <key>', '64-byte Ed25519 Private Key Hex')
229
+ .option('-m, --memo <text>', 'Optional memo / transaction description', '0NGN CLI Transfer')
230
+ .option('--rpc <url>', 'Target RPC Node', API)
231
+ .action(async (options) => {
232
+ const payload = {
233
+ sender_id: options.sender,
234
+ receiver_id: options.receiver,
235
+ amount_kubo: parseInt(options.amount),
236
+ description: options.memo,
237
+ private_key: options.keypair
238
+ };
239
+
240
+ const amount = parseInt(options.amount);
241
+ const ngn = (amount / 100).toLocaleString('en-NG', { minimumFractionDigits: 2 });
242
+
243
+ console.log('\n');
244
+ console.log(divider);
245
+ console.log(chalk.white.bold(' 0NGN TRANSFER BROADCAST'));
246
+ console.log(divider);
247
+ console.log(label('RPC') + chalk.cyan(options.rpc));
248
+ console.log(label('From') + chalk.cyan(options.sender));
249
+ console.log(label('To') + chalk.cyan(options.receiver));
250
+ console.log(label('Amount') + chalk.yellow.bold(`₦${ngn} 0NGN (${amount.toLocaleString()} Kubo)`));
251
+ console.log(label('Memo') + chalk.white(options.memo));
252
+ console.log(divider);
253
+
254
+ try {
255
+ process.stdout.write(chalk.yellow('\n [1/3] Submitting to 0NGN ledger... '));
256
+ const res = await fetch(`${options.rpc}/transaction/send`, {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify(payload)
260
+ });
261
+ const j = await res.json();
262
+
263
+ if (!j.success || !j.data?.tx_hash) {
264
+ console.log(chalk.red('FAILED'));
265
+ console.log(chalk.red(`\n [ERROR] ${j.error || JSON.stringify(j)}\n`));
266
+ return;
267
+ }
268
+
269
+ const txHash = j.data.tx_hash;
270
+ console.log(chalk.green('CONFIRMED'));
271
+ console.log(label('Tx Hash') + chalk.cyan.bold(txHash));
272
+
273
+ process.stdout.write(chalk.yellow('\n [2/3] Awaiting Solana broadcast... '));
274
+ let solanaSig = null;
275
+ for (let i = 0; i < 12; i++) {
276
+ await new Promise(r => setTimeout(r, 3000));
277
+ try {
278
+ const txRes = await fetch(`${options.rpc}/explorer/tx/${txHash}`);
279
+ const txJ = await txRes.json();
280
+ if (txJ.success && txJ.data?.solana_sig) {
281
+ solanaSig = txJ.data.solana_sig;
282
+ break;
283
+ }
284
+ } catch { }
285
+ process.stdout.write(chalk.gray('.'));
286
+ }
287
+
288
+ if (solanaSig) {
289
+ console.log(chalk.green('ANCHORED'));
290
+ console.log(label('Solana Sig') + chalk.magenta(solanaSig));
291
+ console.log(label('Explorer') + chalk.cyan(`https://explorer.solana.com/tx/${solanaSig}?cluster=devnet`));
292
+ } else {
293
+ console.log(chalk.yellow('PENDING'));
294
+ console.log(chalk.gray(' Relay node is processing — run `0ngn tx <hash>` to check.'));
295
+ }
296
+
297
+ console.log(chalk.yellow('\n [3/3] Finalizing '));
298
+ console.log(label('Verify') + chalk.gray(`0ngn tx ${txHash}`));
299
+ console.log(chalk.green.bold('\n TRANSFER COMPLETE'));
300
+ console.log(divider);
301
+ console.log();
302
+
303
+ } catch (e) {
304
+ console.log(chalk.red('FAILED'));
305
+ console.log(chalk.red(`\n [ERROR] RPC unreachable — ${e.message}\n`));
306
+ }
307
+ });
308
+
309
+ // ── CREATE ACCOUNT ────────────────────────────────────────────────────
310
+ program.command('create-account')
311
+ .description('Generate a new Ed25519 keypair and save to keypair.json')
312
+ .option('-o, --output <file>', 'Output file path', join(homedir(), 'keypair.json'))
313
+ .action((opts) => {
314
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
315
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
316
+ publicKeyEncoding: { type: 'spki', format: 'der' }
317
+ });
318
+
319
+ // Extract raw 32-byte keys from DER wrappers
320
+ const privRaw = privateKey.slice(-32);
321
+ const pubRaw = publicKey.slice(-32);
322
+ const fullKey = Buffer.concat([privRaw, pubRaw]);
323
+
324
+ // Derive a deterministic account_id from the public key
325
+ const accountId = createHash('md5').update(pubRaw).digest('hex');
326
+
327
+ const keypair = {
328
+ account_id: accountId,
329
+ public_key: pubRaw.toString('hex'),
330
+ private_key: fullKey.toString('hex'),
331
+ created_at: new Date().toISOString()
332
+ };
333
+
334
+ writeFileSync(opts.output, JSON.stringify(keypair, null, 2));
335
+
336
+ console.log('\n');
337
+ console.log(divider);
338
+ console.log(chalk.green(' NEW ACCOUNT CREATED'));
339
+ console.log(divider);
340
+ console.log(label('Account ID') + chalk.cyan.bold(keypair.account_id));
341
+ console.log(label('Public Key') + chalk.cyan(keypair.public_key));
342
+ console.log(label('Saved to') + chalk.yellow(opts.output));
343
+ console.log(divider);
344
+ console.log(chalk.gray(' Keep your keypair.json safe. Never share your private_key.\n'));
345
+ });
346
+
347
+ program
348
+ .name('0ngn')
349
+ .description('0NGN FinLayer Blockchain Command Line Interface')
350
+ .version('1.0.0')
351
+ .helpOption(false)
352
+ .addHelpCommand(false)
353
+ .addOption(new (await import('commander')).Option('-h, --help', 'Show help').hideHelp())
354
+ .hook('preAction', () => { });
355
+
356
+ if (process.argv.length <= 2 || process.argv.includes('--help') || process.argv.includes('-h') || process.argv[2] === 'help') {
357
+ showHelp();
358
+ process.exit(0);
359
+ }
360
+
361
+ await program.parseAsync(process.argv);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "finlayer-cli",
3
+ "version": "1.1.0",
4
+ "description": "Command Line Interface for the 0NGN FinLayer Nigerian Digital Currency Network",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "finlayer": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "0ngn",
15
+ "finlayer",
16
+ "nigerian",
17
+ "digital-currency",
18
+ "blockchain",
19
+ "cli",
20
+ "solana",
21
+ "ed25519",
22
+ "ledger"
23
+ ],
24
+ "author": "0NGN FinLayer",
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "0ngn-cli": "^1.0.0",
31
+ "chalk": "^5.3.0",
32
+ "commander": "^11.1.0"
33
+ }
34
+ }