diff --git a/.changeset/fix-balance-truncation.md b/.changeset/fix-balance-truncation.md new file mode 100644 index 0000000..4ad94c6 --- /dev/null +++ b/.changeset/fix-balance-truncation.md @@ -0,0 +1,5 @@ +--- +"@walletconnect/staking-cli": patch +--- + +Fix balance display rounding up, causing full-balance stake to revert. `formatWCT` now truncates to 2 decimal places instead of rounding. `stake` checks on-chain balance before approving to avoid wasting gas. diff --git a/packages/staking-cli/src/commands.ts b/packages/staking-cli/src/commands.ts index 888791a..ca61246 100644 --- a/packages/staking-cli/src/commands.ts +++ b/packages/staking-cli/src/commands.ts @@ -62,6 +62,16 @@ export async function stake( console.log(`\nAdding ${amount} WCT${extendingTime ? `, extending lock to ${formatDate(Number(effectiveUnlockTime))}` : ""}...`); + // Check balance before approving to avoid wasting gas on a doomed stake + const bal = await readUint256(buildBalanceOfCallData(address)); + if (bal < amountWei) { + console.error( + `\nInsufficient balance: you have ${formatWCT(bal)} WCT but tried to stake ${amount} WCT.` + + `\nUse the displayed balance (or less) to avoid a reverted transaction.`, + ); + return; + } + // Check allowance and approve if needed const allowance = await readUint256(buildAllowanceCallData(address, STAKE_WEIGHT_ADDRESS)); if (allowance < amountWei) { diff --git a/packages/staking-cli/src/format.ts b/packages/staking-cli/src/format.ts index ab19bb7..44c1bd7 100644 --- a/packages/staking-cli/src/format.ts +++ b/packages/staking-cli/src/format.ts @@ -7,10 +7,17 @@ import { MAX_LOCK_WEEKS, } from "./constants.js"; -/** Format a bigint WCT amount as a human-readable string with 2 decimals */ +/** Format a bigint WCT amount as a human-readable string with 2 decimals (truncated, never rounded up) */ export function formatWCT(amount: bigint): string { const raw = formatUnits(amount, WCT_DECIMALS); - const num = parseFloat(raw); + // Truncate to 2 decimal places instead of rounding to avoid displaying + // a value larger than the actual on-chain balance + const dotIndex = raw.indexOf("."); + const truncated = + dotIndex === -1 + ? `${raw}.00` + : raw.slice(0, dotIndex + 3).padEnd(dotIndex + 3, "0"); + const num = parseFloat(truncated); return num.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, diff --git a/packages/staking-cli/test/format.test.ts b/packages/staking-cli/test/format.test.ts index d47d108..2ccc123 100644 --- a/packages/staking-cli/test/format.test.ts +++ b/packages/staking-cli/test/format.test.ts @@ -19,6 +19,16 @@ describe("formatWCT", () => { // 1.5 WCT expect(formatWCT(1500000000000000000n)).toBe("1.50"); }); + + it("truncates instead of rounding up", () => { + // 112.887964254423744364 WCT — must display 112.88, not 112.89 + expect(formatWCT(112887964254423744364n)).toBe("112.88"); + }); + + it("truncates .999 without rounding to next integer", () => { + // 9.999 WCT — must display 9.99, not 10.00 + expect(formatWCT(9999000000000000000n)).toBe("9.99"); + }); }); describe("formatDate", () => {