Skip to content

feat: support fxUSD and multi-decimal EIP-3009 stablecoins#109

Open
huwangtao123 wants to merge 7 commits intoBlockRunAI:mainfrom
huwangtao123:feat/fxusd-multi-asset-support
Open

feat: support fxUSD and multi-decimal EIP-3009 stablecoins#109
huwangtao123 wants to merge 7 commits intoBlockRunAI:mainfrom
huwangtao123:feat/fxusd-multi-asset-support

Conversation

@huwangtao123
Copy link
Copy Markdown

@huwangtao123 huwangtao123 commented Mar 21, 2026

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

  • /health endpoint now returns paymentAssetDecimals
  • Proxy reuse path preserves decimals from existing proxy (fixes 10^12× balance inflation for 18-decimal assets)
  • transformPaymentError uses asset-native decimals for USD conversion in insufficient_funds errors
  • payment-asset.ts (new) — BasePaymentAsset type, normalizer, fetcher
  • balance.ts — updated USDC-specific comments to generic stablecoin
  • 3 new tests — fxUSD normalization, mixed-decimal assets, real contract address

On-Chain Verification

fxUSD contract (0x55380fe7a1910dff29a47b622057ab4139da42c5) on Base implements full EIP-3009:

  • transferWithAuthorization
  • receiveWithAuthorization
  • cancelAuthorization
  • All typehashes + authorizationState + DOMAIN_SEPARATOR

Testing

  • 363 tests pass, 0 regressions
  • 3 new tests added for fxUSD-specific scenarios

Summary by CodeRabbit

  • New Features
    • Multi-asset Base support with per-request asset selection, runtime asset switching, normalized USD-micros balances, and payment-asset metadata surfaced in health/proxy outputs; default remains USDC on Base.
  • Bug Fixes / UX
    • Balance checks, shortfall messages and funding prompts now show the active asset symbol and use consistent USD formatting.
  • Tests
    • New tests covering payment-asset normalization, fetching, ordering and fallback behavior.

- 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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Balance 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

Cohort / File(s) Summary
Payment Asset Infrastructure
src/payment-asset.ts, src/payment-asset.test.ts
Adds BasePaymentAsset type and DEFAULT_BASE_PAYMENT_ASSET; normalization helpers (normalizeBasePaymentAsset, normalizeBasePaymentAssets), fetchers (fetchBasePaymentAsset(s)), priority/enablement filtering and fallback; includes Vitest coverage for normalization, fetching, fallback, ordering.
Balance Monitor Generalization
src/balance.ts
BalanceMonitor now accepts a BasePaymentAsset (default fallback), reads balances from this.asset.asset, normalizes on-chain amounts to USD micros, adds setAsset()/getAsset()/getAssetSymbol(), per-asset shared monitors cache, updates BalanceInfo to include assetSymbol, and renames USDC format helper to generic formatUSD().
Proxy Server Integration
src/proxy.ts
Introduces paymentAssets/selected asset state, persists/restores active asset selection, exposes asset metadata in /health, selects a sufficient Base asset per request by iterating candidate monitors, threads per-request requestBalanceMonitor for checks/optimistic deductions/invalidation, and surfaces asset metadata in error formatting.
CLI & Doctor output and startup
src/cli.ts, src/doctor.ts
Console messages and startup diagnostics now reference resolved Base payment asset symbol; doctor fetches payment asset (with fallback) and constructs BalanceMonitor(address, paymentAsset); funding/get-buy messages adjusted per asset.
Exports / Public API Surface & Wallet UI
src/index.ts, src/solana-balance.ts
Exports payment-asset utilities/types (DEFAULT_BASE_PAYMENT_ASSET, fetchBasePaymentAsset, normalizeBasePaymentAsset, BasePaymentAsset); Solana monitor now exposes assetSymbol in SolanaBalanceInfo and getAssetSymbol(); /wallet lists supported assets and per-asset balance lines.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support fxUSD and multi-decimal EIP-3009 stablecoins' accurately reflects the primary changes in the PR: adding support for multiple EIP-3009 stablecoins with varying decimals, specifically fxUSD.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.

✏️ 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
Copy Markdown

@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: 1

🧹 Nitpick comments (2)
src/proxy.ts (1)

1568-1577: Async asset refresh in onAfterPaymentCreation may race with concurrent requests.

This callback fetches assets and updates activeBasePaymentAssets and activeBasePaymentAsset while 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 in fetchBasePaymentAssets.

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

📥 Commits

Reviewing files that changed from the base of the PR and between aada5d0 and 62c8b9e.

📒 Files selected for processing (4)
  • src/balance.ts
  • src/payment-asset.test.ts
  • src/payment-asset.ts
  • src/proxy.ts

Adds docstrings to all exported types, constants, and functions
to satisfy the 80% docstring coverage CI requirement.
Copy link
Copy Markdown

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 62c8b9e and 8023115.

📒 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)
Copy link
Copy Markdown

@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

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 | 🟠 Major

Reuse mode still collapses Base multi-asset metadata to one asset.

/health now exposes paymentAssets and selectedPaymentAsset, but checkExistingProxy() only reads the singular paymentAsset* fields and the reuse path rewrites activeBasePaymentAssets to [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

📥 Commits

Reviewing files that changed from the base of the PR and between bef5432 and 5934269.

⛔ Files ignored due to path filters (5)
  • dist/cli.js is excluded by !**/dist/**
  • dist/cli.js.map is excluded by !**/dist/**, !**/*.map
  • dist/index.d.ts is excluded by !**/dist/**
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (5)
  • src/cli.ts
  • src/doctor.ts
  • src/index.ts
  • src/proxy.ts
  • src/solana-balance.ts

Copy link
Copy Markdown

@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: 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 | 🟠 Major

Mirror the full reuse metadata in the late EADDRINUSE path.

This branch reuses the existing proxy, but it drops paymentAsset, paymentAssets, and the selected-asset context that the earlier existingProxy branch 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 the checkExistingProxy() payload through REUSE_EXISTING and 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 instance getSharedMonitorForAsset() treats as canonical, so deductEstimated() / 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 | 🟠 Major

Actual payment accounting still assumes 6-decimal USDC amounts.

context.selectedRequirements.amount is asset-native units. Dividing it by 1_000_000 and storing it in a Number will inflate 18-decimal Base assets by 10^12 and can lose precision before that. Because paymentStore.amountUsd feeds chat, partner, and image usage logs, stats will be wrong for fxUSD payments until this is normalized with the request-selected asset’s decimals via BigInt.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5934269 and c89f668.

📒 Files selected for processing (4)
  • src/balance.ts
  • src/doctor.ts
  • src/index.ts
  • src/proxy.ts

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