Repository navigation
Conversation
…gation markers to reserve flow
- 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.
There was a problem hiding this comment.
🟡 Changes recommended
Provider compatibility, CommonJS loading, history correctness, and TypeScript issues remain unresolved.
7 open findings
Strict test compilation reports latest as used before assignment · New CommonJS build imports incompatible usedapp ESM internals · New Transaction status state omits declared chain and transaction fields · New Provider type excludes supported FallbackProvider configuration · New Sync cursor fails to remove orphaned logs after chain reorganizations · New FallbackProvider incorrectly accessed through unsupported send method · New JSON-RPC batch arrays are not traversed for known errors · New
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"; | |||
| export interface ReserveTransactionStatus extends TransactionStatus { | ||
| errorType?: typeof RESERVE_LIMIT_ERROR; | ||
| /** The unmodified provider/ethers error, for debugging rather than display. */ | ||
| originalError?: unknown; | ||
| } |
| // 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(); |
| } else if (value && typeof value === "object" && !seen.has(value)) { | ||
| seen.add(value); | ||
| const record = value as Record<string, unknown>; |
|
@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 Requested change:
Relevant entry points: Please update the PR description with the added scope and the tests actually run. Keep the separate RPC/history changes intact. |
Implemented in commit |
|
@copilot one focused follow-up on d952cf8: in Please handle that case too:
Relevant block: Keep this small; no broader refactor needed. Also describe the current Safe test accurately as fallback-routing coverage, rather than live Safe execution. |
Implemented in commit |
|
@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
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. |
Implemented in commits |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
| const signer = (library as ethers.providers.Web3Provider).getSigner(account); | ||
| const contractWithSigner = contract.connect(signer); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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]); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (requested) { | ||
| await new Promise(resolve => setTimeout(resolve, HISTORY_REQUEST_DELAY_MS)); | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
| 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]( |
There was a problem hiding this comment.
🟠 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.| 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; |
There was a problem hiding this comment.
🟠 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`.| } catch (error: any) { | ||
| setUsesWalletSigner(true); | ||
| setWalletSignerState({ | ||
| status: "Exception", | ||
| chainId, | ||
| transactionName: "BridgeTransfer", | ||
| errorMessage: error?.message || "Unable to check wallet code" | ||
| }); | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
🟡 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) }, |
There was a problem hiding this comment.
🟡 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 |
There was a problem hiding this comment.
🟡 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.|
@copilot Please fix the alternative-chain Reserve check for ethers |
Fixed in commit |


Description
Improves Reserve swap transaction handling and error messaging, and reduces oversized Microbridge history requests.
Reserve fixes
Microbridge and provider fixes
UI
Testing and integration
Summary by Sourcery
Improve Reserve swap reliability and Microbridge history synchronization while exposing reusable limit-handling UI and SDK integration APIs.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: