@stacks/cli
Version:
Stacks command line tool
1,274 lines (1,273 loc) • 56.4 kB
JavaScript
"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.');
}