// Verification of the signed artifacts produced by the approval popup.
//
// Signing happens in the popup, where the password is entered; the background
// only broadcasts the raw transaction and resolves the pending approval back
// to the requesting page. So that moving the signing out of the background
// does not turn the background into a blind relay, the background re-derives
// the signer from the artifact and checks it against the approval it is
// holding before acting on it. All recovery is delegated to ethers.
//
// Every failure message is a full sentence, because these strings are shown to
// the user and returned to the dApp.

const {
    Transaction,
    getAddress,
    getBytes,
    verifyMessage,
    verifyTypedData,
} = require("ethers");

// Case-insensitive address comparison that tolerates absent values on either
// side. Two absent addresses compare equal (contract creation has no `to`).
function sameAddress(a, b) {
    const aMissing = a === null || a === undefined || a === "";
    const bMissing = b === null || b === undefined || b === "";
    if (aMissing || bMissing) return aMissing && bMissing;
    try {
        return getAddress(a) === getAddress(b);
    } catch {
        return String(a).toLowerCase() === String(b).toLowerCase();
    }
}

// Normalize a transaction value (hex string, decimal string, number or
// bigint) to a bigint. An absent value is zero, matching ethers.
function normalizeValue(v) {
    if (v === null || v === undefined || v === "") return 0n;
    return BigInt(v);
}

// Normalize call data to a lowercase hex string. Absent data is "0x".
function normalizeData(v) {
    if (v === null || v === undefined || v === "" || v === "0x") return "0x";
    return String(v).toLowerCase();
}

// Assert that a raw signed transaction is the transaction the user approved,
// signed by the address the approval was raised for. Returns the parsed
// ethers Transaction on success, throws otherwise.
function verifySignedTx(rawSignedTx, txParams, expectedFrom) {
    if (typeof rawSignedTx !== "string" || !rawSignedTx.startsWith("0x")) {
        throw new Error("The signed transaction is missing or malformed.");
    }

    let parsed;
    try {
        parsed = Transaction.from(rawSignedTx);
    } catch {
        throw new Error("The signed transaction could not be decoded.");
    }

    if (!parsed.from) {
        throw new Error("The signed transaction carries no valid signature.");
    }
    if (!sameAddress(parsed.from, expectedFrom)) {
        throw new Error(
            "The signed transaction was signed by a different address than the one that was approved.",
        );
    }
    if (!sameAddress(parsed.to, txParams.to)) {
        throw new Error(
            "The signed transaction does not go to the approved recipient.",
        );
    }
    if (normalizeValue(parsed.value) !== normalizeValue(txParams.value)) {
        throw new Error(
            "The signed transaction does not carry the approved value.",
        );
    }
    if (normalizeData(parsed.data) !== normalizeData(txParams.data)) {
        throw new Error(
            "The signed transaction does not carry the approved call data.",
        );
    }

    return parsed;
}

// Assert that a signature over the approved message or typed data was
// produced by the address the approval was raised for. Returns the recovered
// address on success, throws otherwise.
function verifySignature(signParams, signature, expectedFrom) {
    if (typeof signature !== "string" || !signature.startsWith("0x")) {
        throw new Error("The signature is missing or malformed.");
    }

    let recovered;
    try {
        if (
            signParams.method === "personal_sign" ||
            signParams.method === "eth_sign"
        ) {
            recovered = verifyMessage(getBytes(signParams.message), signature);
        } else {
            const typedData = JSON.parse(signParams.typedData);
            const { domain, types, message } = typedData;
            // ethers derives EIP712Domain itself and rejects it as an input.
            delete types.EIP712Domain;
            recovered = verifyTypedData(domain, types, message, signature);
        }
    } catch {
        throw new Error("The signature could not be verified.");
    }

    if (!sameAddress(recovered, expectedFrom)) {
        throw new Error(
            "The signature was produced by a different address than the one that was approved.",
        );
    }

    return recovered;
}

module.exports = { verifySignedTx, verifySignature, sameAddress };
