"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseSnapExecutor = void 0;
// eslint-disable-next-line @typescript-eslint/triple-slash-reference, spaced-comment
/// <reference path="../../../../node_modules/ses/types.d.ts" />
const json_rpc_engine_1 = require("@metamask/json-rpc-engine");
const object_multiplex_1 = __importDefault(require("@metamask/object-multiplex"));
const rpc_errors_1 = require("@metamask/rpc-errors");
const snaps_sdk_1 = require("@metamask/snaps-sdk");
const snaps_utils_1 = require("@metamask/snaps-utils");
const superstruct_1 = require("@metamask/superstruct");
const utils_1 = require("@metamask/utils");
const readable_stream_1 = require("readable-stream");
const commands_1 = require("./commands.cjs");
const endowments_1 = require("./endowments/index.cjs");
const globalEvents_1 = require("./globalEvents.cjs");
const SnapProvider_1 = require("./SnapProvider.cjs");
const sortParams_1 = require("./sortParams.cjs");
const utils_2 = require("./utils.cjs");
const validation_1 = require("./validation.cjs");
const logging_1 = require("../logging.cjs");
const fallbackError = {
    code: rpc_errors_1.errorCodes.rpc.internal,
    message: 'Execution Environment Error',
};
const unhandledError = rpc_errors_1.rpcErrors
    .internal({
    message: 'Unhandled Snap Error',
})
    .serialize();
/**
 * The supported methods in the execution environment. The validator checks the
 * incoming JSON-RPC request, and the `params` property is used for sorting the
 * parameters, if they are an object.
 */
const EXECUTION_ENVIRONMENT_METHODS = {
    ping: {
        struct: validation_1.PingRequestArgumentsStruct,
        params: [],
    },
    executeSnap: {
        struct: validation_1.ExecuteSnapRequestArgumentsStruct,
        params: ['snapId', 'sourceCode', 'endowments'],
    },
    terminate: {
        struct: validation_1.TerminateRequestArgumentsStruct,
        params: [],
    },
    snapRpc: {
        struct: validation_1.SnapRpcRequestArgumentsStruct,
        params: ['target', 'handler', 'origin', 'request'],
    },
};
class BaseSnapExecutor {
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    snapData;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    commandStream;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    rpcStream;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    methods;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    snapErrorHandler;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    snapPromiseErrorHandler;
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    lastTeardown = 0;
    constructor(commandStream, rpcStream) {
        this.snapData = new Map();
        this.commandStream = commandStream;
        this.commandStream.on('data', (data) => {
            this.onCommandRequest(data).catch((error) => {
                // TODO: Decide how to handle errors.
                (0, snaps_utils_1.logError)(error);
            });
        });
        this.rpcStream = rpcStream;
        this.methods = (0, commands_1.getCommandMethodImplementations)(this.startSnap.bind(this), async (target, handlerType, args) => {
            const data = this.snapData.get(target);
            // We're capturing the handler in case someone modifies the data object
            // before the call.
            const handler = data?.exports[handlerType];
            const { required } = snaps_utils_1.SNAP_EXPORTS[handlerType];
            (0, utils_1.assert)(!required || handler !== undefined, `No ${handlerType} handler exported for snap "${target}".`, rpc_errors_1.rpcErrors.methodNotSupported);
            // Certain handlers are not required. If they are not exported, we
            // return null.
            if (!handler) {
                return null;
            }
            const result = await this.executeInSnapContext(target, async () => 
            // TODO: fix handler args type cast
            handler(args));
            // The handler might not return anything, but undefined is not valid JSON.
            if (result === undefined || result === null) {
                return null;
            }
            // /!\ Always return only sanitized JSON to prevent security flaws. /!\
            try {
                return (0, utils_1.getSafeJson)(result);
            }
            catch (error) {
                throw rpc_errors_1.rpcErrors.internal(`Received non-JSON-serializable value: ${error.message.replace(/^Assertion failed: /u, '')}`);
            }
        }, this.onTerminate.bind(this));
    }
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    errorHandler(error, data) {
        const serializedError = (0, rpc_errors_1.serializeError)(error, {
            fallbackError: unhandledError,
            shouldIncludeStack: false,
            shouldPreserveMessage: false,
        });
        const errorData = (0, snaps_sdk_1.getErrorData)(serializedError);
        // TODO: Either fix this lint violation or explain why it's necessary to
        //  ignore.
        // eslint-disable-next-line promise/no-promise-in-callback
        this.#notify({
            method: 'UnhandledError',
            params: {
                error: {
                    ...serializedError,
                    data: {
                        ...errorData,
                        ...data,
                    },
                },
            },
        }).catch((notifyError) => {
            (0, snaps_utils_1.logError)(notifyError);
        });
    }
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    async onCommandRequest(message) {
        if (!(0, utils_1.isJsonRpcRequest)(message)) {
            if ((0, utils_1.hasProperty)(message, 'id') &&
                (0, superstruct_1.is)(message.id, utils_1.JsonRpcIdStruct)) {
                // Instead of throwing, we directly respond with an error.
                // We can only do this if the message ID is still valid.
                await this.#write({
                    error: (0, rpc_errors_1.serializeError)(rpc_errors_1.rpcErrors.internal('JSON-RPC requests must be JSON serializable objects.')),
                    id: message.id,
                    jsonrpc: '2.0',
                });
            }
            else {
                (0, snaps_utils_1.logInfo)('Command stream received a non-JSON-RPC request, and was unable to respond.');
            }
            return;
        }
        const { id, method, params } = message;
        if (!(0, utils_1.hasProperty)(EXECUTION_ENVIRONMENT_METHODS, method)) {
            await this.#respond(id, {
                error: rpc_errors_1.rpcErrors
                    .methodNotFound({
                    data: {
                        method,
                    },
                })
                    .serialize(),
            });
            return;
        }
        const methodObject = EXECUTION_ENVIRONMENT_METHODS[method];
        // support params by-name and by-position
        const paramsAsArray = (0, sortParams_1.sortParamKeys)(methodObject.params, params);
        const [error] = (0, superstruct_1.validate)(paramsAsArray, methodObject.struct);
        if (error) {
            await this.#respond(id, {
                error: rpc_errors_1.rpcErrors
                    .invalidParams({
                    message: `Invalid parameters for method "${method}": ${error.message}.`,
                    data: {
                        method,
                        params: paramsAsArray,
                    },
                })
                    .serialize(),
            });
            return;
        }
        try {
            const result = await this.methods[method](...paramsAsArray);
            await this.#respond(id, { result });
        }
        catch (rpcError) {
            await this.#respond(id, {
                error: (0, rpc_errors_1.serializeError)(rpcError, {
                    fallbackError,
                    shouldPreserveMessage: false,
                }),
            });
        }
    }
    // Awaitable function that writes back to the command stream
    // To prevent snap execution from blocking writing we wrap in a promise
    // and await it before continuing execution
    async #write(chunk) {
        return new Promise((resolve, reject) => {
            this.commandStream.write(chunk, (error) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolve();
            });
        });
    }
    async #notify(notification) {
        if (!(0, utils_2.isValidResponse)(notification)) {
            throw rpc_errors_1.rpcErrors.internal('JSON-RPC notifications must be JSON serializable objects smaller than 64 MB.');
        }
        await this.#write({
            ...notification,
            jsonrpc: '2.0',
        });
    }
    async #respond(id, response) {
        if (!(0, utils_2.isValidResponse)(response)) {
            // Instead of throwing, we directly respond with an error.
            // This prevents an issue where we wouldn't respond when errors were non-serializable
            await this.#write({
                error: (0, rpc_errors_1.serializeError)(rpc_errors_1.rpcErrors.internal('JSON-RPC responses must be JSON serializable objects smaller than 64 MB.')),
                id,
                jsonrpc: '2.0',
            });
            return;
        }
        await this.#write({
            ...response,
            id,
            jsonrpc: '2.0',
        });
    }
    /**
     * Attempts to evaluate a snap in SES. Generates APIs for the snap. May throw
     * on errors.
     *
     * @param snapId - The id of the snap.
     * @param sourceCode - The source code of the snap, in IIFE format.
     * @param _endowments - An array of the names of the endowments.
     */
    async startSnap(snapId, sourceCode, _endowments) {
        (0, logging_1.log)(`Starting snap '${snapId}' in worker.`);
        if (this.snapPromiseErrorHandler) {
            (0, globalEvents_1.removeEventListener)('unhandledrejection', this.snapPromiseErrorHandler);
        }
        if (this.snapErrorHandler) {
            (0, globalEvents_1.removeEventListener)('error', this.snapErrorHandler);
        }
        this.snapErrorHandler = (error) => {
            this.errorHandler(error.error, { snapId });
        };
        this.snapPromiseErrorHandler = (error) => {
            this.errorHandler(error instanceof Error ? error : error.reason, {
                snapId,
            });
        };
        const multiplex = new object_multiplex_1.default();
        (0, readable_stream_1.pipeline)(this.rpcStream, multiplex, this.rpcStream, (error) => {
            if (error && !error.message?.match('Premature close')) {
                (0, snaps_utils_1.logError)(`Provider stream failure.`, error);
            }
        });
        const provider = new SnapProvider_1.SnapProvider(multiplex.createStream('metamask-provider'), {
            rpcMiddleware: [(0, json_rpc_engine_1.createIdRemapMiddleware)()],
        });
        provider.initializeSync();
        const multichainProvider = new SnapProvider_1.SnapProvider(multiplex.createStream('metamask-multichain-provider'), {
            rpcMiddleware: [(0, json_rpc_engine_1.createIdRemapMiddleware)()],
        });
        multichainProvider.initializeSync();
        const snap = this.createSnapGlobal(provider, multichainProvider);
        const ethereum = this.createEIP1193Provider(provider);
        // We specifically use any type because the Snap can modify the object any way they want
        const snapModule = { exports: {} };
        try {
            const { endowments, teardown: endowmentTeardown } = (0, endowments_1.createEndowments)({
                snap,
                ethereum,
                snapId,
                endowments: _endowments,
                notify: this.#notify.bind(this),
            });
            // !!! Ensure that this is the only place the data is being set.
            // Other methods access the object value and mutate its properties.
            this.snapData.set(snapId, {
                idleTeardown: endowmentTeardown,
                runningEvaluations: new Set(),
                exports: {},
            });
            (0, globalEvents_1.addEventListener)('unhandledRejection', this.snapPromiseErrorHandler);
            (0, globalEvents_1.addEventListener)('error', this.snapErrorHandler);
            const compartment = new Compartment({
                ...endowments,
                module: snapModule,
                exports: snapModule.exports,
            });
            // All of those are JavaScript runtime specific and self referential,
            // but we add them for compatibility sake with external libraries.
            //
            // We can't do that in the injected globals object above
            // because SES creates its own globalThis
            compartment.globalThis.self = compartment.globalThis;
            compartment.globalThis.global = compartment.globalThis;
            compartment.globalThis.window = compartment.globalThis;
            await this.executeInSnapContext(snapId, async () => {
                compartment.evaluate(sourceCode);
                await this.registerSnapExports(snapId, snapModule);
            });
        }
        catch (error) {
            this.removeSnap(snapId);
            const [cause] = (0, snaps_utils_1.unwrapError)(error);
            throw rpc_errors_1.rpcErrors.internal({
                message: `Error while running snap '${snapId}': ${cause.message}`,
                data: {
                    cause: cause.serialize(),
                },
            });
        }
    }
    /**
     * Cancels all running evaluations of all snaps and clears all snap data.
     * NOTE:** Should only be called in response to the `terminate` RPC command.
     */
    onTerminate() {
        // `stop()` tears down snap endowments.
        // Teardown will also be run for each snap as soon as there are
        // no more running evaluations for that snap.
        this.snapData.forEach((data) => data.runningEvaluations.forEach((evaluation) => evaluation.stop()));
        this.snapData.clear();
    }
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    async registerSnapExports(snapId, snapModule) {
        const data = this.snapData.get(snapId);
        // Somebody deleted the snap before we could register.
        if (!data) {
            return;
        }
        // If the module is async, we must await the exports.
        const snapExports = await snapModule.exports;
        data.exports = snaps_utils_1.SNAP_EXPORT_NAMES.reduce((acc, exportName) => {
            const snapExport = snapExports[exportName];
            const { validator } = snaps_utils_1.SNAP_EXPORTS[exportName];
            if (validator(snapExport)) {
                return { ...acc, [exportName]: snapExport };
            }
            return acc;
        }, {});
        // If the Snap has no valid exports after this, fail.
        (0, utils_1.assert)(Object.keys(data.exports).length > 0, 'Snap has no valid exports.');
    }
    /**
     * Instantiates a snap API object (i.e. `globalThis.snap`).
     *
     * @param provider - A StreamProvider connected to the EIP-1193 client stream.
     * @param multichainProvider - A StreamProvider connected to the CAIP-27 client stream.
     * @returns The snap provider object.
     */
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    createSnapGlobal(provider, multichainProvider) {
        const originalRequest = provider.request.bind(provider);
        const originalMultichainRequest = multichainProvider.request.bind(multichainProvider);
        const request = async (args) => {
            // As part of the sanitization, we validate that the args are valid JSON.
            const sanitizedArgs = (0, utils_2.sanitizeRequestArguments)(args);
            (0, utils_2.assertSnapOutboundRequest)(sanitizedArgs);
            if ((0, utils_2.isMultichainRequest)(sanitizedArgs)) {
                return await (0, utils_2.withTeardown)(originalMultichainRequest(sanitizedArgs), this);
            }
            return await (0, utils_2.withTeardown)(originalRequest(sanitizedArgs), this);
        };
        const snapsProvider = { request };
        return harden(snapsProvider);
    }
    /**
     * Instantiates an EIP-1193 Ethereum provider object (i.e. `globalThis.ethereum`).
     *
     * @param provider - A StreamProvider connected to MetaMask.
     * @returns The EIP-1193 Ethereum provider object.
     */
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    createEIP1193Provider(provider) {
        const originalRequest = provider.request.bind(provider);
        const request = async (args) => {
            // As part of the sanitization, we validate that the args are valid JSON.
            const sanitizedArgs = (0, utils_2.sanitizeRequestArguments)(args);
            (0, utils_2.assertEthereumOutboundRequest)(sanitizedArgs);
            return await (0, utils_2.withTeardown)(originalRequest(sanitizedArgs), this);
        };
        const ethereumProvider = { request };
        return harden(ethereumProvider);
    }
    /**
     * Removes the snap with the given name.
     *
     * @param snapId - The id of the snap to remove.
     */
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    removeSnap(snapId) {
        this.snapData.delete(snapId);
    }
    /**
     * Calls the specified executor function in the context of the specified snap.
     * Essentially, this means that the operation performed by the executor is
     * counted as an evaluation of the specified snap. When the count of running
     * evaluations of a snap reaches zero, its endowments are torn down.
     *
     * @param snapId - The id of the snap whose context to execute in.
     * @param executor - The function that will be executed in the snap's context.
     * @returns The executor's return value.
     * @template Result - The return value of the executor.
     */
    // TODO: Either fix this lint violation or explain why it's necessary to
    //  ignore.
    // eslint-disable-next-line no-restricted-syntax
    async executeInSnapContext(snapId, executor) {
        const data = this.snapData.get(snapId);
        if (data === undefined) {
            throw rpc_errors_1.rpcErrors.internal(`Tried to execute in context of unknown snap: "${snapId}".`);
        }
        let stop;
        const stopPromise = new Promise((_resolve, reject) => (stop = () => reject(
        // TODO(rekmarks): Specify / standardize error code for this case.
        rpc_errors_1.rpcErrors.internal(`The snap "${snapId}" has been terminated during execution.`))));
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const evaluationData = { stop: stop };
        try {
            data.runningEvaluations.add(evaluationData);
            // Notice that we have to await this executor.
            // If we didn't, we would decrease the amount of running evaluations
            // before the promise actually resolves
            return await Promise.race([executor(), stopPromise]);
        }
        catch (error) {
            throw new snaps_utils_1.WrappedSnapError(error);
        }
        finally {
            data.runningEvaluations.delete(evaluationData);
            if (data.runningEvaluations.size === 0) {
                this.lastTeardown += 1;
                await data.idleTeardown();
            }
        }
    }
}
exports.BaseSnapExecutor = BaseSnapExecutor;
//# sourceMappingURL=BaseSnapExecutor.cjs.map