mirror of
https://github.com/alexgo-io/stacks.js.git
synced 2026-01-12 22:52:34 +08:00
1956 lines
59 KiB
TypeScript
1956 lines
59 KiB
TypeScript
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<NameInfoType | null>;
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<any[]> {
|
|
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<string> {
|
|
// 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<string> {
|
|
// 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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
// 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<string> {
|
|
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<string | Buffer> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<T>(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<string> {
|
|
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<void> {
|
|
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<any> = 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<string> {
|
|
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<any> = 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<string> {
|
|
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<string, string> = {};
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
const mnemonic = args[0];
|
|
if (mnemonic.split(/ +/g).length !== 12) {
|
|
throw new Error('Invalid backup phrase: must be 12 words');
|
|
}
|
|
|
|
const passwordPromise: Promise<string> = 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<string> {
|
|
const ciphertext = args[0];
|
|
|
|
const passwordPromise: Promise<string> = 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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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<string | Buffer>;
|
|
|
|
/*
|
|
* Decrypt a backup phrase
|
|
* args:
|
|
* @p
|
|
/*
|
|
* Global set of commands
|
|
*/
|
|
const COMMANDS: Record<string, CommandFunction> = {
|
|
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;
|