// 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.
//
// What the artifact is checked against is the transaction the background
// populated and the popup displayed (see approvalTx.js), not the request the
// dApp made. The two differ in every field a dApp normally leaves out — nonce,
// gas limit, fees — and those are the fields the user reads off the approval
// screen, so comparing against the request would leave the numbers on screen
// vouched for by nothing.
//
// The check is an allowlist, in both directions, because a denylist cannot be
// correct against a transaction format that keeps gaining fields:
//
//  - only transaction types 0, 1 and 2 are accepted. Every later EIP-2718 type
//    adds a field with consequences of its own — EIP-7702's authorizationList
//    rewrites the code at the signer's own account, EIP-4844's blob
//    commitments carry a separate fee — and a check that enumerates the fields
//    it refuses admits every one of them by default.
//  - after the per-field comparisons, the artifact is rebuilt from those
//    checked fields and nothing else, and the two are compared byte for byte.
//    Anything the artifact carries that this module does not name is absent
//    from the rebuild and changes the bytes, so the final assertion is that
//    the artifact *is* the approved transaction, not merely that it is not one
//    of the tampered shapes that were thought of.
//  - every comparison runs against the decode, but the string handed to
//    broadcastTransaction() is the artifact. So the artifact is also required
//    to be the canonical re-encoding of its own decode, which is what makes
//    the checked transaction and the broadcast bytes the same object rather
//    than two things that merely decode alike.
//
// Every consequential field is compared, and a mismatch is a refusal to act,
// never a warning: what the user approved is what gets broadcast, or nothing
// does.
//
// The approved transaction is required to fix every field its type serializes,
// so there is no "the approval did not say" branch to fall through: a quantity
// the approval does not carry is a refusal, because an artifact that cannot be
// compared with what was displayed has not been checked. The chain id is
// checked against the selected network as well as against the approval, which
// is what makes a cross-chain replay impossible.
//
// Every failure message is a full sentence, because these strings are shown to
// the user and returned to the dApp.

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

// The only transaction types this wallet signs: legacy, EIP-2930 and
// EIP-1559. populateTransaction() produces nothing else, so nothing else can
// be an artifact of an approval this wallet raised.
const ALLOWED_TX_TYPES = [0, 1, 2];

// The serialized fields of each allowed type, which is also the complete set
// of fields the checks below compare or bound. The artifact is rebuilt from
// exactly these at the end of verification and compared byte for byte, so a
// field outside this table cannot ride along unexamined.
const SERIALIZED_FIELDS = {
    0: ["chainId", "nonce", "gasPrice", "gasLimit", "to", "value", "data"],
    1: [
        "chainId",
        "nonce",
        "gasPrice",
        "gasLimit",
        "to",
        "value",
        "data",
        "accessList",
    ],
    2: [
        "chainId",
        "nonce",
        "maxPriorityFeePerGas",
        "maxFeePerGas",
        "gasLimit",
        "to",
        "value",
        "data",
        "accessList",
    ],
};

// Fields no allowed type may carry. The type allowlist already excludes every
// type that defines them, and the structural check at the end of verification
// would catch them anyway; they are named here so that an artifact carrying
// one is refused with a message that says what it was.
const FORBIDDEN_FIELDS = [
    {
        key: "authorizationList",
        message:
            "The signed transaction would hand the signing account over to another contract, which was not approved.",
    },
    {
        key: "blobVersionedHashes",
        message:
            "The signed transaction carries blob commitments, which were not approved.",
    },
    {
        key: "blobs",
        message:
            "The signed transaction carries blobs, which were not approved.",
    },
    {
        key: "maxFeePerBlobGas",
        message:
            "The signed transaction carries a blob gas fee, which was not approved.",
    },
];

// Absolute ceilings — a BACKSTOP, not the primary control.
//
// The primary control is equality: every field of the artifact is compared
// with the populated transaction the user was shown, so nothing the popup
// signs can differ from the screen. What equality cannot bound is the
// populated transaction itself, which is built from what the configured RPC
// node answered — a node that reports an absurd fee gets that fee displayed,
// and a user who does not read the fee line would approve it. These ceilings
// bound that, and they are therefore applied where the transaction is
// populated (approvalTx.js) as well as here.
//
// Above the block gas limit of every supported network (see networks.js), so
// no transaction that could ever be included is refused by it.
const MAX_GAS_LIMIT = 100000000n;

// 100,000 gwei per gas: orders of magnitude above the highest fee either
// supported network has produced, and low enough to catch a fee that would
// hand the validator the balance.
const MAX_FEE_PER_GAS = 100000000000000n;

// A refusal to act on an artifact: it is not the thing that was approved, so
// the approval it was offered against is spent and must not be retried. Every
// throw in this module is one of these; the background distinguishes them from
// transient failures (a busy node, a failed broadcast), which leave the
// approval standing so the user can try again.
class ApprovalMismatchError extends Error {
    constructor(message) {
        super(message);
        this.name = "ApprovalMismatchError";
        this.approvalMismatch = true;
    }
}

function refuse(message) {
    return new ApprovalMismatchError(message);
}

// Whether a signing failure leaves the approval usable. Anything that is not a
// mismatch is the user's to correct and retry.
function failureIsRetryable(err) {
    return !(err && err.approvalMismatch === true);
}

// 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();
    }
}

// Whether the approval fixed a value for a field at all.
function present(v) {
    return v !== null && v !== undefined && v !== "";
}

// Whether a field carries anything at all. An empty array is nothing: ethers
// reports an absent access list on a type 2 transaction as `[]`.
function carriesValue(v) {
    if (!present(v)) return false;
    if (Array.isArray(v)) return v.length > 0;
    return true;
}

// Normalize a quantity that must be present, refusing anything that is not a
// number: an approval carrying junk in a fee field cannot be compared, and an
// uncomparable field is a refusal rather than a pass.
function normalizeQuantity(v, label) {
    try {
        return BigInt(v);
    } catch {
        throw refuse(
            "The approved " +
                label +
                " is not a number, so it cannot be" +
                " compared with the signed transaction.",
        );
    }
}

// Normalize a transaction value (hex string, decimal string, number or
// bigint) to a bigint. An absent value is zero, matching ethers. The value is
// page-controlled, so it goes through the same refusal as every other
// quantity rather than throwing a raw BigInt conversion error.
function normalizeValue(v) {
    if (!present(v)) return 0n;
    return normalizeQuantity(v, "value");
}

// Normalize an access list to a comparable string. An absent or empty list is
// the empty string, so absent and `[]` are the same thing.
function normalizeAccessList(v) {
    if (!carriesValue(v)) return "";
    let list;
    try {
        list = accessListify(v);
    } catch {
        throw refuse(
            "The approved access list is not a valid access list, so it cannot be compared with the signed transaction.",
        );
    }
    return list
        .map(
            (entry) =>
                String(entry.address).toLowerCase() +
                ":" +
                entry.storageKeys.map((k) => String(k).toLowerCase()).join(","),
        )
        .join(";");
}

// 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();
}

// How each field of an approved transaction is compared with the artifact.
// There is an entry here for every field any allowed type serializes — a test
// pins that against SERIALIZED_FIELDS — so the comparison loop covers the
// whole of what gets signed and cannot silently skip a field for want of a
// comparator.
//
// `kind` decides how the two sides are made comparable. A `quantity` must be
// fixed by the approval: it is one of the numbers on the approval screen, and
// an absent one means the artifact cannot be checked against what was
// displayed. `to`, `value`, `data` and `accessList` have canonical absent
// forms — contract creation, zero, "0x" and the empty list — so they are
// normalized on both sides instead.
const APPROVED_FIELDS = {
    chainId: {
        kind: "quantity",
        label: "network",
        message:
            "The signed transaction is for a different network than the one that was approved.",
    },
    nonce: {
        kind: "quantity",
        label: "nonce",
        message: "The signed transaction does not carry the approved nonce.",
    },
    gasLimit: {
        kind: "quantity",
        label: "gas limit",
        message:
            "The signed transaction does not carry the approved gas limit.",
    },
    gasPrice: {
        kind: "quantity",
        label: "gas price",
        message:
            "The signed transaction does not carry the approved gas price.",
    },
    maxFeePerGas: {
        kind: "quantity",
        label: "maximum fee per gas",
        message:
            "The signed transaction does not carry the approved maximum fee per gas.",
    },
    maxPriorityFeePerGas: {
        kind: "quantity",
        label: "maximum priority fee per gas",
        message:
            "The signed transaction does not carry the approved maximum priority fee per gas.",
    },
    to: {
        kind: "address",
        label: "recipient",
        message:
            "The signed transaction does not go to the approved recipient.",
    },
    value: {
        kind: "value",
        label: "value",
        message: "The signed transaction does not carry the approved value.",
    },
    data: {
        kind: "data",
        label: "call data",
        message:
            "The signed transaction does not carry the approved call data.",
    },
    accessList: {
        kind: "accessList",
        label: "access list",
        message:
            "The signed transaction does not carry the approved access list.",
    },
};

// Compare one field of the artifact with the approved transaction. A field
// with no entry in the table above is refused rather than skipped: the loop
// below runs over the fields the type serializes, so an unmatched key means
// something that gets signed has no comparator at all.
function assertFieldMatches(key, parsed, approvedTx) {
    const field = APPROVED_FIELDS[key];
    if (!field) {
        throw refuse(
            "The signed transaction carries a field this wallet cannot compare with the approval.",
        );
    }
    switch (field.kind) {
        case "quantity": {
            if (!present(approvedTx[key])) {
                throw refuse(
                    "The approved transaction fixes no " +
                        field.label +
                        ", so the signed transaction cannot be checked" +
                        " against what was shown.",
                );
            }
            const approved = normalizeQuantity(approvedTx[key], field.label);
            if (normalizeQuantity(parsed[key], field.label) !== approved) {
                throw refuse(field.message);
            }
            return;
        }
        case "address":
            if (!sameAddress(parsed[key], approvedTx[key])) {
                throw refuse(field.message);
            }
            return;
        case "value":
            if (normalizeValue(parsed[key]) !== normalizeValue(approvedTx[key]))
                throw refuse(field.message);
            return;
        case "data":
            if (normalizeData(parsed[key]) !== normalizeData(approvedTx[key]))
                throw refuse(field.message);
            return;
        default:
            if (
                normalizeAccessList(parsed[key]) !==
                normalizeAccessList(approvedTx[key])
            ) {
                throw refuse(field.message);
            }
    }
}

// The ceilings, applied to a transaction that is either about to be displayed
// or about to be broadcast. See MAX_GAS_LIMIT above for what they are for:
// they bound what the RPC node can talk this wallet into showing the user,
// which is the one thing comparing the artifact with the screen cannot do.
function assertWithinCeilings(tx) {
    if (
        present(tx.gasLimit) &&
        normalizeQuantity(tx.gasLimit, "gas limit") > MAX_GAS_LIMIT
    ) {
        throw refuse(
            "The signed transaction sets a gas limit no network this wallet supports can accept.",
        );
    }
    for (const key of ["gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"]) {
        if (!present(tx[key])) continue;
        if (normalizeQuantity(tx[key], "fee per gas") > MAX_FEE_PER_GAS) {
            throw refuse(
                "The signed transaction sets a fee per gas far above any plausible value.",
            );
        }
    }
}

// Refuse a field only a transaction type this wallet does not sign can carry.
// The type allowlist keeps these unreachable in production, which is exactly
// what they are for; it also means nothing else exercises them, so this is
// exported and tested on its own rather than left to be believed.
function assertNoForbiddenFields(parsed) {
    for (const field of FORBIDDEN_FIELDS) {
        if (carriesValue(parsed[field.key])) throw refuse(field.message);
    }
}

// Closing structural check. Rebuild the transaction from the fields the
// comparisons cover, and nothing else, then compare the unsigned bytes. Every
// field carried by the artifact but absent from the rebuild changes the
// serialization, so this refuses anything this module does not account for —
// including a field a future ethers learns to parse onto an allowed type —
// instead of waving it through by not naming it. Also exported for its own
// test: nothing reachable today can make the bytes differ.
function assertNothingUnchecked(parsed) {
    let rebuilt;
    try {
        const fields = { type: parsed.type };
        for (const key of SERIALIZED_FIELDS[parsed.type]) {
            fields[key] = parsed[key];
        }
        rebuilt = Transaction.from(fields);
    } catch {
        throw refuse(
            "The signed transaction could not be rebuilt from the fields that were checked, so it cannot be shown to be the approved transaction.",
        );
    }
    if (rebuilt.unsignedSerialized !== parsed.unsignedSerialized) {
        throw refuse(
            "The signed transaction carries data beyond the fields that were checked against the approval.",
        );
    }
}

// The other half of the closing check, and the one that makes it bind on the
// bytes that actually leave: every comparison above runs against the decode,
// so on its own the rebuild proves only that the transaction ethers understood
// is the approved one. What the background hands to broadcastTransaction() is
// the artifact string itself. Requiring the artifact to be exactly the
// canonical re-encoding of its own decode closes the gap between the two —
// no encoding the decoder normalizes away (a leading zero byte on an RLP
// quantity, say) can differ from what was checked. Hex case is not part of the
// encoding, so only that is normalized before comparing.
function assertCanonicalBytes(parsed, rawSignedTx) {
    if (parsed.serialized !== String(rawSignedTx).toLowerCase()) {
        throw refuse(
            "The signed transaction is not encoded canonically, so the bytes that would be broadcast are not the bytes that were checked.",
        );
    }
}

// Assert that a raw signed transaction is the transaction the user approved,
// signed by the address the approval was raised for, on the network that is
// selected. Returns the parsed ethers Transaction on success, throws
// otherwise.
//
// `approvedTx` is the populated transaction the approval screen displayed, and
// `expectedFrom` is the address that was active when the approval was raised —
// not whichever address is active now. An address switch between approval and
// signing therefore refuses here rather than producing a transaction from an
// account the approval did not name.
function verifySignedTx(
    rawSignedTx,
    approvedTx,
    expectedFrom,
    selectedChainId,
) {
    if (typeof rawSignedTx !== "string" || !rawSignedTx.startsWith("0x")) {
        throw refuse("The signed transaction is missing or malformed.");
    }
    // Nothing to compare against is a refusal like any other: an approval that
    // does not carry the transaction it displayed cannot vouch for one.
    if (!approvedTx || typeof approvedTx !== "object") {
        throw refuse(
            "There is no approved transaction to check the signed transaction against.",
        );
    }

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

    if (!parsed.from) {
        throw refuse("The signed transaction carries no valid signature.");
    }
    if (!sameAddress(parsed.from, expectedFrom)) {
        throw refuse(
            "The signed transaction was signed by a different address than the one that was approved.",
        );
    }

    // Before any field is looked at: the type decides which fields exist at
    // all, so an unrecognised type is refused outright rather than compared
    // field by field against an approval that cannot describe it.
    if (!ALLOWED_TX_TYPES.includes(parsed.type)) {
        throw refuse(
            "The signed transaction is of a type this wallet does not sign, so what it would do beyond the approved transfer cannot be checked.",
        );
    }
    assertNoForbiddenFields(parsed);

    // The selected network, not the artifact, is the authority on which chain
    // this may be broadcast to; without it nothing can be verified.
    if (!present(selectedChainId)) {
        throw refuse(
            "The selected network is unknown, so the signed transaction cannot be checked against it.",
        );
    }
    if (parsed.chainId !== normalizeQuantity(selectedChainId, "network")) {
        throw refuse(
            "The signed transaction is for a different network than the one that is selected.",
        );
    }

    // The approved fee mechanism, named before the type comparison below
    // subsumes it: the fee the user agreed to is only meaningful under the
    // mechanism it was quoted in, and saying so is more use than "a different
    // transaction type".
    const approvedEip1559 =
        present(approvedTx.maxFeePerGas) ||
        present(approvedTx.maxPriorityFeePerGas);
    const approvedLegacy = present(approvedTx.gasPrice);
    const signedEip1559 = parsed.type === 2;
    if (
        (approvedEip1559 && !signedEip1559) ||
        (approvedLegacy && signedEip1559)
    ) {
        throw refuse(
            "The signed transaction does not use the approved fee mechanism.",
        );
    }

    // The type decides which fields are compared, so it is compared first and
    // against the approval, not merely checked for membership of the
    // allowlist above.
    if (!present(approvedTx.type)) {
        throw refuse(
            "The approved transaction fixes no transaction type, so the signed transaction cannot be checked against what was shown.",
        );
    }
    if (
        BigInt(parsed.type) !==
        normalizeQuantity(approvedTx.type, "transaction type")
    ) {
        throw refuse(
            "The signed transaction does not use the approved transaction type.",
        );
    }

    // Every field this type serializes, compared with the transaction the user
    // was shown. Driving the loop off SERIALIZED_FIELDS is what keeps this
    // exhaustive: the same table decides what assertNothingUnchecked() rebuilds
    // from, so a field that gets signed and is not compared here cannot exist.
    for (const key of SERIALIZED_FIELDS[parsed.type]) {
        assertFieldMatches(key, parsed, approvedTx);
    }

    assertWithinCeilings(parsed);

    assertNothingUnchecked(parsed);
    assertCanonicalBytes(parsed, rawSignedTx);

    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 refuse("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 refuse("The signature could not be verified.");
    }

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

    return recovered;
}

// The stage a transaction approval failed at. Which stage it is decides
// whether the approval survives the failure.
const TX_STAGE_SIGN = "sign";
const TX_STAGE_VERIFY = "verify";
const TX_STAGE_BROADCAST = "broadcast";
// Not a failure of this request at all: a second response arrived for an
// approval an attempt already holds. The first attempt is still running and
// may yet succeed, so the one thing the popup must not say is "start again
// from the site".
const TX_STAGE_INFLIGHT = "inflight";
// A transaction refused for a nonce that is already spoken for, either by the
// node's own answer or by this wallet's record of what it has broadcast. It is
// the one broadcast-stage failure that is not ambiguous: the transaction was
// not taken, so the user is told it did not reach the network and to send it
// again, rather than being warned that it might already be out there.
const TX_STAGE_NONCE = "nonce";

function errorText(err) {
    if (typeof err === "string" && err !== "") return err;
    if (err && (err.shortMessage || err.message)) {
        return err.shortMessage || err.message;
    }
    return "The transaction could not be sent.";
}

// Every string a failure might carry its reason in. ethers reports the node's
// own words in `shortMessage`, but a JSON-RPC error it could not classify is
// nested under `error` or `info.error` with the node's message intact, and the
// classification below has to see that too.
function failureTexts(err) {
    if (typeof err === "string") return [err];
    if (!err || typeof err !== "object") return [];
    const texts = [];
    for (const text of [err.shortMessage, err.message, err.reason]) {
        if (text) texts.push(String(text));
    }
    const nested = err.error || (err.info && err.info.error);
    if (nested && nested.message) texts.push(String(nested.message));
    return texts;
}

// What the Ethereum clients say when a transaction's nonce is already spoken
// for: either it is below the account's next nonce, or another transaction is
// sitting in the pool at that nonce and this one did not outbid it. Either way
// the node answered, and its answer was that it did not take this transaction.
//
// "already known" is deliberately absent. A node that says it knows the
// transaction has it, so that transaction did reach the network and the
// ambiguous broadcast wording is the correct one for it.
const NONCE_COLLISION_PATTERNS = [
    /nonce too low/i,
    /nonce has already been used/i,
    /invalid nonce/i,
    /oldnonce/i,
    /replacement transaction underpriced/i,
    /replacement fee too low/i,
];

// ethers' own classification of the same two conditions.
const NONCE_COLLISION_CODES = ["NONCE_EXPIRED", "REPLACEMENT_UNDERPRICED"];

// Whether a failed send is a nonce collision.
function isNonceCollision(err) {
    if (!err) return false;
    if (err.code && NONCE_COLLISION_CODES.includes(err.code)) return true;
    return failureTexts(err).some((text) =>
        NONCE_COLLISION_PATTERNS.some((pattern) => pattern.test(text)),
    );
}

// What both the requesting page and the popup are told about a nonce
// collision. The node's own words ("nonce too low") are a fragment and are
// replaced rather than passed through: they are not a sentence, and they say
// less than the wallet knows.
const NONCE_COLLISION_MESSAGE =
    "The transaction was not sent, because its nonce had already been used" +
    " by another transaction.";

// What the background does with a pending transaction approval after a failed
// attempt: what it tells the popup, and whether the approval is spent
// (resolved to the requesting page as an error and deleted) or left standing
// so the user can try the transaction they already saw again.
//
//  - sign: the popup could not produce an artifact, almost always a wrong
//    password. Nothing left the extension, so the approval stands.
//  - verify: a mismatch is a refusal and spends the approval — an artifact
//    that is not the approved transaction must never be retried against that
//    approval. Anything else failed before the check ran and is retryable.
//  - broadcast: always terminal. A broadcast that throws after the node
//    accepted the transaction is routine (a timeout, a dropped response, a
//    node answering "already known"), so the wallet cannot tell a transaction
//    that never left from one that is already in the mempool. The approval is
//    spent and the requesting page has been given its outcome; a second
//    attempt against it would report a second outcome for one request.
//  - nonce: terminal too, and the one case where the wallet does know the
//    transaction never left. The approval carries a nonce that is spent, so
//    the artifact signed against it can never be accepted and the user is told
//    to send it again from the site.
//
// The stage comes back out because a broadcast failure the node blamed on the
// nonce is reclassified here; the caller reports the stage this returns rather
// than the one it passed in.
function describeTxFailure(stage, err) {
    if (
        stage === TX_STAGE_NONCE ||
        (stage === TX_STAGE_BROADCAST && isNonceCollision(err))
    ) {
        return {
            error: NONCE_COLLISION_MESSAGE,
            retryable: false,
            spendApproval: true,
            stage: TX_STAGE_NONCE,
        };
    }
    const error = errorText(err);
    const retryable =
        stage === TX_STAGE_SIGN ||
        (stage === TX_STAGE_VERIFY && failureIsRetryable(err));
    return { error, retryable, spendApproval: !retryable, stage };
}

// What the popup shows and does after the background reports a failed signing
// attempt. A retryable failure leaves the approval pending in the background,
// so the button goes back to being usable; a refusal spent the approval, and
// the popup says so rather than offering a button that cannot succeed.
//
// A failed broadcast gets its own wording: the transaction may already be on
// the network, so telling the user to start again from the site is exactly the
// wrong instruction. A nonce collision is the exception to that exception —
// the transaction demonstrably did not go out, and saying it might have would
// send the user hunting for a transaction that does not exist.
function describeSigningFailure(response, fallbackMessage) {
    let message = (response && response.error) || fallbackMessage;
    if (!/[.!?]$/.test(message)) message += ".";
    const retryable = !!(response && response.retryable);
    const stage = response && response.stage;
    if (!retryable) {
        if (stage === TX_STAGE_NONCE) {
            message +=
                " The transaction did not reach the network." +
                " Please send it again from the site.";
        } else if (stage === TX_STAGE_BROADCAST) {
            message +=
                " The transaction may still have reached the network." +
                " Check the account before sending it again.";
        } else if (stage === TX_STAGE_INFLIGHT) {
            message +=
                " The first attempt is still running and may still succeed." +
                " Wait for it rather than starting again.";
        } else {
            message +=
                " This request can no longer be signed. Please start it" +
                " again from the site.";
        }
    }
    return { message, retryable };
}

module.exports = {
    verifySignedTx,
    verifySignature,
    assertNoForbiddenFields,
    assertNothingUnchecked,
    assertCanonicalBytes,
    assertWithinCeilings,
    sameAddress,
    failureIsRetryable,
    isNonceCollision,
    describeTxFailure,
    describeSigningFailure,
    ApprovalMismatchError,
    NONCE_COLLISION_MESSAGE,
    ALLOWED_TX_TYPES,
    SERIALIZED_FIELDS,
    FORBIDDEN_FIELDS,
    APPROVED_FIELDS,
    TX_STAGE_SIGN,
    TX_STAGE_VERIFY,
    TX_STAGE_BROADCAST,
    TX_STAGE_INFLIGHT,
    TX_STAGE_NONCE,
    MAX_GAS_LIMIT,
    MAX_FEE_PER_GAS,
};
