// The base-unit amount an ERC-20 transfer from the wallet's own Send screen is
// encoded with.
//
// A token amount is a decimal string plus a scale, and the two come from
// different places. The confirmation screen renders the amount, the balance and
// the symbol from the block explorer's cached metadata (see
// fetchTokenBalances() in balances.js); the transfer used to be encoded from
// decimals() read off the contract at signing time, and nothing compared the
// two. A token whose on-chain scale differs from the cached one — an
// upgradeable or proxy token, a caller-dependent one, a stale or wrong explorer
// entry — therefore signed an amount that was never displayed, off by a power
// of ten for every decimal place of disagreement.
//
// So the scale used to encode is the scale the screen rendered with, carried
// forward on the pending transaction, and the contract's own answer is read
// only to be compared with it. A disagreement is a refusal, never a preference
// for either number: the wallet cannot tell which of the two the user meant,
// and both candidate transfers move an amount nobody approved.
//
// This is the confirmTx counterpart to approvalVerify.js, which does the same
// job for the dApp approval path, and it takes the same stance: a quantity that
// cannot be compared with what was displayed has not been checked, so an absent
// or unusable value is refused rather than filled in.
//
// Every message here is shown to the user on the transaction error screen, so
// each is a full sentence and names the numbers it is refusing over.

const { parseUnits } = require("ethers");

// Solidity's decimals() returns a uint8, so anything outside that range is not
// an answer this wallet can use.
const MAX_DECIMALS = 255;

const UNKNOWN_DISPLAYED_DECIMALS_MESSAGE =
    "The transfer was not sent, because the number of decimal places this" +
    " amount was shown with is unknown, so the amount that would be signed" +
    " cannot be shown to be the amount that was displayed.";

const UNREADABLE_CONTRACT_DECIMALS_MESSAGE =
    "The transfer was not sent, because the token contract did not report a" +
    " usable number of decimal places, so the amount that would be signed" +
    " cannot be checked against the amount that was displayed.";

function mismatchMessage(displayed, onChain) {
    return (
        "The transfer was not sent. The token contract reports " +
        onChain +
        " decimal places, but the amount was displayed using " +
        displayed +
        ", so signing it would move a different amount than the one shown." +
        " Reopen the wallet to reload this token's details and try again."
    );
}

// A decimals value from any source as a number, or null if it is not one.
// decimals() comes back from ethers as a bigint and the explorer's copy arrives
// as a string, so both of those are accepted alongside a plain number; anything
// fractional, negative, out of uint8 range, or of any other type at all is not.
//
// The types are enumerated rather than coerced because Number() is far too
// willing: Number([]) is 0 and Number(true) is 1, so a coercing check would
// admit an empty array as a scale of zero and encode a whole-token transfer
// against it. Absence answers null and never a default, and a real scale of
// ZERO answers 0 — the two are different answers, which is the whole point:
// a falsy-collapsing `value || 18` cannot tell them apart, and neither can a
// reader of what it wrote (https://git.eeqj.de/sneak/AutistMask/issues/246).
//
// Exported because every module that has to decide whether it knows a token's
// scale needs exactly this test, and three separate copies of it is three
// places for the answer to drift: approvalAmount.js resolves the scale the
// approval screens display at, and balances.js decides what the explorer
// actually reported before it is stored.
function toDecimals(value) {
    let n;
    if (typeof value === "number") {
        n = value;
    } else if (typeof value === "bigint") {
        if (value < 0n || value > BigInt(MAX_DECIMALS)) return null;
        n = Number(value);
    } else if (typeof value === "string") {
        if (!/^[0-9]+$/.test(value)) return null;
        n = Number(value);
    } else {
        return null;
    }
    if (!Number.isInteger(n) || n < 0 || n > MAX_DECIMALS) return null;
    return n;
}

// The decimals the confirmation screen rendered an amount with, as a number.
// Throws when the pending transaction does not carry a usable one — which is
// also what keeps the gas estimate from quietly estimating a different transfer
// than the one that would be signed.
function displayedDecimals(value) {
    const displayed = toDecimals(value);
    if (displayed === null) {
        throw new Error(UNKNOWN_DISPLAYED_DECIMALS_MESSAGE);
    }
    return displayed;
}

// The transfer amount in the token's base units, or a throw. `amount` is the
// decimal string the user typed and the screen displayed, `displayed` is the
// scale it was displayed at, and `onChain` is what the contract's decimals()
// answered at signing time. The two scales must agree.
function transferAmountUnits(amount, displayed, onChain) {
    const shown = displayedDecimals(displayed);
    const reported = toDecimals(onChain);
    if (reported === null) {
        throw new Error(UNREADABLE_CONTRACT_DECIMALS_MESSAGE);
    }
    if (reported !== shown) {
        throw new Error(mismatchMessage(shown, reported));
    }
    return parseUnits(String(amount), shown);
}

module.exports = {
    displayedDecimals,
    transferAmountUnits,
    mismatchMessage,
    toDecimals,
    MAX_DECIMALS,
    UNKNOWN_DISPLAYED_DECIMALS_MESSAGE,
    UNREADABLE_CONTRACT_DECIMALS_MESSAGE,
};
