UNPKG

@stacks/cli

Version:
1,274 lines (1,273 loc) • 56.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.testables = exports.CLIMain = exports.parseDirectFunctionArgs = void 0; const scureBip39 = __importStar(require("@scure/bip39")); const english_1 = require("@scure/bip39/wordlists/english"); const bns_1 = require("@stacks/bns"); const common_1 = require("@stacks/common"); const transactions_1 = require("@stacks/transactions"); const bitcoin = __importStar(require("bitcoinjs-lib")); const blockstack = __importStar(require("blockstack")); const crypto = __importStar(require("crypto")); const fs = __importStar(require("fs")); const inquirer_1 = require("inquirer"); const node_fetch_1 = __importDefault(require("node-fetch")); const path = __importStar(require("path")); const process = __importStar(require("process")); const winston = __importStar(require("winston")); const c32check = require('c32check'); require("cross-fetch/polyfill"); const stacking_1 = require("@stacks/stacking"); const blockchain_api_client_1 = require("@stacks/blockchain-api-client"); const keys_1 = require("./keys"); const argparse_1 = require("./argparse"); const encrypt_1 = require("./encrypt"); const network_1 = require("./network"); const data_1 = require("./data"); const network_2 = require("@stacks/network"); const transactions_2 = require("@stacks/transactions"); const wallet_sdk_1 = require("@stacks/wallet-sdk"); const common_2 = require("./common"); const utils_1 = require("./utils"); let txOnly = false; let estimateOnly = false; let safetyChecks = true; let receiveFeesPeriod = 52595; let gracePeriod = 5000; const noExit = false; let BLOCKSTACK_TEST = !!process.env.BLOCKSTACK_TEST; function profileSign(_network, args) { const profilePath = args[0]; const profileData = JSON.parse(fs.readFileSync(profilePath).toString()); return Promise.resolve().then(() => (0, utils_1.makeProfileJWT)(profileData, args[1])); } function profileVerify(_network, args) { const profilePath = args[0]; let publicKeyOrAddress = args[1]; if (publicKeyOrAddress.match(argparse_1.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) { 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 (0, utils_1.JSONStringify)(profile); }); } function profileStore(_network, args) { const nameOrAddress = args[0]; const signedProfilePath = args[1]; const privateKey = (0, utils_1.decodePrivateKey)(args[2]); const gaiaHubUrl = args[3]; const signedProfileData = fs.readFileSync(signedProfilePath).toString(); const ownerAddress = (0, common_2.getPrivateKeyAddress)(_network, privateKey); const ownerAddressMainnet = _network.coerceMainnetAddress(ownerAddress); let nameInfoPromise; let name = ''; if (nameOrAddress.startsWith('ID-')) { nameInfoPromise = Promise.resolve().then(() => { return { address: nameOrAddress.slice(3), }; }); } else { nameInfoPromise = (0, utils_1.getNameInfoEasy)(_network, nameOrAddress); name = nameOrAddress; } const verifyProfilePromise = profileVerify(_network, [ signedProfilePath, `ID-${ownerAddressMainnet}`, ]); return Promise.all([nameInfoPromise, verifyProfilePromise]) .then(([nameInfo, _verifiedProfile]) => { 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 (0, data_1.gaiaUploadProfileAll)(_network, [gaiaHubUrl], signedProfileData, args[2], name); }) .then((gaiaUrls) => { if (gaiaUrls.hasOwnProperty('error')) { return (0, utils_1.JSONStringify)({ dataUrls: gaiaUrls.dataUrls, error: gaiaUrls.error }, true); } else { return (0, utils_1.JSONStringify)({ profileUrls: gaiaUrls.dataUrls }); } }); } async function getAppKeys(_network, args) { const mnemonic = await (0, utils_1.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 (0, wallet_sdk_1.generateWallet)({ secretKey: mnemonic, password: '' }); for (let i = 0; i < index; i++) { wallet = (0, wallet_sdk_1.generateNewAccount)(wallet); } const account = wallet.accounts[index - 1]; const privateKey = (0, wallet_sdk_1.getAppPrivateKey)({ account, appDomain }); const address = (0, transactions_1.getAddressFromPrivateKey)(privateKey, (0, network_1.getStacksNetwork)(_network)); return JSON.stringify({ keyInfo: { privateKey, address } }); } async function getOwnerKeys(_network, args) { const mnemonic = await (0, utils_1.getBackupPhrase)(args[0]); let maxIndex = 1; if (args.length > 1 && !!args[1]) { maxIndex = parseInt(args[1]); } const keyInfo = []; for (let i = 0; i < maxIndex; i++) { keyInfo.push(await (0, keys_1.getOwnerKeyInfo)(_network, mnemonic, i)); } return (0, utils_1.JSONStringify)(keyInfo); } async function getPaymentKey(_network, args) { const mnemonic = await (0, utils_1.getBackupPhrase)(args[0]); const keyObj = await (0, keys_1.getPaymentKeyInfo)(_network, mnemonic); const keyInfo = []; keyInfo.push(keyObj); return (0, utils_1.JSONStringify)(keyInfo); } async function getStacksWalletKey(_network, args) { const mnemonic = await (0, utils_1.getBackupPhrase)(args[0]); const derivationPath = args[1] || undefined; const keyObj = await (0, keys_1.getStacksWalletKeyInfo)(_network, mnemonic, derivationPath); const keyInfo = []; keyInfo.push(keyObj); return (0, utils_1.JSONStringify)(keyInfo); } async function migrateSubdomains(_network, args) { const mnemonic = await (0, utils_1.getBackupPhrase)(args[0]); const baseWallet = await (0, wallet_sdk_1.generateWallet)({ secretKey: mnemonic, password: '' }); const network = (0, network_1.getStacksNetwork)(_network); const wallet = await (0, wallet_sdk_1.restoreWalletAccounts)({ wallet: baseWallet, gaiaHubUrl: 'https://hub.blockstack.org', network, }); console.log(`Accounts found: ${wallet.accounts.length}\n(Accounts will be checked for both compressed and uncompressed public keys)`); const payload = { subdomains_list: [] }; const accounts = wallet.accounts .map(account => [ { ...account, dataPrivateKey: account.dataPrivateKey }, { ...account, dataPrivateKey: account.dataPrivateKey + '01' }, ]) .flat(); for (const account of accounts) { console.log('\nAccount:', account); const dataKeyAddress = (0, transactions_1.getAddressFromPrivateKey)(account.dataPrivateKey, network); const walletKeyAddress = (0, transactions_1.getAddressFromPrivateKey)(account.stxPrivateKey, network); console.log(`Finding subdomains for data-key address '${dataKeyAddress}'`); const namesResponse = await (0, node_fetch_1.default)(`${network.client.baseUrl}/v1/addresses/stacks/${dataKeyAddress}`); const namesJson = await namesResponse.json(); if ((namesJson.names?.length || 0) <= 0) { console.log(`No subdomains found for address '${dataKeyAddress}'`); continue; } const regExp = /(\..*){2,}/; const subDomains = namesJson.names.filter((val) => regExp.test(val)); if (subDomains.length === 0) console.log(`No subdomains found for address '${dataKeyAddress}'`); for (const subdomain of subDomains) { const namesResponse = await (0, node_fetch_1.default)(`${network.client.baseUrl}/v1/addresses/stacks/${walletKeyAddress}`); const existingNames = await namesResponse.json(); if (existingNames.names?.includes(subdomain)) { console.log(`Error: Subdomain '${subdomain}' already exists in wallet-key address.`); continue; } const nameInfo = await (0, node_fetch_1.default)(`${network.client.baseUrl}/v1/names/${subdomain}`); const nameInfoJson = await nameInfo.json(); console.log('Subdomain Info: ', nameInfoJson); if (nameInfoJson.address !== dataKeyAddress) { console.log(`Error: The account is not the owner of the subdomain '${subdomain}'`); continue; } const promptName = subdomain.replaceAll('.', '_'); const confirmMigration = await (0, inquirer_1.prompt)([ { name: promptName, message: `Do you want to migrate the domain '${subdomain}'`, type: 'confirm', }, ]); if (!confirmMigration[promptName]) continue; const [subdomainName] = subdomain.split('.'); const subDomainOp = { subdomainName, owner: walletKeyAddress, zonefile: nameInfoJson.zonefile, sequenceNumber: 1, }; const subdomainPieces = (0, utils_1.subdomainOpToZFPieces)(subDomainOp); const textToSign = subdomainPieces.txt.join(','); const hash = crypto.createHash('sha256').update(textToSign).digest('hex'); const sig = (0, transactions_1.signWithKey)(account.dataPrivateKey, hash); subDomainOp.signature = sig; payload.subdomains_list.push(subDomainOp); } } console.log('\nSubdomain Operation Payload:', payload); if (payload.subdomains_list.length <= 0) { return '"No subdomains found or selected. Canceling..."'; } const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }; const registrarUrl = args[1] || 'https://registrar.stacks.co'; const migrationURL = `${registrarUrl}/transfer`; console.log('Sending migration request...'); return (0, node_fetch_1.default)(migrationURL, options) .then(response => { if (response.status === 404) { return Promise.reject({ status: response.status, error: response.statusText, }); } return response.json(); }) .then(response => { if (response.txid) console.log(`The transaction will take some time to complete. Track its progress using the explorer: https://explorer.hiro.so/txid/0x${response.txid}`); return Promise.resolve((0, utils_1.JSONStringify)(response)); }) .catch(error => error); } async function makeKeychain(_network, args) { const mnemonic = args[0] ? await (0, utils_1.getBackupPhrase)(args[0]) : scureBip39.generateMnemonic(english_1.wordlist, keys_1.STX_WALLET_COMPATIBLE_SEED_STRENGTH); const derivationPath = args[1] || undefined; const stacksKeyInfo = await (0, keys_1.getStacksWalletKeyInfo)(_network, mnemonic, derivationPath); return (0, utils_1.JSONStringify)({ mnemonic, keyInfo: stacksKeyInfo, }); } function balance(_network, args) { let address = args[0]; if (BLOCKSTACK_TEST) { address = _network.coerceAddress(address); } const url = _network.nodeAPIUrl; return (0, node_fetch_1.default)(`${url}${transactions_1.ACCOUNT_PATH}/${address}?proof=0`) .then(response => { if (response.status === 404) { return Promise.reject({ status: response.status, error: response.statusText, }); } return response.json(); }) .then(response => { const res = { balance: BigInt(response.balance).toString(10), locked: BigInt(response.locked).toString(10), unlock_height: response.unlock_height, nonce: response.nonce, }; return Promise.resolve((0, utils_1.JSONStringify)(res)); }) .catch(error => error); } function getAccountHistory(_network, args) { 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 => (0, utils_1.JSONStringify)(accountStates.map((s) => { const new_s = { address: c32check.b58ToC32(s.address), credit_value: s.credit_value.toString(), debit_value: s.debit_value.toString(), }; return new_s; }))); } else { let history = []; function getAllAccountHistoryPages(page) { return _network.getAccountHistoryPage(address, page).then((results) => { if (results.length == 0) { return history; } else { history = history.concat(results); return getAllAccountHistoryPages(page + 1); } }); } return getAllAccountHistoryPages(0).then((accountStates) => (0, utils_1.JSONStringify)(accountStates.map((s) => { const new_s = { address: c32check.b58ToC32(s.address), credit_value: s.credit_value.toString(), debit_value: s.debit_value.toString(), }; return new_s; }))); } } async function sendTokens(_network, args) { const recipientAddress = args[0]; const tokenAmount = BigInt(args[1]); const fee = BigInt(args[2]); const nonce = BigInt(args[3]); const privateKey = args[4]; let memo = ''; if (args.length > 4 && !!args[5]) { memo = args[5]; } const network = (0, network_1.getStacksNetwork)(_network); const options = { recipient: recipientAddress, amount: tokenAmount, senderKey: privateKey, fee, nonce, memo, network, }; const tx = await (0, transactions_1.makeSTXTokenTransfer)(options); if (estimateOnly) { return (0, transactions_1.fetchFeeEstimateTransfer)({ transaction: tx, network }).then(cost => { return cost.toString(10); }); } if (txOnly) { return Promise.resolve(tx.serialize()); } return (0, transactions_1.broadcastTransaction)({ transaction: tx, network }) .then((response) => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${tx.txid()}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(tx.txid(), network), }; }) .catch(error => { return error.toString(); }); } async function contractDeploy(_network, args) { const sourceFile = args[0]; const contractName = args[1]; const fee = BigInt(args[2]); const nonce = BigInt(args[3]); const privateKey = args[4]; const source = fs.readFileSync(sourceFile).toString(); const network = (0, network_1.getStacksNetwork)(_network); const options = { contractName, codeBody: source, senderKey: privateKey, fee, nonce, network, postConditionMode: 'allow', }; const tx = await (0, transactions_1.makeContractDeploy)(options); if (estimateOnly) { return (0, transactions_1.fetchFeeEstimateTransaction)({ payload: (0, transactions_1.serializePayload)(tx.payload), estimatedLength: (0, transactions_1.estimateTransactionByteLength)(tx), network, }).then(costs => costs[1].fee.toString(10)); } if (txOnly) { return Promise.resolve(tx.serialize()); } return (0, transactions_1.broadcastTransaction)({ transaction: tx, network }) .then(response => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${tx.txid()}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(tx.txid(), network), }; }) .catch(error => { return error.toString(); }); } function parseDirectFunctionArgs(functionArgsStr) { return (0, transactions_2.internal_parseCommaSeparated)(functionArgsStr); } exports.parseDirectFunctionArgs = parseDirectFunctionArgs; async function getInteractiveFunctionArgs(abiArgs) { const prompts = (0, utils_1.makePromptsFromArgList)(abiArgs); const answers = await (0, inquirer_1.prompt)(prompts); return (0, utils_1.parseClarityFunctionArgAnswers)(answers, abiArgs); } async function contractFunctionCall(_network, args) { const contractAddress = args[0]; const contractName = args[1]; const functionName = args[2]; const fee = BigInt(args[3]); const nonce = BigInt(args[4]); const privateKey = args[5]; const functionArgsStr = args.length > 6 ? args[6] : undefined; const network = (0, network_1.getStacksNetwork)(_network); const abi = await (0, transactions_1.fetchAbi)({ contractAddress, contractName, network }); const filteredFn = abi.functions.filter(fn => fn.name === functionName); if (filteredFn.length !== 1) { throw new Error(`Function ${functionName} not found in contract ${contractName}`); } const abiArgs = filteredFn[0].args; const functionArgs = functionArgsStr ? parseDirectFunctionArgs(functionArgsStr) : await getInteractiveFunctionArgs(abiArgs); const payload = (0, transactions_1.createContractCallPayload)(contractAddress, contractName, functionName, functionArgs); (0, transactions_1.validateContractCall)(payload, abi); const options = { contractAddress, contractName, functionName, functionArgs, senderKey: privateKey, fee, nonce, network, postConditionMode: transactions_1.PostConditionMode.Allow, }; const tx = await (0, transactions_1.makeContractCall)(options); if (!(0, transactions_1.validateContractCall)(tx.payload, abi)) { throw new Error('Failed to validate function arguments against ABI'); } if (estimateOnly) { const costs = await (0, transactions_1.fetchFeeEstimateTransaction)({ payload: (0, transactions_1.serializePayload)(tx.payload), estimatedLength: (0, transactions_1.estimateTransactionByteLength)(tx), network, }); return costs[1].fee.toString(10); } if (txOnly) return tx.serialize(); try { const response = await (0, transactions_1.broadcastTransaction)({ transaction: tx, network }); if (response.hasOwnProperty('error')) return (0, utils_1.JSONStringify)(response); return (0, utils_1.JSONStringify)({ txid: `0x${tx.txid()}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(tx.txid(), network), }); } catch (error) { if (error instanceof Error) return error.message; return 'Unknown error occurred'; } } async function readOnlyContractFunctionCall(_network, args) { const contractAddress = args[0]; const contractName = args[1]; const functionName = args[2]; const senderAddress = args[3]; const network = (0, network_1.getStacksNetwork)(_network); let abi; let abiArgs; let functionArgs = []; return (0, transactions_1.fetchAbi)({ contractAddress, contractName, network }) .then(responseAbi => { abi = responseAbi; const filtered = abi.functions.filter(fn => fn.name === functionName); if (filtered.length === 1) { abiArgs = filtered[0].args; return (0, utils_1.makePromptsFromArgList)(abiArgs); } else { return null; } }) .then(prompts => (0, inquirer_1.prompt)(prompts)) .then(answers => { functionArgs = (0, utils_1.parseClarityFunctionArgAnswers)(answers, abiArgs); const options = { contractAddress, contractName, functionName, functionArgs, senderAddress, network, }; return (0, transactions_1.fetchCallReadOnlyFunction)(options); }) .then(returnValue => { return (0, transactions_1.cvToString)(returnValue); }) .catch(error => { return error.toString(); }); } function decodeCV(_network, args) { const inputArg = args[0]; const format = args[1]; let inputValue; if (inputArg === '-') { inputValue = fs.readFileSync(process.stdin.fd, 'utf-8').trim(); } else { inputValue = inputArg; } const cv = transactions_1.Cl.deserialize(inputValue); let cvString; if (format === 'pretty') { cvString = transactions_1.Cl.prettyPrint(cv, 2); } else if (format === 'json') { cvString = JSON.stringify((0, transactions_1.cvToJSON)(cv)); } else if (format === 'repr' || !format) { cvString = (0, transactions_1.cvToString)(cv); } else { throw new Error('Invalid format option'); } return Promise.resolve(cvString); } function getKeyAddress(_network, args) { const privateKey = (0, utils_1.decodePrivateKey)(args[0]); return Promise.resolve().then(() => { const addr = (0, common_2.getPrivateKeyAddress)(_network, privateKey); return (0, utils_1.JSONStringify)({ BTC: addr, STACKS: c32check.b58ToC32(addr), }); }); } function gaiaGetFile(_network, args) { 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) { appPrivateKey = 'fda1afa3ff9ef25579edb5833b825ac29fae82d03db3f607db048aae018fe882'; } blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return (0, data_1.gaiaAuth)(_network, appPrivateKey, null) .then((_userData) => blockstack.getFile(path, { decrypt: decrypt, verify: verify, app: origin, username: username, })) .then((data) => { if (data instanceof ArrayBuffer) { return Buffer.from(data); } else { return data; } }); } function gaiaPutFile(_network, args) { 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); blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return (0, data_1.gaiaAuth)(_network, appPrivateKey, hubUrl) .then((_userData) => { return blockstack.putFile(gaiaPath, data, { encrypt: encrypt, sign: sign }); }) .then((url) => { return (0, utils_1.JSONStringify)({ urls: [url] }); }); } function gaiaDeleteFile(_network, args) { 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'; } blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return (0, data_1.gaiaAuth)(_network, appPrivateKey, hubUrl) .then((_userData) => { return blockstack.deleteFile(gaiaPath, { wasSigned: wasSigned }); }) .then(() => { return (0, utils_1.JSONStringify)('ok'); }); } function gaiaListFiles(_network, args) { const hubUrl = args[0]; const appPrivateKey = args[1]; let count = 0; blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return (0, data_1.gaiaAuth)(_network, (0, utils_1.canonicalPrivateKey)(appPrivateKey), hubUrl) .then((_userData) => { return blockstack.listFiles((name) => { console.log(name); count += 1; return true; }); }) .then(() => (0, utils_1.JSONStringify)(count)); } function batchify(input, batchSize = 50) { 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; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function gaiaDumpBucket(_network, args) { 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] !== '/') { const cwd = fs.realpathSync('.'); dumpDir = path.normalize(`${cwd}/${dumpDir}`); } (0, utils_1.mkdirs)(dumpDir); function downloadFile(hubConfig, fileName) { 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 (0, node_fetch_1.default)(fileUrl) .then((resp) => { if (resp.status !== 200) { throw new Error(`Bad status code for ${fileUrl}: ${resp.status}`); } 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) => { return new Promise((resolve, reject) => { try { fs.writeFileSync(destPath, Buffer.from(filebytes), { encoding: null, mode: 0o660 }); resolve(); } catch (e) { reject(e); } }); }); } blockstack.config.network.layer1 = bitcoin.networks.bitcoin; const fileNames = []; let gaiaHubConfig; let appPrivateKey; let ownerPrivateKey; return (0, utils_1.getIDAppKeys)(_network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) .then((keyInfo) => { appPrivateKey = keyInfo.appPrivateKey; ownerPrivateKey = keyInfo.ownerPrivateKey; return (0, data_1.gaiaAuth)(_network, appPrivateKey, hubUrl, ownerPrivateKey); }) .then((_userData) => { return (0, data_1.gaiaConnect)(_network, hubUrl, appPrivateKey); }) .then((hubConfig) => { gaiaHubConfig = hubConfig; return blockstack.listFiles(name => { fileNames.push(name); return true; }); }) .then(async (fileCount) => { const batchSize = 99; const sleepTime = 120; console.log(`Download ${fileCount} files...`); if (fileCount > batchSize) { console.log(`This may take a while, downloading around ${batchSize} files per 2 minutes...`); } const fileBatches = batchify(fileNames, batchSize); for (const [index, batch] of fileBatches.entries()) { const filePromises = batch.map(fileName => downloadFile(gaiaHubConfig, fileName)); await Promise.all(filePromises); if (index < fileBatches.length - 1) { console.log(`${(index + 1) * batchSize}/${fileCount} downloaded, waiting ${sleepTime} seconds before next batch...`); await sleep(sleepTime * 1000); } } return (0, utils_1.JSONStringify)(fileCount); }); } function gaiaRestoreBucket(_network, args) { 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] !== '/') { const cwd = fs.realpathSync('.'); dumpDir = path.normalize(`${cwd}/${dumpDir}`); } const fileList = fs.readdirSync(dumpDir); const fileBatches = batchify(fileList, 10); let appPrivateKey; let ownerPrivateKey; blockstack.config.network.layer1 = bitcoin.networks.bitcoin; return (0, utils_1.getIDAppKeys)(_network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) .then((keyInfo) => { appPrivateKey = keyInfo.appPrivateKey; ownerPrivateKey = keyInfo.ownerPrivateKey; return (0, data_1.gaiaAuth)(_network, appPrivateKey, hubUrl, ownerPrivateKey); }) .then(async (_userData) => { const batchSize = 99; const sleepTime = 120; for (const [index, batch] of fileBatches.entries()) { const uploadBatchPromises = batch.map(async (fileName) => { const filePath = path.join(dumpDir, fileName); const dataBuf = fs.readFileSync(filePath); const gaiaPath = fileName.replace(/\\x2f/g, '/'); const url = await blockstack.putFile(gaiaPath, dataBuf, { encrypt: false, sign: false }); console.log(`Uploaded ${fileName} to ${url}`); }); await Promise.all(uploadBatchPromises); if (index < fileBatches.length - 1) { console.log(`${(index + 1) * batchSize}/${fileList.length} uploaded, waiting ${sleepTime} seconds before next batch...`); await sleep(sleepTime * 1000); } } return (0, utils_1.JSONStringify)(fileList.length); }); } async function gaiaSetHub(_network, args) { _network.setCoerceMainnetAddress(true); const blockstackID = args[0]; const ownerHubUrl = args[1]; const appOrigin = args[2]; const hubUrl = args[3]; const mnemonicPromise = (0, utils_1.getBackupPhrase)(args[4]); const nameInfoPromise = (0, utils_1.getNameInfoEasy)(_network, blockstackID).then((nameInfo) => { if (!nameInfo) { throw new Error('Name not found'); } return nameInfo; }); const profilePromise = blockstack.lookupProfile(blockstackID); const [nameInfo, nameProfile, mnemonic] = 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 = {}; } const ownerAddress = _network.coerceMainnetAddress(nameInfo.address); const idAddress = `ID-${ownerAddress}`; const appKeyInfo = await (0, keys_1.getApplicationKeyInfo)(_network, mnemonic, idAddress, appOrigin); const ownerKeyInfo = await (0, keys_1.getOwnerKeyInfo)(_network, mnemonic, appKeyInfo.ownerKeyIndex); let existingAppAddress = null; let appPrivateKey; try { existingAppAddress = (0, data_1.getGaiaAddressFromProfile)(_network, nameProfile, appOrigin); appPrivateKey = (0, keys_1.extractAppKey)(_network, appKeyInfo, existingAppAddress); } catch (e) { console.log(`No profile application entry for ${appOrigin}`); appPrivateKey = (0, keys_1.extractAppKey)(_network, appKeyInfo); } appPrivateKey = `${(0, utils_1.canonicalPrivateKey)(appPrivateKey)}01`; const appAddress = _network.coerceMainnetAddress((0, common_2.getPrivateKeyAddress)(_network, appPrivateKey)); if (existingAppAddress && appAddress !== existingAppAddress) { throw new Error(`BUG: ${existingAppAddress} !== ${appAddress}`); } const profile = nameProfile; const ownerPrivateKey = ownerKeyInfo.privateKey; const ownerGaiaHubPromise = (0, data_1.gaiaConnect)(_network, ownerHubUrl, ownerPrivateKey); const appGaiaHubPromise = (0, data_1.gaiaConnect)(_network, hubUrl, appPrivateKey); const [ownerHubConfig, appHubConfig] = 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 = {}; newAppEntry[appOrigin] = `${gaiaReadUrl}/${appAddress}/`; const apps = Object.assign({}, profile.apps ? profile.apps : {}, newAppEntry); profile.apps = apps; const signedProfile = (0, utils_1.makeProfileJWT)(profile, ownerPrivateKey); const profileUrls = await (0, data_1.gaiaUploadProfileAll)(_network, [ownerHubUrl], signedProfile, ownerPrivateKey, blockstackID); if (profileUrls.error) { return (0, utils_1.JSONStringify)({ error: profileUrls.error, }); } else { return (0, utils_1.JSONStringify)({ profileUrls: profileUrls.dataUrls, }); } } function addressConvert(_network, args) { const addr = args[0]; let b58addr; let testnetb58addr; if (addr.match(argparse_1.STACKS_ADDRESS_PATTERN)) { b58addr = c32check.c32ToB58(addr); } else if (addr.match(/[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+/)) { b58addr = addr; } else { throw new Error(`Unrecognized address ${addr}`); } if ((0, utils_1.isTestnetAddress)(b58addr)) { testnetb58addr = b58addr; } else if (_network.isTestnet()) { testnetb58addr = _network.coerceAddress(b58addr); } return Promise.resolve().then(() => { const mainnetb58addr = _network.coerceMainnetAddress(b58addr); const result = { mainnet: { STACKS: c32check.b58ToC32(mainnetb58addr), BTC: mainnetb58addr, }, testnet: undefined, }; if (testnetb58addr) { result.testnet = { STACKS: c32check.b58ToC32(testnetb58addr), BTC: testnetb58addr, }; } return (0, utils_1.JSONStringify)(result); }); } function encryptMnemonic(_network, args) { const mnemonic = args[0]; if (mnemonic.split(/ +/g).length !== 12) { throw new Error('Invalid backup phrase: must be 12 words'); } const passwordPromise = new Promise((resolve, reject) => { let pass = ''; if (args.length === 2 && !!args[1]) { pass = args[1]; resolve(pass); } else { if (!process.stdin.isTTY) { const errMsg = 'Password argument required on non-interactive mode'; reject(new Error(errMsg)); } else { (0, utils_1.getpass)('Enter password: ', (pass1) => { (0, utils_1.getpass)('Enter password again: ', (pass2) => { if (pass1 !== pass2) { const errMsg = 'Passwords do not match'; reject(new Error(errMsg)); } else { resolve(pass1); } }); }); } } }); return passwordPromise .then((pass) => (0, encrypt_1.encryptBackupPhrase)(mnemonic, pass)) .then((cipherTextBuffer) => cipherTextBuffer.toString('base64')) .catch((e) => { return (0, utils_1.JSONStringify)({ error: e.message }); }); } function decryptMnemonic(_network, args) { const ciphertext = args[0]; const passwordPromise = new Promise((resolve, reject) => { if (args.length === 2 && !!args[1]) { const pass = args[1]; resolve(pass); } else { if (!process.stdin.isTTY) { reject(new Error('Password argument required in non-interactive mode')); } else { (0, utils_1.getpass)('Enter password: ', p => { resolve(p); }); } } }); return passwordPromise .then((pass) => (0, encrypt_1.decryptBackupPhrase)(Buffer.from(ciphertext, 'base64'), pass)) .catch((e) => { return (0, utils_1.JSONStringify)({ error: 'Failed to decrypt (wrong password or corrupt ciphertext), ' + `details: ${e.message}`, }); }); } async function stackingStatus(_network, args) { const address = args[0]; const network = (0, network_1.getStacksNetwork)(_network); const stacker = new stacking_1.StackingClient({ address, network }); return stacker .getStatus() .then((status) => { if (status.stacked) { return { first_reward_cycle: status.details.first_reward_cycle, lock_period: status.details.lock_period, unlock_height: status.details.unlock_height, pox_address: { version: (0, common_1.bytesToHex)(status.details.pox_address.version), hashbytes: (0, common_1.bytesToHex)(status.details.pox_address.hashbytes), }, }; } else { return 'Account not actively participating in Stacking'; } }) .catch((error) => { return error.toString(); }); } async function canStack(_network, args) { const amount = BigInt(args[0]); const cycles = Number(args[1]); const poxAddress = args[2]; const stxAddress = args[3]; const network = (0, network_1.getStacksNetwork)(_network); const stacker = new stacking_1.StackingClient({ address: stxAddress, network }); const apiConfig = new blockchain_api_client_1.Configuration({ basePath: network.client.baseUrl, }); const accounts = new blockchain_api_client_1.AccountsApi(apiConfig); const balancePromise = accounts.getAccountBalance({ principal: stxAddress, }); const poxInfoPromise = stacker.getPoxInfo(); const stackingEligiblePromise = stacker.canStack({ poxAddress, cycles }); return Promise.all([balancePromise, poxInfoPromise, stackingEligiblePromise]) .then(([balance, poxInfo, stackingEligible]) => { const minAmount = BigInt(poxInfo.min_amount_ustx); const balanceBN = BigInt(balance.stx.balance); if (minAmount > amount) { throw new Error(`Stacking amount less than required minimum of ${minAmount.toString()} microstacks`); } if (amount > 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, args) { const amount = BigInt(args[0]); const cycles = Number(args[1]); const poxAddress = args[2]; const privateKey = args[3]; const network = (0, network_1.getStacksNetwork)(_network); const apiConfig = new blockchain_api_client_1.Configuration({ basePath: network.client.baseUrl, }); const accounts = new blockchain_api_client_1.AccountsApi(apiConfig); const stxAddress = (0, transactions_1.getAddressFromPrivateKey)(privateKey, network); const balancePromise = accounts.getAccountBalance({ principal: stxAddress, }); const stacker = new stacking_1.StackingClient({ address: stxAddress, network }); 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 = BigInt(poxInfo.min_amount_ustx); const balanceBN = BigInt(balance.stx.balance); const burnChainBlockHeight = coreInfo.burn_block_height; const startBurnBlock = burnChainBlockHeight + 3; if (minAmount > amount) { throw new Error(`Stacking amount less than required minimum of ${minAmount.toString()} microstacks`); } if (amount > 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) => { if ('error' in response) { return response; } return { txid: `0x${response.txid}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(response.txid, network), }; }) .catch(error => { return error; }); } async function register(_network, args) { const fullyQualifiedName = args[0]; const privateKey = args[1]; const salt = args[2]; const zonefile = args[3]; const publicKey = (0, transactions_1.privateKeyToPublic)(privateKey); const network = (0, network_1.getStacksNetwork)(_network); const unsignedTransaction = await (0, bns_1.buildRegisterNameTx)({ fullyQualifiedName, publicKey, salt, zonefile, network, }); const signer = new transactions_1.TransactionSigner(unsignedTransaction); signer.signOrigin(privateKey); return (0, transactions_1.broadcastTransaction)({ transaction: signer.transaction, network }) .then((response) => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${response.txid}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(response.txid, network), }; }) .catch(error => { return error; }); } async function preorder(_network, args) { const fullyQualifiedName = args[0]; const privateKey = args[1]; const salt = args[2]; const stxToBurn = args[3]; const publicKey = (0, transactions_1.privateKeyToPublic)(privateKey); const network = (0, network_1.getStacksNetwork)(_network); const unsignedTransaction = await (0, bns_1.buildPreorderNameTx)({ fullyQualifiedName, publicKey, salt, stxToBurn, network, }); const signer = new transactions_1.TransactionSigner(unsignedTransaction); signer.signOrigin(privateKey); return (0, transactions_1.broadcastTransaction)({ transaction: signer.transaction, network }) .then((response) => { if (response.hasOwnProperty('error')) { return response; } return { txid: `0x${response.txid}`, transaction: (0, utils_1.generateExplorerTxPageUrl)(response.txid, network), }; }) .catch(error => { return error; }); } function faucetCall(_network, args) { const address = args[0]; const network = (0, network_1.getStacksNetwork)(_network); const config = new blockchain_api_client_1.Configuration({ basePath: network.client.baseUrl }); const faucets = new blockchain_api_client_1.FaucetsApi(config); return faucets .runFaucetStx({ address }) .then((faucetTx) => { return (0, utils_1.JSONStringify)({ txid: faucetTx.txId, transaction: (0, utils_1.generateExplorerTxPageUrl)(faucetTx.txId.replace(/^0x/, ''), network_2.STACKS_TESTNET), }); }) .catch((error) => error.toString()); } function printDocs(_network, _args) { return Promise.resolve().then(() => { const formattedDocs = []; const commandNames = Object.keys(argparse_1.CLI_ARGS.properties); for (let i = 0; i < commandNames.length; i++) { const commandName = commandNames[i]; const args = []; const usage = argparse_1.CLI_ARGS.properties[commandName].help; const group = argparse_1.CLI_ARGS.properties[commandName].group; for (let j = 0; j < argparse_1.CLI_ARGS.properties[commandName].items.length; j++) { const argItem = argparse_1.CLI_ARGS.properties[commandName].items[j]; args.push({ name: argItem.name, type: argItem.type, value: argItem.realtype, format: argItem.pattern ? argItem.pattern : '.+', }); } formattedDocs.push({ command: commandName, args: args, usage: usage, group: group, }); } return (0, utils_1.JSONStringify)(formattedDocs); }); } const COMMANDS = { balance: balance, can_stack: canStack, call_contract_func: contractFunctionCall, call_read_only_contract_func: readOnlyContractFunctionCall, decode_cv: decodeCV, 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, register: register, tx_preorder: preorder, send_tokens: sendTokens, stack: stack, migrate_subdomains: migrateSubdomains, stacking_status: stackingStatus, faucet: faucetCall, }; function CLIMain() { const argv = process.argv; const opts = (0, argparse_1.getCLIOpts)(argv); const cmdArgs = (0, argparse_1.checkArgs)((0, argparse_1.CLIOptAsStringArray)(opts, '_') ? (0, argparse_1.CLIOptAsStringArray)(opts, '_') : []); if (!cmdArgs.success) { if (cmdArgs.error) { console.log(cmdArgs.error); } if (cmdArgs.usage) { if (cmdArgs.command) { console.log((0, argparse_1.makeCommandUsageString)(cmdArgs.command)); console.log('Use "help" to list all commands.'); }