feat: support fxUSD and multi-decimal EIP-3009 stablecoins#109
feat: support fxUSD and multi-decimal EIP-3009 stablecoins#109huwangtao123 wants to merge 7 commits intoBlockRunAI:mainfrom
Conversation
- Add paymentAssetDecimals to /health endpoint and proxy reuse path to prevent decimals data loss when reconnecting to an existing proxy (fixes 10^12x balance inflation for 18-decimal assets like fxUSD) - Fix transformPaymentError to use asset-native decimals instead of hardcoded 6 for USD conversion in insufficient_funds error messages - Update balance.ts and proxy.ts comments from USDC-specific to generic stablecoin language (USD micros) - Add payment-asset.ts module with BasePaymentAsset type, normalizer, and fetcher for multi-asset support - Add 3 new tests: fxUSD 18-decimal normalization, mixed-decimal asset list with disabled filtering, real fxUSD contract address Verified: fxUSD contract (0x55380fe7a1910dff29a47b622057ab4139da42c5) on Base fully implements EIP-3009 (transferWithAuthorization, all typehashes, authorizationState) — confirmed on Basescan.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughBalance monitoring was generalized from a USDC-only model to configurable Base-chain EIP-3009 assets; a new payment-asset module and tests were added; proxy, CLI, doctor, index, and Solana monitor code were updated to fetch, advertise, select, and use per-request Base payment assets for balance checks and deductions. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Proxy as Proxy Server
participant Fetcher as Asset Fetcher
participant Selector as Asset Selector
participant Monitor as BalanceMonitor
participant Chain as Blockchain
Client->>Proxy: incoming request
Proxy->>Fetcher: fetchBasePaymentAssets()
Fetcher-->>Proxy: normalized asset list
Proxy->>Selector: evaluate assets for sufficiency
loop candidates
Selector->>Monitor: getSharedMonitorForAsset(candidate)
Selector->>Monitor: checkSufficient(bufferedCostMicros)
Monitor->>Chain: balanceOf(candidate.asset)
Chain-->>Monitor: raw token balance
Monitor->>Monitor: toUsdMicros(raw, decimals)
Monitor-->>Selector: sufficiency result
alt sufficient
Selector-->>Proxy: select asset & stop
end
end
Proxy->>Monitor: optimistic deduction on selected monitor
Proxy-->>Client: proceed / respond
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/proxy.ts (1)
1568-1577: Async asset refresh inonAfterPaymentCreationmay race with concurrent requests.This callback fetches assets and updates
activeBasePaymentAssetsandactiveBasePaymentAssetwhile other requests may be reading these variables in their asset selection loops (lines 3438, 3447).Since the selection loop creates its own monitors and reads
getBasePaymentAssets()at the start of selection, this should be safe — a request either sees the old list or the new list, both valid. However, consider documenting this non-atomic update if it's intentional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` around lines 1568 - 1577, The async refresh in onAfterPaymentCreation can non-atomically update activeBasePaymentAssets/activeBasePaymentAsset while other requests read them; fetch the assets into a local variable first (e.g., const newAssets = await fetchBasePaymentAssets(apiBase).catch(() => undefined)), then if newAssets is truthy assign activeBasePaymentAssets = newAssets and activeBasePaymentAsset = newAssets[0] ?? activeBasePaymentAsset, and only after that call balanceMonitor.setAsset(activeBasePaymentAsset) (BalanceMonitor); alternatively add a brief comment near getBasePaymentAssets()/onAfterPaymentCreation documenting that updates are intentionally non-atomic and readers accept either the old or new list.src/payment-asset.ts (1)
99-101: Consider handling JSON parse errors infetchBasePaymentAssets.If the API returns a 200 OK with invalid JSON,
response.json()will throw and the error propagates up. This could cause unexpected failures when the upstream API has transient issues.🛡️ Suggested defensive handling
if (!response.ok) return [DEFAULT_BASE_PAYMENT_ASSET]; - const payload = (await response.json()) as PaymentMetadataResponse; - return normalizeBasePaymentAssets(payload); + try { + const payload = (await response.json()) as PaymentMetadataResponse; + return normalizeBasePaymentAssets(payload); + } catch { + return [DEFAULT_BASE_PAYMENT_ASSET]; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/payment-asset.ts` around lines 99 - 101, fetchBasePaymentAssets currently calls response.json() directly which will throw on invalid JSON and propagate; wrap the JSON parsing in a try/catch inside fetchBasePaymentAssets, attempt to read and include the raw body (e.g., response.text()) or the parse error message in the catch, log or attach that context, and then either throw a clearer, typed error or return a safe fallback before calling normalizeBasePaymentAssets(payload); reference fetchBasePaymentAssets, response.json(), payload, and normalizeBasePaymentAssets to locate where to add the try/catch and error enrichment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/balance.ts`:
- Around line 163-172: setAsset mutates BalanceMonitor while async balance
checks (checkSufficient/checkBalance/fetchBalance) can be in-flight, causing
cached balances or responses to be associated with the wrong asset; make
BalanceMonitor immutable instead of mutating it: remove/stop using setAsset and
ensure callers (e.g., the onAfterPaymentCreation path that currently calls
setAsset) create a new BalanceMonitor instance per asset (or per asset change)
so each monitor owns a fixed BasePaymentAsset, or alternatively implement an
atomic/version check in fetchBalance that aborts/cancels caching if this.asset
changed during the fetch; reference: setAsset, BalanceMonitor, checkSufficient,
checkBalance, fetchBalance, onAfterPaymentCreation.
---
Nitpick comments:
In `@src/payment-asset.ts`:
- Around line 99-101: fetchBasePaymentAssets currently calls response.json()
directly which will throw on invalid JSON and propagate; wrap the JSON parsing
in a try/catch inside fetchBasePaymentAssets, attempt to read and include the
raw body (e.g., response.text()) or the parse error message in the catch, log or
attach that context, and then either throw a clearer, typed error or return a
safe fallback before calling normalizeBasePaymentAssets(payload); reference
fetchBasePaymentAssets, response.json(), payload, and normalizeBasePaymentAssets
to locate where to add the try/catch and error enrichment.
In `@src/proxy.ts`:
- Around line 1568-1577: The async refresh in onAfterPaymentCreation can
non-atomically update activeBasePaymentAssets/activeBasePaymentAsset while other
requests read them; fetch the assets into a local variable first (e.g., const
newAssets = await fetchBasePaymentAssets(apiBase).catch(() => undefined)), then
if newAssets is truthy assign activeBasePaymentAssets = newAssets and
activeBasePaymentAsset = newAssets[0] ?? activeBasePaymentAsset, and only after
that call balanceMonitor.setAsset(activeBasePaymentAsset) (BalanceMonitor);
alternatively add a brief comment near
getBasePaymentAssets()/onAfterPaymentCreation documenting that updates are
intentionally non-atomic and readers accept either the old or new list.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7c0df0cb-eb1f-4015-afc0-1c3c0b4d7eab
📒 Files selected for processing (4)
src/balance.tssrc/payment-asset.test.tssrc/payment-asset.tssrc/proxy.ts
Adds docstrings to all exported types, constants, and functions to satisfy the 80% docstring coverage CI requirement.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/payment-asset.ts`:
- Around line 113-124: fetchBasePaymentAssets currently doesn't catch network or
JSON parse errors, so add a try-catch around the fetch + json + normalize steps
in fetchBasePaymentAssets and, on any thrown error, log or ignore the error and
return [DEFAULT_BASE_PAYMENT_ASSET] as the documented fallback; ensure you still
preserve the existing behavior for non-ok responses (the early return when
response.ok is false) and reference the PaymentMetadataResponse parsing and
normalizeBasePaymentAssets call inside the try block so parse/normalize failures
also fall back.
- Around line 87-88: The function normalizeBasePaymentAssets currently returns
[] early when value is null/undefined or not an object, which bypasses the
fallback-to-USDC logic; modify normalizeBasePaymentAssets so it does not
early-return for falsy/non-object inputs but instead allows the existing
fallback path (the code that adds USDC when no valid assets are found) to
run—locate normalizeBasePaymentAssets and remove or change the early return
check so invalid input is handled by the later validation/fallback logic that
produces the USDC default.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e8345e67-07ef-4b45-8ae2-f2ee7023d2c2
📒 Files selected for processing (1)
src/payment-asset.ts
- normalizeBasePaymentAssets now returns [DEFAULT_BASE_PAYMENT_ASSET] instead of [] for null/undefined/non-object input, ensuring the USDC fallback is always applied - fetchBasePaymentAssets wraps fetch+json+normalize in try-catch so network errors and JSON parse failures fall back to USDC instead of propagating (matches documented contract)
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/proxy.ts (1)
550-577:⚠️ Potential issue | 🟠 MajorReuse mode still collapses Base multi-asset metadata to one asset.
/healthnow exposespaymentAssetsandselectedPaymentAsset, butcheckExistingProxy()only reads the singularpaymentAsset*fields and the reuse path rewritesactiveBasePaymentAssetsto[activeBasePaymentAsset]. A process that attaches to an already-running proxy therefore loses the alternate asset list and can report the wrong balance/decimals.Also applies to: 1569-1579
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` around lines 550 - 577, checkExistingProxy() currently reads only the singular paymentAsset* fields and, when reuse mode attaches to an existing proxy, it collapses the base multi-asset metadata by rewriting activeBasePaymentAssets to [activeBasePaymentAsset]; update checkExistingProxy() to read the new /health fields paymentAssets (array) and selectedPaymentAsset (string or object) instead of paymentAsset/paymentAssetSymbol/paymentAssetDecimals, and restore/populate activeBasePaymentAssets from paymentAssets while setting activeBasePaymentAsset from selectedPaymentAsset so the full asset list and correct selected asset (with accurate decimals/balance reporting) are preserved when attaching to a running proxy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/doctor.ts`:
- Around line 132-135: collectWalletInfo() currently resolves paymentAsset via
fetchBasePaymentAsset(...) but does not include the resolved asset symbol in the
returned WalletInfo, so downstream Doctor output cannot replace the hard-coded
"USDC" label. Update collectWalletInfo() to add the resolved paymentAsset (or
its symbol) into the WalletInfo structure (use the same identifier as
paymentAsset / DEFAULT_BASE_PAYMENT_ASSET) and ensure the BalanceMonitor
instantiation still uses the resolved paymentAsset; then update downstream
Doctor code to read this new WalletInfo.paymentAsset (or paymentAsset.symbol)
instead of hard-coding "USDC" when rendering prompts or diagnostics.
- Around line 370-374: The current console.log in the diagnostics output reuses
the USDC buy link even when paymentAsset.symbol is something else (e.g., fxUSD)
and should either show an asset-specific link or a generic hint; update the
console.log at the location using diagnostics.wallet.paymentChain and
paymentAsset.symbol so that if paymentAsset.symbol maps to a known asset you
print its specific buy URL (use a small symbol->url map or a helper like
getBuyLinkForAsset(symbol)), otherwise print a generic message such as "Buy
[symbol] on your preferred exchange" or "Find [symbol] on Coinbase:
https://www.coinbase.com/search?q=[symbol]" so the displayed URL matches the
asset shown. Ensure you change the ternary/templated string that currently
hardcodes the USDC URL and reference getBuyLinkForAsset(paymentAsset.symbol) or
the symbol->url map in the console.log.
In `@src/index.ts`:
- Around line 964-987: evmBalanceText is being derived from the first entry
basePaymentAsset but the detailed lines probe every asset, so if the first asset
is empty we incorrectly show zero even when a later asset has funds; change the
logic to iterate basePaymentAssets with BalanceMonitor.checkBalance until you
find the first asset that reports a positive balance (use
assetBalance.balanceUSD or numeric > 0 as the check) and set both
basePaymentAsset and evmBalanceText from that funded asset; if none report funds
keep the current fallback ("Balance: (could not check)" or
DEFAULT_BASE_PAYMENT_ASSET) and keep the existing baseAssetLines mapping that
shows per-asset statuses.
In `@src/proxy.ts`:
- Around line 1624-1630: The code overwrites the request-selected base asset by
assigning activeBasePaymentAsset = activeBasePaymentAssets[0] after fetching the
list; instead, preserve any already-selected asset (which proxyRequest() sets
via onBaseAssetSelected()), only update the list from
fetchBasePaymentAssets(apiBase) and set activeBasePaymentAsset to the first
asset only if no activeBasePaymentAsset is currently set/defined. Update the
block around fetchBasePaymentAssets to avoid assigning activeBasePaymentAsset
unconditionally and ensure balanceMonitor.setAsset(activeBasePaymentAsset) uses
the preserved/existing asset; reference variables and funcs:
fetchBasePaymentAssets, activeBasePaymentAssets, activeBasePaymentAsset,
proxyRequest/onBaseAssetSelected, and BalanceMonitor/balanceMonitor.setAsset.
- Around line 3548-3577: The loop currently creates disposable per-asset
monitors with new BalanceMonitor(...) so the later deductEstimated() and
invalidate() mutate only a temporary object; change it to obtain and use the
shared per-asset monitor from the existing BalanceMonitor instance (rather than
constructing a new one) so cache updates persist: replace new
BalanceMonitor(balanceMonitor.getWalletAddress(), asset) with a call that
returns the shared monitor for that asset from the existing balanceMonitor (so
requestBalanceMonitor is assigned the shared instance), keeping usages of
checkSufficient, onBaseAssetSelected, requestBasePaymentAsset,
deductEstimated(), and invalidate() unchanged.
- Around line 222-227: transformPaymentError is using the global base asset info
and Number-based parseInt which loses precision for 18-decimal assets; update
callers to pass requestBasePaymentAsset.symbol and
requestBasePaymentAsset.decimals into transformPaymentError, and inside
transformPaymentError replace parseInt(balanceMatch[1]/[2], 10) and Number math
with BigInt parsing (e.g. BigInt(balanceMatch[n])) and BigInt divisor
(BigInt(10) ** BigInt(decimals)), then format amounts by dividing to get integer
and fractional parts and producing a fixed-6 decimal string for
current/required; apply the same BigInt-based fix at the other occurrence
mentioned (lines 4270-4273) so selected asset symbol/decimals and BigInt
arithmetic are used consistently.
---
Outside diff comments:
In `@src/proxy.ts`:
- Around line 550-577: checkExistingProxy() currently reads only the singular
paymentAsset* fields and, when reuse mode attaches to an existing proxy, it
collapses the base multi-asset metadata by rewriting activeBasePaymentAssets to
[activeBasePaymentAsset]; update checkExistingProxy() to read the new /health
fields paymentAssets (array) and selectedPaymentAsset (string or object) instead
of paymentAsset/paymentAssetSymbol/paymentAssetDecimals, and restore/populate
activeBasePaymentAssets from paymentAssets while setting activeBasePaymentAsset
from selectedPaymentAsset so the full asset list and correct selected asset
(with accurate decimals/balance reporting) are preserved when attaching to a
running proxy.
🪄 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
Run ID: 91d12043-864d-4ef9-9ffd-b2b2f4c7c96e
⛔ Files ignored due to path filters (5)
dist/cli.jsis excluded by!**/dist/**dist/cli.js.mapis excluded by!**/dist/**,!**/*.mapdist/index.d.tsis excluded by!**/dist/**dist/index.jsis excluded by!**/dist/**dist/index.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (5)
src/cli.tssrc/doctor.tssrc/index.tssrc/proxy.tssrc/solana-balance.ts
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/proxy.ts (1)
2218-2293:⚠️ Potential issue | 🟠 MajorMirror the full reuse metadata in the late
EADDRINUSEpath.This branch reuses the existing proxy, but it drops
paymentAsset,paymentAssets, and the selected-asset context that the earlierexistingProxybranch preserves. If a running proxy has already switched to fxUSD, callers fall back to the default asset and can show/check the wrong Base balance on the reused handle. Please carry thecheckExistingProxy()payload throughREUSE_EXISTINGand rebuild the handle the same way as the pre-listen reuse path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` around lines 2218 - 2293, The REUSE_EXISTING path created inside tryListen (where checkExistingProxy(listenPort) returns existingProxy2) only rejects with wallet and paymentChain, losing paymentAsset/paymentAssets/selected-asset info; update that rejectAttempt call to include the full existingProxy2 payload (paymentAsset, paymentAssets, selectedAsset or similar fields) so the thrown error carries all reuse metadata, and then in the outer retry loop where you handle error.code === "REUSE_EXISTING" reconstruct the returned handle exactly as the pre-listen reuse branch does (include paymentAsset, paymentAssets, selectedAsset context, and any other fields from checkExistingProxy) instead of only returning walletAddress and balanceMonitor; adjust references in tryListen, rejectAttempt, and the error destructuring to propagate and consume those fields.
♻️ Duplicate comments (2)
src/balance.ts (1)
164-172:⚠️ Potential issue | 🟠 Major
setAsset()still makes monitor state non-deterministic.This instance is now both mutable and shared. If another request flips the asset while
fetchBalance()is awaiting RPC, the raw balance can be normalized or labeled with the new asset, which reintroduces the 6↔18 decimal mix-up this PR is fixing. It also changes which instancegetSharedMonitorForAsset()treats as canonical, sodeductEstimated()/invalidate()can hit a different monitor than later requests read.Also applies to: 198-214, 217-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/balance.ts` around lines 164 - 172, setAsset mutates a shared Balance instance which makes monitor state non-deterministic (race between setAsset and fetchBalance/deductEstimated/invalidate); change the design so asset changes are not performed in-place on shared instances: either make Balance.asset immutable after construction and have setAsset return a new Balance instance (and atomically update the canonical mapping used by getSharedMonitorForAsset), or ensure setAsset performs a safe copy-before-mutate and updates any registry atomically so in-flight fetchBalance uses the original asset copy; apply the same non-mutating/copy-on-write fix pattern to the other affected methods/blocks referenced (the sections around lines 198-214 and 217-250) so all reads use a local snapshot of asset/decimals/symbol for the duration of RPC work.src/proxy.ts (1)
1631-1657:⚠️ Potential issue | 🟠 MajorActual payment accounting still assumes 6-decimal USDC amounts.
context.selectedRequirements.amountis asset-native units. Dividing it by1_000_000and storing it in aNumberwill inflate 18-decimal Base assets by10^12and can lose precision before that. BecausepaymentStore.amountUsdfeeds chat, partner, and image usage logs, stats will be wrong for fxUSD payments until this is normalized with the request-selected asset’s decimals viaBigInt.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` around lines 1631 - 1657, The code currently treats context.selectedRequirements.amount as 6-decimal USDC and uses Number math; instead normalize using the selected asset's decimals with BigInt to avoid inflation/precision loss: parse amountStr = context.selectedRequirements.amount into a BigInt, get decimals from the request-selected asset (e.g., context.selectedRequirements.decimals or fallback to activeBasePaymentAsset.decimals), compute integer and fractional parts via BigInt division and remainder with 10n**BigInt(decimals), format a decimal string (or a safe numeric if within JS safe range) for the normalized asset amount, then convert to USD only after normalization (using the correct asset-to-USD price if required) and store the resulting value in paymentStore.amountUsd (prefer string to preserve precision). Update references: context.selectedRequirements.amount, context.selectedRequirements.decimals (or activeBasePaymentAsset.decimals), and paymentStore.amountUsd to implement this BigInt-based normalization and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/balance.ts`:
- Around line 179-185: The tests call monitor.formatUSDC but the class only
defines formatUSD; add a thin compatibility shim on the BalanceMonitor class:
implement a formatUSDC(amountMicros: bigint): string method that simply calls
and returns this.formatUSD(amountMicros) (preserve the same signature/visibility
and TypeScript types) so existing tests and callers keep working without
changing formatUSD.
In `@src/doctor.ts`:
- Around line 147-159: The current logic only checks a single Base asset via
fetchBasePaymentAsset and one BalanceMonitor instance, which can misreport
isEmpty when a non-priority asset is funded; update the payment detection to
iterate all Base assets (the same list used by the /wallet flow) instead of
relying on a single fetched asset: call fetchBasePaymentAsset or its provider to
obtain the full list of Base assets, loop through them creating a
BalanceMonitor(address, asset) for each (and for Solana keep the
SolanaBalanceMonitor branch), collect each monitor.checkBalance() result, set
paymentAsset to the first asset with sufficient funds, and only mark overall
isEmpty if every asset returns empty; ensure downstream code (e.g.,
analyzeWithAI usage) uses the selected paymentAsset and aggregated isLow/isEmpty
state.
---
Outside diff comments:
In `@src/proxy.ts`:
- Around line 2218-2293: The REUSE_EXISTING path created inside tryListen (where
checkExistingProxy(listenPort) returns existingProxy2) only rejects with wallet
and paymentChain, losing paymentAsset/paymentAssets/selected-asset info; update
that rejectAttempt call to include the full existingProxy2 payload
(paymentAsset, paymentAssets, selectedAsset or similar fields) so the thrown
error carries all reuse metadata, and then in the outer retry loop where you
handle error.code === "REUSE_EXISTING" reconstruct the returned handle exactly
as the pre-listen reuse branch does (include paymentAsset, paymentAssets,
selectedAsset context, and any other fields from checkExistingProxy) instead of
only returning walletAddress and balanceMonitor; adjust references in tryListen,
rejectAttempt, and the error destructuring to propagate and consume those
fields.
---
Duplicate comments:
In `@src/balance.ts`:
- Around line 164-172: setAsset mutates a shared Balance instance which makes
monitor state non-deterministic (race between setAsset and
fetchBalance/deductEstimated/invalidate); change the design so asset changes are
not performed in-place on shared instances: either make Balance.asset immutable
after construction and have setAsset return a new Balance instance (and
atomically update the canonical mapping used by getSharedMonitorForAsset), or
ensure setAsset performs a safe copy-before-mutate and updates any registry
atomically so in-flight fetchBalance uses the original asset copy; apply the
same non-mutating/copy-on-write fix pattern to the other affected methods/blocks
referenced (the sections around lines 198-214 and 217-250) so all reads use a
local snapshot of asset/decimals/symbol for the duration of RPC work.
In `@src/proxy.ts`:
- Around line 1631-1657: The code currently treats
context.selectedRequirements.amount as 6-decimal USDC and uses Number math;
instead normalize using the selected asset's decimals with BigInt to avoid
inflation/precision loss: parse amountStr = context.selectedRequirements.amount
into a BigInt, get decimals from the request-selected asset (e.g.,
context.selectedRequirements.decimals or fallback to
activeBasePaymentAsset.decimals), compute integer and fractional parts via
BigInt division and remainder with 10n**BigInt(decimals), format a decimal
string (or a safe numeric if within JS safe range) for the normalized asset
amount, then convert to USD only after normalization (using the correct
asset-to-USD price if required) and store the resulting value in
paymentStore.amountUsd (prefer string to preserve precision). Update references:
context.selectedRequirements.amount, context.selectedRequirements.decimals (or
activeBasePaymentAsset.decimals), and paymentStore.amountUsd to implement this
BigInt-based normalization and formatting.
🪄 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
Run ID: 85db5e50-4ef2-4a9f-94ac-c642bd634c26
📒 Files selected for processing (4)
src/balance.tssrc/doctor.tssrc/index.tssrc/proxy.ts
What
Add proper multi-decimal EIP-3009 stablecoin support, fixing bugs that would cause incorrect behavior with non-6-decimal assets like fxUSD (18 decimals).
Why
ClawRouter's payment layer was hardcoded to assume 6 decimals (USDC) in several places. Adding fxUSD (18 decimals, verified on Basescan) exposed these gaps.
Changes
/healthendpoint now returnspaymentAssetDecimalsdecimalsfrom existing proxy (fixes 10^12× balance inflation for 18-decimal assets)transformPaymentErroruses asset-native decimals for USD conversion ininsufficient_fundserrorspayment-asset.ts(new) —BasePaymentAssettype, normalizer, fetcherbalance.ts— updated USDC-specific comments to generic stablecoinOn-Chain Verification
fxUSD contract (
0x55380fe7a1910dff29a47b622057ab4139da42c5) on Base implements full EIP-3009:transferWithAuthorization✅receiveWithAuthorization✅cancelAuthorization✅authorizationState+DOMAIN_SEPARATOR✅Testing
Summary by CodeRabbit