fix(scan,faucet): TokenTransfers undefined.toString + faucet confirm-poll#83
Conversation
…poll scan/components/common/TokenTransfers.tsx:189 — t.value.toString() on undefined throws when log decoding returns a Transfer with no value (malformed event, wrong topic count). Throw escapes the boundary → global-error fires (Terjadi kesalahan modal) on every tx page that includes such a log. Reproduced via playwright on a real WSRX deposit tx with the i18n /id/ locale. Fix: guard with t.value == null check, render '?' instead. apps/faucet/src/app/api/faucet/route.ts — REST mempool admit was being treated as success. Real case 2026-05-14: faucet logged 'Sent 0.01 SRX → 0x4b73...' with tx 0c86aa..., but tx evicted from mempool without finalization, recipient got 0 SRX, faucet wallet history count=0. Fix: poll /transactions/<hash> for ≤30s after broadcast; only commit the rate-limit reservation + return success if confirmed. On timeout, release reservation + return 503 with explicit error message + the unfinalized hash so user knows what was attempted.
📝 WalkthroughWalkthroughThis PR spans three independent updates. It adds a Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/scan/components/common/TokenTransfers.tsx`:
- Around line 191-195: The conditional rendering uses t.decimals directly when
calling formatTokenAmount(t.value, t.decimals) which can be non-finite
(Number(dec) from earlier conversion) and cause padStart to throw; in
TokenTransfers.tsx ensure you validate/harden decimals before calling
formatTokenAmount: coerce via Number(dec) if needed, then check
Number.isFinite(decimals) && Number.isInteger(decimals) && decimals >= 0 &&
decimals <= MAX_DECIMALS (choose a safe cap like 36 or 255), and only call
formatTokenAmount when that holds; otherwise fall back to a safe display (e.g.,
t.value.toString() or formatTokenAmount with a safe default like 0). Apply the
same validation where Number(dec) is produced so t.decimals cannot be Infinity
before use and update references to formatTokenAmount to use the
validated/clamped decimals value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 82505d1b-debd-4b0d-9aea-60e0649fdf4f
📒 Files selected for processing (3)
.github/CODEOWNERSapps/faucet/src/app/api/faucet/route.tsapps/scan/components/common/TokenTransfers.tsx
| t.value == null | ||
| ? "?" | ||
| : t.decimals != null | ||
| ? formatTokenAmount(t.value, t.decimals) | ||
| : t.value.toString(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify where decimals/value are derived and where formatting is called.
rg -n -C2 'decimals:|value:\s*args\[2\]\s*as bigint|value:\s*args\.value\s*as bigint|formatTokenAmount\(' apps/scan/components/common/TokenTransfers.tsx
# Demonstrate JS runtime behavior for unbounded decimals.
node - <<'NODE'
const value = 1n;
const badDecimals = Number(10n ** 400n); // => Infinity
try {
value.toString().padStart(badDecimals + 1, "0");
console.log("unexpected success");
} catch (e) {
console.log("expected failure:", e.name, e.message);
}
NODERepository: Sentriscloud/frontend
Length of output: 1192
Harden decimals before calling formatTokenAmount to prevent a runtime crash.
The null guard on t.value (lines 191–192) was added, but t.decimals is still untrusted. At line 92, decimals is coerced from bigint using Number(dec), which can produce Infinity for large values. In formatTokenAmount (line 222), padStart(decimals + 1, "0") will throw RangeError: Invalid string length if decimals is non-finite or very large, still triggering the global error modal.
Suggested fix
- const formatted =
- t.value == null
- ? "?"
- : t.decimals != null
- ? formatTokenAmount(t.value, t.decimals)
- : t.value.toString();
+ const safeDecimals =
+ typeof t.decimals === "number" &&
+ Number.isInteger(t.decimals) &&
+ t.decimals >= 0 &&
+ t.decimals <= 255
+ ? t.decimals
+ : undefined;
+
+ const formatted =
+ t.value == null
+ ? "?"
+ : safeDecimals != null
+ ? formatTokenAmount(t.value, safeDecimals)
+ : t.value.toString();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| t.value == null | |
| ? "?" | |
| : t.decimals != null | |
| ? formatTokenAmount(t.value, t.decimals) | |
| : t.value.toString(); | |
| const safeDecimals = | |
| typeof t.decimals === "number" && | |
| Number.isInteger(t.decimals) && | |
| t.decimals >= 0 && | |
| t.decimals <= 255 | |
| ? t.decimals | |
| : undefined; | |
| const formatted = | |
| t.value == null | |
| ? "?" | |
| : safeDecimals != null | |
| ? formatTokenAmount(t.value, safeDecimals) | |
| : t.value.toString(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/scan/components/common/TokenTransfers.tsx` around lines 191 - 195, The
conditional rendering uses t.decimals directly when calling
formatTokenAmount(t.value, t.decimals) which can be non-finite (Number(dec) from
earlier conversion) and cause padStart to throw; in TokenTransfers.tsx ensure
you validate/harden decimals before calling formatTokenAmount: coerce via
Number(dec) if needed, then check Number.isFinite(decimals) &&
Number.isInteger(decimals) && decimals >= 0 && decimals <= MAX_DECIMALS (choose
a safe cap like 36 or 255), and only call formatTokenAmount when that holds;
otherwise fall back to a safe display (e.g., t.value.toString() or
formatTokenAmount with a safe default like 0). Apply the same validation where
Number(dec) is produced so t.decimals cannot be Infinity before use and update
references to formatTokenAmount to use the validated/clamped decimals value.
Two real bugs hit by operator 2026-05-14
scan tx page errors on /id/ locale (and would on /en/ if any tx had a token transfer with malformed Transfer event)
apps/scan/components/common/TokenTransfers.tsx:189t.value.toString()thrown when log decoding returns Transfer with undefined valueerror.tsx→ user sees 'Terjadi kesalahan / Kami mengalami error' modalt.value == null→ render '?' instead of throwingfaucet logs 'Sent' but tx never finalizes
apps/faucet/src/app/api/faucet/route.ts/transactionsmempool admit was treated as successSent 0.01 SRX → 0x4b73...with tx0c86aa...— tx evicted from mempool, recipient balance = 0, faucet wallet history count = 0/transactions/<hash>for ≤30s; only commit reservation + return success on confirmation. On timeout, release reservation + 503 with explicit message + unfinalized hash so user knows what was attempted.Verify
Both apps rebuild clean.
sentrix-scan+sentrix-faucetservices restarted on vps4 to pick up new builds.Summary by CodeRabbit