@taqueria/plugin-flextesa
Version: 
A plugin for Taqueria providing local sandbox capabilities built on Flextesa
695 lines (619 loc) • 24.9 kB
text/typescript
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;