diff --git a/modules/dev-cli/bin/index.ts b/modules/dev-cli/bin/index.ts new file mode 100644 index 0000000000..a0eb17ecd8 --- /dev/null +++ b/modules/dev-cli/bin/index.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +// Suppress noisy SDK warnings before any imports +const originalStdoutWrite = process.stdout.write.bind(process.stdout); +const originalStderrWrite = process.stderr.write.bind(process.stderr); + +const suppressedPatterns = [ + /@polkadot\/util.*has multiple versions/, + /Either remove and explicitly install matching versions/, + /The following conflicting packages were found/, + /cjs \d+\.\d+\.\d+\s+node_modules\/@polkadot/, +]; + +function shouldSuppress(message: string): boolean { + return suppressedPatterns.some(pattern => pattern.test(message)); +} + +process.stdout.write = function(chunk: any, ...args: any[]): boolean { + const message = chunk.toString(); + if (shouldSuppress(message)) { + return true; + } + return originalStdoutWrite(chunk, ...args); +} as any; + +process.stderr.write = function(chunk: any, ...args: any[]): boolean { + const message = chunk.toString(); + if (shouldSuppress(message)) { + return true; + } + return originalStderrWrite(chunk, ...args); +} as any; + +import * as yargs from 'yargs'; +import { + balanceCommand, + addressCommand, + sendCommand, + transfersCommand, + walletCommand, + lightningCommand, +} from '../src/commands'; + +yargs + .scriptName('sdk-dev-cli') + .usage('$0 [options]') + .command(balanceCommand) + .command(addressCommand) + .command(sendCommand) + .command(transfersCommand) + .command(walletCommand) + .command(lightningCommand) + .example('$0 balance', 'Get wallet balance') + .example('$0 address create', 'Create a new address') + .example('$0 send --to
--amount --confirm', 'Send a transaction') + .demandCommand(1, 'You must specify a command') + .help() + .alias('help', 'h') + .version() + .alias('version', 'v') + .wrap(yargs.terminalWidth()) + .parse(); + diff --git a/modules/dev-cli/cli.js b/modules/dev-cli/cli.js new file mode 100755 index 0000000000..d40ca9006e --- /dev/null +++ b/modules/dev-cli/cli.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +require("../dist/bin/index.js"); + diff --git a/modules/dev-cli/config.example.json b/modules/dev-cli/config.example.json new file mode 100644 index 0000000000..d0426df426 --- /dev/null +++ b/modules/dev-cli/config.example.json @@ -0,0 +1,60 @@ +{ + "test": { + "tbtc": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "...", + "otp": "000000", + "enterpriseId": "..." + }, + "gteth": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + }, + "talgo": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + } + }, + "staging": { + "tbtcsig": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + }, + "teos": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + }, + "tlnbtc": { + "accessToken": "v2x...", + "walletId": "...", + "walletId2": "...", + "walletPassphrase": "..." + } + }, + "prod": { + "btc": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + }, + "eth": { + "accessToken": "v2x...", + "walletId": "...", + "walletPassphrase": "..." + } + }, + "custom": { + "tbtc": { + "accessToken": "v2x...", + "walletId": "...", + "customRootUri": "https://...", + "customBitcoinNetwork": "..." + } + } +} + diff --git a/modules/dev-cli/env.example b/modules/dev-cli/env.example new file mode 100644 index 0000000000..331504506d --- /dev/null +++ b/modules/dev-cli/env.example @@ -0,0 +1,28 @@ +# BitGo Environment (test, staging, prod, custom) +BITGO_ENV=test + +# Coin to test (btc, tbtc, eth, gteth, etc.) +BITGO_COIN=tbtc + +# Access Token for the environment +BITGO_ACCESS_TOKEN=v2x... + +# Wallet ID to use for operations +BITGO_WALLET_ID=... + +# Wallet Passphrase (if needed for signing) +BITGO_WALLET_PASSPHRASE=... + +# Optional: For custom environments +# BITGO_CUSTOM_ROOT_URI=https://... +# BITGO_CUSTOM_BITCOIN_NETWORK=... + +# Optional: For lightning-specific operations +BITGO_WALLET_ID_2=... + +# Optional: OTP code (if needed) +BITGO_OTP=000000 + +# Optional: Enterprise ID for wallet creation +BITGO_ENTERPRISE_ID=... + diff --git a/modules/dev-cli/package.json b/modules/dev-cli/package.json new file mode 100644 index 0000000000..bf96bc0a63 --- /dev/null +++ b/modules/dev-cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "@bitgo/sdk-dev-cli", + "version": "1.0.0", + "description": "Development CLI tool for quickly testing BitGo SDK changes", + "private": true, + "main": "dist/src/index.js", + "bin": "./dist/bin/index.js", + "scripts": { + "build": "tsc --build --incremental --verbose .", + "clean": "rm -rf dist", + "prepare": "npm run build" + }, + "dependencies": { + "bitgo": "workspace:*", + "yargs": "^17.3.1", + "chalk": "4", + "dotenv": "^16.0.3", + "@bitgo/abstract-lightning": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.15.29", + "@types/yargs": "^17.0.19" + } +} + diff --git a/modules/dev-cli/setup.sh b/modules/dev-cli/setup.sh new file mode 100755 index 0000000000..b55e4cfb68 --- /dev/null +++ b/modules/dev-cli/setup.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Quick setup script for dev-cli + +cd "$(dirname "$0")" + +echo "BitGo Dev CLI Setup" +echo "===================" +echo "" + +# Check if config.json exists +if [ ! -f config.json ]; then + echo "Creating config.json from example..." + cp config.example.json config.json + echo "✓ Created config.json" + echo "" + echo "⚠️ Please edit config.json and fill in your configuration:" + echo " Organize by environment (test, staging, prod) and coin" + echo "" +else + echo "✓ config.json already exists" +fi + +# Optionally create .env if user wants it +if [ ! -f .env ]; then + read -p "Do you also want to create .env file? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + cp env.example .env + echo "✓ Created .env file" + echo " Note: config.json takes precedence, .env variables can override" + fi +fi + +# Build the module +echo "" +echo "Building dev-cli..." +yarn build + +echo "" +echo "✓ Setup complete!" +echo "" +echo "Next steps:" +echo " 1. Edit config.json with your credentials for each env/coin" +echo " 2. Try: BITGO_COIN=tbtc BITGO_ENV=test yarn bitgo-dev balance" +echo " 3. Or: BITGO_COIN=gteth BITGO_ENV=test yarn bitgo-dev balance" +echo "" +echo "See QUICKSTART.md for more examples" +echo "" + diff --git a/modules/dev-cli/src/bitgo-client.ts b/modules/dev-cli/src/bitgo-client.ts new file mode 100644 index 0000000000..598b7774af --- /dev/null +++ b/modules/dev-cli/src/bitgo-client.ts @@ -0,0 +1,29 @@ +import { BitGo } from 'bitgo'; +import { Config } from './config'; + +export async function getBitGoInstance(config: Config): Promise { + const bitgoOptions: any = { env: config.env }; + + if (config.customRootUri) { + bitgoOptions.customRootUri = config.customRootUri; + } + + if (config.customBitcoinNetwork) { + bitgoOptions.customBitcoinNetwork = config.customBitcoinNetwork; + } + + const bitgo = new BitGo(bitgoOptions); + await bitgo.authenticateWithAccessToken({ accessToken: config.accessToken }); + + return bitgo; +} + +export async function unlockIfNeeded(bitgo: BitGo, config: Config): Promise { + if (config.otp) { + const unlocked = await bitgo.unlock({ otp: config.otp, duration: 3600 }); + if (!unlocked) { + throw new Error('Failed to unlock BitGo session with provided OTP'); + } + } +} + diff --git a/modules/dev-cli/src/commands/address.ts b/modules/dev-cli/src/commands/address.ts new file mode 100644 index 0000000000..828d6be43b --- /dev/null +++ b/modules/dev-cli/src/commands/address.ts @@ -0,0 +1,75 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance, unlockIfNeeded } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +export const addressCommand: CommandModule = { + command: 'address ', + describe: 'Address operations', + builder: (yargs) => { + return yargs + .positional('action', { + describe: 'Action to perform', + choices: ['create', 'list'], + demandOption: true, + }) + .option('chain', { + alias: 'c', + describe: 'Address chain (for UTXO coins)', + type: 'number', + default: 10, + }) + .option('limit', { + alias: 'l', + describe: 'Number of addresses to list', + type: 'number', + }); + }, + handler: async (argv: any) => { + try { + const config = getConfig(); + const walletId = validateWalletId(config); + const bitgo = await getBitGoInstance(config); + + if (argv.action === 'create') { + logInfo(`Creating new address for wallet ${walletId}...`); + + await unlockIfNeeded(bitgo, config); + + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + const addressOptions: any = {}; + + // For UTXO coins, set chain + if (argv.chain) { + addressOptions.chain = argv.chain; + } + + const address = await wallet.createAddress(addressOptions); + + console.log('\n' + '─'.repeat(50)); + logJSON(address); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Address created successfully'); + } else if (argv.action === 'list') { + logInfo(`Listing addresses for wallet ${walletId}...`); + + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + const addresses = await wallet.addresses(); + + console.log('\n' + '─'.repeat(50)); + if (argv.limit) { + logJSON(addresses.addresses.slice(0, argv.limit)); + } else { + logJSON(addresses); + } + console.log('─'.repeat(50) + '\n'); + + logSuccess('Addresses retrieved successfully'); + } + } catch (error) { + logError(`Failed to perform address operation: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/commands/balance.ts b/modules/dev-cli/src/commands/balance.ts new file mode 100644 index 0000000000..c1f87de2c0 --- /dev/null +++ b/modules/dev-cli/src/commands/balance.ts @@ -0,0 +1,40 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +export const balanceCommand: CommandModule = { + command: 'balance', + describe: 'Get wallet balance', + handler: async () => { + try { + const config = getConfig(); + const walletId = validateWalletId(config); + + logInfo(`Getting balance for wallet ${walletId} on ${config.coin}...`); + + const bitgo = await getBitGoInstance(config); + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + + console.log('\n' + '─'.repeat(50)); + console.log(`Wallet ID: ${wallet.id()}`); + + // Handle different coin types + if (wallet.receiveAddress) { + console.log(`Receive Address: ${wallet.receiveAddress()}`); + } else if (wallet.coinSpecific && wallet.coinSpecific()?.rootAddress) { + console.log(`Root Address: ${wallet.coinSpecific().rootAddress}`); + } + + console.log(`Balance: ${wallet.balanceString()}`); + console.log(`Confirmed Balance: ${wallet.confirmedBalanceString()}`); + console.log(`Spendable Balance: ${wallet.spendableBalanceString()}`); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Balance retrieved successfully'); + } catch (error) { + logError(`Failed to get balance: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/commands/index.ts b/modules/dev-cli/src/commands/index.ts new file mode 100644 index 0000000000..6526b10acd --- /dev/null +++ b/modules/dev-cli/src/commands/index.ts @@ -0,0 +1,7 @@ +export { balanceCommand } from './balance'; +export { addressCommand } from './address'; +export { sendCommand } from './send'; +export { transfersCommand } from './transfers'; +export { walletCommand } from './wallet'; +export { lightningCommand } from './lightning'; + diff --git a/modules/dev-cli/src/commands/lightning.ts b/modules/dev-cli/src/commands/lightning.ts new file mode 100644 index 0000000000..f3b9771231 --- /dev/null +++ b/modules/dev-cli/src/commands/lightning.ts @@ -0,0 +1,133 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +// Import lightning utilities +let getLightningWallet: any; +try { + const lightningModule = require('@bitgo/abstract-lightning'); + getLightningWallet = lightningModule.getLightningWallet; +} catch (e) { + // Lightning module not available +} + +export const lightningCommand: CommandModule = { + command: 'lightning ', + describe: 'Lightning Network operations', + builder: (yargs) => { + return yargs + .positional('action', { + describe: 'Action to perform', + choices: ['invoice', 'pay', 'list-payments', 'balance'], + demandOption: true, + }) + .option('amount', { + alias: 'a', + describe: 'Amount in millisatoshis', + type: 'string', + }) + .option('memo', { + alias: 'm', + describe: 'Invoice memo', + type: 'string', + }) + .option('expiry', { + describe: 'Invoice expiry in seconds', + type: 'number', + default: 36000, + }) + .option('invoice', { + alias: 'i', + describe: 'Lightning invoice string', + type: 'string', + }); + }, + handler: async (argv: any) => { + try { + if (!getLightningWallet) { + throw new Error('@bitgo/abstract-lightning module not available. Install it to use lightning commands.'); + } + + const config = getConfig(); + const walletId = validateWalletId(config); + const bitgo = await getBitGoInstance(config); + + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + const lightningWallet = getLightningWallet(wallet); + + // Helper to handle BigInt JSON serialization + (BigInt.prototype as any).toJSON = function () { + return this.toString(); + }; + + if (argv.action === 'invoice') { + if (!argv.amount) { + throw new Error('--amount is required for creating an invoice'); + } + + logInfo(`Creating invoice for ${argv.amount} msat...`); + + const invoice = await lightningWallet.createInvoice({ + valueMsat: argv.amount, + memo: argv.memo || 'BitGo Dev CLI', + expiry: argv.expiry, + }); + + console.log('\n' + '─'.repeat(50)); + logJSON(invoice); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Invoice created successfully'); + } else if (argv.action === 'pay') { + if (!argv.invoice) { + throw new Error('--invoice is required for payment'); + } + + if (!config.walletPassphrase) { + throw new Error('BITGO_WALLET_PASSPHRASE is required for lightning payments'); + } + + logInfo('Paying invoice...'); + + const paymentOptions: any = { + invoice: argv.invoice, + passphrase: config.walletPassphrase, + }; + + if (argv.amount) { + paymentOptions.amountMsat = BigInt(argv.amount); + } + + const payment = await lightningWallet.payInvoice(paymentOptions); + + console.log('\n' + '─'.repeat(50)); + logJSON(payment); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Payment sent successfully'); + } else if (argv.action === 'list-payments') { + logInfo('Listing payments...'); + + const payments = await lightningWallet.listPayments({}); + + console.log('\n' + '─'.repeat(50)); + logJSON(payments); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Payments retrieved successfully'); + } else if (argv.action === 'balance') { + logInfo('Getting lightning balance...'); + + console.log('\n' + '─'.repeat(50)); + logJSON(wallet.toJSON()); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Balance retrieved successfully'); + } + } catch (error) { + logError(`Failed to perform lightning operation: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/commands/send.ts b/modules/dev-cli/src/commands/send.ts new file mode 100644 index 0000000000..2b9bf23390 --- /dev/null +++ b/modules/dev-cli/src/commands/send.ts @@ -0,0 +1,90 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance, unlockIfNeeded } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +export const sendCommand: CommandModule = { + command: 'send', + describe: 'Send a transaction (sendMany)', + builder: (yargs) => { + return yargs + .option('to', { + alias: 't', + describe: 'Recipient address', + type: 'string', + demandOption: true, + }) + .option('amount', { + alias: 'a', + describe: 'Amount to send (in base units)', + type: 'string', + demandOption: true, + }) + .option('memo', { + alias: 'm', + describe: 'Transaction memo (if supported)', + type: 'string', + }) + .option('fee-rate', { + describe: 'Fee rate (for UTXO coins)', + type: 'number', + }) + .option('confirm', { + describe: 'Skip confirmation prompt', + type: 'boolean', + default: false, + }); + }, + handler: async (argv: any) => { + try { + const config = getConfig(); + const walletId = validateWalletId(config); + + if (!config.walletPassphrase) { + throw new Error('BITGO_WALLET_PASSPHRASE is required for sending transactions'); + } + + logInfo(`Preparing to send ${argv.amount} to ${argv.to}...`); + + if (!argv.confirm) { + logInfo('Use --confirm to execute the transaction'); + return; + } + + const bitgo = await getBitGoInstance(config); + await unlockIfNeeded(bitgo, config); + + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + + const sendOptions: any = { + recipients: [ + { + address: argv.to, + amount: argv.amount, + }, + ], + walletPassphrase: config.walletPassphrase, + }; + + if (argv.memo) { + sendOptions.memo = argv.memo; + } + + if (argv.feeRate) { + sendOptions.feeRate = argv.feeRate; + } + + logInfo('Sending transaction...'); + const result = await wallet.sendMany(sendOptions); + + console.log('\n' + '─'.repeat(50)); + logJSON(result); + console.log('─'.repeat(50) + '\n'); + + logSuccess(`Transaction sent! TX ID: ${result.txid || result.transfer?.id || 'N/A'}`); + } catch (error) { + logError(`Failed to send transaction: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/commands/transfers.ts b/modules/dev-cli/src/commands/transfers.ts new file mode 100644 index 0000000000..35ced46f70 --- /dev/null +++ b/modules/dev-cli/src/commands/transfers.ts @@ -0,0 +1,52 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +export const transfersCommand: CommandModule = { + command: 'transfers', + describe: 'List wallet transfers', + builder: (yargs) => { + return yargs + .option('limit', { + alias: 'l', + describe: 'Number of transfers to retrieve', + type: 'number', + default: 10, + }) + .option('all-tokens', { + describe: 'Include all token transfers', + type: 'boolean', + default: false, + }); + }, + handler: async (argv: any) => { + try { + const config = getConfig(); + const walletId = validateWalletId(config); + + logInfo(`Getting transfers for wallet ${walletId}...`); + + const bitgo = await getBitGoInstance(config); + const walletOptions: any = { id: walletId }; + + if (argv.allTokens) { + walletOptions.allTokens = true; + } + + const wallet = await bitgo.coin(config.coin).wallets().get(walletOptions); + const transfers = await wallet.transfers({ limit: argv.limit }); + + console.log('\n' + '─'.repeat(50)); + console.log(`Total transfers: ${transfers.transfers.length}`); + console.log('─'.repeat(50) + '\n'); + + logJSON(transfers.transfers); + + logSuccess('Transfers retrieved successfully'); + } catch (error) { + logError(`Failed to get transfers: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/commands/wallet.ts b/modules/dev-cli/src/commands/wallet.ts new file mode 100644 index 0000000000..3795a660ce --- /dev/null +++ b/modules/dev-cli/src/commands/wallet.ts @@ -0,0 +1,89 @@ +import { CommandModule } from 'yargs'; +import { getConfig, validateWalletId } from '../config'; +import { getBitGoInstance, unlockIfNeeded } from '../bitgo-client'; +import { logSuccess, logError, logInfo, logJSON } from '../utils'; + +export const walletCommand: CommandModule = { + command: 'wallet ', + describe: 'Wallet operations', + builder: (yargs) => { + return yargs + .positional('action', { + describe: 'Action to perform', + choices: ['info', 'create'], + demandOption: true, + }) + .option('label', { + describe: 'Wallet label (for create)', + type: 'string', + }) + .option('multisig-type', { + describe: 'Multisig type (onchain or tss)', + type: 'string', + choices: ['onchain', 'tss'], + }); + }, + handler: async (argv: any) => { + try { + const config = getConfig(); + const bitgo = await getBitGoInstance(config); + + if (argv.action === 'info') { + const walletId = validateWalletId(config); + logInfo(`Getting info for wallet ${walletId}...`); + + const wallet = await bitgo.coin(config.coin).wallets().get({ id: walletId }); + const walletData = wallet.toJSON(); + + console.log('\n' + '─'.repeat(50)); + logJSON(walletData); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Wallet info retrieved successfully'); + } else if (argv.action === 'create') { + if (!config.walletPassphrase) { + throw new Error('BITGO_WALLET_PASSPHRASE is required for wallet creation'); + } + + await unlockIfNeeded(bitgo, config); + + const label = argv.label || `Test ${config.coin} Wallet - ${Date.now()}`; + + logInfo(`Creating wallet: ${label}...`); + + const walletOptions: any = { + label, + passphrase: config.walletPassphrase, + }; + + if (config.enterpriseId) { + walletOptions.enterprise = config.enterpriseId; + } + + if (argv.multisigType) { + walletOptions.multisigType = argv.multisigType; + } + + const wallet = await bitgo.coin(config.coin).wallets().generateWallet(walletOptions); + + console.log('\n' + '─'.repeat(50)); + console.log(`Wallet ID: ${wallet.wallet.id()}`); + + if (wallet.wallet.receiveAddress) { + console.log(`Receive Address: ${wallet.wallet.receiveAddress()}`); + } else if (wallet.wallet.coinSpecific()?.rootAddress) { + console.log(`Root Address: ${wallet.wallet.coinSpecific().rootAddress}`); + } + + console.log('\n⚠️ BACK THIS UP:'); + console.log(`User keychain encrypted xPrv: ${wallet.userKeychain.encryptedPrv}`); + console.log('─'.repeat(50) + '\n'); + + logSuccess('Wallet created successfully'); + } + } catch (error) { + logError(`Failed to perform wallet operation: ${error.message}`); + process.exit(1); + } + }, +}; diff --git a/modules/dev-cli/src/config.ts b/modules/dev-cli/src/config.ts new file mode 100644 index 0000000000..dee6df05c6 --- /dev/null +++ b/modules/dev-cli/src/config.ts @@ -0,0 +1,92 @@ +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as fs from 'fs'; + +// Load .env from the dev-cli module directory +dotenv.config({ path: path.join(__dirname, '..', '..', '.env') }); + +export interface Config { + env: string; + coin: string; + accessToken: string; + walletId?: string; + walletPassphrase?: string; + otp?: string; + enterpriseId?: string; + customRootUri?: string; + customBitcoinNetwork?: string; + walletId2?: string; +} + +interface ConfigFile { + [env: string]: { + [coin: string]: { + accessToken?: string; + walletId?: string; + walletPassphrase?: string; + otp?: string; + enterpriseId?: string; + customRootUri?: string; + customBitcoinNetwork?: string; + walletId2?: string; + }; + }; +} + +function loadConfigFile(): ConfigFile | null { + const configPath = path.join(__dirname, '..', '..', 'config.json'); + + if (fs.existsSync(configPath)) { + try { + const fileContents = fs.readFileSync(configPath, 'utf8'); + return JSON.parse(fileContents); + } catch (error) { + console.warn(`Warning: Failed to parse config.json: ${error.message}`); + return null; + } + } + + return null; +} + +export function getConfig(): Config { + const env = process.env.BITGO_ENV || 'test'; + const coin = process.env.BITGO_COIN || 'tbtc'; + + // Load from config file first + const configFile = loadConfigFile(); + const fileConfig = configFile?.[env]?.[coin] || {}; + + // Environment variables override config file + const config: Config = { + env, + coin, + accessToken: process.env.BITGO_ACCESS_TOKEN || fileConfig.accessToken || '', + walletId: process.env.BITGO_WALLET_ID || fileConfig.walletId, + walletPassphrase: process.env.BITGO_WALLET_PASSPHRASE || fileConfig.walletPassphrase, + otp: process.env.BITGO_OTP || fileConfig.otp, + enterpriseId: process.env.BITGO_ENTERPRISE_ID || fileConfig.enterpriseId, + customRootUri: process.env.BITGO_CUSTOM_ROOT_URI || fileConfig.customRootUri, + customBitcoinNetwork: process.env.BITGO_CUSTOM_BITCOIN_NETWORK || fileConfig.customBitcoinNetwork, + walletId2: process.env.BITGO_WALLET_ID_2 || fileConfig.walletId2, + }; + + if (!config.accessToken) { + throw new Error( + `BITGO_ACCESS_TOKEN is required for ${env}/${coin}. ` + + `Set it in config.json under ${env}.${coin}.accessToken or via BITGO_ACCESS_TOKEN environment variable.` + ); + } + + return config; +} + +export function validateWalletId(config: Config): string { + if (!config.walletId) { + throw new Error( + `BITGO_WALLET_ID is required for ${config.env}/${config.coin}. ` + + `Set it in config.json under ${config.env}.${config.coin}.walletId or via BITGO_WALLET_ID environment variable.` + ); + } + return config.walletId; +} diff --git a/modules/dev-cli/src/utils.ts b/modules/dev-cli/src/utils.ts new file mode 100644 index 0000000000..e3ccd0c399 --- /dev/null +++ b/modules/dev-cli/src/utils.ts @@ -0,0 +1,25 @@ +import chalk from 'chalk'; + +export function logSuccess(message: string): void { + console.log(chalk.green('✓'), message); +} + +export function logError(message: string): void { + console.log(chalk.red('✗'), message); +} + +export function logInfo(message: string): void { + console.log(chalk.blue('ℹ'), message); +} + +export function logWarning(message: string): void { + console.log(chalk.yellow('⚠'), message); +} + +export function formatJSON(obj: any): string { + return JSON.stringify(obj, null, 2); +} + +export function logJSON(obj: any): void { + console.log(formatJSON(obj)); +} diff --git a/modules/dev-cli/tsconfig.json b/modules/dev-cli/tsconfig.json new file mode 100644 index 0000000000..9bfcd33ad6 --- /dev/null +++ b/modules/dev-cli/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["bin/**/*", "src/**/*"], + "references": [ + { + "path": "../bitgo" + } + ] +} +