diff --git a/.github/MIGRATION.md b/.github/MIGRATION.md index a56bfc82e..83f53ac85 100644 --- a/.github/MIGRATION.md +++ b/.github/MIGRATION.md @@ -8,6 +8,7 @@ - [Clarity Representation](#clarity-representation) - [`serialize` methods](#serialize-methods) - [Asset Helper Methods](#asset-helper-methods) + - [CLI](#cli) - [Stacks.js (\<=4.x.x) → (5.x.x)](#stacksjs-4xx--5xx) - [Breaking Changes](#breaking-changes-1) - [Buffer to Uint8Array](#buffer-to-uint8array) @@ -30,6 +31,7 @@ - The `ClarityType` enum was replaced by a readable version. The previous (wire format compatible) enum is still available as `ClarityWireType`. [Read more...](#clarity-representation) - The `serializeXyz` methods were changed to return `string` (hex-encoded) instead of `Uint8Array`. Compatible `serializeXzyBytes` methods were added to ease the migration. [Read more...](#serialize-methods) - The `AssetInfo` type was renamed to `Asset` for accuracy. The `Asset` helper methods were also renamed to to remove the `Info` suffix. [Read more...](#asset-helper-methods) +- Remove legacy CLI methods. [Read more...](#cli) ### Stacks Network @@ -184,6 +186,10 @@ The following interfaces and methods were renamed: - `createAssetInfo` → `createAsset` - `parseAssetInfoString` → `parseAssetString` +### CLI + +- Removed the `authenticator` method for legacy Blockstack authentication. + ## Stacks.js (<=4.x.x) → (5.x.x) ### Breaking Changes diff --git a/packages/cli/src/auth.ts b/packages/cli/src/auth.ts index 6fca0dc95..814afccd0 100644 --- a/packages/cli/src/auth.ts +++ b/packages/cli/src/auth.ts @@ -1,58 +1,37 @@ -import * as blockstack from 'blockstack'; -import * as express from 'express'; -import * as crypto from 'crypto'; -import * as jsontokens from 'jsontokens'; -import * as logger from 'winston'; - -import { - gaiaConnect, - gaiaUploadProfileAll, - makeAssociationToken, - getGaiaAddressFromProfile, -} from './data'; - -import { getApplicationKeyInfo, getOwnerKeyInfo, extractAppKey } from './keys'; - -import { nameLookup, makeProfileJWT } from './utils'; - -import { CLINetworkAdapter } from './network'; - -import { GaiaHubConfig } from '@stacks/storage'; - export const SIGNIN_CSS = ` -h1 { - font-family: monospace; - font-size: 24px; - font-style: normal; - font-variant: normal; - font-weight: 700; - line-height: 26.4px; -} -h3 { - font-family: monospace; - font-size: 14px; - font-style: normal; - font-variant: normal; - font-weight: 700; - line-height: 15.4px; -} -p { - font-family: monospace; - font-size: 14px; - font-style: normal; - font-variant: normal; - font-weight: 400; - line-height: 20px; +h1 { + font-family: monospace; + font-size: 24px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 26.4px; +} +h3 { + font-family: monospace; + font-size: 14px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 15.4px; +} +p { + font-family: monospace; + font-size: 14px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 20px; } b { background-color: #e8e8e8; } -pre { - font-family: monospace; - font-size: 13px; - font-style: normal; - font-variant: normal; - font-weight: 400; +pre { + font-family: monospace; + font-size: 13px; + font-style: normal; + font-variant: normal; + font-weight: 400; line-height: 18.5714px; }`; @@ -73,647 +52,20 @@ export interface NamedIdentityType { profileUrl: string; } -interface AuthRequestType { - jti: string; - iat: number; - exp: number; - iss: null | string; - public_keys: string[]; - domain_name: string; - manifest_uri: string; - redirect_uri: string; - version: string; - do_not_include_profile: boolean; - supports_hub_url: boolean; - scopes: string[]; -} - -// new ecdsa private key each time -const authTransitNonce = crypto.randomBytes(32).toString('hex'); - /* * Get the app private key */ -async function getAppPrivateKey( - network: CLINetworkAdapter, - mnemonic: string, - id: NamedIdentityType, - appOrigin: string -): Promise { - const appKeyInfo = await getApplicationKeyInfo( - network, - mnemonic, - id.idAddress, - appOrigin, - id.index - ); - let appPrivateKey; - try { - const existingAppAddress = getGaiaAddressFromProfile(network, id.profile, appOrigin); - appPrivateKey = extractAppKey(network, appKeyInfo, existingAppAddress); - } catch (e) { - appPrivateKey = extractAppKey(network, appKeyInfo); - } - - return appPrivateKey; -} - -/* - * Make a sign-in link - */ -async function makeSignInLink( - network: CLINetworkAdapter, - authPort: number, - mnemonic: string, - authRequest: AuthRequestType, - hubUrl: string, - id: NamedIdentityType -): Promise { - const appOrigin = authRequest.domain_name; - const appPrivateKey = await getAppPrivateKey(network, mnemonic, id, appOrigin); - - const associationToken = makeAssociationToken(appPrivateKey, id.privateKey); - const authResponseTmp = blockstack.makeAuthResponse( - id.privateKey, - {}, - id.name, - { email: undefined, profileUrl: id.profileUrl }, - undefined, - appPrivateKey, - undefined, - authRequest.public_keys[0], - hubUrl, - blockstack.config.network.blockstackAPIUrl, - associationToken - ); - - // pass along some helpful data from the authRequest - const authResponsePayload = jsontokens.decodeToken(authResponseTmp).payload; - const id_public = Object.assign({}, id); - id_public.profile = {}; - // @ts-ignore - id_public.privateKey = undefined; - - (authResponsePayload as any).metadata = { - id: id_public, - profileUrl: id.profileUrl, - appOrigin: appOrigin, - redirect_uri: authRequest.redirect_uri, - scopes: authRequest.scopes, - salt: crypto.randomBytes(16).toString('hex'), - nonce: authTransitNonce, - // fill in more CLI-specific fields here - }; - - const tokenSigner = new jsontokens.TokenSigner('ES256k', id.privateKey); - const authResponse = tokenSigner.sign(authResponsePayload); - - return blockstack.updateQueryStringParameter( - `http://localhost:${authPort}/signin`, - 'authResponse', - authResponse - ); -} - -/* - * Make the sign-in page - */ -async function makeAuthPage( - network: CLINetworkAdapter, - authPort: number, - mnemonic: string, - hubUrl: string, - manifest: any, - authRequest: AuthRequestType, - ids: NamedIdentityType[] -): Promise { - let signinBody = SIGNIN_HEADER; - const signinDescription = SIGNIN_DESC.replace(/{appName}/, manifest.name || '(Unknown app)'); - - const signinScopes = SIGNIN_SCOPES.replace( - /{appScopes}/, - authRequest.scopes.length > 0 ? authRequest.scopes.join(', ') : '(none)' - ); - - signinBody = `${signinBody}${signinDescription}${signinScopes}`; - - for (let i = 0; i < ids.length; i++) { - let signinEntry; - if (ids[i].name) { - signinEntry = SIGNIN_FMT_NAME.replace( - /{authRedirect}/, - await makeSignInLink(network, authPort, mnemonic, authRequest, hubUrl, ids[i]) - ) - .replace(/{blockstackID}/, ids[i].name) - .replace(/{idAddress}/, ids[i].idAddress); - } else { - signinEntry = SIGNIN_FMT_ID.replace( - /{authRedirect}/, - await makeSignInLink(network, authPort, mnemonic, authRequest, hubUrl, ids[i]) - ).replace(/{idAddress}/, ids[i].idAddress); - } - - signinBody = `${signinBody}${signinEntry}`; - } - - signinBody = `${signinBody}${SIGNIN_FOOTER}`; - return signinBody; -} - -/* - * Find all identity addresses that have names attached to them. - * Fills in identities. - */ -async function loadNamedIdentitiesLoop( - network: CLINetworkAdapter, - mnemonic: string, - index: number, - identities: NamedIdentityType[] -): Promise { - // 65536 is a ridiculously huge number - if (index > 65536) { - throw new Error('Too many names'); - } - - const keyInfo = await getOwnerKeyInfo(network, mnemonic, index); - const nameList = await network.getNamesOwned(keyInfo.idAddress.slice(3)); - if (nameList.length === 0) { - // out of names - return identities; - } - for (let i = 0; i < nameList.length; i++) { - const identity: NamedIdentityType = { - name: nameList[i], - idAddress: keyInfo.idAddress, - privateKey: keyInfo.privateKey, - index: index, - profile: {}, - profileUrl: '', - }; - identities.push(identity); - } - return await loadNamedIdentitiesLoop(network, mnemonic, index + 1, identities); -} - -/* - * Load all named identities for a mnemonic. - * Keep loading until we find an ID-address that does not have a name. - */ -export function loadNamedIdentities( - network: CLINetworkAdapter, - mnemonic: string -): Promise { - return loadNamedIdentitiesLoop(network, mnemonic, 0, []); -} - -/* - * Generate identity info for an unnamed ID - */ -async function loadUnnamedIdentity( - network: CLINetworkAdapter, - mnemonic: string, - index: number -): Promise { - const keyInfo = await getOwnerKeyInfo(network, mnemonic, index); - const idInfo = { - name: '', - idAddress: keyInfo.idAddress, - privateKey: keyInfo.privateKey, - index: index, - profile: {}, - profileUrl: '', - }; - return idInfo; -} /* * Send a JSON HTTP response */ -// eslint-disable-next-line @typescript-eslint/ban-types -function sendJSON(res: express.Response, data: Object, statusCode: number) { - logger.info(`Respond ${statusCode}: ${JSON.stringify(data)}`); - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify(data)); - res.end(); -} - -/* - * Get all of a 12-word phrase's identities, profiles, and Gaia connections. - * Returns a Promise to an Array of NamedIdentityType instances. - * - * NOTE: should be the *only* promise chain running! - */ -async function getIdentityInfo( - network: CLINetworkAdapter, - mnemonic: string, - _appGaiaHub: string, - _profileGaiaHub: string -): Promise { - network.setCoerceMainnetAddress(false); - let identities: NamedIdentityType[]; - - try { - // load up all of our identity addresses and profile URLs - identities = await loadNamedIdentities(network, mnemonic); - const nameInfoPromises = identities.map(id => { - const lookup: Promise<{ - profile: any; - profileUrl?: string; - zonefile?: string; - } | null> = nameLookup(network, id.name, true).catch(() => null); - return lookup; - }); - - let nameDatas = await Promise.all(nameInfoPromises); - - network.setCoerceMainnetAddress(false); - nameDatas = nameDatas.filter(p => p !== null && p !== undefined); - - for (let i = 0; i < nameDatas.length; i++) { - if (nameDatas[i]!.hasOwnProperty('error') && (nameDatas[i] as any).error) { - // no data for this name - identities[i].profileUrl = ''; - } else { - identities[i].profileUrl = nameDatas[i]!.profileUrl!; - identities[i].profile = nameDatas[i]!.profile; - } - } - - const nextIndex = identities.length + 1; - - // ignore identities with no data - identities = identities.filter(id => !!id.profileUrl); - - // add in the next non-named identity - identities.push(await loadUnnamedIdentity(network, mnemonic, nextIndex)); - } catch (e) { - network.setCoerceMainnetAddress(false); - throw e; - } - - return identities; -} - -/* - * Handle GET /auth?authRequest=... - * If the authRequest is verifiable and well-formed, and if we can fetch the application - * manifest, then we can render an auth page to the user. - * Serves back the sign-in page on success. - * Serves back an error page on error. - * Returns a Promise that resolves to nothing. - * - * NOTE: should be the *only* promise chain running! - */ -export async function handleAuth( - network: CLINetworkAdapter, - mnemonic: string, - gaiaHubUrl: string, - profileGaiaHub: string, - port: number, - req: express.Request, - res: express.Response -): Promise { - const authToken = req.query.authRequest as string; - if (!authToken) { - return Promise.resolve().then(() => { - sendJSON(res, { error: 'No authRequest given' }, 400); - }); - } - - let errorMsg = ''; - let identities: NamedIdentityType[] = []; - - try { - identities = await getIdentityInfo(network, mnemonic, gaiaHubUrl, profileGaiaHub); - - errorMsg = 'Unable to verify authentication token'; - const valid = await blockstack.verifyAuthRequest(authToken); - - if (!valid) { - errorMsg = 'Invalid authentication token: could not verify'; - throw new Error(errorMsg); - } - errorMsg = 'Unable to fetch app manifest'; - const appManifest = await blockstack.fetchAppManifest(authToken); - - errorMsg = 'Unable to decode token'; - const decodedAuthToken = jsontokens.decodeToken(authToken); - const decodedAuthPayload = decodedAuthToken.payload; - if (!decodedAuthPayload) { - errorMsg = 'Invalid authentication token: no payload'; - throw new Error(errorMsg); - } - - errorMsg = 'Unable to make auth page'; - - // make sign-in page - const authPage = await makeAuthPage( - network, - port, - mnemonic, - gaiaHubUrl, - appManifest, - decodedAuthPayload as unknown as AuthRequestType, - identities - ); - - res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': authPage.length }); - res.write(authPage); - res.end(); - } catch (e: any) { - if (!errorMsg) { - errorMsg = e.message; - } - - console.log(e.stack); - logger.error(errorMsg); - sendJSON(res, { error: `Unable to authenticate app request: ${errorMsg}` }, 400); - } -} /* * Update a named identity's profile with new app data, if necessary. * Indicates whether or not the profile was changed. */ -function updateProfileApps( - network: CLINetworkAdapter, - id: NamedIdentityType, - appOrigin: string, - appGaiaConfig: GaiaHubConfig, - profile?: any -): Promise<{ profile: any; changed: boolean }> { - let needProfileUpdate = false; - - // go get the profile from the profile URL in the id - const profilePromise = Promise.resolve().then(() => { - if (profile === null || profile === undefined) { - return nameLookup(network, id.name).catch(_e => null); - } else { - return { profile: profile }; - } - }); - - return profilePromise.then(profileData => { - if (profileData) { - profile = profileData.profile; - } - - if (!profile) { - // instantiate - logger.debug(`Profile for ${id.name} is ${JSON.stringify(profile)}`); - logger.debug(`Instantiating profile for ${id.name}`); - needProfileUpdate = true; - profile = { - type: '@Person', - account: [], - apps: {}, - }; - } - - // do we need to update the Gaia hub read URL in the profile? - if (profile.apps === null || profile.apps === undefined) { - needProfileUpdate = true; - - logger.debug(`Adding multi-reader Gaia links to profile for ${id.name}`); - profile.apps = {}; - } - - const gaiaPrefix = `${appGaiaConfig.url_prefix}${appGaiaConfig.address}/`; - - if (!profile.apps.hasOwnProperty(appOrigin) || !profile.apps[appOrigin]) { - needProfileUpdate = true; - logger.debug( - `Setting Gaia read URL ${gaiaPrefix} for ${appOrigin} ` + `in profile for ${id.name}` - ); - - profile.apps[appOrigin] = gaiaPrefix; - } else if (!profile.apps[appOrigin].startsWith(gaiaPrefix)) { - needProfileUpdate = true; - logger.debug( - `Overriding Gaia read URL for ${appOrigin} from ${profile.apps[appOrigin]} ` + - `to ${gaiaPrefix} in profile for ${id.name}` - ); - - profile.apps[appOrigin] = gaiaPrefix; - } - - return { profile, changed: needProfileUpdate }; - }); -} /* * Updates a named identitie's profile's API settings, if necessary. * Indicates whether or not the profile data changed. */ -function updateProfileAPISettings( - network: CLINetworkAdapter, - id: NamedIdentityType, - appGaiaConfig: GaiaHubConfig, - profile?: any -): Promise<{ profile: any; changed: boolean }> { - let needProfileUpdate = false; - - // go get the profile from the profile URL in the id - const profilePromise = Promise.resolve().then(() => { - if (profile === null || profile === undefined) { - return nameLookup(network, id.name).catch(_e => null); - } else { - return { profile: profile }; - } - }); - - return profilePromise.then(profileData => { - if (profileData) { - profile = profileData.profile; - } - - if (!profile) { - // instantiate - logger.debug(`Profile for ${id.name} is ${JSON.stringify(profile)}`); - logger.debug(`Instantiating profile for ${id.name}`); - needProfileUpdate = true; - profile = { - type: '@Person', - account: [], - api: {}, - }; - } - - // do we need to update the API settings in the profile? - if (profile.api === null || profile.api === undefined) { - needProfileUpdate = true; - - logger.debug(`Adding API settings to profile for ${id.name}`); - profile.api = { - gaiaHubConfig: { - url_prefix: appGaiaConfig.url_prefix, - }, - gaiaHubUrl: appGaiaConfig.server, - }; - } - - if ( - !profile.hasOwnProperty('api') || - !profile.api.hasOwnProperty('gaiaHubConfig') || - !profile.api.gaiaHubConfig.hasOwnProperty('url_prefix') || - !profile.api.gaiaHubConfig.url_prefix || - !profile.api.hasOwnProperty('gaiaHubUrl') || - !profile.api.gaiaHubUrl - ) { - logger.debug(`Existing profile for ${id.name} is ${JSON.stringify(profile)}`); - logger.debug(`Updating API settings to profile for ${id.name}`); - profile.api = { - gaiaHubConfig: { - url_prefix: appGaiaConfig.url_prefix, - }, - gaiaHubUrl: appGaiaConfig.server, - }; - } - - return { profile, changed: needProfileUpdate }; - }); -} - -/* - * Handle GET /signin?encAuthResponse=... - * Takes an encrypted authResponse from the page generated on GET /auth?authRequest=...., - * verifies it, updates the name's profile's app's entry with the latest Gaia - * hub information (if necessary), and redirects the user back to the application. - * - * If adminKey is given, then the new app private key will be automatically added - * as an authorized writer to the Gaia hub. - * - * Redirects the user on success. - * Sends the user an error page on failure. - * Returns a Promise that resolves to nothing. - */ -export async function handleSignIn( - network: CLINetworkAdapter, - mnemonic: string, - appGaiaHub: string, - profileGaiaHub: string, - req: express.Request, - res: express.Response -): Promise { - const authResponseQP = req.query.authResponse as string; - if (!authResponseQP) { - return Promise.resolve().then(() => { - sendJSON(res, { error: 'No authResponse given' }, 400); - }); - } - const nameLookupUrl = `${network.legacyNetwork.blockstackAPIUrl}/v1/names/`; - - let errorMsg = ''; - let errorStatusCode = 400; - let authResponsePayload: any; - - let id: NamedIdentityType; - let profileUrl: string; - let appOrigin: string; - let redirectUri: string; - let scopes: string[]; - let authResponse: string; - let hubConfig: GaiaHubConfig; - let needProfileAPIUpdate = false; - let profileAPIUpdate: boolean; - - try { - const valid = await blockstack.verifyAuthResponse(authResponseQP, nameLookupUrl); - if (!valid) { - errorMsg = `Unable to verify authResponse token ${authResponseQP}`; - throw new Error(errorMsg); - } - - const authResponseToken = jsontokens.decodeToken(authResponseQP); - authResponsePayload = authResponseToken.payload; - - id = authResponsePayload.metadata.id; - profileUrl = authResponsePayload.metadata.profileUrl; - appOrigin = authResponsePayload.metadata.appOrigin; - redirectUri = authResponsePayload.metadata.redirect_uri; - scopes = authResponsePayload.metadata.scopes; - const nonce = authResponsePayload.metadata.nonce; - - if (nonce != authTransitNonce) { - throw new Error('Invalid auth response: not generated by this authenticator'); - } - - // restore - id.privateKey = (await getOwnerKeyInfo(network, mnemonic, id.index)).privateKey; - - const appPrivateKey = await getAppPrivateKey(network, mnemonic, id, appOrigin); - - // remove sensitive (CLI-specific) information - authResponsePayload.metadata = { - profileUrl: profileUrl, - }; - - authResponse = new jsontokens.TokenSigner('ES256K', id.privateKey).sign(authResponsePayload); - - logger.debug(`App ${appOrigin} requests scopes ${JSON.stringify(scopes)}`); - - // connect to the app gaia hub - const appHubConfig = await gaiaConnect(network, appGaiaHub, appPrivateKey); - - hubConfig = appHubConfig; - let newProfileData = await updateProfileAPISettings(network, id, hubConfig); - - needProfileAPIUpdate = newProfileData.changed; - profileAPIUpdate = newProfileData.profile; - newProfileData = await updateProfileApps(network, id, appOrigin, hubConfig, profileAPIUpdate); - - const profile = newProfileData.profile; - const needProfileSigninUpdate = newProfileData.changed && scopes.includes('store_write'); - - logger.debug(`Resulting profile for ${id.name} is ${JSON.stringify(profile)}`); - - let gaiaUrls: any; - - // sign and replicate new profile if we need to. - // otherwise do nothing - if (needProfileSigninUpdate) { - logger.debug(`Upload new profile with new sign-in data to ${profileGaiaHub}`); - const profileJWT = makeProfileJWT(profile, id.privateKey); - gaiaUrls = await gaiaUploadProfileAll( - network, - [profileGaiaHub], - profileJWT, - id.privateKey, - id.name - ); - } else if (needProfileAPIUpdate) { - // API settings changed, but we won't be adding an app entry - logger.debug(`Upload new profile with new API settings to ${profileGaiaHub}`); - const profileJWT = makeProfileJWT(profileAPIUpdate, id.privateKey); - gaiaUrls = await gaiaUploadProfileAll( - network, - [profileGaiaHub], - profileJWT, - id.privateKey, - id.name - ); - } else { - logger.debug(`Gaia read URL for ${appOrigin} is ${profile.apps[appOrigin]}`); - gaiaUrls = { dataUrls: [], error: null }; - } - - if (gaiaUrls.hasOwnProperty('error') && gaiaUrls.error) { - errorMsg = `Failed to upload new profile: ${gaiaUrls.error}`; - errorStatusCode = 502; - throw new Error(errorMsg); - } - - // success! - // redirect to application - logger.debug(`Handled sign-in to ${appOrigin} using ${id.name}`); - const appUri = blockstack.updateQueryStringParameter(redirectUri, 'authResponse', authResponse); - - logger.info(`Redirect to ${appUri}`); - res.writeHead(302, { Location: appUri }); - res.end(); - } catch (e) { - logger.error(e); - logger.error(errorMsg); - sendJSON(res, { error: `Unable to process signin request: ${errorMsg}` }, errorStatusCode); - } -} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 37c87e8e8..34a3aef89 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,25 +1,21 @@ -import { bytesToHex, HIRO_MAINNET_URL, HIRO_TESTNET_URL } from '@stacks/common'; -import { StacksNodeApi } from '@stacks/api'; -import * as bitcoin from 'bitcoinjs-lib'; -import * as blockstack from 'blockstack'; -import cors from 'cors'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import * as process from 'process'; -import * as winston from 'winston'; - import * as scureBip39 from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; +import { StacksNodeApi } from '@stacks/api'; import { buildPreorderNameTx, buildRegisterNameTx } from '@stacks/bns'; +import { bytesToHex, HIRO_MAINNET_URL, HIRO_TESTNET_URL } from '@stacks/common'; import { + ACCOUNT_PATH, AnchorMode, broadcastTransaction, callReadOnlyFunction, + Cl, ClarityAbi, ClarityValue, ContractCallPayload, - SignedContractDeployOptions, + cvToJSON, cvToString, + estimateTransaction, + estimateTransactionByteLength, estimateTransfer, getAbi, getAddressFromPrivateKey, @@ -27,32 +23,33 @@ import { makeContractDeploy, makeSTXTokenTransfer, PostConditionMode, + privateKeyToPublic, ReadOnlyFunctionOptions, + serializePayload, SignedContractCallOptions, + SignedContractDeployOptions, SignedTokenTransferOptions, signWithKey, StacksTransaction, TransactionSigner, TxBroadcastResult, validateContractCall, - Cl, - cvToJSON, - ACCOUNT_PATH, - estimateTransaction, - serializePayload, - estimateTransactionByteLength, - privateKeyToPublic, } from '@stacks/transactions'; -import express from 'express'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as blockstack from 'blockstack'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; import { prompt } from 'inquirer'; import fetch from 'node-fetch'; import * as path from 'path'; +import * as process from 'process'; +import * as winston from 'winston'; // eslint-disable-next-line @typescript-eslint/no-var-requires const c32check = require('c32check'); import { UserData } from '@stacks/auth'; -import crossfetch from 'cross-fetch'; +import 'cross-fetch/polyfill'; import { StackerInfo, StackingClient } from '@stacks/stacking'; @@ -74,10 +71,10 @@ import { import { checkArgs, + CLI_ARGS, CLIOptAsBool, CLIOptAsString, CLIOptAsStringArray, - CLI_ARGS, DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_TESTNET_PATH, getCLIOpts, @@ -91,7 +88,7 @@ import { import { decryptBackupPhrase, encryptBackupPhrase } from './encrypt'; -import { CLINetworkAdapter, CLI_NETWORK_OPTS, getNetwork, NameInfoType } from './network'; +import { CLI_NETWORK_OPTS, CLINetworkAdapter, getNetwork, NameInfoType } from './network'; import { gaiaAuth, gaiaConnect, gaiaUploadProfileAll, getGaiaAddressFromProfile } from './data'; @@ -115,20 +112,19 @@ import { subdomainOpToZFPieces, } from './utils'; +import { + deriveDefaultUrl, + STACKS_MAINNET, + STACKS_TESTNET, + TransactionVersion, +} from '@stacks/network'; import { generateNewAccount, generateWallet, getAppPrivateKey, restoreWalletAccounts, } from '@stacks/wallet-sdk'; -import { handleAuth, handleSignIn } from './auth'; import { getMaxIDSearchIndex, getPrivateKeyAddress, setMaxIDSearchIndex } from './common'; -import { - deriveDefaultUrl, - STACKS_MAINNET, - STACKS_TESTNET, - TransactionVersion, -} from '@stacks/network'; // global CLI options let txOnly = false; @@ -136,7 +132,7 @@ let estimateOnly = false; let safetyChecks = true; let receiveFeesPeriod = 52595; let gracePeriod = 5000; -let noExit = false; +const noExit = false; let BLOCKSTACK_TEST = !!process.env.BLOCKSTACK_TEST; @@ -1557,60 +1553,6 @@ function addressConvert(network: CLINetworkAdapter, args: string[]): Promise { - const gaiaHubUrl = args[0]; - const mnemonicOrCiphertext = args[1]; - let port = 3000; // default port - let profileGaiaHub = gaiaHubUrl; - - if (args.length > 2 && !!args[2]) { - profileGaiaHub = args[2]; - } - - if (args.length > 3 && !!args[3]) { - port = parseInt(args[3]); - } - - if (port < 0 || port > 65535) { - return Promise.resolve().then(() => JSONStringify({ error: 'Invalid port' })); - } - - const mnemonicPromise = getBackupPhrase(mnemonicOrCiphertext); - - return mnemonicPromise - .then((mnemonic: string) => { - noExit = true; - - // load up all of our identity addresses, profiles, profile URLs, and Gaia connections - const authServer = express(); - authServer.use(cors()); - - authServer.get(/^\/auth\/*$/, (req: express.Request, res: express.Response) => { - void handleAuth(network, mnemonic, gaiaHubUrl, profileGaiaHub, port, req, res); - }); - - authServer.get(/^\/signin\/*$/, (req: express.Request, res: express.Response) => { - void handleSignIn(network, mnemonic, gaiaHubUrl, profileGaiaHub, req, res); - }); - - authServer.listen(port, () => console.log(`Authentication server started on ${port}`)); - return 'Press Ctrl+C to exit'; - }) - .catch((e: Error) => { - return JSONStringify({ error: e.message }); - }); -} - /* * Encrypt a backup phrase * args: @@ -1737,7 +1679,6 @@ async function canStack(_network: CLINetworkAdapter, args: string[]): Promise { // console.log(address); const apiConfig = new Configuration({ - fetchApi: crossfetch, basePath: 'https://api.testnet.hiro.so', }); @@ -2012,8 +1951,6 @@ type CommandFunction = (network: CLINetworkAdapter, args: string[]) => Promise = { - authenticator: authDaemon, - // 'announce': announce, balance: balance, can_stack: canStack, call_contract_func: contractFunctionCall, diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index 1d9efe31a..ab3c29f3d 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -431,6 +431,9 @@ describe('Subdomain Migration', () => { test('can_stack', async () => { fetchMock.resetMocks(); + fetchMock.mockOnce( + `{"stx":{"balance":"16216000000000","total_sent":"0","total_received":"0","total_fees_sent":"0","total_miner_rewards_received":"0","lock_tx_id":"","locked":"0","lock_height":0,"burnchain_lock_height":0,"burnchain_unlock_height":0},"fungible_tokens":{},"non_fungible_tokens":{}}` + ); fetchMock.mockOnce( '{"contract_id":"ST000000000000000000002AMW42H.pox","pox_activation_threshold_ustx":827381723155441,"first_burnchain_block_height":2000000,"prepare_phase_block_length":50,"reward_phase_block_length":1000,"reward_slots":2000,"rejection_fraction":12,"total_liquid_supply_ustx":41369086157772050,"current_cycle":{"id":269,"min_threshold_ustx":5180000000000,"stacked_ustx":0,"is_pox_active":false},"next_cycle":{"id":270,"min_threshold_ustx":5180000000000,"min_increment_ustx":5171135769721,"stacked_ustx":5600000000000,"prepare_phase_start_block_height":2283450,"blocks_until_prepare_phase":146,"reward_phase_start_block_height":2283500,"blocks_until_reward_phase":196,"ustx_until_pox_rejection":4964290338932640},"min_amount_ustx":5180000000000,"prepare_cycle_length":50,"reward_cycle_id":269,"reward_cycle_length":1050,"rejection_votes_left_required":4964290338932640,"next_reward_cycle_in":196}' ); @@ -448,9 +451,9 @@ test('can_stack', async () => { const response = await canStack(testnetNetwork, params.split(' ')); expect(response.eligible).toBe(true); - expect(fetchMock.mock.calls).toHaveLength(4); - expect(fetchMock.mock.calls[3][0]).toContain('/pox/can-stack-stx'); - expect(fetchMock.mock.calls[3][1]?.body).toBe( + expect(fetchMock.mock.calls).toHaveLength(5); + expect(fetchMock.mock.calls[4][0]).toContain('/pox/can-stack-stx'); + expect(fetchMock.mock.calls[4][1]?.body).toBe( '{"sender":"ST3VJVZ265JZMG1N61YE3EQ7GNTQHF6PXP0E7YACV","arguments":["0x0c000000020968617368627974657302000000147046a658021260485e1ba9eb6c3e4c26b60953290776657273696f6e020000000100","0x010000000000000000000005a74678d000","0x010000000000000000000000000000010d","0x010000000000000000000000000000000a"]}' ); });