"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useDappLogic = exports.useDappCurrentAccount = exports.currentAccountAtom = void 0;
const react_1 = require("react");
const jotai_1 = require("jotai");
const account_1 = require("../account");
const bridge_1 = require("../bridge");
const live_env_1 = require("@ledgerhq/live-env");
const network_1 = __importDefault(require("@ledgerhq/live-network/network"));
const converters_1 = require("./converters");
const index_1 = require("../hw/signMessage/index");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const utils_1 = require("@ledgerhq/coin-evm/utils");
const SmartWebsocket_1 = require("./SmartWebsocket");
const helpers_1 = require("./helpers");
const txTrackingHelper_1 = require("./utils/txTrackingHelper");
const state_1 = require("@ledgerhq/cryptoassets/state");
const errors = {
    ParseError: -32700,
    InvalidRequest: -32600,
    MethodNotFound: -32601,
    InvalidParams: -32602,
    InternalError: -32603,
    UserRejected: 4001,
    Unauthorized: 4100,
    UnsupportedMethod: 4200,
    Disconnected: 4900,
    ChainDisconnected: 4901,
};
const rejectedError = (code, message, data = {}) => ({
    code,
    message,
    data: {
        code,
        message,
        ...data,
    },
});
// TODO remove any usage
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function convertEthToLiveTX(ethTX) {
    return {
        family: "ethereum",
        amount: ethTX.value !== undefined
            ? new bignumber_js_1.default(ethTX.value.replace("0x", ""), 16)
            : new bignumber_js_1.default(0),
        recipient: (0, utils_1.safeEncodeEIP55)(ethTX.to),
        gasPrice: ethTX.gasPrice !== undefined
            ? new bignumber_js_1.default(ethTX.gasPrice.replace("0x", ""), 16)
            : undefined,
        gasLimit: ethTX.gas !== undefined ? new bignumber_js_1.default(ethTX.gas.replace("0x", ""), 16) : undefined,
        data: ethTX.data ? Buffer.from(ethTX.data.replace("0x", ""), "hex") : undefined,
    };
}
exports.currentAccountAtom = (0, jotai_1.atom)(null);
function useDappCurrentAccount(currentAccountHistDb) {
    const [currentAccount, setCurrentAccount] = (0, jotai_1.useAtom)(exports.currentAccountAtom);
    // prefer using this setter when the user manually sets a current account
    const setCurrentAccountHist = (0, react_1.useCallback)((manifestId, account) => {
        if (!currentAccountHistDb)
            return;
        const [_, _setCurrentAccountHist] = currentAccountHistDb;
        _setCurrentAccountHist(state => {
            const newState = {
                ...state,
                currentAccountHist: {
                    ...state.currentAccountHist,
                    [manifestId]: account.id,
                },
            };
            return newState;
        });
    }, [currentAccountHistDb]);
    return { currentAccount, setCurrentAccount, setCurrentAccountHist };
}
exports.useDappCurrentAccount = useDappCurrentAccount;
const globCurrencies = ["**"];
function useDappAccountLogic({ manifest, accounts, currentAccountHistDb, initialAccountId, }) {
    const [initialAccountSelected, setInitialAccountSelected] = (0, react_1.useState)(false);
    const currencyIds = manifest.currencies === "*" ? globCurrencies : manifest.currencies;
    const { currentAccount, setCurrentAccount, setCurrentAccountHist } = useDappCurrentAccount(currentAccountHistDb);
    const currentParentAccount = (0, react_1.useMemo)(() => {
        if (currentAccount) {
            return (0, account_1.getParentAccount)(currentAccount, accounts);
        }
    }, [currentAccount, accounts]);
    const firstAccountAvailable = (0, react_1.useMemo)(() => {
        // Return an account for manifests with wildcard currencyIds
        if (currencyIds.includes("**") && accounts.length)
            return (0, account_1.getParentAccount)(accounts[0], accounts);
        const account = accounts.find(account => {
            if (account.type === "Account" && currencyIds.includes(account.currency.id)) {
                return account;
            }
            if (account.type === "TokenAccount" && currencyIds.includes(account.token.id)) {
                return (0, account_1.getParentAccount)(account, accounts);
            }
        });
        // might not even need to set parent here
        if (account) {
            return (0, account_1.getParentAccount)(account, accounts);
        }
    }, [accounts, currencyIds]);
    const storedCurrentAccountIsPermitted = (0, react_1.useCallback)(() => {
        if (!currentAccount)
            return false;
        return accounts.some(account => account.type === "Account" &&
            currencyIds.includes(account.currency.id) &&
            account.id === currentAccount.id);
    }, [currentAccount, accounts, currencyIds]);
    const currentAccountIdFromHist = (0, react_1.useMemo)(() => {
        if (manifest && currentAccountHistDb) {
            return currentAccountHistDb[0]?.[manifest.id];
        }
        return null;
    }, [manifest, currentAccountHistDb]);
    const currentAccountFromHist = (0, react_1.useMemo)(() => {
        return accounts.find(account => account.id === currentAccountIdFromHist);
    }, [accounts, currentAccountIdFromHist]);
    const initialAccount = (0, react_1.useMemo)(() => {
        if (!initialAccountId)
            return;
        return accounts.find(account => account.id === initialAccountId);
    }, [accounts, initialAccountId]);
    (0, react_1.useEffect)(() => {
        if (initialAccountSelected) {
            return;
        }
        if (initialAccount && !initialAccountSelected) {
            setCurrentAccount(initialAccount);
            setCurrentAccountHist(manifest.id, initialAccount);
            setInitialAccountSelected(true);
            return;
        }
        if (currentAccountFromHist) {
            setCurrentAccount(currentAccountFromHist);
            return;
        }
        if (!currentAccount || !(currentAccount && storedCurrentAccountIsPermitted())) {
            /** if there is no current account OR if there is a current account but it is not in the manifest currencies then fall back to the first permitted account */
            setCurrentAccount(firstAccountAvailable ?? null);
        }
    }, [
        currentAccount,
        currentAccountFromHist,
        firstAccountAvailable,
        initialAccount,
        initialAccountSelected,
        manifest.id,
        setCurrentAccount,
        setCurrentAccountHist,
        storedCurrentAccountIsPermitted,
    ]);
    return {
        currentAccount,
        setCurrentAccount,
        currentParentAccount,
        setCurrentAccountHist,
    };
}
// Type guard function to make typescript happy
function isParentAccountPresent(account, parentAccount) {
    if (account.type === "TokenAccount") {
        return !!parentAccount;
    }
    return true;
}
function useDappLogic({ manifest, accounts, postMessage, uiHook, tracking, currentAccountHistDb, initialAccountId, mevProtected, }) {
    const nanoApp = manifest.dapp?.nanoApp;
    const dependencies = manifest.dapp?.dependencies;
    const ws = (0, react_1.useRef)();
    const { currentAccount, currentParentAccount, setCurrentAccount, setCurrentAccountHist } = useDappAccountLogic({
        manifest,
        accounts,
        currentAccountHistDb,
        initialAccountId,
    });
    /** Current network is needed for recognising the current chain id.
     * If a token account is selected, this depends on the parent currency. */
    const currentNetwork = (0, react_1.useMemo)(() => {
        if (!currentAccount) {
            return undefined;
        }
        // If the current account is a token account, and the chain id is not specified for that specific token, we can also use the network of the parent currency to determine the correct chain id.
        return manifest.dapp?.networks.find(network => {
            const accountCurrencyId = currentAccount.type === "TokenAccount"
                ? currentAccount.token.id
                : currentAccount.currency.id;
            const accountNetworkCurrency = currentAccount.type === "TokenAccount"
                ? currentAccount.token.parentCurrency.id
                : currentAccount.currency.id;
            return network.currency === accountCurrencyId || network.currency === accountNetworkCurrency;
        });
    }, [currentAccount, manifest.dapp?.networks]);
    const currentAddress = (0, react_1.useMemo)(() => {
        return currentAccount?.type === "Account"
            ? currentAccount.freshAddress
            : currentParentAccount?.freshAddress;
    }, [currentAccount, currentParentAccount?.freshAddress]);
    (0, react_1.useEffect)(() => {
        if (!currentAddress) {
            return;
        }
        postMessage(JSON.stringify({
            jsonrpc: "2.0",
            method: "accountsChanged",
            params: [[currentAddress]],
        }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentAddress]);
    (0, react_1.useEffect)(() => {
        if (!currentNetwork) {
            return;
        }
        postMessage(JSON.stringify({
            jsonrpc: "2.0",
            method: "chainChanged",
            params: [`0x${currentNetwork.chainID.toString(16)}`],
        }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentNetwork?.chainID]);
    (0, react_1.useEffect)(() => {
        if (currentNetwork?.nodeURL) {
            const rpcURL = new URL(currentNetwork.nodeURL);
            if (rpcURL.protocol === "wss:") {
                const websocket = new SmartWebsocket_1.SmartWebsocket(rpcURL.toString(), {
                    reconnect: true,
                    reconnectMaxAttempts: Infinity,
                });
                websocket.on("message", message => {
                    postMessage(JSON.stringify(message));
                });
                websocket.connect();
                ws.current = websocket;
                return () => {
                    websocket.close();
                    ws.current = undefined;
                };
            }
        }
    }, [currentNetwork?.nodeURL, postMessage]);
    const onDappMessage = (0, react_1.useCallback)(async (data) => {
        if (data.jsonrpc !== "2.0") {
            console.error("Request is not a jsonrpc 2.0: ", data);
            return;
        }
        if (!currentNetwork) {
            console.error("No network selected: ", data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No network selected"),
            }));
            return;
        }
        if (!currentAccount) {
            console.error("No account selected: ", data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No account selected"),
            }));
            return;
        }
        if (!isParentAccountPresent(currentAccount, currentParentAccount)) {
            console.error("No parent account found for the currentAccount: ", currentAccount, data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No parent account found"),
            }));
            return;
        }
        switch (data.method) {
            // https://eips.ethereum.org/EIPS/eip-695
            case "eth_chainId": {
                postMessage(JSON.stringify({
                    id: data.id,
                    jsonrpc: "2.0",
                    result: `0x${currentNetwork.chainID.toString(16)}`,
                }));
                break;
            }
            // https://eips.ethereum.org/EIPS/eip-1102
            // https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts
            case "eth_requestAccounts":
            // legacy method, cf. https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods
            // eslint-disable-next-line no-fallthrough
            case "enable":
            // https://eips.ethereum.org/EIPS/eip-1474#eth_accounts
            // https://eth.wiki/json-rpc/API#eth_accounts
            // eslint-disable-next-line no-fallthrough
            case "eth_accounts": {
                const address = currentAccount.type === "Account"
                    ? currentAccount.freshAddress
                    : currentParentAccount.freshAddress;
                postMessage(JSON.stringify({
                    id: data.id,
                    jsonrpc: "2.0",
                    result: [address],
                }));
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3326.md
            case "wallet_switchEthereumChain": {
                const { chainId } = data.params[0];
                // Check chanId is valid hex string
                const decimalChainId = parseInt(chainId, 16);
                if (isNaN(decimalChainId)) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.InvalidParams, "Invalid chainId"),
                    }));
                    break;
                }
                // Check chain ID is known to the wallet
                const requestedCurrency = manifest.dapp?.networks.find(network => network.chainID === decimalChainId);
                if (!requestedCurrency) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.InvalidParams, `Chain ID ${chainId} is not supported`),
                    }));
                    break;
                }
                try {
                    await new Promise((resolve, reject) => uiHook["account.request"]({
                        currencyIds: [requestedCurrency.currency],
                        areCurrenciesFiltered: true,
                        onSuccess: account => {
                            setCurrentAccountHist(manifest.id, account);
                            setCurrentAccount(account);
                            resolve();
                        },
                        onCancel: () => {
                            reject("User canceled");
                        },
                    }));
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: null,
                    }));
                }
                catch (error) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, `error switching chain: ${error}`),
                    }));
                }
                break;
            }
            // https://eth.wiki/json-rpc/API#eth_sendtransaction
            case "eth_sendTransaction": {
                const ethTX = data.params[0];
                const tx = convertEthToLiveTX(ethTX);
                const address = currentAccount.type === "Account"
                    ? currentAccount.freshAddress
                    : currentParentAccount.freshAddress;
                if (address.toLowerCase() === ethTX.from.toLowerCase()) {
                    let trackingData;
                    try {
                        const signFlowInfos = (0, converters_1.getWalletAPITransactionSignFlowInfos)({
                            walletApiTransaction: tx,
                            account: currentAccount,
                        });
                        const transactionType = (0, txTrackingHelper_1.getTxType)(signFlowInfos.liveTx);
                        const accountCurrencyName = currentAccount.type === "TokenAccount"
                            ? currentAccount.token.name
                            : currentAccount.currency.name;
                        const accountNetwork = currentAccount.type === "TokenAccount"
                            ? currentAccount.token.parentCurrency.id
                            : currentAccount.currency.id;
                        const token = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tx.recipient, accountNetwork);
                        trackingData = {
                            type: transactionType,
                            currency: token ? token.name : accountCurrencyName,
                            network: token ? token.parentCurrency.id : accountNetwork,
                        };
                        const options = nanoApp
                            ? { hwAppId: nanoApp, dependencies: dependencies }
                            : undefined;
                        tracking.dappSendTransactionRequested(manifest, trackingData);
                        const signedTransaction = await new Promise((resolve, reject) => uiHook["transaction.sign"]({
                            account: currentAccount,
                            parentAccount: undefined,
                            signFlowInfos,
                            options,
                            onSuccess: signedOperation => {
                                resolve(signedOperation);
                            },
                            onError: error => {
                                reject(error);
                            },
                        }));
                        const bridge = (0, bridge_1.getAccountBridge)(currentAccount, undefined);
                        const mainAccount = (0, account_1.getMainAccount)(currentAccount, undefined);
                        let optimisticOperation = signedTransaction.operation;
                        if (!(0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")) {
                            optimisticOperation = await bridge.broadcast({
                                account: mainAccount,
                                signedOperation: signedTransaction,
                                broadcastConfig: { mevProtected: !!mevProtected },
                            });
                        }
                        uiHook["transaction.broadcast"](currentAccount, undefined, mainAccount, optimisticOperation);
                        tracking.dappSendTransactionSuccess(manifest, trackingData);
                        postMessage(JSON.stringify({
                            id: data.id,
                            jsonrpc: "2.0",
                            result: optimisticOperation.hash,
                        }));
                    }
                    catch {
                        tracking.dappSendTransactionFail(manifest, trackingData);
                        postMessage(JSON.stringify({
                            id: data.id,
                            jsonrpc: "2.0",
                            error: rejectedError(errors.UserRejected, "Transaction declined"),
                        }));
                    }
                }
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-191.md
            // https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign
            // https://docs.walletconnect.com/json-rpc-api-methods/ethereum
            // Discussion about the diff between eth_sign and personal_sign:
            // https://github.com/WalletConnect/walletconnect-docs/issues/32#issuecomment-644697172
            case "personal_sign": {
                try {
                    /**
                     * The message is received as a prefixed hex string.
                     * We need to strip the "0x" prefix.
                     */
                    const message = (0, helpers_1.stripHexPrefix)(data.params[0]);
                    tracking.dappPersonalSignRequested(manifest);
                    const formattedMessage = (0, index_1.prepareMessageToSign)(currentAccount.type === "Account" ? currentAccount : currentParentAccount, message);
                    const options = nanoApp ? { hwAppId: nanoApp, dependencies: dependencies } : undefined;
                    const signedMessage = await new Promise((resolve, reject) => uiHook["message.sign"]({
                        account: currentAccount,
                        message: formattedMessage,
                        options,
                        onSuccess: resolve,
                        onError: reject,
                        onCancel: () => {
                            reject("Canceled by user");
                        },
                    }));
                    tracking.dappPersonalSignSuccess(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: signedMessage,
                    }));
                }
                catch {
                    tracking.dappPersonalSignFail(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, "Personal message signed declined"),
                    }));
                }
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
            case data.method.match(/eth_signTypedData(_v.)?$/)?.input: {
                try {
                    const message = data.params[1];
                    tracking.dappSignTypedDataRequested(manifest);
                    const formattedMessage = (0, index_1.prepareMessageToSign)(currentAccount.type === "Account" ? currentAccount : currentParentAccount, Buffer.from(message).toString("hex"));
                    const options = nanoApp ? { hwAppId: nanoApp, dependencies: dependencies } : undefined;
                    const signedMessage = await new Promise((resolve, reject) => uiHook["message.sign"]({
                        account: currentAccount,
                        message: formattedMessage,
                        options,
                        onSuccess: resolve,
                        onError: reject,
                        onCancel: () => {
                            reject("Canceled by user");
                        },
                    }));
                    tracking.dappSignTypedDataSuccess(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: signedMessage,
                    }));
                }
                catch {
                    tracking.dappSignTypedDataFail(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, "Typed Data message signed declined"),
                    }));
                }
                break;
            }
            default: {
                if (ws.current) {
                    ws.current.send(data);
                }
                else if (currentNetwork.nodeURL?.startsWith("https:")) {
                    void (0, network_1.default)({
                        method: "POST",
                        url: currentNetwork.nodeURL,
                        data,
                    }).then(res => {
                        postMessage(JSON.stringify(res.data));
                    });
                }
                break;
            }
        }
    }, [
        currentAccount,
        currentNetwork,
        currentParentAccount,
        dependencies,
        manifest,
        mevProtected,
        nanoApp,
        postMessage,
        setCurrentAccount,
        setCurrentAccountHist,
        tracking,
        uiHook,
    ]);
    return { onDappMessage, noAccounts: !currentAccount };
}
exports.useDappLogic = useDappLogic;
//# sourceMappingURL=useDappLogic.js.map