const { parseEther } = require("ethers"); const { CODES, FEE_PENDING, FEE_KNOWN, FEE_UNAVAILABLE, toFixedPoint, validateTransfer, } = require("../src/shared/txValidation"); // A plausible mainnet fee: 21000 gas at 20 gwei. const FEE = 21000n * 20000000000n; // 0.00042 ETH describe("toFixedPoint", () => { test("scales human decimals to 18 places", () => { expect(toFixedPoint("1.5")).toBe(parseEther("1.5")); expect(toFixedPoint("0")).toBe(0n); }); test("rejects values it cannot represent exactly", () => { expect(toFixedPoint("not a number")).toBe(null); expect(toFixedPoint("")).toBe(null); expect(toFixedPoint(null)).toBe(null); // More precision than 18 decimals can hold. expect(toFixedPoint("0.0000000000000000001")).toBe(null); }); }); describe("validateTransfer, native ETH", () => { const eth = (over) => ({ isErc20: false, amount: "0.5", ethBalance: "1.0", feeStatus: FEE_KNOWN, feeWei: FEE, ...over, }); test("allows a send comfortably within balance", () => { const r = validateTransfer(eth()); expect(r).toEqual({ canSend: true, codes: [] }); }); test("blocks a send whose amount plus fee exceeds the balance", () => { // The whole balance: passes an amount-only check, fails once the fee // is counted. This is the bug this module exists to prevent. const r = validateTransfer(eth({ amount: "1.0", ethBalance: "1.0" })); expect(r.canSend).toBe(false); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_WITH_FEE]); }); test("blocks a send left short by less than one fee", () => { const balance = "1.0"; // One wei less headroom than the fee needs. const amount = "0.99958000000000001"; // 1.0 - 0.00042 + 1e-17 const r = validateTransfer(eth({ amount, ethBalance: balance })); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_WITH_FEE]); }); test("allows a send that leaves exactly the fee behind", () => { const r = validateTransfer( eth({ amount: "0.99958", ethBalance: "1.0" }), ); expect(r).toEqual({ canSend: true, codes: [] }); }); test("reports plain insufficient balance when the amount alone is too big", () => { const r = validateTransfer(eth({ amount: "2.0", ethBalance: "1.0" })); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH]); }); test("blocks while the fee estimate is still pending", () => { const r = validateTransfer( eth({ feeStatus: FEE_PENDING, feeWei: null }), ); expect(r.canSend).toBe(false); expect(r.codes).toEqual([CODES.FEE_PENDING]); }); test("blocks when the fee estimate failed, without assuming zero", () => { const r = validateTransfer( eth({ amount: "1.0", ethBalance: "1.0", feeStatus: FEE_UNAVAILABLE, feeWei: null, }), ); expect(r.canSend).toBe(false); expect(r.codes).toEqual([CODES.FEE_UNAVAILABLE]); // A zero fee would have let this exact transfer through. expect( validateTransfer( eth({ amount: "1.0", ethBalance: "1.0", feeWei: 0n }), ).canSend, ).toBe(true); }); test("still reports an over-balance amount before the estimate lands", () => { const r = validateTransfer( eth({ amount: "2.0", ethBalance: "1.0", feeStatus: FEE_PENDING, feeWei: null, }), ); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH, CODES.FEE_PENDING]); }); test("rejects an amount it cannot do exact arithmetic on", () => { const r = validateTransfer(eth({ amount: "abc" })); expect(r.canSend).toBe(false); expect(r.codes).toEqual([CODES.AMOUNT_INVALID]); }); test("treats a missing balance as zero, not as unlimited", () => { const r = validateTransfer(eth({ ethBalance: undefined })); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH]); }); }); describe("validateTransfer, ERC-20", () => { const erc20 = (over) => ({ isErc20: true, amount: "100.0", tokenBalance: "250.0", ethBalance: "1.0", feeStatus: FEE_KNOWN, feeWei: FEE, ...over, }); test("allows a transfer with tokens to spend and ETH for the fee", () => { expect(validateTransfer(erc20())).toEqual({ canSend: true, codes: [] }); }); test("checks the token amount against the token balance", () => { const r = validateTransfer(erc20({ amount: "250.000001" })); expect(r.codes).toEqual([CODES.INSUFFICIENT_TOKEN]); }); test("does not charge the fee against the token balance", () => { // The full token balance is sendable: the fee is paid in ETH. expect(validateTransfer(erc20({ amount: "250.0" })).canSend).toBe(true); }); test("blocks when the ETH balance does not cover the fee", () => { const r = validateTransfer(erc20({ ethBalance: "0.0001" })); expect(r.canSend).toBe(false); expect(r.codes).toEqual([CODES.INSUFFICIENT_ETH_FOR_FEE]); }); test("allows a fee exactly equal to the ETH balance", () => { const r = validateTransfer(erc20({ ethBalance: "0.00042" })); expect(r).toEqual({ canSend: true, codes: [] }); }); test("reports both shortfalls when tokens and ETH are both short", () => { const r = validateTransfer( erc20({ amount: "300.0", ethBalance: "0.0" }), ); expect(r.codes).toEqual([ CODES.INSUFFICIENT_TOKEN, CODES.INSUFFICIENT_ETH_FOR_FEE, ]); }); test("blocks while the fee estimate is pending or failed", () => { expect( validateTransfer(erc20({ feeStatus: FEE_PENDING, feeWei: null })) .codes, ).toEqual([CODES.FEE_PENDING]); expect( validateTransfer( erc20({ feeStatus: FEE_UNAVAILABLE, feeWei: null }), ).codes, ).toEqual([CODES.FEE_UNAVAILABLE]); }); test("treats a missing token balance as zero", () => { const r = validateTransfer(erc20({ tokenBalance: undefined })); expect(r.codes).toEqual([CODES.INSUFFICIENT_TOKEN]); }); });