Skip to content

Fix Reserve swap handling and Microbridge history reads - #274

Open
L03TJ3 wants to merge 7 commits into
masterfrom
reserve-and-bridge-fixes
Open

L03TJ3 wants to merge 7 commits into
masterfrom
reserve-and-bridge-fixes

Conversation

@L03TJ3

@L03TJ3 L03TJ3 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Improves Reserve swap transaction handling and error messaging, and reduces oversized Microbridge history requests.

Reserve fixes

  • Recognize known trading-limit errors within nested RPC responses, keeping wallet rejection separate.
  • Submit approvals and swaps through the wallet signer, avoiding incorrect Safe detection for wallets with EIP-7702 delegation markers.
  • Handle signature, mining, success and failure states, including mined reverts and successful replacement transactions.
  • Expose an alternative-Reserve availability check for Celo/XDC using quotes, slippage bounds and trading limits. Failed, stale or timed-out checks produce no recommendation.

Microbridge and provider fixes

  • Fetch wallet-filtered Fuse/Celo history in sequential chunks of at most 500 blocks, preserving the existing 20,000-block lookback.
  • Cache history progress per account, chain and deployment; retain partial results for retry after failures.
  • Allow read-only SDK providers without getSigner.

UI

  • Add a reusable Reserve limit modal with chain-specific DEX links and optional verified alternative-chain messaging.
  • Support custom titles and footers in ErrorModal.

Testing and integration

  • Add mocked regression tests for history fetching and provider compatibility.
  • The modal and alternative-chain check are exported for consumer integration; this PR does not wire them into an application.
  • Tests and builds were not rerun during this review against the final commit.

Summary by Sourcery

Improve Reserve swap reliability and Microbridge history synchronization while exposing reusable limit-handling UI and SDK integration APIs.

New Features:

  • Add Reserve alternative-chain availability checks based on live quotes, slippage bounds, trading limits, and fresh read-only provider data.
  • Expose a reusable Reserve trading-limit modal with chain-specific DEX links and optional alternative-chain messaging.
  • Add development-only QA support for forcing Reserve trading-limit failures through the SDK context.

Bug Fixes:

  • Improve Reserve transaction error classification, including nested trading-limit reverts, wallet rejection separation, mined reverts, and successful replacement transactions.
  • Route Reserve and Microbridge submissions through the appropriate wallet signer path, including support for EIP-7702 delegated wallets.
  • Reduce Microbridge history RPC requests by fetching account-filtered events sequentially in chunks of up to 500 blocks while retaining the 20,000-block lookback.
  • Preserve partial Microbridge history results and retry progress after provider failures or cancellation.

Enhancements:

  • Support read-only SDK providers that do not expose getSigner.
  • Add cache scoping and incremental synchronization for Microbridge history by account, chain, and deployment.
  • Allow ErrorModal consumers to provide custom titles and footers.

Build:

  • Extend SDK lint-staged checks to include production and test TypeScript compilation.
  • Add a dedicated TypeScript configuration for SDK tests.

Documentation:

  • Document the development-only Reserve trade-limit QA mode and SDK publishing workflow.

Tests:

  • Add regression coverage for Reserve QA behavior, alternative-chain checks, transaction handling, provider compatibility, and Microbridge history synchronization.

L03TJ3 added 2 commits October 6, 2026 20:11
- Fetch wallet-filtered Microbridge history in sequential chunks of at most 500 blocks.
- Cache successful history progress and retain partial results when requests fail.
- Support read-only SDK providers without getSigner.
- Add a reusable Reserve limit modal with chain-specific DEX links and optional alternative-chain messaging.
- Add history and provider regression tests.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Provider compatibility, CommonJS loading, history correctness, and TypeScript issues remain unresolved.

7 open findings
What changed in this PR

Improves Reserve swap reliability, Microbridge history synchronization, read-only provider support, and reusable error UI.

Changes:

  • Adds Reserve error detection, signer-based transactions, and alternative-chain checks.
  • Reworks Microbridge history into chunked, account-scoped cached reads.
  • Extends provider compatibility, tests, type-checking, and modal customization.
File Description
.eslintrc.cjs Adds SDK test lint configuration.
packages/​sdk-v2/​tsconfig.tests.json Adds test-specific TypeScript configuration.
packages/​sdk-v2/​lint-staged.config.js Type-checks production and test sources.
packages/​sdk-v2/​src/​sdk/​base/​sdk.ts Guards signer access for read-only providers.
packages/​sdk-v2/​src/​sdk/​base/​sdk.test.ts Tests provider and signer compatibility.
packages/​sdk-v2/​src/​sdk/​base/​react.readProviders.test.tsx Tests fallback-provider hook integration.
packages/​sdk-v2/​src/​sdk/​reserve/​react.tsx Adds direct wallet submission and alternative checks.
packages/​sdk-v2/​src/​sdk/​reserve/​errors.ts Classifies nested Reserve-limit errors.
packages/​sdk-v2/​src/​sdk/​reserve/​alternative.ts Validates alternative-chain quotes and limits.
packages/​sdk-v2/​src/​sdk/​reserve/​index.ts Exports new Reserve APIs.
packages/​sdk-v2/​src/​sdk/​microbridge/​history.helpers.ts Implements chunked, cached history synchronization.
packages/​sdk-v2/​src/​sdk/​microbridge/​history.helpers.test.ts Tests chunking, retries, caching, and decoding.
packages/​sdk-v2/​src/​sdk/​microbridge/​useMicroBridgeHistory.ts Adds the history synchronization hook.
packages/​sdk-v2/​src/​sdk/​microbridge/​useMicroBridgeHistory.test.tsx Tests hook refresh and account isolation.
packages/​sdk-v2/​src/​sdk/​microbridge/​react.ts Uses the new history hook.
packages/​sdk-v2/​src/​sdk/​microbridge/​index.ts Exports history APIs.
packages/​good-design/​src/​core/​web3/​modals/​ReserveLimitModal.tsx Adds Reserve-limit guidance UI.
packages/​good-design/​src/​core/​web3/​modals/​ErrorModal.tsx Supports custom titles and footers.
packages/​good-design/​src/​core/​web3/​modals/​index.ts Exports the Reserve modal.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

jest.mock("../../hooks/useRefreshOrNever", () => ({ __esModule: true, default: () => 5 }));
jest.mock("../storage", () => ({ AsyncStorage: { getItem: jest.fn(), setItem: jest.fn() } }));

let latest: ReturnType<typeof useMicroBridgeHistory>;
@@ -0,0 +1,86 @@
import { useBlockNumbers, useEthers } from "@usedapp/core";
import { useReadonlyNetworks } from "@usedapp/core/dist/esm/src/providers/network/readonlyNetworks";
Comment on lines +13 to +17
export interface ReserveTransactionStatus extends TransactionStatus {
errorType?: typeof RESERVE_LIMIT_ERROR;
/** The unmodified provider/ethers error, for debugging rather than display. */
originalError?: unknown;
}
Comment on lines +104 to +105
// Read-only BaseProviders (including FallbackProvider) do not expose a signer.
if (typeof provider.getSigner === "function") {
cancelled: () => boolean = () => false
) => {
const windowStart = Math.max(0, latestBlock - HISTORY_LOOKBACK_BLOCKS);
const fromBlock = Math.max(windowStart, (cache.lastSyncedBlock ?? -1) + 1);
): Promise<ReserveAlternative | undefined> => {
if (!deployment?.MentoBroker || !deployment.MentoExchangeProvider || !deployment.GoodDollar) return undefined;
if (!!request.exactInput === !!request.exactOutput) return undefined;
const actualChainId = BigNumber.from(await provider.send("eth_chainId", [])).toNumber();
Comment on lines +78 to +80
} else if (value && typeof value === "object" && !seen.has(value)) {
seen.add(value);
const record = value as Record<string, unknown>;

L03TJ3 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot please extend the wallet-signing fix in this PR to the MicroBridge flow as well.

Reserve now avoids the incorrect Safe detection for wallets with EIP-7702 delegation markers, but MicroBridge still goes through useContractFunctionWithDefaultGasFees → usedapp useContractFunction. In usedapp 1.2.16, a wallet with nonempty code is treated as a Safe and enters CollectingSignaturePool. We reproduced that with mocked wallet responses; this is not yet confirmation that delegation markers caused the reported Fuse issues.

Requested change:

  • Apply the same underlying correction to MicroBridge's transferAndCall submission, reusing the Reserve approach where practical. Keep it small and avoid a wider refactor.
  • Delegated EOAs must use the normal wallet signing flow. Preserve genuine Safe support rather than classifying every address with code as a Safe.
  • Keep pending-signature, mining, success and failure states accurate, including rejection and a reverted receipt. Do not resubmit a transaction just because signature/status handling fails.
  • Add focused regression tests for an ordinary EOA, a delegation-marker wallet, and genuine Safe handling. Confirm the existing MicroBridge history tests still pass.

Relevant entry points: packages/sdk-v2/src/sdk/microbridge/react.ts and packages/sdk-v2/src/sdk/base/hooks/useGasFees.ts.

Please update the PR description with the added scope and the tests actually run. Keep the separate RPC/history changes intact.

Copilot AI commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@copilot please extend the wallet-signing fix in this PR to the MicroBridge flow as well.

Reserve now avoids the incorrect Safe detection for wallets with EIP-7702 delegation markers, but MicroBridge...

Implemented in commit COMMIT_HASH: only the EIP-7702 delegation designator bypasses usedapp Safe detection; ordinary EOAs and genuine Safes retain usedapp submission. Added transaction-state and wallet-routing regression tests. The MicroBridge signing and history Jest tests (7), SDK test/production TypeScript checks, targeted ESLint, and Prettier passed.

L03TJ3 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot one focused follow-up on d952cf8: in useMicroBridgeTransfer.ts, the transaction.wait() catch only handles errors with a receipt. If receipt lookup fails without one, the catch silently ends and leaves the state at Mining indefinitely.

Please handle that case too:

  • Keep the submitted transaction and hash available.
  • Surface a clear confirmation/tracking error. The on-chain result is unknown, so don't label it as a confirmed revert.
  • Do not resubmit the transaction automatically.
  • Add a focused test where wait() rejects without a receipt, and rerun the existing focused tests.

Relevant block:

.catch((error: any) => {
const receipt = error?.receipt as ethers.providers.TransactionReceipt | undefined;
const replacement = error?.code === "TRANSACTION_REPLACED" && !error.cancelled;
if (receipt && (error?.code !== "TRANSACTION_REPLACED" || replacement || error.cancelled)) {
setWalletSignerState({
status: !error.cancelled && receipt.status !== 0 ? "Success" : "Fail",
chainId,
transaction: replacement ? error.replacement : submittedTransaction,
receipt,
transactionName: "BridgeTransfer",
errorMessage: error.cancelled || receipt.status === 0 ? "Bridge transaction failed" : undefined
});
}
});

Keep this small; no broader refactor needed. Also describe the current Safe test accurately as fallback-routing coverage, rather than live Safe execution.

Copilot AI commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@copilot one focused follow-up on d952cf8: in useMicroBridgeTransfer.ts, the transaction.wait() catch only handles errors with a receipt. If receipt lookup fails without one, the catch silently en...

Implemented in commit 594ff25. Receipt-wait errors without a receipt now keep the submitted transaction in Mining with an explicit unconfirmed tracking error and no automatic retry. Added a regression test; renamed the Safe test to clarify it covers usedapp fallback routing only. The focused signing and history tests pass (8 total).

L03TJ3 commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot please add a minimal, opt-in QA configuration path to this PR for the Reserve-limit check in #275. This is paired with GoodDollar/GoodProtocolUI#645; preserve the existing Reserve, Microbridge and provider fixes.

Proposed shared contract: optional qa?: { forceTradeLimits?: boolean } on the SDK Web3Provider, exposed through useWeb3Context, default off. ProtocolUI will map REACT_APP_QA_FORCE_TRADE_LIMITS=true to that option only in a development build. Please use this same contract across the paired work.

  • Extend packages/sdk-v2/src/contexts/Web3Context.tsx and consume the option in packages/sdk-v2/src/sdk/reserve/react.tsx.
  • On Celo/XDC, force a recognized limit error (for example L0Out Exceeded) inside the existing swap error-handling path, before obtaining a signer or submitting anything. Cover both swapIn and swapOut. Keep the selected chainId and real error classifier; do not directly fake modal state.
  • In this QA mode, block real Reserve approval/submission even if those methods are called directly. Do not report a blocked approval as a successful approval or trade-limit error.
  • Default/false must leave normal behaviour unchanged. Add explicit production-build safeguards and document them. Development-build gating is separate from the SDK deployment/network env: QA still needs Celo/XDC mainnet read-only data. Do not require changing contract deployment env to test.
  • ProtocolUI must allow an empty wallet to reach the labelled simulation without funding or allowance. Do not fake displayed balances or implement a general mock harness. Expose a reliable effective mode/capability if needed so the UI cannot bypass its gates with an old or inactive SDK.
  • Add focused tests for no signer/send calls, approval blocking, classifier/chainId, both swap methods and off/production behaviour. Preserve existing focused tests. Report exact commands and outcomes, including anything not run.

Keep the current yalc workflow for QA; npm publication is not a prerequisite. Document the exact paired API and any package rebuild steps. Do not expand this into success/receipt/error simulations or claim live transaction/delegated-wallet execution was tested.

Copilot AI commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

@copilot please add a minimal, opt-in QA configuration path to this PR for the Reserve-limit check in #275. This is paired with GoodDollar/GoodProtoco...

Implemented in commits c3ed576 and LATEST_COMMIT: added the opt-in SDK API and effective development-only capability, guarded Reserve submissions in production, and routed Celo/XDC swapIn/swapOut through the existing classifier before signer access. QA approvals are separately blocked. Added focused tests and documented the paired ProtocolUI contract and yalc workflow. Validation: 4 focused suites/14 tests, both TypeScript checks, targeted ESLint/Prettier, and CodeQL (0 alerts) passed. The package build's final yalc step could not run because yalc is unavailable; GitHub Actions status lookup failed authentication.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T16:55:53.843706Z 958b5cd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 958b5cdc1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +231 to +232
const signer = (library as ethers.providers.Web3Provider).getSigner(account);
const contractWithSigner = contract.connect(signer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the Safe-aware path for contract wallets

When the connected account is an actual Safe or another contract wallet whose connector cannot sign directly as that address, unconditionally calling library.getSigner(account) bypasses the Safe-specific submission path previously provided by useContractFunctionWithDefaultGasFees, so Reserve approvals and swaps fail instead of opening the Safe transaction flow. Restrict this direct signer path to confirmed EIP-7702 delegation bytecode, as the new MicroBridge hook does, and retain the existing usedapp path for other accounts with code.

Useful? React with 👍 / 👎.

Comment on lines +35 to +42
for (const chunk of chunks) {
if (requested) {
await new Promise(resolve => setTimeout(resolve, HISTORY_REQUEST_DELAY_MS));
}
if (cancelled()) return { cache: next };
requested = true;
const logs = await contract.provider.getLogs({ address: contract.address, topics, ...chunk });
next[eventName] = dedupeLogs([...(next[eventName] || []), ...logs]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resume after the last successful history chunk

When an RPC consistently permits only a bounded number of requests before returning 429, every retry starts again with the newest chunk of the first event/topic filter. Because lastSyncedBlock advances only after all 164 requests succeed, the already-successful chunks are repeatedly fetched and older chunks or later filters may never be reached, leaving history permanently incomplete despite retaining the partial logs. Persist per-filter chunk progress or otherwise skip ranges already completed during the partial sync.

Useful? React with 👍 / 👎.

Comment on lines +36 to +38
if (requested) {
await new Promise(resolve => setTimeout(resolve, HISTORY_REQUEST_DELAY_MS));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid the 81-second minimum initial history load

For an uncached 20,000-block window this loop makes 41 chunks × 2 account-topic positions × 2 event types = 164 requests, and the unconditional 500 ms pause after the first request imposes at least 81.5 seconds of latency per chain before the hook publishes the fetched history, excluding RPC time. This makes the initial history view remain empty far beyond its existing 30-second expectation; pace only after throttling, reduce the delay, or expose completed chunks incrementally.

Useful? React with 👍 / 👎.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 12 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/sdk-v2/src/sdk/reserve/react.tsx" line_range="231-237" />
<code_context>
+          },
+          value => value != null
+        );
+        const signer = (library as ethers.providers.Web3Provider).getSigner(account);
+        transaction = (await contract
+          .connect(signer)
</code_context>
<issue_to_address>
**Safe Reserve swaps fail**

When the connected account is a Safe wallet whose provider requires Safe-aware transaction submission, `useReserveContractFunction` sends every approval and swap through `getSigner(account)` and invokes the contract directly, bypassing usedapp’s Safe-aware submission path. For a connected Safe that cannot sign transactions from its own contract address, the call fails and the user cannot complete the Reserve action.

Use the direct signer path only for the affected EIP-7702 accounts and retain a Safe-aware submission path for Safe wallets.
</issue_to_address>

### Comment 2
<location path="packages/sdk-v2/src/sdk/reserve/alternative.ts" line_range="93" />
<code_context>
+): Promise<ReserveAlternative | undefined> => {
+  if (!deployment?.MentoBroker || !deployment.MentoExchangeProvider || !deployment.GoodDollar) return undefined;
+  if (!!request.exactInput === !!request.exactOutput) return undefined;
+  const actualChainId = BigNumber.from(await provider.send("eth_chainId", [])).toNumber();
+  if (actualChainId !== chainId) return undefined;
+  const block = await provider.getBlock("latest");
</code_context>
<issue_to_address>
**Fallback providers lose recommendations**

When the alternative-chain read provider is a FallbackProvider, `checkReserveAlternative` calls `provider.send("eth_chainId", [])`, which the provider does not expose; `useSwap.checkAlternativeReserve` catches the error and returns `undefined`, so users do not receive an otherwise valid recommendation.

Use a chain-ID check supported by the configured provider instead of calling `provider.send`.
</issue_to_address>

### Comment 3
<location path="packages/sdk-v2/src/sdk/reserve/react.tsx" line_range="243-264" />
<code_context>
+        const receipt = await transaction.wait();
</code_context>
<issue_to_address>
**Submitted swaps appear blocked or failed**

When a caller awaits a Reserve send for its transaction response, or receipt polling fails after broadcast, `useReserveContractFunction.send` waits for `transaction.wait()` before returning, so callers do not get the submitted transaction promptly; if receipt polling fails without a receipt, the catch path marks the submitted transaction `Fail` even though it may still succeed.

Return the transaction after submission and track its receipt separately, treating missing receipt confirmation as pending rather than a mined failure.

Also at `packages/sdk-v2/src/sdk/reserve/react.tsx:282-294`.
</issue_to_address>

### Comment 4
<location path="packages/sdk-v2/src/sdk/reserve/alternative.ts" line_range="70" />
<code_context>
+    return next.gte(minInt48) && next.lte(maxInt48) && next.gte(BigNumber.from(limitOut).mul(-1)) && next.lte(limitIn);
+  };
+  if (config.flags & 1) {
+    const flow = chainTimestamp > state.lastUpdated0 + config.timestep0 ? 0 : state.netflow0;
+    if (!within(flow, config.limit0In, config.limit0Out)) return false;
+  }
</code_context>
<issue_to_address>
**Alternative Reserve check aborts**

When an alternative Reserve has either time-window limit enabled and the decoded uint32 fields are ethers BigNumbers, `tradingLimitsConfig` and `tradingLimitsState` return decoded tuple values that are cast to `LimitConfig` and `LimitState` without converting their `uint32` fields. With ethers `BigNumber` values, adding `state.lastUpdated0` to `config.timestep0` (and likewise for window 1) throws during implicit conversion, so the check fails and no alternative-chain recommendation is shown.

Normalize the decoded timestamp and timestep fields to numbers (or compare using BigNumber arithmetic) before checking whether a window has expired.

Also at `packages/sdk-v2/src/sdk/reserve/alternative.ts:75`.
</issue_to_address>

### Comment 5
<location path="packages/sdk-v2/src/sdk/microbridge/useMicroBridgeTransfer.ts" line_range="32-41" />
<code_context>
+      let code: string;
+      try {
+        code = await library.getCode(account);
+      } catch (error: any) {
+        setUsesWalletSigner(true);
+        setWalletSignerState({
+          status: "Exception",
+          chainId,
+          transactionName: "BridgeTransfer",
+          errorMessage: error?.message || "Unable to check wallet code"
+        });
+        return undefined;
+      }
+
+      if (!isEip7702DelegationCode(code)) {
</code_context>
<issue_to_address>
**Code lookup blocks bridge transfers**

When the wallet RPC fails or times out while fetching the account’s code, `getCode(account)` errors set an exception and return without calling `usedapp.send`, so a temporary RPC failure prevents a Microbridge transfer that the existing submission path could otherwise attempt.

On lookup failure, fall back to the existing submission path or retry the code lookup before blocking the transfer.
</issue_to_address>

### Comment 6
<location path="packages/sdk-v2/src/sdk/microbridge/useMicroBridgeHistory.ts" line_range="56" />
<code_context>
+        const result = await syncMicroBridgeHistory(contract, latestBlock, account, cache, () => cancelled);
+        if (cancelled) return;
+        cache = result.cache;
+        setHistory({ key: cacheKey, cache });
+        await AsyncStorage.setItem(cacheKey, cache).catch(() => undefined);
+        if (result.error) console.warn("Microbridge history refresh failed", { chainId });
</code_context>
<issue_to_address>
**First history appears only after the scan**

When a wallet has no cached history and its chain requires a full lookback scan, `useMicroBridgeHistory` publishes history only after `syncMicroBridgeHistory` finishes; the sequential requests and pauses take roughly 82 seconds, leaving users without history while the scan runs.

Publish and persist scan progress as ranges complete so users can see the history collected so far.

Also at `packages/sdk-v2/src/sdk/microbridge/history.helpers.ts:37`.
</issue_to_address>

### Comment 7
<location path="packages/sdk-v2/src/sdk/reserve/react.tsx" line_range="243-251" />
<code_context>
+        )) as ethers.providers.TransactionResponse;
+        setState({ status: "Mining", transaction, chainId });
+
+        const receipt = await transaction.wait();
+        const failed = receipt.status === 0;
+        setState({
+          status: failed ? "Fail" : "Success",
+          transaction,
+          receipt,
+          chainId,
+          errorMessage: failed ? "Reserve transaction failed" : undefined
+        });
+        setEvents(
+          receipt.logs.reduce<ethers.utils.LogDescription[]>((parsed, log) => {
</code_context>
<issue_to_address>
**Older submissions overwrite current state**

When a caller starts another submission while one is pending, or resets a Microbridge send before its receipt settles, the earlier receipt callback can still update the shared state after a newer submission or reset, so callers see the wrong submission status, transaction, or receipt.

Ignore completion callbacks for submissions that have been superseded or reset.

Also at `packages/sdk-v2/src/sdk/microbridge/useMicroBridgeTransfer.ts:50`, `packages/sdk-v2/src/sdk/microbridge/useMicroBridgeTransfer.ts:81-94`, `packages/sdk-v2/src/sdk/reserve/react.tsx:222`.
</issue_to_address>

### Comment 8
<location path="packages/sdk-v2/src/sdk/reserve/react.tsx" line_range="269-279" />
<code_context>
+
+        const receipt = await transaction.wait();
+        const failed = receipt.status === 0;
+        setState({
+          status: failed ? "Fail" : "Success",
+          transaction,
+          receipt,
+          chainId,
+          errorMessage: failed ? "Reserve transaction failed" : undefined
+        });
+        setEvents(
+          receipt.logs.reduce<ethers.utils.LogDescription[]>((parsed, log) => {
+            try {
+              if (log.address.toLowerCase() === contract.address.toLowerCase()) {
+                parsed.push(contract.interface.parseLog(log));
+              }
+            } catch {
+              // Ignore logs not described by the contract ABI.
+            }
+            return parsed;
+          }, [])
+        );
+        return receipt;
+      } catch (error: any) {
+        // ethers rejects wait() for mined reverts and replacement transactions.
</code_context>
<issue_to_address>
**Replacement receipts have no decoded events**

When ethers reports a successful `TRANSACTION_REPLACED` result, the replacement branch stores the receipt and returns before parsing its logs or updating `events`, so callers see no decoded events for the successfully mined transaction.

Parse the replacement receipt’s logs and update `events` before returning it.

Also at `packages/sdk-v2/src/sdk/reserve/react.tsx:194`.
</issue_to_address>

### Comment 9
<location path="packages/sdk-v2/src/sdk/microbridge/useMicroBridgeHistory.ts" line_range="82" />
<code_context>
+  return useMemo(() => {
+    if (!contract || !cacheKey || history?.key !== cacheKey) return {};
+    return {
+      requests: { value: decodeMicroBridgeHistoryLogs(contract, "BridgeRequest", history.cache.BridgeRequest) },
+      transfers: { value: decodeMicroBridgeHistoryLogs(contract, "ExecutedTransfer", history.cache.ExecutedTransfer) }
+    };
</code_context>
<issue_to_address>
**History appears empty during initial scan**

When the account has no stored history cache, or cache hydration fails, and an initial RPC scan is still in progress, `useMicroBridgeHistory` exposes hydrated empty caches as `value: []` before the first history sync finishes. `useBridgeHistory` therefore returns an empty `historySorted` array, which the bridge UI treats as loaded and renders as an empty history instead of showing its loading spinner while the sequential RPC scan runs.

Keep the history result undefined until the initial sync completes, or expose a loading state that the bridge UI uses while syncing.

Also at `packages/sdk-v2/src/sdk/microbridge/history.helpers.ts:36-37`.
</issue_to_address>

### Comment 10
<location path="packages/sdk-v2/src/sdk/reserve/react.qa.test.tsx" line_range="103" />
<code_context>
+    expect(latest.swap.state).toMatchObject({
+      status: "Exception",
+      chainId: 42220,
+      errorType: RESERVE_LIMIT_ERROR
+    });
+    expect((latest.swap.state as any).originalError.message).toBe("L0Out Exceeded");
</code_context>
<issue_to_address>
**Every swap error looks like a limit**

When the limit classifier returns the Reserve-limit type for unrelated swap errors, when `getReserveLimitReason` incorrectly classifies every exception as a Reserve limit, the “routes swapIn through the existing limit classifier” test still passes at line 103 because it exercises only the expected `L0Out Exceeded` exception. Unrelated swap failures would then receive the limit-error type without this test detecting it.

Add a swap case that throws a non-limit error and assert that its status has no Reserve-limit error type.
</issue_to_address>

### Comment 11
<location path="packages/sdk-v2/src/sdk/reserve/alternative.ts" line_range="6-7" />
<code_context>
+const brokerAbi = [
+  "function getAmountOut(address,bytes32,address,address,uint256) view returns (uint256)",
+  "function getAmountIn(address,bytes32,address,address,uint256) view returns (uint256)",
+  "function tradingLimitsConfig(bytes32) view returns (tuple(uint32 timestep0,uint32 timestep1,int48 limit0In,int48 limit0Out,int48 limit1In,int48 limit1Out,int48 limitGlobal,uint8 flags))",
+  "function tradingLimitsState(bytes32) view returns (tuple(uint32 lastUpdated0,uint32 lastUpdated1,int48 netflow0,int48 netflow1,int48 netflowGlobal))"
+];
+const exchangeAbi = ["function getExchanges() view returns (tuple(bytes32 exchangeId,address[] assets)[])"];
</code_context>
<issue_to_address>
**New lines exceed configured width**

When the new Reserve files are formatted, the new ABI declarations and revert-matching expression exceed the repository’s 120-character line width, so these additions do not follow the configured formatting convention.

Reformat or split the overlong declarations and expression to keep lines within 120 characters.

Also at `packages/sdk-v2/src/sdk/reserve/errors.ts:47`.
</issue_to_address>

### Comment 12
<location path="packages/good-design/src/core/web3/modals/ReserveLimitModal.tsx" line_range="38" />
<code_context>
+  return [];
+};
+
+export const ReserveLimitModal = ({ chainId, otherChainId, onClose }: ReserveLimitModalProps) => {
+  const { i18n } = useLingui();
+  if (chainId === undefined) return null;
</code_context>
<issue_to_address>
**New modal is absent from Storybook**

When the new modal and ErrorModal customization are added, `ReserveLimitModal` is added without a corresponding story, and the existing `ErrorModal` story still uses only the default title and reason. The new modal and title/footer customization are therefore not represented in Storybook.

Add or update Storybook stories to cover `ReserveLimitModal` and the custom title and footer options.

Also at `packages/good-design/src/core/web3/modals/ErrorModal.tsx:10`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 10 findings to address first, and the Reserve swap and Microbridge transfer paths now submit transactions through custom wallet-signing logic, so a parameter or transaction-handling defect could exchange or transfer tokens on-chain. Reverting stops future use but cannot undo a confirmed transaction.

Blocking findings: packages/sdk-v2/src/sdk/reserve/react.tsx:237, packages/sdk-v2/src/sdk/reserve/alternative.ts:93, packages/sdk-v2/src/sdk/reserve/react.tsx:264, packages/sdk-v2/src/sdk/reserve/alternative.ts:70, packages/sdk-v2/src/sdk/microbridge/useMicroBridgeTransfer.ts:41, and 5 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +231 to +237
const signer = (library as ethers.providers.Web3Provider).getSigner(account);
const contractWithSigner = contract.connect(signer);
const overrides: ethers.Overrides = {};
// Match useContractFunctionWithDefaultGasFees without entering usedapp's Safe detection.
if (gasFees.gasPrice != null) overrides.maxFeePerGas = gasFees.gasPrice;
if (gasFees.maxPriorityFeePerGas != null) overrides.maxPriorityFeePerGas = gasFees.maxPriorityFeePerGas;
transaction = (await contractWithSigner[functionName](

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Safe Reserve swaps fail

When the connected account is a Safe wallet whose provider requires Safe-aware transaction submission, useReserveContractFunction sends every approval and swap through getSigner(account) and invokes the contract directly, bypassing usedapp’s Safe-aware submission path. For a connected Safe that cannot sign transactions from its own contract address, the call fails and the user cannot complete the Reserve action.

Use the direct signer path only for the affected EIP-7702 accounts and retain a Safe-aware submission path for Safe wallets.

Prompt for AI agents
In `packages/sdk-v2/src/sdk/reserve/react.tsx` at lines 231-237:

**Safe Reserve swaps fail**

When the connected account is a Safe wallet whose provider requires Safe-aware transaction submission, `useReserveContractFunction` sends every approval and swap through `getSigner(account)` and invokes the contract directly, bypassing usedapp’s Safe-aware submission path. For a connected Safe that cannot sign transactions from its own contract address, the call fails and the user cannot complete the Reserve action.

Use the direct signer path only for the affected EIP-7702 accounts and retain a Safe-aware submission path for Safe wallets.

Comment on lines +243 to +264
const receipt = await transaction.wait();
const failed = receipt.status === 0;
setState({
status: failed ? "Fail" : "Success",
transaction,
receipt,
chainId,
errorMessage: failed ? "Reserve transaction failed" : undefined
});
setEvents(
receipt.logs.reduce<ethers.utils.LogDescription[]>((parsed, log) => {
try {
if (log.address.toLowerCase() === contract.address.toLowerCase()) {
parsed.push(contract.interface.parseLog(log));
}
} catch {
// Ignore logs not described by the contract ABI.
}
return parsed;
}, [])
);
return receipt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Submitted swaps appear blocked or failed

When a caller awaits a Reserve send for its transaction response, or receipt polling fails after broadcast, useReserveContractFunction.send waits for transaction.wait() before returning, so callers do not get the submitted transaction promptly; if receipt polling fails without a receipt, the catch path marks the submitted transaction Fail even though it may still succeed.

Return the transaction after submission and track its receipt separately, treating missing receipt confirmation as pending rather than a mined failure.

Also at packages/sdk-v2/src/sdk/reserve/react.tsx:282-294.

Prompt for AI agents
In `packages/sdk-v2/src/sdk/reserve/react.tsx` at lines 243-264:

**Submitted swaps appear blocked or failed**

When a caller awaits a Reserve send for its transaction response, or receipt polling fails after broadcast, `useReserveContractFunction.send` waits for `transaction.wait()` before returning, so callers do not get the submitted transaction promptly; if receipt polling fails without a receipt, the catch path marks the submitted transaction `Fail` even though it may still succeed.

Return the transaction after submission and track its receipt separately, treating missing receipt confirmation as pending rather than a mined failure.

Also at `packages/sdk-v2/src/sdk/reserve/react.tsx:282-294`.

Comment on lines +32 to +41
} catch (error: any) {
setUsesWalletSigner(true);
setWalletSignerState({
status: "Exception",
chainId,
transactionName: "BridgeTransfer",
errorMessage: error?.message || "Unable to check wallet code"
});
return undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Code lookup blocks bridge transfers

When the wallet RPC fails or times out while fetching the account’s code, getCode(account) errors set an exception and return without calling usedapp.send, so a temporary RPC failure prevents a Microbridge transfer that the existing submission path could otherwise attempt.

On lookup failure, fall back to the existing submission path or retry the code lookup before blocking the transfer.

Prompt for AI agents
In `packages/sdk-v2/src/sdk/microbridge/useMicroBridgeTransfer.ts` at lines 32-41:

**Code lookup blocks bridge transfers**

When the wallet RPC fails or times out while fetching the account’s code, `getCode(account)` errors set an exception and return without calling `usedapp.send`, so a temporary RPC failure prevents a Microbridge transfer that the existing submission path could otherwise attempt.

On lookup failure, fall back to the existing submission path or retry the code lookup before blocking the transfer.

return useMemo(() => {
if (!contract || !cacheKey || history?.key !== cacheKey) return {};
return {
requests: { value: decodeMicroBridgeHistoryLogs(contract, "BridgeRequest", history.cache.BridgeRequest) },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · History appears empty during initial scan

When the account has no stored history cache, or cache hydration fails, and an initial RPC scan is still in progress, useMicroBridgeHistory exposes hydrated empty caches as value: [] before the first history sync finishes. useBridgeHistory therefore returns an empty historySorted array, which the bridge UI treats as loaded and renders as an empty history instead of showing its loading spinner while the sequential RPC scan runs.

Keep the history result undefined until the initial sync completes, or expose a loading state that the bridge UI uses while syncing.

Also at packages/sdk-v2/src/sdk/microbridge/history.helpers.ts:36-37.

Prompt for AI agents
In `packages/sdk-v2/src/sdk/microbridge/useMicroBridgeHistory.ts` at line 82:

**History appears empty during initial scan**

When the account has no stored history cache, or cache hydration fails, and an initial RPC scan is still in progress, `useMicroBridgeHistory` exposes hydrated empty caches as `value: []` before the first history sync finishes. `useBridgeHistory` therefore returns an empty `historySorted` array, which the bridge UI treats as loaded and renders as an empty history instead of showing its loading spinner while the sequential RPC scan runs.

Keep the history result undefined until the initial sync completes, or expose a loading state that the bridge UI uses while syncing.

Also at `packages/sdk-v2/src/sdk/microbridge/history.helpers.ts:36-37`.

expect(latest.swap.state).toMatchObject({
status: "Exception",
chainId: 42220,
errorType: RESERVE_LIMIT_ERROR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Every swap error looks like a limit

When the limit classifier returns the Reserve-limit type for unrelated swap errors, when getReserveLimitReason incorrectly classifies every exception as a Reserve limit, the “routes swapIn through the existing limit classifier” test still passes at line 103 because it exercises only the expected L0Out Exceeded exception. Unrelated swap failures would then receive the limit-error type without this test detecting it.

Add a swap case that throws a non-limit error and assert that its status has no Reserve-limit error type.

Prompt for AI agents
In `packages/sdk-v2/src/sdk/reserve/react.qa.test.tsx` at line 103:

**Every swap error looks like a limit**

When the limit classifier returns the Reserve-limit type for unrelated swap errors, when `getReserveLimitReason` incorrectly classifies every exception as a Reserve limit, the “routes swapIn through the existing limit classifier” test still passes at line 103 because it exercises only the expected `L0Out Exceeded` exception. Unrelated swap failures would then receive the limit-error type without this test detecting it.

Add a swap case that throws a non-limit error and assert that its status has no Reserve-limit error type.

L03TJ3 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot Please fix the alternative-chain Reserve check for ethers FallbackProvider. The current provider.send('eth_chainId', ...) call is not supported by that provider, so an XDC Reserve-limit error can fail to show the verified Celo alternative in GoodProtocolUI. Use the provider's getNetwork() to obtain the chain ID, retain the existing mismatch and quote/limit checks, and add a focused test using a FallbackProvider. Please keep this change scoped; the suggestion must not switch chains or initiate a transaction.

Copilot AI commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@copilot Please fix the alternative-chain Reserve check for ethers FallbackProvider. The current provider.send('eth_chainId', ...) call is not supported by that provider, so an XDC Reserve-limit e...

Fixed in commit 7c8e220. The alternative Reserve check now uses provider.getNetwork().chainId and accepts the common ethers providers.Provider type. Added a FallbackProvider regression verifying the read-only check can complete without child JSON-RPC send; quote/limit checks remain intact. Focused Reserve tests (2 suites, 6 tests), both SDK TypeScript checks, ESLint, Prettier, and CodeQL passed.

This branch has not been deployed

No deployments
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.

3 participants