From 34f6a1153419741593187ffd293d46ed78089d57 Mon Sep 17 00:00:00 2001 From: EDOHWARES Date: Wed, 22 Apr 2026 18:09:20 +0100 Subject: [PATCH] feat(cli): build custom bc-forge administrative CLI (@bc-forge/cli) --- cli/package.json | 28 +++++++ cli/src/index.ts | 157 ++++++++++++++++++++++++++++++++++++++++ cli/src/utils/config.ts | 37 ++++++++++ cli/tsconfig.json | 14 ++++ 4 files changed, 236 insertions(+) create mode 100644 cli/package.json create mode 100644 cli/src/index.ts create mode 100644 cli/src/utils/config.ts create mode 100644 cli/tsconfig.json diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..471078df --- /dev/null +++ b/cli/package.json @@ -0,0 +1,28 @@ +{ + "name": "@bc-forge/cli", + "version": "1.0.0", + "description": "Administrative CLI for bc-forge", + "main": "dist/index.js", + "type": "module", + "bin": { + "bc-forge": "./dist/index.js" + }, + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsc && chmod +x dist/index.js", + "start": "node dist/index.js" + }, + "dependencies": { + "@bc-forge/sdk": "file:../sdk", + "@stellar/stellar-sdk": "^11.3.0", + "commander": "^12.0.0", + "dotenv": "^16.4.5", + "chalk": "^5.3.0", + "conf": "^12.0.0" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "tsx": "^4.7.1", + "typescript": "^5.3.3" + } +} diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 00000000..ba8a77aa --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { bcForgeClient } from '@bc-forge/sdk'; +import { Keypair } from '@stellar/stellar-sdk'; +import config, { getClientConfig, getSecretKey } from './utils/config.js'; + +const program = new Command(); + +program + .name('bc-forge') + .description('Administrative CLI for bc-forge token contracts') + .version('1.0.0'); + +// ─── Config Commands ──────────────────────────────────────────────────────── + +const configCmd = program.command('config').description('Manage CLI configuration'); + +configCmd + .command('set ') + .description('Set a configuration value (rpcUrl, networkPassphrase, contractId, secretKey)') + .action((key, value) => { + config.set(key, value); + console.log(chalk.green(`✓ Set ${key} to ${value}`)); + }); + +configCmd + .command('list') + .description('List current configuration') + .action(() => { + console.log(chalk.blue('Current Configuration:')); + console.log(config.store); + }); + +// ─── Token Commands ───────────────────────────────────────────────────────── + +program + .command('balance
') + .description('Check token balance for an address') + .action(async (address) => { + try { + const client = new bcForgeClient(getClientConfig()); + const balance = await client.getBalance(address); + console.log(chalk.cyan(`Balance for ${address}: `) + chalk.white(balance.toString())); + } catch (err: any) { + console.error(chalk.red(`Error: ${err.message}`)); + } + }); + +program + .command('initialize') + .description('Initialize a new token contract') + .requiredOption('--admin
', 'Admin address') + .requiredOption('--decimals ', 'Decimal places', '7') + .requiredOption('--name ', 'Token name') + .requiredOption('--symbol ', 'Token symbol') + .action(async (options) => { + try { + const secret = getSecretKey(); + if (!secret) throw new Error('Secret key not configured. Use `bc-forge config set secretKey `'); + + const source = Keypair.fromSecret(secret); + const client = new bcForgeClient(getClientConfig()); + + console.log(chalk.yellow('Initializing contract...')); + const result = await client.initialize( + options.admin, + parseInt(options.decimals), + options.name, + options.symbol, + source + ); + + if (result.success) { + console.log(chalk.green(`✓ Contract initialized. TX: ${result.hash}`)); + } else { + console.log(chalk.red(`✗ Initialization failed. TX: ${result.hash}`)); + } + } catch (err: any) { + console.error(chalk.red(`Error: ${err.message}`)); + } + }); + +program + .command('mint ') + .description('Mint tokens to an address') + .action(async (to, amount) => { + try { + const secret = getSecretKey(); + if (!secret) throw new Error('Secret key not configured'); + + const source = Keypair.fromSecret(secret); + const client = new bcForgeClient(getClientConfig()); + + console.log(chalk.yellow(`Minting ${amount} tokens to ${to}...`)); + const result = await client.mint(to, BigInt(amount), source); + + if (result.success) { + console.log(chalk.green(`✓ Minted successfully. TX: ${result.hash}`)); + } else { + console.log(chalk.red('✗ Minting failed.')); + } + } catch (err: any) { + console.error(chalk.red(`Error: ${err.message}`)); + } + }); + +program + .command('pause') + .description('Pause token operations') + .action(async () => { + try { + const secret = getSecretKey(); + if (!secret) throw new Error('Secret key not configured'); + + const source = Keypair.fromSecret(secret); + const client = new bcForgeClient(getClientConfig()); + + console.log(chalk.yellow('Pausing contract...')); + const result = await client.pause(source); + + if (result.success) { + console.log(chalk.green(`✓ Contract paused. TX: ${result.hash}`)); + } else { + console.log(chalk.red('✗ Pause failed.')); + } + } catch (err: any) { + console.error(chalk.red(`Error: ${err.message}`)); + } + }); + +program + .command('unpause') + .description('Unpause token operations') + .action(async () => { + try { + const secret = getSecretKey(); + if (!secret) throw new Error('Secret key not configured'); + + const source = Keypair.fromSecret(secret); + const client = new bcForgeClient(getClientConfig()); + + console.log(chalk.yellow('Unpausing contract...')); + const result = await client.unpause(source); + + if (result.success) { + console.log(chalk.green(`✓ Contract unpaused. TX: ${result.hash}`)); + } else { + console.log(chalk.red('✗ Unpause failed.')); + } + } catch (err: any) { + console.error(chalk.red(`Error: ${err.message}`)); + } + }); + +program.parse(); diff --git a/cli/src/utils/config.ts b/cli/src/utils/config.ts new file mode 100644 index 00000000..dba435c5 --- /dev/null +++ b/cli/src/utils/config.ts @@ -0,0 +1,37 @@ +import Conf from 'conf'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const schema = { + rpcUrl: { + type: 'string' as const, + default: 'https://soroban-testnet.stellar.org' + }, + networkPassphrase: { + type: 'string' as const, + default: 'Test SDF Network ; September 2015' + }, + contractId: { + type: 'string' as const, + }, + secretKey: { + type: 'string' as const, + } +}; + +const config = new Conf({ schema, projectName: 'bc-forge-cli' }); + +export function getClientConfig() { + return { + rpcUrl: (process.env.RPC_URL || config.get('rpcUrl')) as string, + networkPassphrase: (process.env.NETWORK_PASSPHRASE || config.get('networkPassphrase')) as string, + contractId: (process.env.CONTRACT_ID || config.get('contractId')) as string, + }; +} + +export function getSecretKey() { + return (process.env.SECRET_KEY || config.get('secretKey')) as string; +} + +export default config; diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 00000000..4bd5297d --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +}