Skip to content

feat/solana alt#16

Merged
l3wi merged 26 commits intomainfrom
feat/solana-alt
Feb 22, 2026
Merged

feat/solana alt#16
l3wi merged 26 commits intomainfrom
feat/solana-alt

Conversation

@l3wi
Copy link
Copy Markdown
Owner

@l3wi l3wi commented Feb 19, 2026

  • fix(cctp): handle MessageExpired (0x1780) on Solana mint with auto re-attestation
  • fix(cctp): add CCTP v2 Solana error code mapping with user-friendly messages
  • fix(cctp): toast-friendly error messages with specific titles and full log capture
  • fix(cctp): add EVM revert reason mapping with toast-friendly error messages
  • fix(history): resolve Solana wallet address from ATA when recovering transactions
  • feat(bridge): ship multi-page tracking and app-owned rpc pipeline — add /bridge recovery flow, wallet-first transports, and generated metadata hooks
  • chore(build): move generated artifacts to ignored .generated dir — switch metadata refresh to prebuild

l3wi added 7 commits February 11, 2026 10:07
…-attestation

- Parse expirationBlock from CCTP v2 burn message body (byte 368)
- Pre-send expiration check before wallet prompt in Solana mint path
- Detect 0x1780 error code in Solana transaction simulation failures
- Auto-request re-attestation via Iris API when message expires
- Poll every 15s for fresh attestation after re-attest request
- Show polling state in claim button (spinner + status text)
- Unified messageExpired handling for both EVM and Solana paths
- Manual re-attest fallback button if auto-reattest fails
…essages

- New lib/cctp/solana/errors.ts with full error code → message map for
  MessageTransmitterV2 and TokenMessengerMinterV2 programs
- Explicit simulation before send to capture program logs on failure
  (SendTransactionError.getLogs() doesn't work for simulation failures)
- 0x1779 InvalidMintRecipient: tells user to connect the right wallet
- 0x1780 MessageExpired: triggers auto re-attestation
- 0x176e InvalidDestinationCaller: connect specific caller wallet
- 0x1785 DenylistedAccount: account restricted by Circle
- 0x177e/177f FeeExceedsAmount/MaxFee: request re-attestation
- All unknown errors still show raw message for debugging
- Full simulation logs always printed to console.error for diagnostics
…l log capture

- Shorten all CCTP error userMessages to fit 420px toast (1-2 sentences)
- Add 'title' field to CctpErrorInfo — used as toast title instead of
  generic 'Claim failed' (e.g. 'Wrong wallet', 'Fee too high', 'CCTP paused')
- Add 'errorTitle' to MintResult type, thread through useClaimHandler
- Explicit simulateTransaction before send captures full program logs
  (.simulationLogs on error object) — always logged to console.error
- 0x1779 InvalidMintRecipient now shows: title='Wrong wallet',
  desc='Connect the wallet that was set as recipient when this transfer
  was initiated.'
- Unknown errors show 'Error 0x{code}' as title with raw message
…ssages

- New lib/cctp/evm/errors.ts with regex-based revert reason → message map
- Covers all CCTP v2 EVM revert strings from MessageTransmitterV2,
  TokenMessengerV2, and TokenMinterV2 contracts
- EVM handler now returns errorTitle for specific toast titles
  (e.g. 'Wrong chain', 'Already claimed', 'Attestation expired')
- EVM handler passes attestation nonce for auto re-attestation on expiry
- Full error context always logged to console.error (shortMessage, raw)
- Matches Solana error handling parity: same flow for expired/claimed/known
…transactions

When recovering a transaction via the history modal, mintRecipient from
Iris API is the ATA (token account) for Solana destinations, not the
user's wallet address. This caused targetAddress to store the ATA,
making it impossible to identify the actual recipient.

Now:
- Derives ATA from connected Solana wallet and compares to mintRecipient
- Stores actual wallet pubkey as targetAddress when matched
- Shows warning if connected wallet doesn't match the transaction recipient
- Prompts user to connect Solana wallet if none connected
- Allows 'Add Anyway' to bypass check when needed
- Also properly strips zero-padding for EVM recipient addresses
…dd /bridge recovery flow, wallet-first transports, and generated metadata hooks
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cctp-bridge Ready Ready Preview, Comment Feb 22, 2026 8:02pm

Request Review

Repository owner deleted a comment from claude bot Feb 19, 2026
Repository owner deleted a comment from claude bot Feb 19, 2026
Repository owner deleted a comment from claude bot Feb 19, 2026
@l3wi l3wi marked this pull request as ready for review February 19, 2026 20:40
@l3wi
Copy link
Copy Markdown
Owner Author

l3wi commented Feb 19, 2026

@codex pls take a look

Copy link
Copy Markdown

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

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: 785d870956

ℹ️ 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 +1175 to +1176
if (executedIntentKeys.has(executeIntentKey)) {
return;
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 Clear executed intent guard after execution attempts

The execute-intent dedupe uses a module-level Set and returns early when the same key is seen, but that key is only removed in handleBackToNew; the normal success path redirects to /bridge/... without clearing it, and failed starts also leave it set, so revisiting the same execute URL in-session can silently skip handleSend and leave users stuck on the waiting state.

Useful? React with 👍 / 👎.

Repository owner deleted a comment from claude bot Feb 20, 2026
Repository owner deleted a comment from claude bot Feb 20, 2026
Repository owner deleted a comment from claude bot Feb 20, 2026
Repository owner deleted a comment from claude bot Feb 21, 2026
@l3wi
Copy link
Copy Markdown
Owner Author

l3wi commented Feb 21, 2026

@claude please take a look

@l3wi
Copy link
Copy Markdown
Owner Author

l3wi commented Feb 21, 2026

@claude review the changes

Repository owner deleted a comment from claude bot Feb 21, 2026
Repository owner deleted a comment from claude bot Feb 21, 2026
Repository owner deleted a comment from claude bot Feb 21, 2026
Repository owner deleted a comment from claude bot Feb 21, 2026
…fore claim, tighten RPC/Iris validation, and add regression tests
Repository owner deleted a comment from claude bot Feb 22, 2026
Repository owner deleted a comment from claude bot Feb 22, 2026
Repository owner deleted a comment from claude bot Feb 22, 2026
Repository owner deleted a comment from claude bot Feb 22, 2026
Repository owner deleted a comment from claude bot Feb 22, 2026
@claude
Copy link
Copy Markdown

claude bot commented Feb 22, 2026

PR Review: feat/solana alt

This is a substantial PR — 10k+ lines covering multi-page tracking, an app-owned RPC pipeline, transaction recovery, structured error mapping, and auto re-attestation. Overall the architecture is solid and well-tested. Below are findings organized by severity.


Medium Issues

1. Silent failure when requestReattestation fails (lib/hooks/useMintPolling.ts)

The auto re-attestation useEffect swallows errors silently:

requestReattestation(burnChainId, reattestNonce).catch((err) => {
  console.error("[useMintPolling] reattestation request failed:", err);
});

If the Iris re-attestation POST fails (network error, rate limit, API down), the polling loop will keep checking for a fresh attestation that never arrives, and the user will eventually hit the 10-minute reattestTimedOut wall with no intermediate feedback. Consider showing a toast on catch so the user knows re-attestation is struggling, or at least set a dedicated error state.

2. allowsPostMethod fallback in scripts/generate-rpc-candidates.ts

function allowsPostMethod(response: Response): boolean {
  const allowMethods = response.headers.get("access-control-allow-methods");
  if (!allowMethods) {
    // Falls back to checking if origin is wildcard
    return response.headers.get("access-control-allow-origin") === "*";
  }
  ...
}

Absence of Access-Control-Allow-Methods in a preflight response does not mean POST is allowed — it means the preflight itself is malformed/incomplete. Some servers omit the header and still accept the actual request, so this produces false positives in the validation report. The workaround is acceptable for a build-time heuristic, but the warnings field should document when this fallback was triggered so it's visible in the report.

3. timedOut state in useBurnPolling may not be surfaced to the user

useBurnPolling now exposes timedOut: true after 60 seconds, but it's unclear from the diff whether the consuming components (bridging-state.tsx, etc.) render any distinct UI for this state. If timedOut is silently dropped, the user will see an indefinitely-spinning burn step with no error message. Worth verifying the full render path.


Low / Style Issues

4. setBoundedCacheEntry eviction order (lib/iris.ts)

function setBoundedCacheEntry<T>(cache, key, value, ttlMs) {
  // 1. sweep expired entries
  for (const [k, entry] of cache) {
    if (entry.expiresAt <= now) cache.delete(k);
  }
  cache.set(key, ...);
  // 2. if still over cap, evict oldest-by-insertion-order
  while (cache.size > IRIS_MAX_CACHE_ENTRIES) {
    const oldestKey = cache.keys().next().value;
    if (!oldestKey) break;
    cache.delete(oldestKey);
  }
}

After the expired-entry sweep, step 2 evicts the oldest-inserted key — which may be a recently written entry with a long TTL (e.g., a complete result with 60s TTL written just before 200 short-lived pending entries). A min-heap or evicting the entry with the nearest expiresAt would be more correct, though for 200 entries the practical impact is minimal.

5. formatAmount always produces 6 decimal places (lib/transactionRecovery.ts)

formatAmount("1000000") returns "1.000000" rather than "1". This may produce awkward display strings in the tracking page (e.g., "Transferred 1.000000 USDC"). Consider trimming trailing zeros: return ${whole}.${String(fractional).padStart(6, "0")}.replace(/\.?0+$/, '').

6. add-pending-transaction-card.tsx is 581 lines

The component blends attestation fetching, validation logic, wallet resolution, and rendering. This works, but it will be harder to test and maintain as edge cases accumulate. Extracting a useAddPendingTransaction hook for the async logic would help keep the component focused on rendering.

7. isRequestKeyForNetwork key-splitting assumption

const isRequestKeyForNetwork = (requestKey: string, ...) => {
  const [domain, , network] = requestKey.split(":");
  ...
};

The key format is {domain}:{normalizedHash}:{network}. EVM hashes are 0x… (no colons) and Solana hashes are Base58 (no colons), so this is safe today. But if the format ever changes this will silently mismatch. A comment or named constant for the separator position would make this more robust.


Behaviour Changes Worth Documenting

8. Analytics event timing change

Previously POST /api/meta was called client-side immediately after a successful burn (in useCrossEcosystemBridge). Now trackVerifiedBridgeView fires server-side when the /bridge/[sourceChainId]/[id] page is loaded. This means:

  • Bridges that never navigate to the tracking page won't generate an analytics event
  • The event now fires on every page load (including reloads and share-link visits), not just once on completion

The recipientResolution: "verified_from_iris" field is a nice addition for data quality, but the semantics of the bridge event have changed. Update the analytics README/docs if you track event definitions anywhere.

9. requestReattestation POST has no Content-Type header

The old implementation sent Content-Type: application/json with a presumably empty body. The new version sends a bare POST with no content-type. Verify that the Iris API documents this as the expected call signature — the lack of a request body with a CCTP-domain parameter is unusual and worth a comment confirming intent.


Positive Observations

  • Deduplication of concurrent Iris requests — the pending*Requests maps cleanly prevent duplicate in-flight calls, which is important during rapid re-renders.
  • Comprehensive CCTP error maps — covering 15+ Solana program errors and EVM revert patterns with user-friendly copy is excellent for debuggability.
  • simulateSignedTransaction before Solana send — attaching simulationLogs to thrown errors will make Solana debugging dramatically easier.
  • SSR-safe Zustand storage — the serverStorage shim is the correct pattern for Next.js.
  • RPC validation script — the CORS preflight + chain ID verification approach is robust and the CORS_ORIGIN env guard in CI is a good footgun preventer.
  • Test coverage — the new tests cover the non-trivial paths (nonce lookup, re-attestation polling, EVM/Solana error parsing, transaction recovery). The useMint tests that simulate MessageExpired are particularly valuable.
  • recoveryAttemptRef in tracking page — cleanly prevents redundant Iris calls on re-render without reaching for useCallback over the entire recovery path.

@l3wi l3wi merged commit 5813741 into main Feb 22, 2026
3 checks passed
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