Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
157 changes: 157 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 <key> <value>')
.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 <address>')
.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 <address>', 'Admin address')
.requiredOption('--decimals <number>', 'Decimal places', '7')
.requiredOption('--name <string>', 'Token name')
.requiredOption('--symbol <string>', 'Token symbol')
.action(async (options) => {
try {
const secret = getSecretKey();
if (!secret) throw new Error('Secret key not configured. Use `bc-forge config set secretKey <key>`');

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 <to> <amount>')
.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();
37 changes: 37 additions & 0 deletions cli/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 14 additions & 0 deletions cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/**/*"]
}
Loading