import { Buffer } from '@stacks/common'; import * as blockstack from 'blockstack'; import * as bitcoin from 'bitcoinjs-lib'; import * as process from 'process'; import * as fs from 'fs'; import * as winston from 'winston'; import cors from 'cors'; import BN from 'bn.js'; import * as crypto from 'crypto'; import * as bip39 from 'bip39'; import express from 'express'; import * as path from 'path'; import { prompt } from 'inquirer'; import fetch from 'node-fetch'; import { makeSTXTokenTransfer, makeContractDeploy, makeContractCall, callReadOnlyFunction, broadcastTransaction, estimateTransfer, estimateContractDeploy, estimateContractFunctionCall, SignedTokenTransferOptions, ContractDeployOptions, SignedContractCallOptions, ReadOnlyFunctionOptions, ContractCallPayload, ClarityValue, ClarityAbi, getAbi, validateContractCall, PostConditionMode, cvToString, StacksTransaction, TxBroadcastResult, getAddressFromPrivateKey, TransactionVersion, AnchorMode, } from '@stacks/transactions'; import { StacksMainnet, StacksTestnet } from '@stacks/network'; // eslint-disable-next-line @typescript-eslint/no-var-requires const c32check = require('c32check'); import { UserData } from '@stacks/auth'; import crossfetch from 'cross-fetch'; import { StackingClient, StackerInfo } from '@stacks/stacking'; import { FaucetsApi, AccountsApi, Configuration } from '@stacks/blockchain-api-client'; import { GaiaHubConfig } from '@stacks/storage'; import { getOwnerKeyInfo, getPaymentKeyInfo, getStacksWalletKeyInfo, getApplicationKeyInfo, extractAppKey, STX_WALLET_COMPATIBLE_SEED_STRENGTH, PaymentKeyInfoType, OwnerKeyInfoType, StacksKeyInfoType, } from './keys'; import { CLI_ARGS, getCLIOpts, CLIOptAsString, CLIOptAsStringArray, CLIOptAsBool, checkArgs, loadConfig, makeCommandUsageString, makeAllCommandsList, USAGE, DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_REGTEST_PATH, DEFAULT_CONFIG_TESTNET_PATH, ID_ADDRESS_PATTERN, STACKS_ADDRESS_PATTERN, DEFAULT_MAX_ID_SEARCH_INDEX, } from './argparse'; import { encryptBackupPhrase, decryptBackupPhrase } from './encrypt'; import { CLINetworkAdapter, CLI_NETWORK_OPTS, getNetwork, NameInfoType } from './network'; import { gaiaAuth, gaiaConnect, gaiaUploadProfileAll, getGaiaAddressFromProfile } from './data'; import { JSONStringify, getPrivateKeyAddress, canonicalPrivateKey, decodePrivateKey, makeProfileJWT, getNameInfoEasy, getpass, getBackupPhrase, mkdirs, IDAppKeys, getIDAppKeys, makePromptsFromArgList, parseClarityFunctionArgAnswers, ClarityFunctionArg, generateExplorerTxPageUrl, isTestnetAddress, } from './utils'; import { handleAuth, handleSignIn } from './auth'; import { generateNewAccount, generateWallet, getAppPrivateKey } from '@stacks/wallet-sdk'; // global CLI options let txOnly = false; let estimateOnly = false; let safetyChecks = true; let receiveFeesPeriod = 52595; let gracePeriod = 5000; let noExit = false; let maxIDSearchIndex = DEFAULT_MAX_ID_SEARCH_INDEX; let BLOCKSTACK_TEST = !!process.env.BLOCKSTACK_TEST; export function getMaxIDSearchIndex() { return maxIDSearchIndex; } /* * Sign a profile. * @path (string) path to the profile * @privateKey (string) the owner key (must be single-sig) */ // TODO: fix, network is never used // @ts-ignore function profileSign(network: CLINetworkAdapter, args: string[]): Promise { const profilePath = args[0]; const profileData = JSON.parse(fs.readFileSync(profilePath).toString()); return Promise.resolve().then(() => makeProfileJWT(profileData, args[1])); } /* * Verify a profile with an address or public key * @path (string) path to the profile * @publicKeyOrAddress (string) public key or address */ function profileVerify(network: CLINetworkAdapter, args: string[]): Promise { const profilePath = args[0]; let publicKeyOrAddress = args[1]; // need to coerce mainnet if (publicKeyOrAddress.match(ID_ADDRESS_PATTERN)) { publicKeyOrAddress = network.coerceMainnetAddress(publicKeyOrAddress.slice(3)); } const profileString = fs.readFileSync(profilePath).toString(); return Promise.resolve().then(() => { let profileToken = null; try { const profileTokens = JSON.parse(profileString); profileToken = profileTokens[0].token; } catch (e) { // might be a raw token profileToken = profileString; } if (!profileToken) { throw new Error(`Data at ${profilePath} does not appear to be a signed profile`); } const profile = blockstack.extractProfile(profileToken, publicKeyOrAddress); return JSONStringify(profile); }); } /* * Store a signed profile for a name or an address. * * verify that the profile was signed by the name's owner address * * verify that the private key matches the name's owner address * * Assumes that the URI records are all Gaia hubs * * @nameOrAddress (string) name or address that owns the profile * @path (string) path to the signed profile token * @privateKey (string) owner private key for the name * @gaiaUrl (string) this is the write endpoint of the Gaia hub to use */ function profileStore(network: CLINetworkAdapter, args: string[]): Promise { const nameOrAddress = args[0]; const signedProfilePath = args[1]; const privateKey = decodePrivateKey(args[2]); const gaiaHubUrl = args[3]; const signedProfileData = fs.readFileSync(signedProfilePath).toString(); const ownerAddress = getPrivateKeyAddress(network, privateKey); const ownerAddressMainnet = network.coerceMainnetAddress(ownerAddress); let nameInfoPromise: Promise; let name = ''; if (nameOrAddress.startsWith('ID-')) { // ID-address nameInfoPromise = Promise.resolve().then(() => { return { address: nameOrAddress.slice(3), }; }); } else { // name; find the address nameInfoPromise = getNameInfoEasy(network, nameOrAddress); name = nameOrAddress; } const verifyProfilePromise = profileVerify(network, [ signedProfilePath, `ID-${ownerAddressMainnet}`, ]); return Promise.all([nameInfoPromise, verifyProfilePromise]) .then(([nameInfo, _verifiedProfile]: [NameInfoType | null, any]) => { if ( safetyChecks && (!nameInfo || network.coerceAddress(nameInfo.address) !== network.coerceAddress(ownerAddress)) ) { throw new Error( 'Name owner address either could not be found, or does not match ' + `private key address ${ownerAddress}` ); } return gaiaUploadProfileAll(network, [gaiaHubUrl], signedProfileData, args[2], name); }) .then((gaiaUrls: { dataUrls?: string[] | null; error?: string | null }) => { if (gaiaUrls.hasOwnProperty('error')) { return JSONStringify({ dataUrls: gaiaUrls.dataUrls!, error: gaiaUrls.error! }, true); } else { return JSONStringify({ profileUrls: gaiaUrls.dataUrls! }); } }); } /* * Get the app private key(s) from a backup phrase * and an index of the enumerated accounts * args: * @mnemonic (string) the 12-word phrase * @index (number) the index of the account * @appOrigin (string) the application's origin URL */ async function getAppKeys(network: CLINetworkAdapter, args: string[]): Promise { const mnemonic = await getBackupPhrase(args[0]); const index = parseInt(args[1]); if (index <= 0) throw new Error('index must be greater than 0'); const appDomain = args[2]; let wallet = await generateWallet({ secretKey: mnemonic, password: '' }); for (let i = 0; i < index; i++) { wallet = generateNewAccount(wallet); } const account = wallet.accounts[index - 1]; const privateKey = getAppPrivateKey({ account, appDomain }); const address = getAddressFromPrivateKey( privateKey, network.isMainnet() ? TransactionVersion.Mainnet : TransactionVersion.Testnet ); return JSON.stringify({ keyInfo: { privateKey, address } }); } /* * Get the owner private key(s) from a backup phrase * args: * @mnemonic (string) the 12-word phrase * @max_index (integer) (optional) the profile index maximum */ async function getOwnerKeys(network: CLINetworkAdapter, args: string[]): Promise { const mnemonic = await getBackupPhrase(args[0]); let maxIndex = 1; if (args.length > 1 && !!args[1]) { maxIndex = parseInt(args[1]); } const keyInfo: OwnerKeyInfoType[] = []; for (let i = 0; i < maxIndex; i++) { keyInfo.push(await getOwnerKeyInfo(network, mnemonic, i)); } return JSONStringify(keyInfo); } /* * Get the payment private key from a backup phrase * args: * @mnemonic (string) the 12-word phrase */ async function getPaymentKey(network: CLINetworkAdapter, args: string[]): Promise { const mnemonic = await getBackupPhrase(args[0]); // keep the return value consistent with getOwnerKeys const keyObj = await getPaymentKeyInfo(network, mnemonic); const keyInfo: PaymentKeyInfoType[] = []; keyInfo.push(keyObj); return JSONStringify(keyInfo); } /* * Get the payment private key from a backup phrase used by the Stacks wallet * args: * @mnemonic (string) the 24-word phrase */ async function getStacksWalletKey(network: CLINetworkAdapter, args: string[]): Promise { const mnemonic = await getBackupPhrase(args[0]); const derivationPath: string | undefined = args[1] || undefined; // keep the return value consistent with getOwnerKeys const keyObj = await getStacksWalletKeyInfo(network, mnemonic, derivationPath); const keyInfo: StacksKeyInfoType[] = []; keyInfo.push(keyObj); return JSONStringify(keyInfo); } /* * Make a private key and output it * args: * @mnemonic (string) OPTIONAL; the 12-word phrase */ async function makeKeychain(network: CLINetworkAdapter, args: string[]): Promise { let mnemonic: string; if (args[0]) { mnemonic = await getBackupPhrase(args[0]); } else { // eslint-disable-next-line @typescript-eslint/await-thenable mnemonic = await bip39.generateMnemonic( STX_WALLET_COMPATIBLE_SEED_STRENGTH, crypto.randomBytes ); } const derivationPath: string | undefined = args[1] || undefined; const stacksKeyInfo = await getStacksWalletKeyInfo(network, mnemonic, derivationPath); return JSONStringify({ mnemonic: mnemonic, keyInfo: stacksKeyInfo, }); } /* * Get an address's tokens and their balances. * Takes either a Bitcoin or Stacks address * args: * @address (string) the address */ function balance(network: CLINetworkAdapter, args: string[]): Promise { let address = args[0]; if (BLOCKSTACK_TEST) { // force testnet address if we're in regtest or testnet mode address = network.coerceAddress(address); } // temporary hack to use network config from stacks-transactions lib const txNetwork = network.isMainnet() ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); return fetch(txNetwork.getAccountApiUrl(address)) .then(response => { if (response.status === 404) { return Promise.reject({ status: response.status, error: response.statusText, }); } return response.json(); }) .then(response => { let balanceHex = response.balance; if (response.balance.startsWith('0x')) { balanceHex = response.balance.substr(2); } let lockedHex = response.locked; if (response.locked.startsWith('0x')) { lockedHex = response.locked.substr(2); } const unlockHeight = response.unlock_height; const balance = new BN(balanceHex, 16); const locked = new BN(lockedHex, 16); const res = { balance: balance.toString(10), locked: locked.toString(10), unlock_height: unlockHeight, nonce: response.nonce, }; return Promise.resolve(JSONStringify(res)); }) .catch(error => error); } /* * Get a page of the account's history * args: * @address (string) the account address * @page (int) the page of the history to fetch (optional) */ function getAccountHistory(network: CLINetworkAdapter, args: string[]): Promise { const address = c32check.c32ToB58(args[0]); if (args.length >= 2 && !!args[1]) { const page = parseInt(args[1]); return Promise.resolve() .then(() => { return network.getAccountHistoryPage(address, page); }) .then(accountStates => JSONStringify( accountStates.map((s: any) => { const new_s = { address: c32check.b58ToC32(s.address), credit_value: s.credit_value.toString(), debit_value: s.debit_value.toString(), }; return new_s; }) ) ); } else { // all pages let history: any[] = []; function getAllAccountHistoryPages(page: number): Promise { return network.getAccountHistoryPage(address, page).then((results: any[]) => { if (results.length == 0) { return history; } else { history = history.concat(results); return getAllAccountHistoryPages(page + 1); } }); } return getAllAccountHistoryPages(0).then((accountStates: any[]) => JSONStringify( accountStates.map((s: any) => { const new_s = { address: c32check.b58ToC32(s.address), credit_value: s.credit_value.toString(), debit_value: s.debit_value.toString(), }; return new_s; }) ) ); } } // /* // * Get the account's state(s) at a particular block height // * args: // * @address (string) the account address // * @blockHeight (int) the height at which to query // */ // function getAccountAt(network: CLINetworkAdapter, args: string[]) : Promise { // const address = c32check.c32ToB58(args[0]); // const blockHeight = parseInt(args[1]); // return Promise.resolve().then(() => { // return network.getAccountAt(address, blockHeight); // }) // .then(accountStates => accountStates.map((s : any) => { // const new_s = { // address: c32check.b58ToC32(s.address), // credit_value: s.credit_value.toString(), // debit_value: s.debit_value.toString() // }; // return new_s; // })) // .then(history => JSONStringify(history)); // } // /* // * Sends BTC from one private key to another address // * args: // * @recipientAddress (string) the recipient's address // * @amount (string) the amount of BTC to send // * @privateKey (string) the private key that owns the BTC // */ // function sendBTC(network: CLINetworkAdapter, args: string[]) : Promise { // const destinationAddress = args[0]; // const amount = parseInt(args[1]); // const paymentKeyHex = decodePrivateKey(args[2]); // if (amount <= 5500) { // throw new Error('Invalid amount (must be greater than 5500)'); // } // let paymentKey; // if (typeof paymentKeyHex === 'string') { // // single-sig // paymentKey = blockstack.PubkeyHashSigner.fromHexString(paymentKeyHex); // } // else { // // multi-sig or segwit // paymentKey = paymentKeyHex; // } // const txPromise = blockstack.transactions.makeBitcoinSpend(destinationAddress, paymentKey, amount, !hasKeys(paymentKeyHex)) // .catch((e : Error) => { // if (e.name === 'InvalidAmountError') { // return JSONStringify({ // 'status': false, // 'error': e.message // }, true); // } // else { // throw e; // } // }); // if (txOnly) { // return txPromise; // } // else { // return txPromise.then((tx : string) => { // return network.broadcastTransaction(tx); // }) // .then((txid : string) => { // return txid; // }); // } // } /* * Send tokens from one account private key to another account's address. * args: * @recipientAddress (string) the recipient's account address * @tokenAmount (int) the number of tokens to send * @fee (int) the transaction fee to be paid * @nonce (int) integer nonce needs to be incremented after each transaction from an account * @privateKey (string) the hex-encoded private key to use to send the tokens * @memo (string) OPTIONAL: a 34-byte memo to include */ async function sendTokens(network: CLINetworkAdapter, args: string[]): Promise { const recipientAddress = args[0]; const tokenAmount = new BN(args[1]); const fee = new BN(args[2]); const nonce = new BN(args[3]); const privateKey = args[4]; let memo = ''; if (args.length > 4 && !!args[5]) { memo = args[5]; } // temporary hack to use network config from stacks-transactions lib const txNetwork = network.isMainnet() ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); const options: SignedTokenTransferOptions = { recipient: recipientAddress, amount: tokenAmount, senderKey: privateKey, fee, nonce, memo, network: txNetwork, anchorMode: AnchorMode.Any, }; const tx: StacksTransaction = await makeSTXTokenTransfer(options); if (estimateOnly) { return estimateTransfer(tx, txNetwork).then(cost => { return cost.toString(10); }); } if (txOnly) { return Promise.resolve(tx.serialize().toString('hex')); } return broadcastTransaction(tx, txNetwork) .then((response: TxBroadcastResult) => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${tx.txid()}`, transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), }; }) .catch(error => { return error.toString(); }); } /* * Depoly a Clarity smart contract. * args: * @source (string) path to the contract source file * @contractName (string) the name of the contract * @fee (int) the transaction fee to be paid * @nonce (int) integer nonce needs to be incremented after each transaction from an account * @privateKey (string) the hex-encoded private key to use to send the tokens */ async function contractDeploy(network: CLINetworkAdapter, args: string[]): Promise { const sourceFile = args[0]; const contractName = args[1]; const fee = new BN(args[2]); const nonce = new BN(args[3]); const privateKey = args[4]; const source = fs.readFileSync(sourceFile).toString(); // temporary hack to use network config from stacks-transactions lib const txNetwork = network.isMainnet() ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); const options: ContractDeployOptions = { contractName, codeBody: source, senderKey: privateKey, fee, nonce, network: txNetwork, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }; const tx = await makeContractDeploy(options); if (estimateOnly) { return estimateContractDeploy(tx, txNetwork).then(cost => { return cost.toString(10); }); } if (txOnly) { return Promise.resolve(tx.serialize().toString('hex')); } return broadcastTransaction(tx, txNetwork) .then(response => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${tx.txid()}`, transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), }; }) .catch(error => { return error.toString(); }); } /* * Call a Clarity smart contract function. * args: * @contractAddress (string) the address of the contract * @contractName (string) the name of the contract * @functionName (string) the name of the function to call * @fee (int) the transaction fee to be paid * @nonce (int) integer nonce needs to be incremented after each transaction from an account * @privateKey (string) the hex-encoded private key to use to send the tokens */ async function contractFunctionCall(network: CLINetworkAdapter, args: string[]): Promise { const contractAddress = args[0]; const contractName = args[1]; const functionName = args[2]; const fee = new BN(args[3]); const nonce = new BN(args[4]); const privateKey = args[5]; // temporary hack to use network config from stacks-transactions lib const txNetwork = network.isMainnet() ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); let abi: ClarityAbi; let abiArgs: ClarityFunctionArg[]; let functionArgs: ClarityValue[] = []; return getAbi(contractAddress, contractName, txNetwork) .then(responseAbi => { abi = responseAbi; const filtered = abi.functions.filter(fn => fn.name === functionName); if (filtered.length === 1) { abiArgs = filtered[0].args; return makePromptsFromArgList(abiArgs); } else { return null; } }) .then(prompts => prompt(prompts!)) .then(answers => { functionArgs = parseClarityFunctionArgAnswers(answers, abiArgs); const options: SignedContractCallOptions = { contractAddress, contractName, functionName, functionArgs, senderKey: privateKey, fee, nonce, network: txNetwork, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }; return makeContractCall(options); }) .then(tx => { if (!validateContractCall(tx.payload as ContractCallPayload, abi)) { throw new Error('Failed to validate function arguments against ABI'); } if (estimateOnly) { return estimateContractFunctionCall(tx, txNetwork).then(cost => { return cost.toString(10); }); } if (txOnly) { return Promise.resolve(tx.serialize().toString('hex')); } return broadcastTransaction(tx, txNetwork) .then(response => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${tx.txid()}`, transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), }; }) .catch(error => { return error.toString(); }); }); } /* * Call a read-only Clarity smart contract function. * args: * @contractAddress (string) the address of the contract * @contractName (string) the name of the contract * @functionName (string) the name of the function to call * @senderAddress (string) the sender address */ async function readOnlyContractFunctionCall( network: CLINetworkAdapter, args: string[] ): Promise { const contractAddress = args[0]; const contractName = args[1]; const functionName = args[2]; const senderAddress = args[3]; // temporary hack to use network config from stacks-transactions lib const txNetwork = network.isMainnet() ? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl }) : new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl }); let abi: ClarityAbi; let abiArgs: ClarityFunctionArg[]; let functionArgs: ClarityValue[] = []; return getAbi(contractAddress, contractName, txNetwork) .then(responseAbi => { abi = responseAbi; const filtered = abi.functions.filter(fn => fn.name === functionName); if (filtered.length === 1) { abiArgs = filtered[0].args; return makePromptsFromArgList(abiArgs); } else { return null; } }) .then(prompts => prompt(prompts!)) .then(answers => { functionArgs = parseClarityFunctionArgAnswers(answers, abiArgs); const options: ReadOnlyFunctionOptions = { contractAddress, contractName, functionName, functionArgs, senderAddress, network: txNetwork, }; return callReadOnlyFunction(options); }) .then(returnValue => { return cvToString(returnValue); }) .catch(error => { return error.toString(); }); } // /* // * Get the number of confirmations of a txid. // * args: // * @txid (string) the transaction ID as a hex string // */ // function getConfirmations(network: CLINetworkAdapter, args: string[]) : Promise { // const txid = args[0]; // return Promise.all([network.getBlockHeight(), network.getTransactionInfo(txid)]) // .then(([blockHeight, txInfo]) => { // return JSONStringify({ // 'blockHeight': txInfo.block_height, // 'confirmations': blockHeight - txInfo.block_height + 1 // }); // }) // .catch((e) => { // if (e.message.toLowerCase() === 'unconfirmed transaction') { // return JSONStringify({ // 'blockHeight': 'unconfirmed', // 'confirmations': 0 // }); // } // else { // throw e; // } // }); // } /* * Get the address of a private key * args: * @private_key (string) the hex-encoded private key or key bundle */ function getKeyAddress(network: CLINetworkAdapter, args: string[]): Promise { const privateKey = decodePrivateKey(args[0]); return Promise.resolve().then(() => { const addr = getPrivateKeyAddress(network, privateKey); return JSONStringify({ BTC: addr, STACKS: c32check.b58ToC32(addr), }); }); } /* * Get a file from Gaia. * args: * @username (string) the blockstack ID of the user who owns the data * @origin (string) the application origin * @path (string) the file to read * @appPrivateKey (string) OPTIONAL: the app private key to decrypt/verify with * @decrypt (string) OPTINOAL: if '1' or 'true', then decrypt * @verify (string) OPTIONAL: if '1' or 'true', then search for and verify a signature file * along with the data */ function gaiaGetFile(network: CLINetworkAdapter, args: string[]): Promise { const username = args[0]; const origin = args[1]; const path = args[2]; let appPrivateKey = args[3]; let decrypt = false; let verify = false; if (!!appPrivateKey && args.length > 4 && !!args[4]) { decrypt = args[4].toLowerCase() === 'true' || args[4].toLowerCase() === '1'; } if (!!appPrivateKey && args.length > 5 && !!args[5]) { verify = args[5].toLowerCase() === 'true' || args[5].toLowerCase() === '1'; } if (!appPrivateKey) { // make a fake private key (it won't be used) appPrivateKey = 'fda1afa3ff9ef25579edb5833b825ac29fae82d03db3f607db048aae018fe882'; } // force mainnet addresses blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return gaiaAuth(network, appPrivateKey, null) .then((_userData: UserData) => blockstack.getFile(path, { decrypt: decrypt, verify: verify, app: origin, username: username, }) ) .then((data: ArrayBuffer | Buffer | string) => { if (data instanceof ArrayBuffer) { return Buffer.from(data); } else { return data; } }); } /* * Put a file into a Gaia hub * args: * @hubUrl (string) the URL to the write endpoint of the gaia hub * @appPrivateKey (string) the private key used to authenticate to the gaia hub * @dataPath (string) the path (on disk) to the data to store * @gaiaPath (string) the path (in Gaia) where the data will be stored * @encrypt (string) OPTIONAL: if '1' or 'true', then encrypt the file * @sign (string) OPTIONAL: if '1' or 'true', then sign the file and store the signature too. */ function gaiaPutFile(network: CLINetworkAdapter, args: string[]): Promise { const hubUrl = args[0]; const appPrivateKey = args[1]; const dataPath = args[2]; const gaiaPath = path.normalize(args[3].replace(/^\/+/, '')); let encrypt = false; let sign = false; if (args.length > 4 && !!args[4]) { encrypt = args[4].toLowerCase() === 'true' || args[4].toLowerCase() === '1'; } if (args.length > 5 && !!args[5]) { sign = args[5].toLowerCase() === 'true' || args[5].toLowerCase() === '1'; } const data = fs.readFileSync(dataPath); // force mainnet addresses // TODO blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return gaiaAuth(network, appPrivateKey, hubUrl) .then((_userData: UserData) => { return blockstack.putFile(gaiaPath, data, { encrypt: encrypt, sign: sign }); }) .then((url: string) => { return JSONStringify({ urls: [url] }); }); } /* * Delete a file in a Gaia hub * args: * @hubUrl (string) the URL to the write endpoint of the gaia hub * @appPrivateKey (string) the private key used to authenticate to the gaia hub * @gaiaPath (string) the path (in Gaia) to delete * @wasSigned (string) OPTIONAL: if '1' or 'true'. Delete the signature file as well. */ function gaiaDeleteFile(network: CLINetworkAdapter, args: string[]): Promise { const hubUrl = args[0]; const appPrivateKey = args[1]; const gaiaPath = path.normalize(args[2].replace(/^\/+/, '')); let wasSigned = false; if (args.length > 3 && !!args[3]) { wasSigned = args[3].toLowerCase() === 'true' || args[3].toLowerCase() === '1'; } // force mainnet addresses // TODO blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return gaiaAuth(network, appPrivateKey, hubUrl) .then((_userData: UserData) => { return blockstack.deleteFile(gaiaPath, { wasSigned: wasSigned }); }) .then(() => { return JSONStringify('ok'); }); } /* * List files in a Gaia hub * args: * @hubUrl (string) the URL to the write endpoint of the gaia hub * @appPrivateKey (string) the private key used to authenticate to the gaia hub */ function gaiaListFiles(network: CLINetworkAdapter, args: string[]): Promise { const hubUrl = args[0]; const appPrivateKey = args[1]; // force mainnet addresses // TODO let count = 0; blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return gaiaAuth(network, canonicalPrivateKey(appPrivateKey), hubUrl) .then((_userData: UserData) => { return blockstack.listFiles((name: string) => { // print out incrementally console.log(name); count += 1; return true; }); }) .then(() => JSONStringify(count)); } /* * Group array items into batches */ function batchify(input: T[], batchSize: number = 50): T[][] { const output = []; let currentBatch = []; for (let i = 0; i < input.length; i++) { currentBatch.push(input[i]); if (currentBatch.length >= batchSize) { output.push(currentBatch); currentBatch = []; } } if (currentBatch.length > 0) { output.push(currentBatch); } return output; } /* * Dump all files from a Gaia hub bucket to a directory on disk. * args: * @nameOrIDAddress (string) the name or ID address that owns the bucket to dump * @appOrigin (string) the application for which to dump data * @hubUrl (string) the URL to the write endpoint of the gaia hub * @mnemonic (string) the 12-word phrase or ciphertext * @dumpDir (string) the directory to hold the dumped files */ function gaiaDumpBucket(network: CLINetworkAdapter, args: string[]): Promise { const nameOrIDAddress = args[0]; const appOrigin = args[1]; const hubUrl = args[2]; const mnemonicOrCiphertext = args[3]; let dumpDir = args[4]; if (dumpDir.length === 0) { throw new Error('Invalid directory (not given)'); } if (dumpDir[0] !== '/') { // relative path. make absolute const cwd = fs.realpathSync('.'); dumpDir = path.normalize(`${cwd}/${dumpDir}`); } mkdirs(dumpDir); function downloadFile(hubConfig: GaiaHubConfig, fileName: string): Promise { const gaiaReadUrl = `${hubConfig.url_prefix.replace(/\/+$/, '')}/${hubConfig.address}`; const fileUrl = `${gaiaReadUrl}/${fileName}`; const destPath = `${dumpDir}/${fileName.replace(/\//g, '\\x2f')}`; console.log(`Download ${fileUrl} to ${destPath}`); return fetch(fileUrl) .then((resp: any) => { if (resp.status !== 200) { throw new Error(`Bad status code for ${fileUrl}: ${resp.status}`); } // javascript can be incredibly stupid at fetching data despite being a Web language... const contentType = resp.headers.get('Content-Type'); if ( contentType === null || contentType.startsWith('text') || contentType === 'application/json' ) { return resp.text(); } else { return resp.arrayBuffer(); } }) .then((filebytes: Buffer | ArrayBuffer) => { return new Promise((resolve, reject) => { try { fs.writeFileSync(destPath, Buffer.from(filebytes), { encoding: null, mode: 0o660 }); resolve(); } catch (e) { reject(e); } }); }); } // force mainnet addresses // TODO: better way of doing this blockstack.config.network.layer1 = bitcoin.networks.bitcoin; const fileNames: string[] = []; let gaiaHubConfig: GaiaHubConfig; let appPrivateKey: string; let ownerPrivateKey: string; return getIDAppKeys(network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) .then((keyInfo: IDAppKeys) => { appPrivateKey = keyInfo.appPrivateKey; ownerPrivateKey = keyInfo.ownerPrivateKey; return gaiaAuth(network, appPrivateKey, hubUrl, ownerPrivateKey); }) .then((_userData: UserData) => { return gaiaConnect(network, hubUrl, appPrivateKey); }) .then((hubConfig: GaiaHubConfig) => { gaiaHubConfig = hubConfig; return blockstack.listFiles(name => { fileNames.push(name); return true; }); }) .then((fileCount: number) => { console.log(`Download ${fileCount} files...`); const fileBatches: string[][] = batchify(fileNames); let filePromiseChain: Promise = Promise.resolve(); for (let i = 0; i < fileBatches.length; i++) { const filePromises = fileBatches[i].map(fileName => downloadFile(gaiaHubConfig, fileName)); const batchPromise = Promise.all(filePromises); filePromiseChain = filePromiseChain.then(() => batchPromise); } return filePromiseChain.then(() => JSONStringify(fileCount)); }); } /* * Restore all of the files in a Gaia bucket dump to a new Gaia hub * args: * @nameOrIDAddress (string) the name or ID address that owns the bucket to dump * @appOrigin (string) the origin of the app for which to restore data * @hubUrl (string) the URL to the write endpoint of the new gaia hub * @mnemonic (string) the 12-word phrase or ciphertext * @dumpDir (string) the directory to hold the dumped files */ function gaiaRestoreBucket(network: CLINetworkAdapter, args: string[]): Promise { const nameOrIDAddress = args[0]; const appOrigin = args[1]; const hubUrl = args[2]; const mnemonicOrCiphertext = args[3]; let dumpDir = args[4]; if (dumpDir.length === 0) { throw new Error('Invalid directory (not given)'); } if (dumpDir[0] !== '/') { // relative path. make absolute const cwd = fs.realpathSync('.'); dumpDir = path.normalize(`${cwd}/${dumpDir}`); } const fileList = fs.readdirSync(dumpDir); const fileBatches = batchify(fileList, 10); let appPrivateKey: string; let ownerPrivateKey: string; // force mainnet addresses // TODO better way of doing this blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return getIDAppKeys(network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) .then((keyInfo: IDAppKeys) => { appPrivateKey = keyInfo.appPrivateKey; ownerPrivateKey = keyInfo.ownerPrivateKey; return gaiaAuth(network, appPrivateKey, hubUrl, ownerPrivateKey); }) .then((_userData: UserData) => { let uploadPromise: Promise = Promise.resolve(); for (let i = 0; i < fileBatches.length; i++) { const uploadBatchPromises = fileBatches[i].map((fileName: string) => { const filePath = path.join(dumpDir, fileName); const dataBuf = fs.readFileSync(filePath); const gaiaPath = fileName.replace(/\\x2f/g, '/'); return blockstack .putFile(gaiaPath, dataBuf, { encrypt: false, sign: false }) .then((url: string) => { console.log(`Uploaded ${fileName} to ${url}`); }); }); uploadPromise = uploadPromise.then(() => Promise.all(uploadBatchPromises)); } return uploadPromise; }) .then(() => JSONStringify(fileList.length)); } /* * Set the Gaia hub for an application for a blockstack ID. * args: * @blockstackID (string) the blockstack ID of the user * @profileHubUrl (string) the URL to the write endpoint of the user's profile gaia hub * @appOrigin (string) the application's Origin * @hubUrl (string) the URL to the write endpoint of the app's gaia hub * @mnemonic (string) the 12-word backup phrase, or the ciphertext of it */ async function gaiaSetHub(network: CLINetworkAdapter, args: string[]): Promise { network.setCoerceMainnetAddress(true); const blockstackID = args[0]; const ownerHubUrl = args[1]; const appOrigin = args[2]; const hubUrl = args[3]; const mnemonicPromise = getBackupPhrase(args[4]); const nameInfoPromise = getNameInfoEasy(network, blockstackID).then( (nameInfo: NameInfoType | null) => { if (!nameInfo) { throw new Error('Name not found'); } return nameInfo; } ); const profilePromise = blockstack.lookupProfile(blockstackID); const [nameInfo, nameProfile, mnemonic]: [NameInfoType, any, string] = await Promise.all([ nameInfoPromise, profilePromise, mnemonicPromise, ]); if (!nameProfile) { throw new Error('No profile found'); } if (!nameInfo) { throw new Error('Name not found'); } if (!nameInfo.zonefile) { throw new Error('No zone file found'); } if (!nameProfile.apps) { nameProfile.apps = {}; } // get owner ID-address const ownerAddress = network.coerceMainnetAddress(nameInfo.address); const idAddress = `ID-${ownerAddress}`; // get owner and app key info const appKeyInfo = await getApplicationKeyInfo(network, mnemonic, idAddress, appOrigin); const ownerKeyInfo = await getOwnerKeyInfo(network, mnemonic, appKeyInfo.ownerKeyIndex); // do we already have an address set for this app? let existingAppAddress: string | null = null; let appPrivateKey: string; try { existingAppAddress = getGaiaAddressFromProfile(network, nameProfile, appOrigin); appPrivateKey = extractAppKey(network, appKeyInfo, existingAppAddress); } catch (e) { console.log(`No profile application entry for ${appOrigin}`); appPrivateKey = extractAppKey(network, appKeyInfo); } appPrivateKey = `${canonicalPrivateKey(appPrivateKey)}01`; const appAddress = network.coerceMainnetAddress(getPrivateKeyAddress(network, appPrivateKey)); if (existingAppAddress && appAddress !== existingAppAddress) { throw new Error(`BUG: ${existingAppAddress} !== ${appAddress}`); } const profile = nameProfile; const ownerPrivateKey = ownerKeyInfo.privateKey; const ownerGaiaHubPromise = gaiaConnect(network, ownerHubUrl, ownerPrivateKey); const appGaiaHubPromise = gaiaConnect(network, hubUrl, appPrivateKey); const [ownerHubConfig, appHubConfig]: [GaiaHubConfig, GaiaHubConfig] = await Promise.all([ ownerGaiaHubPromise, appGaiaHubPromise, ]); if (!ownerHubConfig.url_prefix) { throw new Error('Invalid owner hub config: no url_prefix defined'); } if (!appHubConfig.url_prefix) { throw new Error('Invalid app hub config: no url_prefix defined'); } const gaiaReadUrl = appHubConfig.url_prefix.replace(/\/+$/, ''); const newAppEntry: Record = {}; newAppEntry[appOrigin] = `${gaiaReadUrl}/${appAddress}/`; const apps = Object.assign({}, profile.apps ? profile.apps : {}, newAppEntry); profile.apps = apps; // sign the new profile const signedProfile = makeProfileJWT(profile, ownerPrivateKey); const profileUrls: { dataUrls?: string[] | null; error?: string | null; } = await gaiaUploadProfileAll( network, [ownerHubUrl], signedProfile, ownerPrivateKey, blockstackID ); if (profileUrls.error) { return JSONStringify({ error: profileUrls.error, }); } else { return JSONStringify({ profileUrls: profileUrls.dataUrls!, }); } } /* * Convert an address between mainnet and testnet, and between * base58check and c32check. * args: * @address (string) the input address. can be in any format */ function addressConvert(network: CLINetworkAdapter, args: string[]): Promise { const addr = args[0]; let b58addr: string; let testnetb58addr: string; if (addr.match(STACKS_ADDRESS_PATTERN)) { b58addr = c32check.c32ToB58(addr); } else if (addr.match(/[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+/)) { b58addr = addr; } else { throw new Error(`Unrecognized address ${addr}`); } if (isTestnetAddress(b58addr)) { testnetb58addr = b58addr; } else if (network.isTestnet()) { testnetb58addr = network.coerceAddress(b58addr); } return Promise.resolve().then(() => { const mainnetb58addr = network.coerceMainnetAddress(b58addr); const result: any = { mainnet: { STACKS: c32check.b58ToC32(mainnetb58addr), BTC: mainnetb58addr, }, testnet: undefined, }; if (testnetb58addr) { result.testnet = { STACKS: c32check.b58ToC32(testnetb58addr), BTC: testnetb58addr, }; } return JSONStringify(result); }); } /* * Run an authentication daemon on a given port. * args: * @gaiaHubUrl (string) the write endpoint of your app Gaia hub, where app data will be stored * @mnemonic (string) your 12-word phrase, optionally encrypted. If encrypted, then * a password will be prompted. * @profileGaiaHubUrl (string) the write endpoint of your profile Gaia hub, where your profile * will be stored (optional) * @port (number) the port to listen on (optional) */ function authDaemon(network: CLINetworkAdapter, args: string[]): Promise { const gaiaHubUrl = args[0]; const mnemonicOrCiphertext = args[1]; let port = 3000; // default port let profileGaiaHub = gaiaHubUrl; if (args.length > 2 && !!args[2]) { profileGaiaHub = args[2]; } if (args.length > 3 && !!args[3]) { port = parseInt(args[3]); } if (port < 0 || port > 65535) { return Promise.resolve().then(() => JSONStringify({ error: 'Invalid port' })); } const mnemonicPromise = getBackupPhrase(mnemonicOrCiphertext); return mnemonicPromise .then((mnemonic: string) => { noExit = true; // load up all of our identity addresses, profiles, profile URLs, and Gaia connections const authServer = express(); authServer.use(cors()); authServer.get(/^\/auth\/*$/, (req: express.Request, res: express.Response) => { void handleAuth(network, mnemonic, gaiaHubUrl, profileGaiaHub, port, req, res); }); authServer.get(/^\/signin\/*$/, (req: express.Request, res: express.Response) => { void handleSignIn(network, mnemonic, gaiaHubUrl, profileGaiaHub, req, res); }); authServer.listen(port, () => console.log(`Authentication server started on ${port}`)); return 'Press Ctrl+C to exit'; }) .catch((e: Error) => { return JSONStringify({ error: e.message }); }); } /* * Encrypt a backup phrase * args: * @backup_phrase (string) the 12-word phrase to encrypt * @password (string) the password (will be interactively prompted if not given) */ // TODO: fix: network is never used // @ts-ignore function encryptMnemonic(network: CLINetworkAdapter, args: string[]): Promise { const mnemonic = args[0]; if (mnemonic.split(/ +/g).length !== 12) { throw new Error('Invalid backup phrase: must be 12 words'); } const passwordPromise: Promise = new Promise((resolve, reject) => { let pass = ''; if (args.length === 2 && !!args[1]) { pass = args[1]; resolve(pass); } else { if (!process.stdin.isTTY) { // password must be given as an argument const errMsg = 'Password argument required on non-interactive mode'; reject(new Error(errMsg)); } else { // prompt password getpass('Enter password: ', (pass1: string) => { getpass('Enter password again: ', (pass2: string) => { if (pass1 !== pass2) { const errMsg = 'Passwords do not match'; reject(new Error(errMsg)); } else { resolve(pass1); } }); }); } } }); return passwordPromise .then((pass: string) => encryptBackupPhrase(mnemonic, pass)) .then((cipherTextBuffer: Buffer) => cipherTextBuffer.toString('base64')) .catch((e: Error) => { return JSONStringify({ error: e.message }); }); } /* Decrypt a backup phrase * args: * @encrypted_backup_phrase (string) the encrypted base64-encoded backup phrase * @password 9string) the password (will be interactively prompted if not given) */ // TODO: fix: network is never used // @ts-ignore function decryptMnemonic(network: CLINetworkAdapter, args: string[]): Promise { const ciphertext = args[0]; const passwordPromise: Promise = new Promise((resolve, reject) => { if (args.length === 2 && !!args[1]) { const pass = args[1]; resolve(pass); } else { if (!process.stdin.isTTY) { // password must be given reject(new Error('Password argument required in non-interactive mode')); } else { // prompt password getpass('Enter password: ', p => { resolve(p); }); } } }); return passwordPromise .then((pass: string) => decryptBackupPhrase(Buffer.from(ciphertext, 'base64'), pass)) .catch((e: Error) => { return JSONStringify({ error: 'Failed to decrypt (wrong password or corrupt ciphertext), ' + `details: ${e.message}`, }); }); } async function stackingStatus(network: CLINetworkAdapter, args: string[]): Promise { const stxAddress = args[0]; const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); const stacker = new StackingClient(stxAddress, txNetwork); return stacker .getStatus() .then((status: StackerInfo) => { if (status.stacked) { return { amount_microstx: status.details.amount_microstx, first_reward_cycle: status.details.first_reward_cycle, lock_period: status.details.lock_period, unlock_height: status.details.unlock_height, pox_address: { version: status.details.pox_address.version.toString('hex'), hashbytes: status.details.pox_address.hashbytes.toString('hex'), }, }; } else { return 'Account not actively participating in Stacking'; } }) .catch((error: any) => { return error.toString(); }); } async function canStack(network: CLINetworkAdapter, args: string[]): Promise { const amount = new BN(args[0]); const cycles = Number(args[1]); const poxAddress = args[2]; const stxAddress = args[3]; const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); const apiConfig = new Configuration({ fetchApi: crossfetch, basePath: txNetwork.coreApiUrl, }); const accounts = new AccountsApi(apiConfig); const balancePromise = accounts.getAccountBalance({ principal: stxAddress, }); const stacker = new StackingClient(stxAddress, txNetwork); const poxInfoPromise = stacker.getPoxInfo(); const stackingEligiblePromise = stacker.canStack({ poxAddress, cycles }); return Promise.all([balancePromise, poxInfoPromise, stackingEligiblePromise]) .then(([balance, poxInfo, stackingEligible]) => { const minAmount = new BN(poxInfo.min_amount_ustx); const balanceBN = new BN(balance.stx.balance); if (minAmount.gt(amount)) { throw new Error( `Stacking amount less than required minimum of ${minAmount.toString()} microstacks` ); } if (amount.gt(balanceBN)) { throw new Error( `Stacking amount greater than account balance of ${balanceBN.toString()} microstacks` ); } if (!stackingEligible.eligible) { throw new Error(`Account cannot participate in stacking. ${stackingEligible.reason}`); } return stackingEligible; }) .catch(error => { return error; }); } async function stack(network: CLINetworkAdapter, args: string[]): Promise { const amount = new BN(args[0]); const cycles = Number(args[1]); const poxAddress = args[2]; const privateKey = args[3]; // let fee = new BN(0); // let nonce = new BN(0); // if (args.length > 3 && !!args[4]) { // fee = new BN(args[4]); // } // if (args.length > 4 && !!args[5]) { // nonce = new BN(args[5]); // } const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); const txVersion = txNetwork.isMainnet() ? TransactionVersion.Mainnet : TransactionVersion.Testnet; const apiConfig = new Configuration({ fetchApi: crossfetch, basePath: txNetwork.coreApiUrl, }); const accounts = new AccountsApi(apiConfig); const stxAddress = getAddressFromPrivateKey(privateKey, txVersion); const balancePromise = accounts.getAccountBalance({ principal: stxAddress, }); const stacker = new StackingClient(stxAddress, txNetwork); const poxInfoPromise = stacker.getPoxInfo(); const coreInfoPromise = stacker.getCoreInfo(); const stackingEligiblePromise = stacker.canStack({ poxAddress, cycles }); return Promise.all([balancePromise, poxInfoPromise, coreInfoPromise, stackingEligiblePromise]) .then(([balance, poxInfo, coreInfo, stackingEligible]) => { const minAmount = new BN(poxInfo.min_amount_ustx); const balanceBN = new BN(balance.stx.balance); const burnChainBlockHeight = coreInfo.burn_block_height; const startBurnBlock = burnChainBlockHeight + 3; if (minAmount.gt(amount)) { throw new Error( `Stacking amount less than required minimum of ${minAmount.toString()} microstacks` ); } if (amount.gt(balanceBN)) { throw new Error( `Stacking amount greater than account balance of ${balanceBN.toString()} microstacks` ); } if (!stackingEligible.eligible) { throw new Error(`Account cannot participate in stacking. ${stackingEligible.reason}`); } return stacker.stack({ amountMicroStx: amount, poxAddress, cycles, privateKey, burnBlockHeight: startBurnBlock, }); }) .then((response: TxBroadcastResult) => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${response.txid}`, transaction: generateExplorerTxPageUrl(response.txid, txNetwork), }; }) .catch(error => { return error; }); } function faucetCall(_: CLINetworkAdapter, args: string[]): Promise { const address = args[0]; // console.log(address); const apiConfig = new Configuration({ fetchApi: crossfetch, basePath: 'https://stacks-node-api.testnet.stacks.co', }); const faucets = new FaucetsApi(apiConfig); return faucets .runFaucetStx({ address }) .then((faucetTx: any) => { return JSONStringify({ txid: faucetTx.txId!, transaction: generateExplorerTxPageUrl( faucetTx.txId!.replace(/^0x/, ''), new StacksTestnet() ), }); }) .catch((error: any) => error.toString()); } /* Print out all documentation on usage in JSON */ type DocsArgsType = { name: string; type: string; value: string; format: string; }; type FormattedDocsType = { command: string; args: DocsArgsType[]; usage: string; group: string; }; function printDocs(_network: CLINetworkAdapter, _args: string[]): Promise { return Promise.resolve().then(() => { const formattedDocs: FormattedDocsType[] = []; const commandNames: string[] = Object.keys(CLI_ARGS.properties); for (let i = 0; i < commandNames.length; i++) { const commandName = commandNames[i]; const args: DocsArgsType[] = []; const usage = CLI_ARGS.properties[commandName].help; const group = CLI_ARGS.properties[commandName].group; for (let j = 0; j < CLI_ARGS.properties[commandName].items.length; j++) { const argItem = CLI_ARGS.properties[commandName].items[j]; args.push({ name: argItem.name, type: argItem.type, value: argItem.realtype, format: argItem.pattern ? argItem.pattern : '.+', } as DocsArgsType); } formattedDocs.push({ command: commandName, args: args, usage: usage, group: group, } as FormattedDocsType); } return JSONStringify(formattedDocs); }); } type CommandFunction = (network: CLINetworkAdapter, args: string[]) => Promise; /* * Decrypt a backup phrase * args: * @p /* * Global set of commands */ const COMMANDS: Record = { authenticator: authDaemon, // 'announce': announce, balance: balance, can_stack: canStack, call_contract_func: contractFunctionCall, call_read_only_contract_func: readOnlyContractFunctionCall, convert_address: addressConvert, decrypt_keychain: decryptMnemonic, deploy_contract: contractDeploy, docs: printDocs, encrypt_keychain: encryptMnemonic, gaia_deletefile: gaiaDeleteFile, gaia_dump_bucket: gaiaDumpBucket, gaia_getfile: gaiaGetFile, gaia_listfiles: gaiaListFiles, gaia_putfile: gaiaPutFile, gaia_restore_bucket: gaiaRestoreBucket, gaia_sethub: gaiaSetHub, get_address: getKeyAddress, get_account_history: getAccountHistory, get_app_keys: getAppKeys, get_owner_keys: getOwnerKeys, get_payment_key: getPaymentKey, get_stacks_wallet_key: getStacksWalletKey, make_keychain: makeKeychain, profile_sign: profileSign, profile_store: profileStore, profile_verify: profileVerify, // 'send_btc': sendBTC, send_tokens: sendTokens, stack: stack, stacking_status: stackingStatus, faucet: faucetCall, }; /* * CLI main entry point */ export function CLIMain() { const argv = process.argv; const opts = getCLIOpts(argv); const cmdArgs: any = checkArgs( CLIOptAsStringArray(opts, '_') ? CLIOptAsStringArray(opts, '_')! : [] ); if (!cmdArgs.success) { if (cmdArgs.error) { console.log(cmdArgs.error); } if (cmdArgs.usage) { if (cmdArgs.command) { console.log(makeCommandUsageString(cmdArgs.command)); console.log('Use "help" to list all commands.'); } else { console.log(USAGE); console.log(makeAllCommandsList()); } } process.exit(1); } else { txOnly = CLIOptAsBool(opts, 'x'); estimateOnly = CLIOptAsBool(opts, 'e'); safetyChecks = !CLIOptAsBool(opts, 'U'); receiveFeesPeriod = opts['N'] ? parseInt(CLIOptAsString(opts, 'N')!) : receiveFeesPeriod; gracePeriod = opts['G'] ? parseInt(CLIOptAsString(opts, 'N')!) : gracePeriod; maxIDSearchIndex = opts['M'] ? parseInt(CLIOptAsString(opts, 'M')!) : maxIDSearchIndex; const debug = CLIOptAsBool(opts, 'd'); const consensusHash = CLIOptAsString(opts, 'C'); const integration_test = CLIOptAsBool(opts, 'i'); const testnet = CLIOptAsBool(opts, 't'); const localnet = CLIOptAsBool(opts, 'l'); const magicBytes = CLIOptAsString(opts, 'm'); const apiUrl = CLIOptAsString(opts, 'H'); const transactionBroadcasterUrl = CLIOptAsString(opts, 'T'); const nodeAPIUrl = CLIOptAsString(opts, 'I'); const utxoUrl = CLIOptAsString(opts, 'X'); const bitcoindUsername = CLIOptAsString(opts, 'u'); const bitcoindPassword = CLIOptAsString(opts, 'p'); if (integration_test) { BLOCKSTACK_TEST = integration_test; } const configPath = CLIOptAsString(opts, 'c') ? CLIOptAsString(opts, 'c') : integration_test ? DEFAULT_CONFIG_REGTEST_PATH : testnet ? DEFAULT_CONFIG_TESTNET_PATH : DEFAULT_CONFIG_PATH; const namespaceBurnAddr = CLIOptAsString(opts, 'B'); const feeRate = CLIOptAsString(opts, 'F') ? parseInt(CLIOptAsString(opts, 'F')!) : 0; const priceToPay = CLIOptAsString(opts, 'P') ? CLIOptAsString(opts, 'P') : '0'; const priceUnits = CLIOptAsString(opts, 'D'); const networkType = testnet ? 'testnet' : localnet ? 'localnet' : integration_test ? 'regtest' : 'mainnet'; const configData = loadConfig(configPath!, networkType); if (debug) { configData.logConfig.level = 'debug'; } else { configData.logConfig.level = 'info'; } if (bitcoindUsername) { configData.bitcoindUsername = bitcoindUsername; } if (bitcoindPassword) { configData.bitcoindPassword = bitcoindPassword; } if (utxoUrl) { configData.utxoServiceUrl = utxoUrl; } winston.configure({ level: configData.logConfig.level, transports: [new winston.transports.Console(configData.logConfig)], }); const cliOpts: CLI_NETWORK_OPTS = { consensusHash: consensusHash ? consensusHash : null, feeRate: feeRate ? feeRate : null, namespaceBurnAddress: namespaceBurnAddr ? namespaceBurnAddr : null, priceToPay: priceToPay ? priceToPay : null, priceUnits: priceUnits ? priceUnits : null, receiveFeesPeriod: receiveFeesPeriod ? receiveFeesPeriod : null, gracePeriod: gracePeriod ? gracePeriod : null, altAPIUrl: apiUrl ? apiUrl : configData.blockstackAPIUrl, altTransactionBroadcasterUrl: transactionBroadcasterUrl ? transactionBroadcasterUrl : configData.broadcastServiceUrl, nodeAPIUrl: nodeAPIUrl ? nodeAPIUrl : configData.blockstackNodeUrl, }; // wrap command-line options const wrappedNetwork = getNetwork( configData, !!BLOCKSTACK_TEST || !!integration_test || !!testnet || !!localnet ); const blockstackNetwork = new CLINetworkAdapter(wrappedNetwork, cliOpts); if (magicBytes) { // blockstackNetwork.MAGIC_BYTES = magicBytes; } // blockstack.config.network = blockstackNetwork; blockstack.config.logLevel = 'error'; if (cmdArgs.command === 'help') { console.log(makeCommandUsageString(cmdArgs.args[0])); process.exit(0); } const method = COMMANDS[cmdArgs.command]; let exitcode = 0; method(blockstackNetwork, cmdArgs.args) .then((result: string | Buffer) => { try { // if this is a JSON object with 'status', set the exit code if (result instanceof Buffer) { return result; } else { const resJson: any = JSON.parse(result); if (resJson.hasOwnProperty('status') && !resJson.status) { exitcode = 1; } return result; } } catch (e) { return result; } }) .then((result: string | Buffer) => { if (result instanceof Buffer) { process.stdout.write(result); } else { console.log(result); } }) .then(() => { if (!noExit) { process.exit(exitcode); } }) .catch((e: Error) => { console.error(e.stack); console.error(e.message); if (!noExit) { process.exit(1); } }); } } /* test only exports */ export const testables = process.env.NODE_ENV === 'test' ? { addressConvert, contractFunctionCall, makeKeychain, getStacksWalletKey, } : undefined;