Skip to content

feat: add AllScale stablecoin payment gateway integration#3434

Open
surfingtomchen wants to merge 3 commits intoQuantumNous:mainfrom
allscale-io:feat/allscale-payment-gateway
Open

feat: add AllScale stablecoin payment gateway integration#3434
surfingtomchen wants to merge 3 commits intoQuantumNous:mainfrom
allscale-io:feat/allscale-payment-gateway

Conversation

@surfingtomchen
Copy link

@surfingtomchen surfingtomchen commented Mar 25, 2026

Summary

  • Add AllScale payment gateway supporting USDT stablecoin checkout
  • Implement HMAC-SHA256 request signing and webhook signature verification
  • Add checkout intent creation, status polling, and webhook callback handling
  • Add AllScale settings UI: API key, secret, webhook ID, and USD/quota currency rate
  • Add live price preview and topup history with transaction details
  • Support configurable USD/quota currency rate via admin settings
  • Add i18n translations for all AllScale-related UI strings (en/zh/fr/ru/ja/vi)

Key Changes

Backend (controller/, model/, setting/, router/)

  • controller/topup_allscale.go — new controller: checkout intent creation, status polling, and webhook callback verification
  • controller/topup.go — integrate AllScale into the topup flow
  • model/topup.go — extend topup model with AllScale transaction fields
  • model/option.go — add AllScale configuration keys (API key, secret, webhook ID, currency rate)
  • setting/payment_allscale.go — AllScale settings accessor
  • router/api-router.go — register AllScale payment routes

Frontend (web/src/)

  • web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx — admin settings panel with live price preview
  • web/src/components/topup/index.jsx — AllScale checkout flow in topup UI
  • web/src/components/topup/modals/TopupHistoryModal.jsx — display AllScale transaction details
  • web/src/components/settings/PaymentSetting.jsx — register AllScale settings tab
  • web/src/components/topup/RechargeCard.jsx — AllScale payment option card

Internationalization

  • Full i18n coverage across all 6 frontend locales: en, zh-CN, zh-TW, fr, ru, ja, vi

Validation

# Backend
go build ./...
go test ./controller/... ./model/... ./router/...

# Frontend
cd web && bun install && bun run build
  • Manual end-to-end test: checkout intent creation, polling, and webhook callback verified locally
  • Admin settings panel renders correctly; live price preview updates on rate change
  • Topup history modal displays AllScale transaction metadata

Notes

  • AllScale uses USDT (ERC-20 / TRC-20) as the settlement currency. The admin configures a USD→quota conversion rate.
  • Webhook verification uses HMAC-SHA256 over the raw request body; the secret is stored in admin settings.
  • No schema migrations required beyond ALTER TABLE ... ADD COLUMN (fully SQLite/MySQL/PostgreSQL compatible).

✅ Submission Checklist / 提交清单

  • Changes are scoped to AllScale integration only — no unrelated modifications
  • All three databases supported (SQLite, MySQL, PostgreSQL)
  • Frontend built successfully with bun run build
  • i18n keys added for all 6 supported locales
  • No credentials or secrets committed
  • Webhook signature verification implemented (no unauthenticated callbacks accepted)

Summary by CodeRabbit

  • New Features

    • Added AllScale as a USDT payment option for online recharges (selectable alongside EPay and Stripe)
    • UI: AllScale amount preview, USDT payment button, payment status polling, and history refresh after completion
    • Admin settings: new AllScale configuration page to enable and enter API credentials
  • Documentation

    • Updated README and localized guides to list AllScale as a supported online top-up provider

- Add AllScale payment gateway with USDT crypto checkout support
- Implement HMAC-SHA256 request signing and webhook signature verification
- Add checkout intent creation, status polling, and webhook callback handling
- Add AllScale settings UI (API key, secret, webhook ID, currency rate)
- Add live price preview and topup history with transaction details
- Support configurable USD/quota currency rate via admin settings
- Add i18n translations for all AllScale-related UI strings (en/zh/fr/ru/ja/vi)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Walkthrough

This PR adds AllScale USDT top-up support: README updates, new AllScale settings and flags, backend controllers for checkout/initiate/status/webhook with HMAC signing, model recharge handling, router endpoints, and frontend UI/i18n changes to support AllScale flows.

Changes

Cohort / File(s) Summary
Documentation
README.md, README.fr.md, README.ja.md, README.zh_CN.md, README.zh_TW.md
Added AllScale to the online top-up/payment provider listings.
Backend config & settings
setting/payment_allscale.go, model/option.go
New AllScale config variables and integration into option map and runtime updates (enabled flag, API key/secret, base URL, unit price, derived key-set flags).
Backend top-up surface
controller/topup.go
Expose AllScale enablement and minimum top-up in GetTopUpInfo response.
AllScale payment controller
controller/topup_allscale.go
New controller implementing checkout initiation, status polling, amount preview, webhook verification, HMAC-SHA256 request signing, and handlers: RequestAllScalePay, AllScaleWebhook, RequestAllScaleAmount, GetAllScaleStatus.
Model recharge logic
model/topup.go
Added RechargeAllScale(tradeNo string) to atomically complete a pending AllScale top-up and credit quota.
Routing
router/api-router.go
Registered /api/allscale/webhook and user endpoints /api/user/allscale/amount, /api/user/allscale/pay, /api/user/allscale/status.
Frontend settings UI
web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx, web/src/components/settings/PaymentSetting.jsx
Added AllScale settings page component and integrated a new settings card into payment settings UI with form validation and credential handling.
Frontend top-up UI
web/src/components/topup/index.jsx, web/src/components/topup/RechargeCard.jsx, web/src/components/topup/modals/TopupHistoryModal.jsx
Added AllScale enablement state, amount preview, localStorage-persisted pending-payment polling, polling cleanup/resume, UI buttons/labels for AllScale, and history reload hook; updated modal to accept reloadKey.
i18n
web/src/i18n/locales/... (en.json, fr.json, ja.json, ru.json, vi.json, zh-CN.json, zh-TW.json)
Added AllScale/USDT translation keys and small JSON fixes (trailing commas) across multiple locales.

Sequence Diagram

sequenceDiagram
    participant Client as Client (Browser)
    participant Server as Backend Server
    participant AllScale as AllScale API
    participant DB as Database

    Client->>Server: POST /api/user/allscale/pay (amount)
    activate Server
    Server->>DB: Create pending TopUp record
    Server->>AllScale: POST /v1/checkout_intents (signed HMAC request)
    activate AllScale
    AllScale-->>Server: { intent_id, checkout_url, trade_no }
    deactivate AllScale
    Server-->>Client: { checkout_url, intent_id, order_id }
    deactivate Server

    Client->>AllScale: Open checkout_url (user completes payment)

    AllScale->>Server: POST /api/allscale/webhook (signed webhook)
    activate Server
    Server->>Server: Verify webhook signature & timestamp
    Server->>DB: Lock TopUp by trade_no
    Server->>DB: Mark TopUp as Success, increment quota
    Server-->>AllScale: HTTP 200 OK
    deactivate Server

    Client->>Server: GET /api/user/allscale/status (intent_id, trade_no)
    activate Server
    Server->>DB: Check local TopUp status
    alt status terminal locally
        Server-->>Client: { status: success/failed }
    else
        Server->>AllScale: GET /v1/checkout_intents/{intent_id}/status (signed)
        AllScale-->>Server: { status }
        alt CONFIRMED
            Server->>DB: Lock and Recharge if needed
        else other terminal
            Server->>DB: Mark failure state
        end
        Server-->>Client: { status, ... }
    end
    deactivate Server
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • creamlike1024
  • seefs001

Poem

🐰 I hopped through settings, docs, and code,
Signed webhooks, polling, and a checkout road.
AllScale whispers USDT so bright,
Quotas bloom, the top-ups light.
Hooray — a tiny rabbit dance tonight! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add AllScale stablecoin payment gateway integration' directly and clearly summarizes the main change: adding AllScale as a new payment gateway for stablecoin (USDT) transactions across backend and frontend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (4)
controller/topup.go (1)

100-100: Consider making allscale_min_topup configurable.

The value is hardcoded to 1, while other payment providers use configurable settings (setting.StripeMinTopUp, setting.WaffoMinTopUp). For consistency and admin flexibility, consider adding an AllScaleMinTopUp setting.

If this is intentionally hardcoded (e.g., due to USDT stablecoin constraints), a brief comment explaining the rationale would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup.go` at line 100, The hardcoded "allscale_min_topup": 1
should be made configurable: add a new setting (e.g., AllScaleMinTopUp) similar
to StripeMinTopUp/WaffoMinTopUp, wire it into the configuration/bootstrap so
setting.AllScaleMinTopUp is available, and replace the literal 1 in the topup
payload (allscale_min_topup) with that setting; if keeping it fixed is
intentional, add a brief inline comment next to allscale_min_topup explaining
the rationale (e.g., USDT constraint) instead of leaving the magic number.
controller/topup_allscale.go (3)

91-105: Remove redundant * 1.0 multiplication.

Line 104 has amount * 1.0 * topupGroupRatio * discount where * 1.0 serves no purpose since amount is already a float64.

Proposed fix
-	return amount * 1.0 * topupGroupRatio * discount
+	return amount * topupGroupRatio * discount
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 91 - 105, In getAllScalePayMoney
remove the redundant multiplication by 1.0: change the return expression from
"amount * 1.0 * topupGroupRatio * discount" to simply multiply amount by
topupGroupRatio and discount (e.g., "amount * topupGroupRatio * discount") so
the result remains float64 without the no-op "* 1.0".

489-491: Handle io.ReadAll error for consistency.

Same pattern as line 266 — the error is ignored. Consider handling it for consistency and better diagnostics.

Proposed fix
-	respBody, _ := io.ReadAll(resp.Body)
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Printf("AllScale status: failed to read response body: %v", err)
+		c.JSON(200, gin.H{"message": "error", "data": "failed to read status response"})
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 489 - 491, The call to
io.ReadAll(resp.Body) ignores its error; change it to capture (respBody, err :=
io.ReadAll(resp.Body)) and handle err similarly to the pattern at line 266 —
e.g., log the error via the existing logger or return the error from the
enclosing function; ensure resp.Body is still closed and that any subsequent use
of respBody only happens when err == nil. This affects the code around respBody
and io.ReadAll calls and should mirror the error-handling behavior used
elsewhere in this file.

265-267: Handle io.ReadAll error.

The error from io.ReadAll is silently ignored. While unlikely to fail after a successful HTTP response, it's good practice to handle it, especially since this affects error message quality on parse failures.

Proposed fix
-	respBody, _ := io.ReadAll(resp.Body)
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Printf("AllScale: failed to read response body: %v", err)
+		topUp.Status = common.TopUpStatusFailed
+		_ = topUp.Update()
+		c.JSON(200, gin.H{"message": "error", "data": "failed to read payment response"})
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 265 - 267, The call respBody, _ :=
io.ReadAll(resp.Body) ignores read errors; update the code in the function that
reads the Allscale response to capture the error (e.g., respBody, err :=
io.ReadAll(resp.Body)), check err, and handle it by logging and/or returning an
appropriate error response (or wrapping it into the existing error handling path
that uses respBody) so parse failures produce meaningful messages; make sure to
reference the respBody and resp.Body usage when implementing the check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/topup_allscale.go`:
- Around line 69-87: The verifyAllScaleWebhook function currently verifies the
signature but omits timestamp window checks; update verifyAllScaleWebhook to
parse the timestamp string (e.g., as Unix seconds or RFC3339 depending on how
AllScale sends it), validate parsing succeeds, compute the difference between
time.Now().UTC() and the parsed timestamp, and reject the request (return false)
if the absolute difference exceeds a configurable window (suggest 5 minutes);
keep the rest of the signature verification using setting.AllScaleApiSecret,
allScaleSha256Hex, and allScaleSign unchanged and ensure parse errors also
return false.
- Line 8: Remove the direct encoding/json import and replace any use of
json.RawMessage with the []byte type (e.g., change function parameters, structs,
or variables currently typed as json.RawMessage to []byte), and update any JSON
marshal/unmarshal calls in the same scope to call the wrapper functions in
common/json.go instead of encoding/json; search for usages of the symbol
json.RawMessage in controller/topup_allscale.go and the surrounding code paths
to update types and call sites, then delete the encodingjson import entry.

In `@model/topup.go`:
- Around line 398-420: In RechargeAllScale, before marking the topUp as success
and crediting quota, add a guard that verifies topUp.PaymentMethod is the
allscale payment method (e.g. compare topUp.PaymentMethod to
common.PaymentMethodAllScale or the literal "allscale") and return an error if
it isn't; place this check after verifying Status is pending (using the existing
topUp.Status checks) and before computing quotaToAdd/setting CompleteTime/Status
and saving/updating the User quota so non-allscale orders cannot be settled by
trade_no.

In `@router/api-router.go`:
- Line 52: The route registration apiRouter.POST("/allscale/webhook",
controller.AllScaleWebhook) should include the endpoint-level limiter; update
the call to attach middleware.CriticalRateLimit() so the route uses that
specific middleware (e.g., apiRouter.POST("/allscale/webhook",
middleware.CriticalRateLimit(), controller.AllScaleWebhook)). Locate the POST
registration for "/allscale/webhook" in router/api-router.go and insert
middleware.CriticalRateLimit() into the handler chain before
controller.AllScaleWebhook.

In `@setting/payment_allscale.go`:
- Line 7: The AllScaleUnitPrice global currently defaults to 1.0 which conflicts
with UI/docs; change the default value of the AllScaleUnitPrice variable to
0.002 so the code aligns with the documented "USD amount per quota unit, default
0.002" (update the declaration of AllScaleUnitPrice accordingly and run
tests/greps to ensure no other hardcoded 1.0 defaults remain).

In `@web/src/components/topup/index.jsx`:
- Around line 497-500: Replace the hard-coded minimum of 1 in allScaleTopUp (and
any other USDT checkout branches) with the configured AllScale minimum loaded
into component state: add a state variable (e.g., minAllScale) and populate it
when the component mounts or when gateway/config props change by reading the
AllScale-specific minimum from the same config/store used for gateway settings
(the code block around the existing config load referenced in the file). Then
change the check in allScaleTopUp from "if (topUpCount < 1)" to "if (topUpCount
< minAllScale)" and update the showError call to use minAllScale so the frontend
enforces the backend's minimum floor.
- Around line 421-455: startAllScalePolling: avoid overlapping async intervals
by preventing reentrancy—replace the setInterval(async ...) approach with either
a self-scheduling loop using setTimeout that waits for the async
pollAllScaleStatusOnce(intentId, tradeNo) to complete before scheduling the next
tick, or add an inFlight boolean guard (e.g. allScalePollingInFlight) checked at
the top of the interval callback and cleared after the await so a new invocation
cannot run concurrently; ensure clearAllScalePolling/clearAllScalePendingPayment
and terminal toast/state updates only run once when result.state is 'success' or
'failed'. allScaleTopUp: fix minimum amount validation by comparing the
user-entered topUpCount to the dynamic minTopUp variable (use if (topUpCount <
minTopUp)) instead of the hard-coded 1 so AllScale follows the same validation
as Stripe and other payment methods.

In `@web/src/components/topup/modals/TopupHistoryModal.jsx`:
- Line 56: The new i18n key 'allscale' in the payment-method mapping inside
TopupHistoryModal.jsx is not following the repo convention; replace the English
key with a Chinese source string (e.g., the Chinese label you want shown) and
update any usages to call t('你的中文源字符串') instead of t('allscale'); locate the
payment-methods mapping (where the entry is allscale: 'AllScale Checkout') and
change the property key to the Chinese text, ensuring the component uses the
same Chinese key when calling t(...) so it resolves via the locale files.

In `@web/src/i18n/locales/en.json`:
- Around line 3348-3370: The en.json is missing the exact locale key requested
by render.jsx: add the missing key "输出价格:{{symbol}}{{price}} / 1M tokens" to
web/src/i18n/locales/en.json with the English value matching the existing
variant (e.g. "Output Price: {{symbol}}{{price}} / 1M tokens") so render.jsx
(line ~2320) can find the translation and avoid raw/untranslated text; ensure
the key string exactly matches what render.jsx calls.

In `@web/src/i18n/locales/fr.json`:
- Around line 3323-3324: Replace the English keys "AllScale API Key" and
"AllScale API Secret" in the locale file with the Chinese source strings you use
as canonical keys (e.g., the project's standard Chinese labels), update the
French translations as values for those Chinese keys in fr.json (keeping the
French text "Clé API AllScale" and "Secret API AllScale"), and then update every
caller that currently uses t('AllScale API Key') or t('AllScale API Secret') to
use t('中文key') instead (search for those exact English keys in components that
use useTranslation()). Ensure keys remain flat in the JSON and use the exact
Chinese strings everywhere so translation lookups match.

In `@web/src/i18n/locales/zh-TW.json`:
- Around line 2990-2992: The zh-TW locale values use Simplified wording ("充值")
and mixed simplified characters; update the values for the keys "使用 USDT 充值",
"充值成功", and "支付处理中,请稍后在充值账单中查看结果" to proper Traditional Chinese – e.g. change
"使用 USDT 充值" -> "使用 USDT 儲值", "充值成功" -> "儲值成功", and "支付处理中,请稍后在充值账单中查看结果" ->
"支付處理中,請稍後在儲值帳單中查看結果" so the zh-TW file is consistently Traditional.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx`:
- Around line 87-115: The current flow builds options and issues parallel
API.put('/api/option/') calls via requestQueue = options.map(...) and
Promise.all, which can enable AllScale before credentials/base URL are
persisted; change the logic to persist AllScaleApiKey, AllScaleApiSecret and
AllScaleBaseURL first (using API.put calls for those keys) and only after those
succeed send the API.put for AllScaleEnabled, or replace the multiple API.put
calls with a single backend endpoint that atomically updates all options; update
the code paths that reference options, requestQueue, Promise.all and results to
reflect the sequential or single-call approach.
- Around line 156-166: The component SettingsPaymentGatewayAllScale.jsx mixes
raw English strings with translation keys; replace all literal English UI copy
(e.g., the anchor text "AllScale Official Site" and labels "API Key", "API
Secret", "API Base URL" used in this component) with Chinese source-string keys
passed to t(...) (for example t('AllScale 官方站点'), t('API 密钥'), t('API 密钥(私有)'),
t('API 基础 URL') or other approved Chinese keys) so the component consistently
uses i18n lookups (not raw English). Update the Form.Section text prop and the
Text/anchor content and any input labels or placeholders rendered in this file
to use t('中文key') and add the corresponding Chinese keys and translations into
the locale JSON(s) under web/src/i18n. Ensure no literal English UI strings
remain in this component so all locales will correctly fall back through the
i18n system.

---

Nitpick comments:
In `@controller/topup_allscale.go`:
- Around line 91-105: In getAllScalePayMoney remove the redundant multiplication
by 1.0: change the return expression from "amount * 1.0 * topupGroupRatio *
discount" to simply multiply amount by topupGroupRatio and discount (e.g.,
"amount * topupGroupRatio * discount") so the result remains float64 without the
no-op "* 1.0".
- Around line 489-491: The call to io.ReadAll(resp.Body) ignores its error;
change it to capture (respBody, err := io.ReadAll(resp.Body)) and handle err
similarly to the pattern at line 266 — e.g., log the error via the existing
logger or return the error from the enclosing function; ensure resp.Body is
still closed and that any subsequent use of respBody only happens when err ==
nil. This affects the code around respBody and io.ReadAll calls and should
mirror the error-handling behavior used elsewhere in this file.
- Around line 265-267: The call respBody, _ := io.ReadAll(resp.Body) ignores
read errors; update the code in the function that reads the Allscale response to
capture the error (e.g., respBody, err := io.ReadAll(resp.Body)), check err, and
handle it by logging and/or returning an appropriate error response (or wrapping
it into the existing error handling path that uses respBody) so parse failures
produce meaningful messages; make sure to reference the respBody and resp.Body
usage when implementing the check.

In `@controller/topup.go`:
- Line 100: The hardcoded "allscale_min_topup": 1 should be made configurable:
add a new setting (e.g., AllScaleMinTopUp) similar to
StripeMinTopUp/WaffoMinTopUp, wire it into the configuration/bootstrap so
setting.AllScaleMinTopUp is available, and replace the literal 1 in the topup
payload (allscale_min_topup) with that setting; if keeping it fixed is
intentional, add a brief inline comment next to allscale_min_topup explaining
the rationale (e.g., USDT constraint) instead of leaving the magic number.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 997cf817-bec7-410a-b7de-3c2901321472

📥 Commits

Reviewing files that changed from the base of the PR and between dbf900a and ac9f2e0.

📒 Files selected for processing (23)
  • README.fr.md
  • README.ja.md
  • README.md
  • README.zh_CN.md
  • README.zh_TW.md
  • controller/topup.go
  • controller/topup_allscale.go
  • model/option.go
  • model/topup.go
  • router/api-router.go
  • setting/payment_allscale.go
  • web/src/components/settings/PaymentSetting.jsx
  • web/src/components/topup/RechargeCard.jsx
  • web/src/components/topup/index.jsx
  • web/src/components/topup/modals/TopupHistoryModal.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx

"crypto/hmac"
"crypto/sha256"
"encoding/base64"
encodingjson "encoding/json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid direct encoding/json import per coding guidelines.

The coding guidelines prohibit direct import of encoding/json in business code. While this import is only used for the json.RawMessage type (line 392), it still violates the guideline. Since json.RawMessage is simply a []byte alias, you can replace it with []byte directly.

Proposed fix
-	encodingjson "encoding/json"

Then at line 392:

-	Payload encodingjson.RawMessage `json:"payload"`
+	Payload []byte `json:"payload"`

And at line 400:

-func parseAllScaleNumericStatus(raw encodingjson.RawMessage) (int, error) {
+func parseAllScaleNumericStatus(raw []byte) (int, error) {

As per coding guidelines: "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go. Do NOT directly import or call encoding/json in business code."

📝 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.

Suggested change
encodingjson "encoding/json"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` at line 8, Remove the direct encoding/json
import and replace any use of json.RawMessage with the []byte type (e.g., change
function parameters, structs, or variables currently typed as json.RawMessage to
[]byte), and update any JSON marshal/unmarshal calls in the same scope to call
the wrapper functions in common/json.go instead of encoding/json; search for
usages of the symbol json.RawMessage in controller/topup_allscale.go and the
surrounding code paths to update types and call sites, then delete the
encodingjson import entry.

var AllScaleApiKey = ""
var AllScaleApiSecret = ""
var AllScaleBaseURL = "https://openapi.allscale.io"
var AllScaleUnitPrice float64 = 1.0
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for AllScaleUnitPrice usage and documented defaults across the codebase
echo "=== Checking AllScaleUnitPrice references ==="
rg -n "AllScaleUnitPrice" --type go

echo ""
echo "=== Checking i18n references to 0.002 default ==="
rg -n "0\.002" web/src/i18n/locales/

echo ""
echo "=== Checking controller usage ==="
rg -n -C3 "UnitPrice" controller/

Repository: QuantumNous/new-api

Length of output: 3990


Critical billing miscalculation risk: AllScaleUnitPrice default value mismatch.

The AllScaleUnitPrice default is hardcoded as 1.0, but all UI translations consistently document the default as 0.002 ("USD amount per quota unit, default 0.002" across en.json, zh-CN.json, zh-TW.json, vi.json, ru.json, ja.json, and fr.json). This 500× discrepancy will cause billing to charge $0.002 per unit when administrators expect $1.0, or vice versa, leading to severe revenue loss or overcharging.

Update the default to match documented specifications:

Fix
-var AllScaleUnitPrice float64 = 1.0
+var AllScaleUnitPrice float64 = 0.002
📝 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.

Suggested change
var AllScaleUnitPrice float64 = 1.0
var AllScaleUnitPrice float64 = 0.002
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setting/payment_allscale.go` at line 7, The AllScaleUnitPrice global
currently defaults to 1.0 which conflicts with UI/docs; change the default value
of the AllScaleUnitPrice variable to 0.002 so the code aligns with the
documented "USD amount per quota unit, default 0.002" (update the declaration of
AllScaleUnitPrice accordingly and run tests/greps to ensure no other hardcoded
1.0 defaults remain).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0.002 default documented in the locale files (key "每个配额单位对应的 USD 金额,默认 0.002") belongs to a different setting inherited from Waffo — it is not the AllScale unit-price field. The AllScale label key is "货币汇率(每 1 美元等于多少货币单位)" (currency rate: units per 1 USD), and its default of 1.0 is correct: 1 USD = 1 USDT (stablecoins are pegged 1:1 to USD). Changing it to 0.002 would mean 0.002 USDT = 1 USD, which is a 500x error in the opposite direction. Leaving as-is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@surfingtomchen, thank you for the clear explanation. You're right — I incorrectly conflated two distinct settings. The 0.002 documented in the locale files belongs to a different Waffo-inherited setting (每个配额单位对应的 USD 金额,默认 0.002), not the AllScale field. The AllScale UnitPrice represents a currency exchange rate (units of currency per 1 USD), and since USDT is a stablecoin pegged 1:1 to USD, the default of 1.0 is semantically correct. Apologies for the noise.


🧠 Learnings used
Learnt from: RedwindA
Repo: QuantumNous/new-api PR: 2925
File: controller/ratio_sync.go:601-614
Timestamp: 2026-02-12T06:23:03.642Z
Learning: OpenRouter API can return models with free input pricing (prompt="0") but non-zero completion pricing, requiring division-by-zero guards when computing completion_ratio and cache_ratio in controller/ratio_sync.go.

Comment on lines +497 to +500
const allScaleTopUp = async () => {
if (topUpCount < 1) {
showError(t('充值数量不能小于') + 1);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the configured minimum for AllScale checkout.

Lines 662-678 never load an AllScale-specific minimum into state, and Line 498 still hard-codes 1. If the gateway is configured with a higher floor, the USDT path can open checkout for an amount the backend will reject.

Also applies to: 662-678

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 497 - 500, Replace the
hard-coded minimum of 1 in allScaleTopUp (and any other USDT checkout branches)
with the configured AllScale minimum loaded into component state: add a state
variable (e.g., minAllScale) and populate it when the component mounts or when
gateway/config props change by reading the AllScale-specific minimum from the
same config/store used for gateway settings (the code block around the existing
config load referenced in the file). Then change the check in allScaleTopUp from
"if (topUpCount < 1)" to "if (topUpCount < minAllScale)" and update the
showError call to use minAllScale so the frontend enforces the backend's minimum
floor.

Comment on lines +3348 to +3370
"支持精确匹配;使用 regex: 开头可按正则匹配。": "Supports exact matching. Use a regex: prefix for regex matching.",
"AllScale USDT 设置": "AllScale USDT Settings",
"AllScale是一个专注稳定币支付的super app,提供稳定币收单及各类支付产品。": "AllScale is a super app focused on stablecoin payments, offering stablecoin acquiring and various payment products.",
"请在 AllScale 后台获取 API Key 和 API Secret,并在回调地址中填写 {your_domain}/api/allscale/webhook。": "Get your API Key and API Secret from the AllScale dashboard, and set the webhook callback URL to {your_domain}/api/allscale/webhook.",
"启用 AllScale": "Enable AllScale",
"默认 https://openapi.allscale.io,无需修改": "Default https://openapi.allscale.io, no change needed",
"单价 (USD / 配额单位)": "Unit Price (USD / quota unit)",
"每个配额单位对应的 USD 金额,默认 0.002": "USD amount per quota unit, default 0.002",
"最低充值金额 (USD)": "Minimum Top-up Amount (USD)",
"最低支付金额(美元),默认 $0.20": "Minimum payment amount in USD, default $0.20",
"启用 AllScale 时,API Key 不能为空": "API Key is required when AllScale is enabled",
"启用 AllScale 时,API Secret 不能为空": "API Secret is required when AllScale is enabled",
"AllScale API Key": "AllScale API Key",
"AllScale API Secret": "AllScale API Secret",
"使用 USDT 充值": "Pay with USDT",
"充值成功": "Top-up successful",
"支付处理中,请稍后在充值账单中查看结果": "Payment is being processed, please check your top-up history later for the result",
"支付已取消": "Payment cancelled",
"支付被拒绝": "Payment rejected",
"支付金额不足": "Insufficient payment amount",
"更新 AllScale USDT 设置": "Update AllScale USDT Settings",
"货币汇率(每 1 美元等于多少货币单位)": "Currency Rate (units per 1 USD)",
"默认为 1,即使用美元。例如填写 10 表示 10 个货币单位 = 1 USD": "Defaults to 1 (USD). Enter 10 to mean 10 currency units = 1 USD"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restore missing output-price locale keys to prevent untranslated text in English UI.

web/src/helpers/render.jsx (Line 2320) still requests 输出价格:{{symbol}}{{price}} / 1M tokens, but this key is missing in web/src/i18n/locales/en.json (only the non-colon variant exists). This can surface raw/untranslated text in billing displays.

🔧 Proposed fix
+    "输出价格:{{symbol}}{{price}} / 1M tokens": "Output price: {{symbol}}{{price}} / 1M tokens",
+    "输出价格:{{symbol}}{{total}} / 1M tokens": "Output price: {{symbol}}{{total}} / 1M tokens",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/en.json` around lines 3348 - 3370, The en.json is
missing the exact locale key requested by render.jsx: add the missing key
"输出价格:{{symbol}}{{price}} / 1M tokens" to web/src/i18n/locales/en.json with the
English value matching the existing variant (e.g. "Output Price:
{{symbol}}{{price}} / 1M tokens") so render.jsx (line ~2320) can find the
translation and avoid raw/untranslated text; ensure the key string exactly
matches what render.jsx calls.

Comment on lines +3323 to +3324
"AllScale API Key": "Clé API AllScale",
"AllScale API Secret": "Secret API AllScale",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Chinese source strings as i18n keys for new AllScale fields.

"AllScale API Key" and "AllScale API Secret" break the locale key convention used by this repo. Please switch these keys to Chinese source keys and update callers to use t('中文key') accordingly.

As per coding guidelines: "Frontend translation files must use flat JSON structure with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/fr.json` around lines 3323 - 3324, Replace the English
keys "AllScale API Key" and "AllScale API Secret" in the locale file with the
Chinese source strings you use as canonical keys (e.g., the project's standard
Chinese labels), update the French translations as values for those Chinese keys
in fr.json (keeping the French text "Clé API AllScale" and "Secret API
AllScale"), and then update every caller that currently uses t('AllScale API
Key') or t('AllScale API Secret') to use t('中文key') instead (search for those
exact English keys in components that use useTranslation()). Ensure keys remain
flat in the JSON and use the exact Chinese strings everywhere so translation
lookups match.

Comment on lines +156 to +166
<Form.Section text={t('AllScale USDT 设置')}>
<Text>
{t('AllScale是一个专注稳定币支付的super app,提供稳定币收单及各类支付产品。')}
<a
href='https://docs.allscale.io/allscale-checkout/getting-started/getting-started-with-new-api'
target='_blank'
rel='noreferrer'
>
{' '}
AllScale Official Site
</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Chinese source-string keys for the new settings copy.

This page mixes raw English text (AllScale Official Site) with English translation keys (API Key, API Secret, API Base URL). API Secret and API Base URL are not part of the new locale payload here, so non-Chinese locales will fall back to English on the new settings page.

As per coding guidelines, web/src/**/*.{ts,tsx,js,jsx} and web/src/i18n/**/*.json: use t('中文key') in components and keep Chinese source strings as locale keys.

Also applies to: 188-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx` around
lines 156 - 166, The component SettingsPaymentGatewayAllScale.jsx mixes raw
English strings with translation keys; replace all literal English UI copy
(e.g., the anchor text "AllScale Official Site" and labels "API Key", "API
Secret", "API Base URL" used in this component) with Chinese source-string keys
passed to t(...) (for example t('AllScale 官方站点'), t('API 密钥'), t('API 密钥(私有)'),
t('API 基础 URL') or other approved Chinese keys) so the component consistently
uses i18n lookups (not raw English). Update the Form.Section text prop and the
Text/anchor content and any input labels or placeholders rendered in this file
to use t('中文key') and add the corresponding Chinese keys and translations into
the locale JSON(s) under web/src/i18n. Ensure no literal English UI strings
remain in this component so all locales will correctly fall back through the
i18n system.

…tion

- Remove encoding/json alias; use clean import for type-only json.RawMessage
- Add timestamp window validation (±5min) to webhook HMAC verification
- Remove redundant * 1.0 in getAllScalePayMoney
- Handle io.ReadAll errors explicitly in RequestAllScalePay and GetAllScaleStatus
- Add payment method guard in RechargeAllScale to prevent cross-gateway settlement
- Add CriticalRateLimit middleware to /allscale/webhook endpoint
- Add inFlight guard to setInterval polling to prevent overlapping requests
- Use dynamic minTopUp instead of hardcoded 1 in allScaleTopUp validation
- Save AllScale credentials before enabling to prevent broken active state
- Fix zh-TW translations: use 儲值 consistently for all AllScale-related keys
@surfingtomchen surfingtomchen force-pushed the feat/allscale-payment-gateway branch from 83321f9 to f2e72ae Compare March 26, 2026 07:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (3)
web/src/components/topup/index.jsx (1)

502-505: ⚠️ Potential issue | 🟠 Major

AllScale still isn't using its own minimum top-up.

Lines 667-673 only derive minTopUpValue from online/Stripe/Waffo, so Lines 503-505 and 701-703 never see data.allscale_min_topup. If AllScale’s floor differs from the other gateways, the USDT path will either reject valid amounts or start checkout for an amount the backend rejects.

💡 Suggested shape of the fix
+ const [allScaleMinTopUp, setAllScaleMinTopUp] = useState(1);
...
+ const parsedAllScaleMinTopUp = Number(data.allscale_min_topup);
+ setAllScaleMinTopUp(
+   Number.isFinite(parsedAllScaleMinTopUp) && parsedAllScaleMinTopUp > 0
+     ? parsedAllScaleMinTopUp
+     : 1,
+ );
...
- if (topUpCount < minTopUp) {
-   showError(t('充值数量不能小于') + minTopUp);
+ if (topUpCount < allScaleMinTopUp) {
+   showError(t('充值数量不能小于') + allScaleMinTopUp);
    return;
  }

If the AllScale card displays the floor, plumb the same state through to RechargeCard as well.

Also applies to: 667-703

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 502 - 505, The AllScale
minimum top-up is never used: ensure minTopUpValue includes
data.allscale_min_topup when the selected gateway is AllScale and pass that
computed value into RechargeCard and into the allScaleTopUp validation;
specifically update the logic that computes minTopUpValue (referenced where
minTopUpValue is derived around lines handling online/Stripe/Waffo) to consider
data.allscale_min_topup, then propagate that state/prop into RechargeCard and
use it in the allScaleTopUp function (which checks topUpCount) so the showError
check and USDT checkout use AllScale’s floor.
web/src/i18n/locales/en.json (1)

3348-3370: ⚠️ Potential issue | 🟡 Minor

Restore the exact 输出价格:... locale keys used by render.jsx.

web/src/helpers/render.jsx still looks up 输出价格:{{symbol}}{{price}} / 1M tokens and 输出价格:{{symbol}}{{total}} / 1M tokens. This file only keeps the non-colon variant, so those billing rows will still fall back to the Chinese source string in the English UI.

🔧 Proposed fix
+    "输出价格:{{symbol}}{{price}} / 1M tokens": "Output price: {{symbol}}{{price}} / 1M tokens",
+    "输出价格:{{symbol}}{{total}} / 1M tokens": "Output price: {{symbol}}{{total}} / 1M tokens",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/en.json` around lines 3348 - 3370, The en.json is
missing the exact locale keys looked up by web/src/helpers/render.jsx; add
entries for the keys "输出价格:{{symbol}}{{price}} / 1M tokens" and
"输出价格:{{symbol}}{{total}} / 1M tokens" with their English translations (e.g.
"Output price: {{symbol}}{{price}} / 1M tokens" and "Output price:
{{symbol}}{{total}} / 1M tokens") so render.jsx finds the English strings
instead of falling back to Chinese.
controller/topup_allscale.go (1)

9-9: ⚠️ Potential issue | 🟡 Minor

Drop the direct encoding/json dependency from this controller.

json.RawMessage is the only remaining encoding/json use here. []byte is enough for Payload, and keeping the file on common.Marshal/common.Unmarshal avoids drifting away from the repo’s Go JSON wrapper convention. As per coding guidelines, "All JSON marshal/unmarshal operations MUST use wrapper functions in common/json.go (common.Marshal, common.Unmarshal, common.UnmarshalJsonStr, common.DecodeJson, common.GetJsonType). Do NOT directly import or call encoding/json in business code."

Also applies to: 405-413

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` at line 9, The file currently imports
encoding/json only to use json.RawMessage; change the Payload field type from
json.RawMessage to []byte (e.g., in the request/struct that declares Payload)
and remove the encoding/json import; then update all places referencing
json.RawMessage or calling encoding/json functions (search for json.RawMessage,
json.Unmarshal, json.Marshal) to use []byte and call the repo JSON wrappers in
common (common.Unmarshal/common.Marshal or common.DecodeJson as appropriate)
instead so the controller follows the project's JSON wrapper convention.
🧹 Nitpick comments (1)
web/src/i18n/locales/ja.json (1)

3306-3306: Use consistent top-up wording for the USDT CTA.

Line 3306 uses 支払う, while adjacent recharge strings use チャージ. Keeping one term improves UI consistency.

Suggested wording tweak
-    "使用 USDT 充值": "USDTで支払う",
+    "使用 USDT 充值": "USDTでチャージ",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/ja.json` at line 3306, The JS locale entry for the key
"使用 USDT 充值" currently maps to "USDTで支払う" which is inconsistent with adjacent
recharge wording; update the translation value to use the same "チャージ" phrasing
(e.g., "USDTでチャージする" or the project's canonical recharge string) so the mapping
for "使用 USDT 充值" matches other recharge CTAs and preserves UI consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/topup_allscale.go`:
- Around line 230-245: The code computes amountCents with int64(payMoney /
unitPrice * 100) which truncates and can mismatch the UI formatted value
(RequestAllScaleAmount); instead, compute a single rounded USD amount (e.g.,
round(payMoney/unitPrice to 2 decimals) into a variable like roundedUSD) and
derive amountCents by multiplying that roundedUSD by 100 (and converting to
int64), then reuse that roundedUSD/amountCents for both the preview display and
the checkout creation; update both the block using unitPrice/amountCents and the
other occurrence referenced (lines ~395-401) to read from the same rounded
variables (e.g., roundedUSD and amountCents) rather than recalculating.
- Around line 69-97: Verify the incoming webhookId against the configured
AllScale webhook ID before accepting the callback: in verifyAllScaleWebhook
compare the provided webhookId parameter to your configured value (e.g.
setting.AllScaleWebhookID or the actual config key used for the webhook ID) and
return false (and log a warning) if they differ; perform this check early
(before HMAC/timestamp acceptance) so only callbacks targeting the configured
webhook ID are processed.
- Around line 446-550: The handler trusts the caller-provided intent_id for
confirming a top-up, enabling replay attacks; fix it by reading and verifying
the checkout intent stored on the TopUp record instead of trusting the query
param: after loading topUp via model.GetTopUpByTradeNo, ensure topUp.Provider
(or equivalent) equals "allscale" and that topUp.CheckoutIntentId is present,
reject if not; then ignore the incoming intent_id and set intentId =
topUp.CheckoutIntentId (use that value when building path and calling AllScale),
and only call model.RechargeAllScale(tradeNo) when the DB-backed intent matches
the confirmed status; also update the TopUp creation code elsewhere to persist
CheckoutIntentId if not already done.

In `@web/src/components/topup/index.jsx`:
- Around line 43-45: The pending-payment cache uses a global key
ALLSCALE_PENDING_PAYMENT_KEY; change it to be user-specific by incorporating the
current user id (userState.user.id) into the key or by storing an ownerId
alongside the persisted object and validating it before resuming. Update the
constant/usage sites (where ALLSCALE_PENDING_PAYMENT_KEY is defined and where it
is written at the save paths around lines 423-425) to include the user id, and
update the resume logic that reads/restores the cached entry (the resume block
around lines 463-480) to verify that the stored owner id matches
userState.user.id before restoring intentId/tradeNo or starting polls; if it
doesn’t match, discard the entry. Ensure reads, writes, and resume checks
consistently use the same user-scoped key or owner id field so one user cannot
pick up another user’s pending payment.
- Around line 482-497: getAllScaleAmount currently leaves the previous
allScaleAmount on non-success responses or exceptions; update the function
(getAllScaleAmount) so that whenever the response is not message === 'success'
or an exception is caught you clear the preview by calling setAllScaleAmount
with the "empty" value your component expects (e.g., null or 0) to avoid showing
stale USDT quotes; keep the successful branch unchanged
(setAllScaleAmount(parseFloat(data))).
- Around line 421-460: The poll handler can act on stale async results because
clearing the interval doesn't cancel an in-flight poll; update
startAllScalePolling to create a unique session id (e.g., sessionId via
Date.now() or UUID) stored in a ref like currentAllScaleSessionRef and included
in the localStorage pending value (ALLSCALE_PENDING_PAYMENT_KEY), set that ref
when starting and clear it in clearAllScalePolling, then inside the async
callback from pollAllScaleStatusOnce compare the saved session id against
currentAllScaleSessionRef and return early if they differ so only the latest
session can call clearAllScalePendingPayment(), showSuccess/showError, and
setHistoryReloadKey; ensure clearAllScalePolling also nulls the ref to
invalidate any in-flight completions.

---

Duplicate comments:
In `@controller/topup_allscale.go`:
- Line 9: The file currently imports encoding/json only to use json.RawMessage;
change the Payload field type from json.RawMessage to []byte (e.g., in the
request/struct that declares Payload) and remove the encoding/json import; then
update all places referencing json.RawMessage or calling encoding/json functions
(search for json.RawMessage, json.Unmarshal, json.Marshal) to use []byte and
call the repo JSON wrappers in common (common.Unmarshal/common.Marshal or
common.DecodeJson as appropriate) instead so the controller follows the
project's JSON wrapper convention.

In `@web/src/components/topup/index.jsx`:
- Around line 502-505: The AllScale minimum top-up is never used: ensure
minTopUpValue includes data.allscale_min_topup when the selected gateway is
AllScale and pass that computed value into RechargeCard and into the
allScaleTopUp validation; specifically update the logic that computes
minTopUpValue (referenced where minTopUpValue is derived around lines handling
online/Stripe/Waffo) to consider data.allscale_min_topup, then propagate that
state/prop into RechargeCard and use it in the allScaleTopUp function (which
checks topUpCount) so the showError check and USDT checkout use AllScale’s
floor.

In `@web/src/i18n/locales/en.json`:
- Around line 3348-3370: The en.json is missing the exact locale keys looked up
by web/src/helpers/render.jsx; add entries for the keys
"输出价格:{{symbol}}{{price}} / 1M tokens" and "输出价格:{{symbol}}{{total}} / 1M
tokens" with their English translations (e.g. "Output price: {{symbol}}{{price}}
/ 1M tokens" and "Output price: {{symbol}}{{total}} / 1M tokens") so render.jsx
finds the English strings instead of falling back to Chinese.

---

Nitpick comments:
In `@web/src/i18n/locales/ja.json`:
- Line 3306: The JS locale entry for the key "使用 USDT 充值" currently maps to
"USDTで支払う" which is inconsistent with adjacent recharge wording; update the
translation value to use the same "チャージ" phrasing (e.g., "USDTでチャージする" or the
project's canonical recharge string) so the mapping for "使用 USDT 充值" matches
other recharge CTAs and preserves UI consistency.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d10c3be6-2398-44f9-aa79-54ec9da86d9d

📥 Commits

Reviewing files that changed from the base of the PR and between ac9f2e0 and 45c32b5.

📒 Files selected for processing (23)
  • README.fr.md
  • README.ja.md
  • README.md
  • README.zh_CN.md
  • README.zh_TW.md
  • controller/topup.go
  • controller/topup_allscale.go
  • model/option.go
  • model/topup.go
  • router/api-router.go
  • setting/payment_allscale.go
  • web/src/components/settings/PaymentSetting.jsx
  • web/src/components/topup/RechargeCard.jsx
  • web/src/components/topup/index.jsx
  • web/src/components/topup/modals/TopupHistoryModal.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx
✅ Files skipped from review due to trivial changes (5)
  • README.zh_CN.md
  • README.zh_TW.md
  • README.ja.md
  • README.md
  • setting/payment_allscale.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/src/components/settings/PaymentSetting.jsx
  • controller/topup.go
  • web/src/i18n/locales/fr.json
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayAllScale.jsx
  • web/src/components/topup/RechargeCard.jsx

Comment on lines +69 to +97
func verifyAllScaleWebhook(requestPath, queryString, webhookId, timestamp, nonce string, body []byte, sigHeader string) bool {
if setting.AllScaleApiSecret == "" {
log.Printf("AllScale webhook secret not configured")
return false
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
log.Printf("AllScale webhook: invalid timestamp format")
return false
}
now := time.Now().Unix()
if now-ts > 300 || ts-now > 60 {
log.Printf("AllScale webhook: timestamp outside acceptable window (ts=%d, now=%d)", ts, now)
return false
}
bodyHash := allScaleSha256Hex(body)
canonical := []byte(strings.Join([]string{
"allscale:webhook:v1",
"POST",
requestPath,
queryString,
webhookId,
timestamp,
nonce,
bodyHash,
}, "\n"))
expected := allScaleSign(setting.AllScaleApiSecret, canonical)
return hmac.Equal([]byte(expected), []byte(sigHeader))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bind webhook verification to the configured AllScale webhook ID.

The HMAC only proves this header/body pair was signed with your secret; it does not prove the callback targets this specific webhook configuration. Since this integration has a dedicated webhook ID, compare X-Webhook-Id against the configured value before accepting the callback, otherwise a valid webhook from another AllScale endpoint on the same account can be replayed here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 69 - 97, Verify the incoming
webhookId against the configured AllScale webhook ID before accepting the
callback: in verifyAllScaleWebhook compare the provided webhookId parameter to
your configured value (e.g. setting.AllScaleWebhookID or the actual config key
used for the webhook ID) and return false (and log a warning) if they differ;
perform this check early (before HMAC/timestamp acceptance) so only callbacks
targeting the configured webhook ID are processed.

Comment on lines +230 to +245
unitPrice := setting.AllScaleUnitPrice
if unitPrice <= 0 {
unitPrice = 1.0
}
amountCents := int64(payMoney / unitPrice * 100)
redirectURL := system_setting.ServerAddress + "/console/topup?show_history=true"

reqBody, _ := common.Marshal(map[string]any{
"currency": 1, // USD = 1
"amount_cents": amountCents,
"order_id": tradeNo, // reconciliation key returned in webhook
"user_id": fmt.Sprintf("%d", userId),
"user_name": user.Username,
"order_description": fmt.Sprintf("Top up %d credits", req.Amount),
"redirect_url": redirectURL,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the same rounded USD amount for preview and checkout creation.

int64(payMoney / unitPrice * 100) truncates, while RequestAllScaleAmount formats the value to two decimals. For values like 0.205, the UI will show 0.21 but the created checkout intent will only request 20 cents. Compute one rounded USD amount/cents value and reuse it in both paths.

Also applies to: 395-401

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 230 - 245, The code computes
amountCents with int64(payMoney / unitPrice * 100) which truncates and can
mismatch the UI formatted value (RequestAllScaleAmount); instead, compute a
single rounded USD amount (e.g., round(payMoney/unitPrice to 2 decimals) into a
variable like roundedUSD) and derive amountCents by multiplying that roundedUSD
by 100 (and converting to int64), then reuse that roundedUSD/amountCents for
both the preview display and the checkout creation; update both the block using
unitPrice/amountCents and the other occurrence referenced (lines ~395-401) to
read from the same rounded variables (e.g., roundedUSD and amountCents) rather
than recalculating.

Comment on lines +446 to +550
intentId := c.Query("intent_id")
tradeNo := c.Query("trade_no")
if intentId == "" || tradeNo == "" {
c.JSON(200, gin.H{"message": "error", "data": "missing intent_id or trade_no"})
return
}

userId := c.GetInt("id")

// Fast path: check local DB first to avoid unnecessary API calls.
topUp := model.GetTopUpByTradeNo(tradeNo)
if topUp == nil || topUp.UserId != userId {
c.JSON(200, gin.H{"message": "error", "data": "order not found"})
return
}
if topUp.Status == common.TopUpStatusSuccess || topUp.Status == common.TopUpStatusFailed || topUp.Status == common.TopUpStatusExpired {
localStatus := allScaleCheckoutStatusCreated
switch topUp.Status {
case common.TopUpStatusSuccess:
localStatus = allScaleCheckoutStatusConfirmed
case common.TopUpStatusFailed:
localStatus = allScaleCheckoutStatusFailed
case common.TopUpStatusExpired:
localStatus = allScaleCheckoutStatusCanceled
}
c.JSON(200, gin.H{
"message": "success",
"data": gin.H{
"status": localStatus,
"topup_status": topUp.Status,
"completed": topUp.Status == common.TopUpStatusSuccess,
},
})
return
}

// Hit the AllScale /status endpoint for the current intent status.
path := "/v1/checkout_intents/" + intentId + "/status"
apiURL := strings.TrimRight(setting.AllScaleBaseURL, "/") + path
apiKey, ts, nonce, sig := buildAllScaleRequestHeaders("GET", path, "", []byte{})

httpReq, err := http.NewRequestWithContext(c.Request.Context(), "GET", apiURL, nil)
if err != nil {
log.Printf("AllScale status: failed to build request: %v", err)
c.JSON(200, gin.H{"message": "error", "data": "failed to build request"})
return
}
httpReq.Header.Set("X-API-Key", apiKey)
httpReq.Header.Set("X-Timestamp", ts)
httpReq.Header.Set("X-Nonce", nonce)
httpReq.Header.Set("X-Signature", sig)

client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
log.Printf("AllScale status: API call failed: %v", err)
c.JSON(200, gin.H{"message": "error", "data": "failed to contact payment gateway"})
return
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("AllScale status: failed to read response body: %v", err)
c.JSON(200, gin.H{"message": "error", "data": "failed to read status response"})
return
}

var statusResp allScaleStatusAPIResponse
if err := common.Unmarshal(respBody, &statusResp); err != nil || statusResp.Code != 0 {
errMsg := "unexpected response from payment gateway"
if statusResp.Error != nil {
errMsg = statusResp.Error.Message
}
log.Printf("AllScale status: bad response for intentId=%s: %s", intentId, respBody)
c.JSON(200, gin.H{"message": "error", "data": errMsg})
return
}

status, err := parseAllScaleNumericStatus(statusResp.Payload)
if err != nil {
log.Printf("AllScale status: failed to parse payload for intentId=%s: %s", intentId, respBody)
c.JSON(200, gin.H{"message": "error", "data": "failed to parse payment status"})
return
}

log.Printf("AllScale status: intentId=%s tradeNo=%s status=%d", intentId, tradeNo, status)

if status == allScaleCheckoutStatusConfirmed {
LockOrder(tradeNo)
defer UnlockOrder(tradeNo)
if err := model.RechargeAllScale(tradeNo); err != nil {
log.Printf("AllScale status poll: recharge failed - tradeNo=%s err=%v", tradeNo, err)
c.JSON(200, gin.H{"message": "error", "data": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "success",
"data": gin.H{
"status": status,
"topup_status": common.TopUpStatusSuccess,
"completed": true,
},
})
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not let the client choose which intent confirms an order.

This handler only verifies that trade_no belongs to the caller, then trusts the caller-provided intent_id. A user can create a fresh pending order and replay any previously confirmed AllScale intent against it, which will call model.RechargeAllScale(tradeNo) again. Persist the checkout intent ID on the TopUp, reject non-allscale orders here, and load/verify the expected intent from the DB instead of trusting the query string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_allscale.go` around lines 446 - 550, The handler trusts the
caller-provided intent_id for confirming a top-up, enabling replay attacks; fix
it by reading and verifying the checkout intent stored on the TopUp record
instead of trusting the query param: after loading topUp via
model.GetTopUpByTradeNo, ensure topUp.Provider (or equivalent) equals "allscale"
and that topUp.CheckoutIntentId is present, reject if not; then ignore the
incoming intent_id and set intentId = topUp.CheckoutIntentId (use that value
when building path and calling AllScale), and only call
model.RechargeAllScale(tradeNo) when the DB-backed intent matches the confirmed
status; also update the TopUp creation code elsewhere to persist
CheckoutIntentId if not already done.

Comment on lines +43 to +45
const ALLSCALE_PENDING_PAYMENT_KEY = 'allscale_pending_payment';
const ALLSCALE_MAX_POLL_ATTEMPTS = 120;
const ALLSCALE_POLL_INTERVAL = 5000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the pending-payment cache user-specific.

Line 43 uses one browser-wide key, and Lines 463-480 resume whatever is stored there without checking the current account. On a shared device, a different user can inherit another account’s intentId/tradeNo, which leaks payment metadata and can trigger bogus status polls/toasts. Scope the persisted entry by userState.user.id (or store the owner id and verify it before resuming).

Also applies to: 423-425, 463-480

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 43 - 45, The pending-payment
cache uses a global key ALLSCALE_PENDING_PAYMENT_KEY; change it to be
user-specific by incorporating the current user id (userState.user.id) into the
key or by storing an ownerId alongside the persisted object and validating it
before resuming. Update the constant/usage sites (where
ALLSCALE_PENDING_PAYMENT_KEY is defined and where it is written at the save
paths around lines 423-425) to include the user id, and update the resume logic
that reads/restores the cached entry (the resume block around lines 463-480) to
verify that the stored owner id matches userState.user.id before restoring
intentId/tradeNo or starting polls; if it doesn’t match, discard the entry.
Ensure reads, writes, and resume checks consistently use the same user-scoped
key or owner id field so one user cannot pick up another user’s pending payment.

Comment on lines +421 to +460
const startAllScalePolling = useCallback((intentId, tradeNo) => {
clearAllScalePolling();
localStorage.setItem(
ALLSCALE_PENDING_PAYMENT_KEY,
JSON.stringify({ intentId, tradeNo, startedAt: Date.now() })
);
let attempts = 0;
let inFlight = false;
allScalePollingRef.current = setInterval(async () => {
if (inFlight) return;
attempts += 1;
if (attempts > ALLSCALE_MAX_POLL_ATTEMPTS) {
clearAllScalePolling();
showInfo(t('支付处理中,请稍后在充值账单中查看结果'));
return;
}
inFlight = true;
try {
const result = await pollAllScaleStatusOnce(intentId, tradeNo);
if (result.state === 'success') {
clearAllScalePolling();
clearAllScalePendingPayment();
showSuccess(t('充值成功'));
getUserQuota().then();
setHistoryReloadKey((k) => k + 1);
} else if (result.state === 'failed') {
clearAllScalePolling();
clearAllScalePendingPayment();
showError(result.message || t('支付失败'));
setHistoryReloadKey((k) => k + 1);
}
} catch (error) {
if (attempts >= ALLSCALE_MAX_POLL_ATTEMPTS) {
clearAllScalePolling();
showInfo(t('支付处理中,请稍后在充值账单中查看结果'));
}
} finally {
inFlight = false;
}
}, ALLSCALE_POLL_INTERVAL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drop results from stale poll sessions.

Line 422 clears the interval, but it does not invalidate a pollAllScaleStatusOnce() call that is already awaiting. If a second AllScale checkout starts before the first request settles, the old callback can still hit Lines 440-450 and call clearAllScalePendingPayment(), wiping the newer pending entry and surfacing the wrong terminal toast/history refresh.

🔧 One way to invalidate stale poll completions
+ const allScalePollSessionRef = useRef(0);
+
  const clearAllScalePolling = useCallback(() => {
+   allScalePollSessionRef.current += 1;
    if (allScalePollingRef.current) {
      clearInterval(allScalePollingRef.current);
      allScalePollingRef.current = null;
    }
  }, []);
...
  const startAllScalePolling = useCallback((intentId, tradeNo) => {
    clearAllScalePolling();
+   const sessionId = allScalePollSessionRef.current;
    localStorage.setItem(
      ALLSCALE_PENDING_PAYMENT_KEY,
      JSON.stringify({ intentId, tradeNo, startedAt: Date.now() })
    );
...
      try {
        const result = await pollAllScaleStatusOnce(intentId, tradeNo);
+       if (allScalePollSessionRef.current !== sessionId) return;
        if (result.state === 'success') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 421 - 460, The poll handler
can act on stale async results because clearing the interval doesn't cancel an
in-flight poll; update startAllScalePolling to create a unique session id (e.g.,
sessionId via Date.now() or UUID) stored in a ref like currentAllScaleSessionRef
and included in the localStorage pending value (ALLSCALE_PENDING_PAYMENT_KEY),
set that ref when starting and clear it in clearAllScalePolling, then inside the
async callback from pollAllScaleStatusOnce compare the saved session id against
currentAllScaleSessionRef and return early if they differ so only the latest
session can call clearAllScalePendingPayment(), showSuccess/showError, and
setHistoryReloadKey; ensure clearAllScalePolling also nulls the ref to
invalidate any in-flight completions.

Comment on lines +482 to +497
const getAllScaleAmount = async (value) => {
if (value === undefined) value = topUpCount;
try {
const res = await API.post('/api/user/allscale/amount', {
amount: parseFloat(value),
});
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success') {
setAllScaleAmount(parseFloat(data));
}
}
} catch (e) {
// silent fail
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear stale USDT quotes when preview refresh fails.

Lines 488-496 keep the previous allScaleAmount on both non-success responses and exceptions. If the user changes topUpCount and the preview request fails, the card can still show an old quote for the new amount.

🧹 Reset the preview on failure
        if (message === 'success') {
          setAllScaleAmount(parseFloat(data));
+       } else {
+         setAllScaleAmount(0);
        }
      }
    } catch (e) {
-      // silent fail
+      setAllScaleAmount(0);
    }
📝 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.

Suggested change
const getAllScaleAmount = async (value) => {
if (value === undefined) value = topUpCount;
try {
const res = await API.post('/api/user/allscale/amount', {
amount: parseFloat(value),
});
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success') {
setAllScaleAmount(parseFloat(data));
}
}
} catch (e) {
// silent fail
}
};
const getAllScaleAmount = async (value) => {
if (value === undefined) value = topUpCount;
try {
const res = await API.post('/api/user/allscale/amount', {
amount: parseFloat(value),
});
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success') {
setAllScaleAmount(parseFloat(data));
} else {
setAllScaleAmount(0);
}
}
} catch (e) {
setAllScaleAmount(0);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 482 - 497, getAllScaleAmount
currently leaves the previous allScaleAmount on non-success responses or
exceptions; update the function (getAllScaleAmount) so that whenever the
response is not message === 'success' or an exception is caught you clear the
preview by calling setAllScaleAmount with the "empty" value your component
expects (e.g., null or 0) to avoid showing stale USDT quotes; keep the
successful branch unchanged (setAllScaleAmount(parseFloat(data))).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant