UNPKG

@taqueria/flextesa-docker

Version:

Extends the oxheadalpha/flextesa image for use with Taqueria

340 lines (288 loc) 10.1 kB
import { writeJsonFile, readJsonFile } from '@taqueria/node-sdk' import yargs from 'yargs' import {exec} from 'child_process' import retry from 'promise-retry' // @ts-ignore - partial.lenses doesn't have corresponding @types import * as L from 'partial.lenses' type Args = ReturnType<typeof yargs> & {config: string, configure: boolean, importAccounts: boolean, sandbox: string} interface Failure { kind: 'E_INVALID_CONFIG' | 'E_ACCOUNT_KEY' | 'E_EXEC', context: unknown } interface AccountKeys { alias: string encryptedKey: string publicKeyHash: string secretKey: string } interface AccountDetailsInput { readonly initialBalance?: string, keys?: AccountKeys } interface AccountDetails { readonly initialBalance: string, } interface AccountDetailsWithKey extends AccountDetails{ readonly keys: AccountKeys } type Accounts = Record<string, AccountDetails> type AccountsInput = Record<string, AccountDetailsInput|string> interface SandboxSettingsInput { readonly label?: string readonly rpcUrl?: string readonly protocol?: string readonly accounts?: AccountsInput } interface SandboxSettings { label: string readonly rpcUrl: string readonly protocol: string readonly accounts: Accounts } type Sandboxes = Record<string, SandboxSettings> type SandboxesInput = Record<string, SandboxSettingsInput> interface Config { readonly sandbox: Sandboxes } interface ConfigInput { readonly sandbox?: SandboxesInput } const writeConfigFile = (filename: string) => (config: Config) => writeJsonFile(filename) (config) .then(() => config) .catch(err => Promise.reject({kind: 'E_WRITE_CONFIG', context: config, previous: err})) const run = (cmd: string): Promise<string> => new Promise((resolve, reject) => exec(`flextesa_node_cors_origin='*' ${cmd}`, (err, stdout, stderr) => { if (err) reject({kind: 'E_EXEC', context: cmd, previous: err}) else if (stderr.length) reject({kind: 'E_EXEC', context: {cmd, stderr}}) else resolve(stdout) })) const parseConfig = (input: ConfigInput) => { const parseAccountDetails = (input: AccountDetailsInput): AccountDetails | null => { if (typeof input.initialBalance === 'string' && /^(\d+_?\d+)+/.test(input.initialBalance)) { return { initialBalance: input.initialBalance } } return null } const parseAccounts = (input: AccountsInput): Accounts | null => Object.entries(input).reduce( (retval, [accountName, accountDetailsInput]) => { if (typeof(accountDetailsInput) !== 'string') { const temp: Record<string, (null|AccountDetails)> = {} temp[accountName] = parseAccountDetails(accountDetailsInput) return temp[accountName] ? {...retval, ...temp} : retval } else { return {...retval, default: accountDetailsInput} } }, {} ) const parseUrl = (input:string) => { try { new URL(input) return input } catch (_) { return false } } const parseString = (input:string) => typeof input === 'string' && input.length >= 1 const parseSandboxSettings = (input: SandboxSettingsInput) : SandboxSettings | null => { if (input.label && parseString(input.label) && input.protocol && parseString(input.protocol)) { if (input.rpcUrl && parseUrl(input.rpcUrl) && input.accounts) { const accounts = parseAccounts(input.accounts) return accounts ? {accounts, label: input.label, protocol: input.protocol, rpcUrl: input.rpcUrl} : null } } return null } const parseSandboxes = (input: SandboxesInput): Sandboxes | undefined => Object.entries(input).reduce( (retval: Sandboxes | undefined, input: [string, SandboxSettingsInput]) => { const [sandboxName, settingsInput] = (input as [string, SandboxSettingsInput]) if (sandboxName !== 'default') { const temp: Record<string, (null|SandboxSettings)> = {} temp[sandboxName] = parseSandboxSettings(settingsInput) return temp[sandboxName] !== undefined ? {...retval, ...temp} as Sandboxes : retval } }, {} as Sandboxes ) if (input.sandbox) { const sandboxes = parseSandboxes(input.sandbox) if (sandboxes) return Promise.resolve({...input, sandbox: sandboxes}) } return Promise.reject({kind: 'E_INVALID_CONFIG', context: input}) } const getAccountKeys = (accountName: string): Promise<AccountKeys> => run(`flextesa key ${accountName}`) .then((result: string) => { const [alias, encryptedKey, publicKeyHash, secretKey] = result.trim().split(',') return {alias, encryptedKey, publicKeyHash, secretKey} }) const addAccountKeys = async ([accountName, accountDetails]: [string, AccountDetails]) => { return [ accountName, accountName === 'default' ? accountDetails : {...accountDetails, keys: await getAccountKeys(accountName)} ] } const getBootstrapFlags = (sandboxName: string, config:Config) => { const lens = L.compose( 'sandbox', sandboxName, 'accounts', L.values, ) return L.collect(lens, config) .reduce( (retval: string[], accountDetails: AccountDetailsWithKey | string) => { if (typeof accountDetails === 'string') { return retval } const {keys, initialBalance} = accountDetails return [ ...retval, `--add-bootstrap-account="${keys.alias},${keys.encryptedKey},${keys.publicKeyHash},${keys.secretKey}@${initialBalance}"` ] }, [] ) .join(' ') } const getNoDaemonFlags = (sandboxName: string, config:Config) => { const lens = L.compose( 'sandbox', sandboxName, 'accounts', L.keys ) return L.collect(lens, config) .map((alias:string) => `--no-daemons-for=${alias}`) .join(' ') } const getSandboxProtocol = (sandboxName: string, config: Config) => { const lens = L.compose( 'sandbox', sandboxName, 'protocol' ) // TODO - This shouldn't be here. // A plugin should provide a list of protocols to Taqueria, and // Taqueria should make that list known to any plugins that require // the list. switch (L.get(lens, config)) { case 'PsiThaCaT47Zboaw71QWScM8sXeMM7bbQFncK9FLqYc6EKdpjVP': return 'Ithaca' case 'PtHangz2aRngywmSRGGvrcTyMbbdpWdpFKuS4uMWxg2RaH9i1qx': return 'Hangzhou' case 'PtGRANADsDU8R9daYKAgWnQYAJ64omN1o3KMGVCykShA97vQbvV': return 'Granada' default: return 'Alpha' } } const runMininet = (sandboxName: string) => (config: Config) => { const cmdArgs = [ 'flextesa', 'mini-network', '--root /tmp/mini-box', '--size 1', '--number-of-b 1', '--set-history-mode N000:archive', '--time-b 5', '--balance-of-bootstrap-accounts tez:100_000_000', getNoDaemonFlags(sandboxName, config), getBootstrapFlags(sandboxName, config), // '--until-level 200_000_000', `--protocol-kind "${getSandboxProtocol(sandboxName, config)}"` ] return run(cmdArgs.join(' ')) } const configureTezosClient = (config: Config) => run(`tezos-client --endpoint http://localhost:20000 config update`) .then(() => config) const importAccounts = (sandboxName: string, config: Config) => { const accountLens = L.compose( 'sandbox', sandboxName, 'accounts', L.values, 'keys' ) const processes = L.collect(accountLens, config) .reduce( (retval: Promise<string>[], keys: AccountKeys) => [ ...retval, retry(() => isAccountImported(keys.alias) .then(hasAccount => hasAccount ? Promise.resolve('success') : run(`tezos-client --protocol ${config.sandbox[sandboxName].protocol} import secret key ${keys.alias} ${keys.secretKey} --force | tee /tmp/import-key.log`) ) ) ], [] ) return Promise.all(processes) .then(() => config) } const isAccountImported = (accountName: string) => run(`tezos-client list known addresses`) .then(output => output.indexOf(accountName) >= 0) .catch(() => false) /**** Program Execution Starts Here */ // @ts-ignore const inputArgs: Args = (yargs(process.argv) as unknown as Args) .option('config', { default: "/project/.taq/config.json" }) .option('sandbox', { default: '' }) .option('configure', { default: false, boolean: true }) .option('importAccounts', { default: false, boolean: true }) .parse() if (!inputArgs.sandbox.length) { console.log({kind: 'E_INVALID_USAGE', context: inputArgs}) process.exit(-1) } readJsonFile<ConfigInput>(inputArgs.config) .then(parseConfig) .then((config: Config) => { const lens = L.compose( 'sandbox', inputArgs.sandbox, 'accounts', L.entries ) return L.modifyAsync(lens, addAccountKeys, config) }) .then(writeConfigFile(inputArgs.config)) .then((config: Config) => { if (inputArgs.configure) return configureTezosClient(config) else if (inputArgs.importAccounts) return importAccounts(inputArgs.sandbox, config) else return runMininet(inputArgs.sandbox) (config).then(() => config) }) .then(() => process.exit(0)) .catch((err: Error) => { console.error(err) process.exit(-1) })