UNPKG

@taqueria/plugin-flextesa

Version:

A plugin for Taqueria providing local sandbox capabilities built on Flextesa

695 lines (619 loc) • 24.9 kB
import { execCmd, getArch, getDefaultSandboxAccount, getDockerImage, NonEmptyString, noop, readJsonFile, sendAsyncErr, sendAsyncRes, sendErr, sendJsonRes, spawnCmd, stringToSHA256, writeJsonFile, } from '@taqueria/node-sdk'; import { Config } from '@taqueria/node-sdk'; import { LoadedConfig, Protocol, SandboxAccountConfig, SandboxConfig, StdIO } from '@taqueria/node-sdk/types'; import { Config as RawConfig, ConfigAccount } from '@taqueria/protocol/types'; import retry from 'async-retry'; import { BigNumber } from 'bignumber.js'; import type { ExecException } from 'child_process'; import { getPortPromise } from 'portfinder'; import { last } from 'rambda'; import { getTzKtContainerNames, getTzKtStartCommands } from './tzkt-manager'; const { Url } = Protocol; import { getImage } from './docker'; import type { FlextesaAnnotations, Opts, ValidLoadedConfig, ValidOpts } from './types'; // ATTENTION: There is a duplicate of this function in taqueria-vscode-extension/src/lib/gui/SandboxesDataProvider.ts // Please make sure the two are kept in-sync export const getUniqueSandboxName = async (sandboxName: string, projectDir: string) => { const hash = String(await stringToSHA256(sandboxName + projectDir)); return `${sandboxName.substring(0, 10)}-${hash.substring(0, 5)}`; }; export const getContainerName = async (parsedArgs: ValidOpts) => { const uniqueSandboxName = await getUniqueSandboxName(parsedArgs.sandboxName, parsedArgs.projectDir); return `taq-flextesa-${uniqueSandboxName}`; }; export const getNewPortIfPortInUse = async (port: number): Promise<number> => { const newPort = await getPortPromise({ port }); return newPort; }; const replaceRpcUrlInConfig = async (newPort: string, oldUrl: string, sandboxName: string, opts: ValidOpts) => { await updateConfig(opts, (config: RawConfig) => { const newUrl = oldUrl.replace(/:\d+/, ':' + newPort) as Protocol.Url.t; const sandbox = config.sandbox; const sandboxConfig = sandbox ? sandbox[sandboxName] : undefined; if (typeof sandboxConfig === 'string' || sandboxConfig === undefined) { return; } const updatedConfig: RawConfig = { ...config, sandbox: { ...sandbox, [sandboxName]: { ...sandboxConfig, rpcUrl: newUrl, }, }, }; return updatedConfig; }); }; export const updateConfig = async (opts: ValidOpts, update: (config: RawConfig) => RawConfig | undefined) => { const config = await readJsonFile<RawConfig>(opts.config.configFile); const updatedConfig = update(config); if (!updatedConfig) { return; } await writeJsonFile(opts.config.configFile)(updatedConfig); return config; }; // TODO: We should adjust our plugins to have a types.ts file just like the taqueria-protocol, and // have our code generator generate type modules that use Zod schemas. // // We can then use those modules to parse things like annotations into plugin-specifc types // For now, I'll do things the old-fashioned way and just manually validate the annotations const getFlextesaAnnotations = (sandbox: SandboxConfig.t): Promise<FlextesaAnnotations> => { const defaults = { baking: 'enabled', block_time: 1, }; const settings = { ...defaults, ...sandbox.annotations, }; if (!['enabled', 'disabled'].includes(settings.baking)) { return Promise.reject( 'The "baking" setting of a Flextesa Sandbox must to set to either "enabled" or "disabled".', ); } else if (!Number.isInteger(settings.block_time)) { return Promise.reject( 'The "block_time" setting of a Flextesa Sandbox must be an integer, and set to a value greater than 0.', ); } else if (settings.block_time <= 0) { return Promise.reject( 'The "block_time" setting of a Flextesa Sandbox must be set to a value greater than 0. If you are trying to disable baking, please set the "baking" setting to "disabled" instead.', ); } return Promise.resolve(settings as FlextesaAnnotations); }; const getBakingFlags = (sandbox: SandboxConfig.t) => getFlextesaAnnotations(sandbox) .then(settings => { // Enabled if (settings.baking === 'enabled') { return [ `--time-b ${settings.block_time}`, ``, ]; } // Disabled else if (settings.baking === 'disabled') { return [ '--no-baking', `--time-b 1`, ]; } // Auto return [ '--no-baking', `--time-b 1`, ]; }); // TODO work with Oxhead on this. // Uses memoization const getSupportedProtocolKinds = (() => { let protocols: string[] = []; const getAll = (opts: ValidOpts): Promise<string[] | [string]> => { const image = getImage(opts); return execCmd(`docker run --rm ${image} flextesa mini-net --protocol-kind=foobar`) .catch(err => { const { stderr } = err; const protocols = stderr.match(/'[A-Z][a-z]+'/gm) ?? []; return Promise.resolve(protocols.map((protocol: string) => protocol.replace(/'/gm, ''))); }); }; return async (opts: ValidOpts): Promise<string[] | [string]> => { if (protocols.length == 0) protocols = await getAll(opts); return protocols ?? ['Alpha']; // if no known protocols are found, return Alpha which is always a valid protocol }; })(); const getProtocolKind = (sandbox: SandboxConfig.t, opts: ValidOpts) => getSupportedProtocolKinds(opts) .then(protocols => { const validProtocols = protocols.filter(p => p != 'Alpha' && p != 'Oxford'); // Oxford is filtered only because it's not supported in a image. Alpha is not a valid protocol as it doesn't work with the indexers if (!sandbox.protocol || sandbox.protocol.includes('lpha')) { return last(validProtocols); } return validProtocols.reduce( (retval, protocolKind) => { if (retval) return retval; const givenProtocolHash = (sandbox.protocol!).toLowerCase(); const testProtocol = protocolKind.toLowerCase().slice(0, 4); return givenProtocolHash.includes(testProtocol) ? protocolKind : undefined; }, undefined as string | undefined, ) ?? last(validProtocols); }); const getBootstrapBalance = (opts: ValidOpts) => Object.values(opts.config.accounts || {}) .reduce( (retval, amount) => retval.plus(new BigNumber(amount.replaceAll('_', ''))), new BigNumber(0).multipliedBy(1000000), ) .multipliedBy(1000); // give the baker lots to work with const getMininetCommand = (sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts) => Promise.all([ // getAccountFlags(sandbox, opts.config), getBakingFlags(sandbox), getProtocolKind(sandbox, opts), ]) .then(([bakingFlags, protocolKind]) => [ 'flextesa mini-net', '--root /tmp/mini-box', '--set-history-mode N000:archive', // TODO: Add annotation for this setting '--until-level 200_000_000', // TODO: Add annotation for this setting `--number-of-b 1`, `--protocol-kind="${protocolKind}"`, '--size 1', `--balance-of-bootstrap-accounts=mutez:${getBootstrapBalance(opts)}`, // ...accountFlags, ...bakingFlags, ]) .then(flags => flags.join(' ')); const getStartCommand = async (sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts) => { const port = new URL(sandbox.rpcUrl).port; const newPort = (await getNewPortIfPortInUse(parseInt(port))).toString(); if (newPort !== port) { console.log( `${port} is already in use, ${newPort} will be used for sandbox ${sandboxName} instead and .taq/config.json will be updated to reflect this.`, ); await replaceRpcUrlInConfig(newPort, sandbox.rpcUrl.toString(), sandboxName, opts); } const ports = `-p ${newPort}:20000 --expose 20000`; const containerName = await getContainerName(opts); const mininetCmd = await getMininetCommand(sandboxName, sandbox, opts); const arch = await getArch(); const image = getImage(opts); const projectDir = process.env.PROJECT_DIR ?? opts.config.projectDir; const proxyAbsPath = `${__dirname}/proxy.py`; return `docker run -i --network sandbox_${sandboxName}_net --name ${containerName} --rm --detach --platform ${arch} ${ports} -v ${projectDir}:/project ${image} /bin/sh -c "flextesa_node_cors_origin=* ${mininetCmd}"`; }; // const startMininet = async (sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts) => { // const containerName = await getContainerName(opts); // const mininetCmd = await getMininetCommand(sandboxName, sandbox, opts); // const cmd = `docker exec -d ${containerName} sh -c "flextesa_node_cors_origin='*' ${mininetCmd}"`; // return execCmd(cmd); // }; const startSandbox = (sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts): Promise<void> => { if (doesNotUseFlextesa(sandbox)) { return sendAsyncErr(`Cannot start ${sandbox.label} as its configured to use the ${sandbox.plugin} plugin.`); } return Promise.resolve(opts) .then(addSandboxAccounts) .then(loadedConfig => { console.log('Booting sandbox...'); return getStartCommand(sandboxName, sandbox, opts).then(execCmd) .then(() => { console.log('Importing accounts...'); return importSandboxAccounts(opts)(loadedConfig); }); }) .then(() => importBaker(opts)) // .then(() => { // console.log('Starting node...'); // return startMininet(sandboxName, sandbox, opts); // }) .then(() => configureTezosClient(sandboxName, opts)) .then(() => { console.log('Waiting for bootstrapping to complete...'); return waitForBootstrap(opts); }) .then(() => { console.log('Funding declared accounts (please wait)...'); return new Promise(resolve => setTimeout(resolve, 10000)).then(() => fundDeclaredAccounts(opts)); }) .then(() => { console.log(`The sandbox "${sandboxName}" is ready.`); }); }; const getConfigureCommand = async (opts: ValidOpts): Promise<string> => { const containerName = await getContainerName(opts); return `docker exec -d ${containerName} octez-client --endpoint http://localhost:20000 config update`; }; const doesUseFlextesa = (sandbox: SandboxConfig.t) => !sandbox.plugin || sandbox.plugin === 'flextesa'; const doesNotUseFlextesa = (sandbox: SandboxConfig.t) => !doesUseFlextesa(sandbox); const waitForBootstrap = (parsedArgs: ValidOpts): unknown => { const sandbox = getValidSandbox(parsedArgs.sandboxName, parsedArgs.config); const containerName = getContainerName(parsedArgs); return getContainerName(parsedArgs) .then(container => execCmd(`docker exec ${container} octez-client bootstrapped`)) .catch(({ stderr }) => { if (stderr.includes('Failed to acquire the protocol version from the node')) return waitForBootstrap(parsedArgs); throw stderr; }); }; type Transfer = { destination: string; amount: string }; const createTransferList = (sandbox: SandboxConfig.t, parsedArgs: ValidOpts) => { const transferList = Object.keys(sandbox.accounts ?? {}).reduce( (retval, accountName) => { if (accountName === 'default') return retval; const balance = new BigNumber(parsedArgs.config.accounts[accountName].replaceAll('_', '')).div(1000000); return [...retval, { destination: accountName, amount: balance.toString() }]; }, [] as Transfer[], ); return transferList; }; const writeTransferList = (containerName: string, transferList: Transfer[]) => { const fileAbsPath = '/tmp/transferList.json'; const cmd = `docker cp ${fileAbsPath} ${containerName}:${fileAbsPath}`; return writeJsonFile(fileAbsPath)(transferList) .then(() => execCmd(cmd)) .then(() => fileAbsPath); }; const fundDeclaredAccounts = async (parsedArgs: ValidOpts) => { const sandbox = getValidSandbox(parsedArgs.sandboxName, parsedArgs.config); const transferList = createTransferList(sandbox, parsedArgs); try { const containerName = await getContainerName(parsedArgs); const transferListAbsPath = await writeTransferList(containerName, transferList); const cmd = `docker exec ${containerName} octez-client multiple transfers from baker0 using ${transferListAbsPath} --burn-cap 1`; const result = await execCmd(cmd); return result; } catch (e) { if (parsedArgs.debug) console.warn(e); return sendAsyncErr('Failed to fund declared accounts.'); } }; const startContainer = async (container: { name: string; command: string }): Promise<void> => { console.log(`Starting ${container.name}`); try { const result = await execCmd(container.command); if (result.stderr) { console.error(result.stderr); } console.log(result.stdout); } catch (e: unknown) { throw e; } }; const startInstance = async (sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts): Promise<void> => { await execCmd( `docker network ls | grep 'sandbox_${sandboxName}_net' > /dev/null || docker network create --driver bridge sandbox_${sandboxName}_net`, ); const isRunning = await isSandboxRunning(opts.sandboxName, opts); if (isRunning) { await sendAsyncRes('Already running.'); return; } await startSandbox(sandboxName, sandbox, opts); if (sandbox.tzkt?.disableAutostartWithSandbox === true) { return; } const { postgres, sync, api } = await getTzKtStartCommands(sandboxName, sandbox, opts); const tzKtContainers = [ { name: 'postgresql', command: postgres }, { name: 'TzKt.Sync', command: sync }, { name: 'TzKt.Api', command: api }, ]; for (const container of tzKtContainers) { await startContainer(container); } }; const configureTezosClient = (sandboxName: string, opts: ValidOpts): Promise<StdIO> => retry( () => getConfigureCommand(opts) .then(execCmd) .then(({ stderr, stdout }) => { if (stderr.length) return Promise.reject(stderr); return ({ stderr, stdout }); }), ); const importBaker = (opts: ValidOpts) => getContainerName(opts) .then(container => `docker exec -d ${container} octez-client import secret key baker0 unencrypted:edsk3RFgDiCt7tWB2oe96w1eRw72iYiiqZPLu9nnEY23MYRp2d8Kkx` ) .then(execCmd); const startAll = (opts: ValidOpts): Promise<void> => { if (opts.config.sandbox === undefined) return sendAsyncErr('No sandboxes configured to start'); const processes = Object.entries(opts.config.sandbox).reduce( (retval, [sandboxName, sandboxDetails]) => { if (sandboxName === 'default') return retval; return [...retval, startInstance(sandboxName, sandboxDetails as SandboxConfig.t, opts)]; }, [] as Promise<void>[], ); return Promise.all(processes).then(_ => sendAsyncRes('Done.')); }; const getSandbox = ({ sandboxName, config }: Opts) => { if (sandboxName && config.sandbox && config.sandbox[sandboxName]) { const sandboxConfig = config.sandbox![sandboxName] as SandboxConfig.t; return sandboxConfig; } return undefined; }; const getValidSandbox = (sandboxName: string, config: ValidLoadedConfig) => { const retval = config.sandbox[sandboxName] as SandboxConfig.t; retval.rpcUrl = retval.rpcUrl && retval.rpcUrl.length > 0 ? retval.rpcUrl : Url.create('http://localhost:20000'); return retval; }; const startSandboxTask = (parsedArgs: ValidOpts): Promise<void> => { const sandbox = getValidSandbox(parsedArgs.sandboxName, parsedArgs.config); return sandbox ? startInstance(parsedArgs.sandboxName, sandbox, parsedArgs) : sendAsyncErr(`There is no sandbox configuration with the name ${parsedArgs.sandboxName}.`); }; const isSandboxRunning = (sandboxName: string, opts: ValidOpts) => { return getContainerName(opts) .then(containerName => execCmd(`docker ps --filter name=${containerName} | grep -w ${containerName}`)) .then(_ => true) .catch(_ => false); }; type AccountBalance = { account: string; balance: string; address: string | undefined }; // TODO - we should run all `octez-client` calls in a single `docker exec` call. // That will decrease response latency const getAccountBalances = ( sandboxName: string, sandbox: SandboxConfig.t, opts: ValidOpts, ): Promise<AccountBalance[]> => { const processes = Object.entries(sandbox.accounts ?? {}).reduce( (retval: Promise<AccountBalance>[], [accountName, accountDetails]) => { if (accountName === 'default') return retval; const getBalanceProcess = getArch() .then(_ => getContainerName(opts)) .then(containerName => `docker exec ${containerName} octez-client get balance for ${accountName.trim()}`) .then(execCmd) .then(({ stdout, stderr }) => { if (stderr.length > 0) sendErr(stderr); return { account: accountName, balance: stdout.trim(), address: (accountDetails as SandboxAccountConfig.t).publicKeyHash, }; }) .catch((err: ExecException) => { sendErr(err.message); return { account: accountName, balance: 'Error', address: (accountDetails as SandboxAccountConfig.t).publicKeyHash, }; }); return [...retval, getBalanceProcess]; }, [], ); return Promise.all(processes); }; const listAccountsTask = async <T>(parsedArgs: ValidOpts): Promise<void> => { if (parsedArgs.sandboxName) { const sandbox = getSandbox(parsedArgs); if (sandbox) { if (doesUseFlextesa(sandbox)) { return await isSandboxRunning(parsedArgs.sandboxName, parsedArgs) ? getAccountBalances(parsedArgs.sandboxName, sandbox, parsedArgs) .then(sendJsonRes) : sendAsyncErr(`The ${parsedArgs.sandboxName} sandbox is not running.`); } return sendAsyncErr( `Cannot start ${sandbox.label} as its configured to use the ${sandbox.plugin} plugin.`, ); } return sendAsyncErr(`There is no sandbox configuration with the name ${parsedArgs.sandboxName}.`); } return sendAsyncErr(`Please specify a sandbox. E.g. taq list accounts local`); }; const stopSandboxTask = async (parsedArgs: ValidOpts): Promise<void> => { if (parsedArgs.sandboxName) { const sandbox = getSandbox(parsedArgs); if (sandbox) { if (doesUseFlextesa(sandbox)) { await isSandboxRunning(parsedArgs.sandboxName, parsedArgs) ? execCmd(`docker kill ${await getContainerName(parsedArgs)}`) .then(_ => sendAsyncRes(`Stopped ${parsedArgs.sandboxName}.`)) : sendAsyncRes(`The ${parsedArgs.sandboxName} sandbox was not running.`); await stopTzKtContainers(parsedArgs.sandboxName, sandbox, parsedArgs); return; } return sendAsyncErr(`Cannot stop ${sandbox.label} as its configured to use the ${sandbox.plugin} plugin.`); } return sendAsyncErr(`There is no sandbox configuration with the name ${parsedArgs.sandboxName}.`); } return sendAsyncErr(`No sandbox specified`); }; const restartSandboxTask = async (parsedArgs: ValidOpts): Promise<void> => { await stopSandboxTask(parsedArgs); await startSandboxTask(parsedArgs); }; const stopTzKtContainers = async ( sandboxName: string, sandbox: SandboxConfig.t, parsedArgs: ValidOpts, ): Promise<void> => { const containerNames = await getTzKtContainerNames(sandboxName, parsedArgs); const containersToStop = [containerNames.api, containerNames.sync, containerNames.postgres]; for (const container of containersToStop) { try { const result = await execCmd(`docker stop ${container}`); if (result.stderr) { console.error(result.stderr); } console.log(result.stdout); } catch (e: unknown) { // ignore } } }; const listProtocolsTask = (parsedArgs: Opts) => { const image = getImage(parsedArgs); const cmd = `docker run --rm ${image} octez-client -M mockup list mockup protocols 2>/dev/null`; return execCmd(cmd) .then(({ stdout }) => stdout.trim().split('\n').map(hash => ({ 'protocols': hash }))) .then(sendJsonRes); }; const bakeTask = (parsedArgs: ValidOpts) => getContainerName(parsedArgs) .then(async containerName => { if (parsedArgs.watch) { console.log('Baking on demand as operations are injected.'); console.log('Press CTRL-C to stop and exit.'); console.log(); while (true) { console.log('Waiting for operations to be injected...'); while (true) { const { stdout } = await execCmd( `docker exec ${containerName} octez-client rpc get /chains/main/mempool/pending_operations`, ); const ops = JSON.parse(stdout); if ( (Array.isArray(ops.applied) && ops.applied.length > 0) || (Array.isArray(ops.validated) && ops.validated.length > 0) ) break; } await spawnCmd(`docker exec ${containerName} octez-client bake for baker0`); noop(); } } return spawnCmd(`docker exec ${containerName} octez-client bake for baker0`).then(noop); }); // TODO - we should run all `flextesa key` calls in a single `docker run` call. // That will decrease response latency const instantiateAccounts = (parsedArgs: ValidOpts) => { console.log('Generating account keys...'); return Object.entries(parsedArgs.config.accounts).reduce( (lastConfig, [accountName, _]) => // TODO: This could probably be more performant by generating the key pairs using TS rather than proxy to docker/flextesa lastConfig .then(_ => execCmd(`docker run --rm ${getImage(parsedArgs)} flextesa key ${accountName}`)) .then(result => result.stdout.trim().split(',')) .then(([_alias, encryptedKey, publicKeyHash, secretKey]) => SandboxAccountConfig.create({ encryptedKey, publicKeyHash, secretKey, }) ) .then(async accountConfig => { const config = await lastConfig; const accounts = config.sandbox[parsedArgs.sandboxName].accounts ?? { 'default': NonEmptyString.create(accountName) }; accounts[accountName] = accountConfig; config.sandbox[parsedArgs.sandboxName].accounts = accounts; return config; }), Promise.resolve(parsedArgs.config), ) .then(Config.create) .then(config => writeJsonFile(parsedArgs.config.configFile)(config).then(_ => config)) .then(config => LoadedConfig.create({ ...parsedArgs.config, ...config, }) as ValidLoadedConfig ); }; const hasInstantiatedAccounts = (parsedArgs: ValidOpts) => { const sandbox = getValidSandbox(parsedArgs.sandboxName, parsedArgs.config); const accounts = sandbox.accounts ?? {}; return Object.keys(accounts).length > 0; }; const maybeInstantiateAccounts = (parsedArgs: ValidOpts) => { return hasInstantiatedAccounts(parsedArgs) ? Promise.resolve(parsedArgs.config) : instantiateAccounts(parsedArgs); }; const importSandboxAccounts = (parsedArgs: ValidOpts) => async (updatedConfig: ValidLoadedConfig) => { const containerName = await getContainerName(parsedArgs); const cmds = Object.entries(getValidSandbox(parsedArgs.sandboxName, updatedConfig).accounts ?? {}).reduce( (retval, [accountName, account]) => typeof account === 'string' ? retval : [...retval, `octez-client import secret key ${accountName} ${account.secretKey} --force`], [] as string[], ); await execCmd(`docker exec -d ${containerName} sh -c '${cmds.join(' && ')}'`); }; const addSandboxAccounts = (parsedArgs: ValidOpts) => maybeInstantiateAccounts(parsedArgs); const getDefaultSandboxName = (parsedArgs: Opts) => { const env = parsedArgs.config.environment[parsedArgs.env] as Protocol.Environment.t; if (env.sandboxes && env.sandboxes.length > 0) { return env.sandboxes[0]; } return undefined; }; const taskMap: Record<string, (opts: ValidOpts) => Promise<void>> = { 'list accounts': listAccountsTask, 'show protocols': listProtocolsTask, 'list protocols': listProtocolsTask, 'start sandbox': startSandboxTask, 'start flextesa': startSandboxTask, 'stop sandbox': stopSandboxTask, 'stop flextesa': stopSandboxTask, 'bake': bakeTask, 'b': bakeTask, 'restart sandbox': restartSandboxTask, 'restart flextesa': restartSandboxTask, }; const validateRequest = async (unparsedArgs: Opts) => { // Validate that we have what we need const origSandboxName = unparsedArgs.sandboxName; const sandboxName = unparsedArgs.sandboxName ?? getDefaultSandboxName(unparsedArgs); const modifiedArgs = { ...unparsedArgs, sandboxName: sandboxName }; const sandbox = getSandbox(modifiedArgs); if (!sandbox) { return sendAsyncErr( unparsedArgs.sandboxName ? `There is no sandbox called ${origSandboxName} in your .taq/config.json.` : `No sandbox name was specified. We couldn't find a valid sandbox config for the current environment.`, ); } if (!unparsedArgs.task || !Object.keys(taskMap).includes(unparsedArgs.task)) { return await sendAsyncErr(`${unparsedArgs.task} is not an understood task by the Flextesa plugin`); } if (doesNotUseFlextesa(sandbox)) { return sendAsyncErr( `Cannot ${unparsedArgs.task} for ${sandbox.label} as its configured to use the ${sandbox.plugin} plugin.`, ); } if (!unparsedArgs.config.accounts || Object.keys(unparsedArgs.config.accounts).length === 0) { return await sendAsyncErr(`This task required a list of declared accounts in your .taq/config.json.`); } return modifiedArgs; }; export const proxy = (unparsedArgs: Opts): Promise<void> => { if (unparsedArgs.task && (unparsedArgs.task == 'list protocols' || unparsedArgs.task === 'show protocols')) { return listProtocolsTask(unparsedArgs); } else { return validateRequest(unparsedArgs).then(modifiedArgs => { const parsedArgs = modifiedArgs as ValidOpts; return taskMap[parsedArgs.task](parsedArgs); }); } }; export default proxy;