feat(cctp): payout-preference module and forwarding-fee UX - #236
feat(cctp): payout-preference module and forwarding-fee UX#236armandocodecr wants to merge 10 commits into
Conversation
# Conflicts: # tsconfig.json
…m as a direct dep
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 46 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe frontend adds CCTP payout destination management and EVM mint completion. It introduces bridge services, React Query hooks, validation, dialogs, chain configuration, receiver authorization, escrow menu integration, and local development documentation. ChangesCCTP bridge foundation
Payout preference workflow
Attestation and mint completion
Escrow authorization and integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Receiver
participant EscrowMenu
participant PayoutPreferenceDialog
participant BridgeService
participant StellarWallet
Receiver->>EscrowMenu: open payout preference
EscrowMenu->>PayoutPreferenceDialog: render authorized action
PayoutPreferenceDialog->>BridgeService: request quote and unsigned transaction
BridgeService-->>PayoutPreferenceDialog: return fee and unsigned XDR
PayoutPreferenceDialog->>StellarWallet: sign transaction
StellarWallet-->>PayoutPreferenceDialog: return signed XDR
PayoutPreferenceDialog->>BridgeService: submit signed XDR
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (5)
src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx (1)
138-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the spinner-only loading state with a matching skeleton.
Render
Skeletonelements for the current-preference text, chain select, address input, and footer actions. The loading layout must match the form layout to prevent layout shift.As per coding guidelines: “Use co-located loading skeletons built with
@/components/ui/skeleton, and make the skeleton mirror the loaded layout exactly.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx` around lines 138 - 142, Update the isLoading branch in PayoutPreferenceDialog to replace the centered Loader2 spinner with co-located Skeleton components imported from `@/components/ui/skeleton`. Mirror the loaded form layout with skeletons for the current-preference text, chain select, address input, and footer actions so the dialog dimensions remain stable.Source: Coding guidelines
src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts (2)
11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
interfacefor these object shapes.
src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts#L11-L17: ConvertUsePayoutPreferenceFormOptionsto aninterface.src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx#L46-L52: ConvertPayoutPreferenceDialogPropsto aninterface.As per coding guidelines: “Use
interfacefor object shapes andtypefor unions, intersections, mapped types, and conditional types.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts` around lines 11 - 17, Convert UsePayoutPreferenceFormOptions from type to interface in src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts lines 11-17 since it is an object shape, not a union or mapped type. Similarly, convert PayoutPreferenceDialogProps from type to interface in src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx lines 46-52 for the same reason. Both changes align with the coding guideline to use interface for object shapes and type for unions, intersections, mapped types, and conditional types.Source: Coding guidelines
19-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeclare explicit return types for the exported hooks.
src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts#L19-L44: Define and return a named form-hook result type.src/features/cctp-bridge/hooks/usePayoutPreferenceMutations.ts#L37-L59: Declare the build-mutation hook result type.src/features/cctp-bridge/hooks/usePayoutPreferenceMutations.ts#L65-L92: Declare the confirm-mutation hook result type.src/features/cctp-bridge/hooks/usePayoutPreferenceMutations.ts#L98-L133: Declare the clear-mutation hook result type.As per coding guidelines: “Add explicit types for public API surfaces such as payloads, responses, hook return values, service methods, and route handlers.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts` around lines 19 - 44, Exported hook functions lack explicit return type declarations as required by the coding guidelines. In src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts (lines 19-44), add an explicit return type annotation to the usePayoutPreferenceForm function that captures the shape of the returned object (form, onSubmit, isSubmitting). In src/features/cctp-bridge/hooks/usePayoutPreferenceMutations.ts, add explicit return type annotations to: the build-mutation hook (lines 37-59), the confirm-mutation hook (lines 65-92), and the clear-mutation hook (lines 98-133), each returning the appropriate mutation hook result structure. Define named types for each hook's return value and use them as the function return type annotation.Source: Coding guidelines
src/features/escrows/ui/actions/PayoutPreferenceAction.tsx (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftRemove the direct feature-to-feature UI dependency.
src/features/escrowsimports the implementation componentPayoutPreferenceDialogfromsrc/features/cctp-bridge/ui. Move this composition to the router layer or a shared integration boundary so feature slices do not depend on each other’s UI internals.As per coding guidelines: “Do not import implementation details from one feature slice into another feature slice; shared needs should go through shared modules or the router layer.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/escrows/ui/actions/PayoutPreferenceAction.tsx` at line 4, Remove the direct import of PayoutPreferenceDialog from src/features/cctp-bridge/ui in PayoutPreferenceAction.tsx to eliminate the cross-feature-slice UI dependency. Identify where PayoutPreferenceDialog is used within the escrows feature and move that composition to the router layer or a shared integration boundary module instead, so the escrows slice no longer depends on cctp-bridge's UI internals while preserving the intended functionality.Source: Coding guidelines
src/features/cctp-bridge/hooks/useCrossChainDestination.ts (1)
10-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd explicit return types to the public hooks.
src/features/cctp-bridge/hooks/useCrossChainDestination.ts#L10-L40: type the query key andUseQueryResult.src/features/cctp-bridge/hooks/useFeeQuote.ts#L18-L27: declareUseQueryResult<FeeQuote, Error>.src/features/cctp-bridge/hooks/useCctpAttestation.ts#L13-L20: declare the attestation query result.src/features/cctp-bridge/hooks/useCompleteCctpMint.ts#L16-L42: declare the mutation variables andUseMutationResult.As per coding guidelines, public API surfaces and hook return values require explicit types.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/cctp-bridge/hooks/useCrossChainDestination.ts` around lines 10 - 40, Add explicit return type annotations to the public hooks in the CCTP bridge module. In src/features/cctp-bridge/hooks/useCrossChainDestination.ts (lines 10-40), add an explicit return type annotation to the crossChainDestinationQueryKey function and declare UseQueryResult with explicit type parameters on the useCrossChainDestination function. In src/features/cctp-bridge/hooks/useFeeQuote.ts (lines 18-27), declare the hook's return type as UseQueryResult<FeeQuote, Error>. In src/features/cctp-bridge/hooks/useCctpAttestation.ts (lines 13-20), add an explicit query result return type declaration to the attestation hook. In src/features/cctp-bridge/hooks/useCompleteCctpMint.ts (lines 16-42), declare explicit types for the mutation variables and add a UseMutationResult return type declaration to the mutation hook.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CCTP_SESSION_HANDOFF.md`:
- Around line 151-156: Update both shell code blocks in the
CCTP_SESSION_HANDOFF.md file to include the bash language identifier in their
opening code fences. Change the opening triple backticks from plain ``` to
```bash for both the first block containing docker compose and npx prisma
commands (around lines 151-156) and the second block mentioned in the comment
(around lines 162-165). This will resolve the MD040 markdown linting warnings
for missing language specifications on code fences.
- Around line 119-126: The payout-policy example in the handoff must reflect the
full canManagePayoutPreference behavior, including rejection for released or
resolved single-release escrows and terminal milestones, not only receiver-role
checks. Update the snippet and surrounding explanation associated with
EscrowActionPolicy.canManagePayoutPreference to document these state and
milestone constraints accurately.
- Line 92: Replace the absolute checkout path in the handoff entry with a
repository-relative or generic placeholder path, while preserving the repository
URL, branch name, and sibling-directory context.
In `@src/features/cctp-bridge/lib/evm.ts`:
- Around line 112-120: Store the result returned by
publicClient.waitForTransactionReceipt as a receipt object, then validate that
receipt.status equals "success" before returning the hash. If the status is not
"success", throw an error to prevent the UI from showing mint completion for a
failed receiveMessage transaction.
- Around line 60-82: After the wallet_addEthereumChain provider.request call
succeeds in the code === 4902 error handler, add a second provider.request call
with method wallet_switchEthereumChain using the same hexChainId to activate the
chain in the wallet. This ensures the wallet switches to the newly-added chain
rather than remaining on the previous chain, which would cause the next
writeContract call to execute on the wrong chain.
In `@src/features/cctp-bridge/schemas/payout-preference.schema.ts`:
- Around line 9-29: In
src/features/cctp-bridge/schemas/payout-preference.schema.ts lines 9-29,
restrict the destinationDomain field to valid CctpDestinationDomain literals
instead of accepting any coerced integer, and add discriminated union validation
logic in the superRefine block to ensure the recipientAddress format (EVM,
Stellar, or Solana) matches the selected domain—rejecting addresses that are
valid in general but not valid for that specific domain. In
src/features/cctp-bridge/hooks/useFeeQuote.ts lines 18-25, remove the unchecked
type assertion on the destination domain and use the narrowed
CctpDestinationDomain type directly from the validated schema result.
In `@src/features/cctp-bridge/services/cctp-bridge.service.ts`:
- Around line 73-78: The CCTP attestation flow must validate external responses
and narrow complete states before use. In
src/features/cctp-bridge/services/cctp-bridge.service.ts#L73-L78, parse the HTTP
payload with a Zod discriminated union; in
src/features/cctp-bridge/types/cctp-bridge.types.ts#L56-L60, require usable
message and attestation values for the complete variant; update
useCctpAttestation in
src/features/cctp-bridge/hooks/useCctpAttestation.ts#L13-L20 to stop polling
only for that validated variant; update useCompleteCctpMint in
src/features/cctp-bridge/hooks/useCompleteCctpMint.ts#L18-L32 to remove Hex
assertions and consume narrowed values; and update CompleteCctpMintDialog in
src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx#L45-L95 to derive
readiness from the validated complete variant.
In `@src/features/cctp-bridge/types/cctp-bridge.types.ts`:
- Around line 6-10: In src/features/cctp-bridge/types/cctp-bridge.types.ts
(lines 6-10), refactor EscrowRef from a single interface with an optional
milestoneIndex into a discriminated union with two variants: one where
escrowKind is "multi-release" and milestoneIndex is required, and another where
escrowKind is not "multi-release" and milestoneIndex does not exist. Then in
src/features/cctp-bridge/services/cctp-bridge.service.ts (lines 53-60), update
the path construction logic to use the type-narrowed union variant, which will
guarantee that milestoneIndex exists when escrowKind is "multi-release" and
eliminate the possibility of undefined URL paths.
In `@src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx`:
- Line 45: Update isReady and the related action flow in CompleteCctpMintDialog
so readiness requires a complete attestation status plus both message and
attestation payloads. Base the check on the validated complete-attestation
variant, and replace the silent click-handler guard with handling that cannot
enable the action for invalid or incomplete state.
In `@src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx`:
- Around line 99-102: Wrap the await buildPreference.mutateAsync(values) call in
the onSubmit handler with a try/catch block, and similarly wrap the await
confirmPreference.mutateAsync(...) call in its corresponding submit handler. The
catch blocks should not rethrow the error, allowing the dialog to remain open
while the mutation's onError handler and toast UI supply feedback to the user.
---
Nitpick comments:
In `@src/features/cctp-bridge/hooks/useCrossChainDestination.ts`:
- Around line 10-40: Add explicit return type annotations to the public hooks in
the CCTP bridge module. In
src/features/cctp-bridge/hooks/useCrossChainDestination.ts (lines 10-40), add an
explicit return type annotation to the crossChainDestinationQueryKey function
and declare UseQueryResult with explicit type parameters on the
useCrossChainDestination function. In
src/features/cctp-bridge/hooks/useFeeQuote.ts (lines 18-27), declare the hook's
return type as UseQueryResult<FeeQuote, Error>. In
src/features/cctp-bridge/hooks/useCctpAttestation.ts (lines 13-20), add an
explicit query result return type declaration to the attestation hook. In
src/features/cctp-bridge/hooks/useCompleteCctpMint.ts (lines 16-42), declare
explicit types for the mutation variables and add a UseMutationResult return
type declaration to the mutation hook.
In `@src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts`:
- Around line 11-17: Convert UsePayoutPreferenceFormOptions from type to
interface in src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts lines
11-17 since it is an object shape, not a union or mapped type. Similarly,
convert PayoutPreferenceDialogProps from type to interface in
src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx lines 46-52 for the same
reason. Both changes align with the coding guideline to use interface for object
shapes and type for unions, intersections, mapped types, and conditional types.
- Around line 19-44: Exported hook functions lack explicit return type
declarations as required by the coding guidelines. In
src/features/cctp-bridge/hooks/usePayoutPreferenceForm.ts (lines 19-44), add an
explicit return type annotation to the usePayoutPreferenceForm function that
captures the shape of the returned object (form, onSubmit, isSubmitting). In
src/features/cctp-bridge/hooks/usePayoutPreferenceMutations.ts, add explicit
return type annotations to: the build-mutation hook (lines 37-59), the
confirm-mutation hook (lines 65-92), and the clear-mutation hook (lines 98-133),
each returning the appropriate mutation hook result structure. Define named
types for each hook's return value and use them as the function return type
annotation.
In `@src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx`:
- Around line 138-142: Update the isLoading branch in PayoutPreferenceDialog to
replace the centered Loader2 spinner with co-located Skeleton components
imported from `@/components/ui/skeleton`. Mirror the loaded form layout with
skeletons for the current-preference text, chain select, address input, and
footer actions so the dialog dimensions remain stable.
In `@src/features/escrows/ui/actions/PayoutPreferenceAction.tsx`:
- Line 4: Remove the direct import of PayoutPreferenceDialog from
src/features/cctp-bridge/ui in PayoutPreferenceAction.tsx to eliminate the
cross-feature-slice UI dependency. Identify where PayoutPreferenceDialog is used
within the escrows feature and move that composition to the router layer or a
shared integration boundary module instead, so the escrows slice no longer
depends on cctp-bridge's UI internals while preserving the intended
functionality.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c8078302-78fe-43dc-9633-583cdf06b8bd
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
.claude/launch.jsonCCTP_SESSION_HANDOFF.mdnext.config.tspackage.jsonsrc/features/cctp-bridge/hooks/useCctpAttestation.tssrc/features/cctp-bridge/hooks/useCompleteCctpMint.tssrc/features/cctp-bridge/hooks/useCrossChainDestination.tssrc/features/cctp-bridge/hooks/useFeeQuote.tssrc/features/cctp-bridge/hooks/usePayoutPreferenceForm.tssrc/features/cctp-bridge/hooks/usePayoutPreferenceMutations.tssrc/features/cctp-bridge/lib/chains.tssrc/features/cctp-bridge/lib/evm.tssrc/features/cctp-bridge/schemas/payout-preference.schema.tssrc/features/cctp-bridge/services/cctp-bridge.service.tssrc/features/cctp-bridge/types/cctp-bridge.types.tssrc/features/cctp-bridge/ui/CompleteCctpMintDialog.tsxsrc/features/cctp-bridge/ui/PayoutPreferenceDialog.tsxsrc/features/escrows/domain/escrow-action-policy.test.tssrc/features/escrows/domain/escrow-action-policy.tssrc/features/escrows/ui/actions/PayoutPreferenceAction.tsxsrc/features/escrows/ui/detail/EscrowGeneralActions.tsxsrc/features/escrows/ui/detail/MilestoneActionsMenu.tsx
| The frontend's own `develop` branch landed a full escrows feature that imports `@trustless-work/escrow` directly (types, REST client, hooks) — but this package **was never added to `package.json`**, and worse, **the published npm version (3.0.5) is stale** — the real v2 rewrite (matching the V2 contracts) lives unpublished on a branch. | ||
|
|
||
| What was done: | ||
| 1. Cloned `https://github.com/Trustless-Work/trustlesswork-sdk-react` (branch `feature/version-2`) to `/Users/armandocodecr/Documents/Programacion/Proyectos/trustlesswork-sdk-react` — a sibling directory to the other repos, **not inside** `dApp-Trustless-Work`. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'CCTP_SESSION_HANDOFF.md line count and context:\n'
wc -l CCTP_SESSION_HANDOFF.md
sed -n '80,100p' CCTP_SESSION_HANDOFF.md
printf '\nOther occurrences of username-like local paths or host layout markers:\n'
rg -n '(/Users/|Documents/Programacion|Proyectos|/Users/armandocodecr|dApp-Trustless-Work|trustlesswork-sdk-react)' CCTP_SESSION_HANDOFF.mdRepository: Trustless-Work/trustlesswork-backoffice
Length of output: 3963
Remove the absolute local checkout path.
Line 92 exposes a home-directory username and local filesystem layout; replace it with a repository-relative or placeholder path before publishing this handoff document.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CCTP_SESSION_HANDOFF.md` at line 92, Replace the absolute checkout path in
the handoff entry with a repository-relative or generic placeholder path, while
preserving the repository URL, branch name, and sibling-directory context.
| - **`src/features/escrows/domain/escrow-action-policy.ts`** — added a concrete (non-abstract) method on the base `EscrowActionPolicy` class: | ||
| ```ts | ||
| canManagePayoutPreference(milestoneIndex?: number): boolean { | ||
| if (milestoneIndex === undefined) return this.roles.isEscrowReceiver(); | ||
| return this.roles.isMilestoneReceiver(milestoneIndex); | ||
| } | ||
| ``` | ||
| Same logic works for both single- and multi-release because `EscrowRoleContext` already had `isEscrowReceiver()`/`isMilestoneReceiver(index)` (used internally for dispute-opening gating) — just needed a public policy method mirroring the existing `canOpenDispute(milestoneIndex?)` pattern. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the payout-policy example.
The example only checks the receiver role. The current canManagePayoutPreference implementation also rejects released or resolved single-release escrows and terminal milestones. Update the snippet and surrounding text so the handoff does not describe weaker policy behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CCTP_SESSION_HANDOFF.md` around lines 119 - 126, The payout-policy example in
the handoff must reflect the full canManagePayoutPreference behavior, including
rejection for released or resolved single-release escrows and terminal
milestones, not only receiver-role checks. Update the snippet and surrounding
explanation associated with EscrowActionPolicy.canManagePayoutPreference to
document these state and milestone constraints accurately.
| ``` | ||
| docker compose up -d # Postgres + RabbitMQ | ||
| npx prisma generate && npx prisma migrate deploy | ||
| # .env needs WALLET_AUTH_SIGNING_KEY, WALLET_AUTH_HOME_DOMAIN added (see §4) | ||
| npx dotenv -e <env-file> -- nest start --watch -- # or plain `npm run start:dev` if .env is already complete | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add languages to both shell code fences.
Add bash to the opening fences. Static analysis reports MD040 warnings for both blocks.
Also applies to: 162-165
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 151-151: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CCTP_SESSION_HANDOFF.md` around lines 151 - 156, Update both shell code
blocks in the CCTP_SESSION_HANDOFF.md file to include the bash language
identifier in their opening code fences. Change the opening triple backticks
from plain ``` to ```bash for both the first block containing docker compose and
npx prisma commands (around lines 151-156) and the second block mentioned in the
comment (around lines 162-165). This will resolve the MD040 markdown linting
warnings for missing language specifications on code fences.
Source: Linters/SAST tools
| } catch (err) { | ||
| // 4902 = chain not added to the wallet yet. | ||
| const code = (err as { code?: number })?.code; | ||
| if (code === 4902) { | ||
| const rpcUrl = chain.viemChain.rpcUrls.default.http[0]; | ||
| await provider.request({ | ||
| method: "wallet_addEthereumChain", | ||
| params: [ | ||
| { | ||
| chainId: hexChainId, | ||
| chainName: chain.viemChain.name, | ||
| nativeCurrency: chain.viemChain.nativeCurrency, | ||
| rpcUrls: [rpcUrl], | ||
| blockExplorerUrls: chain.viemChain.blockExplorers?.default?.url | ||
| ? [chain.viemChain.blockExplorers.default.url] | ||
| : [], | ||
| }, | ||
| ], | ||
| }); | ||
| } else { | ||
| throw err; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target file:"
git ls-files | rg '(^|/)evm\.ts$' || true
if [ -f src/features/cctp-bridge/lib/evm.ts ]; then
echo
echo "Relevant lines:"
nl -ba src/features/cctp-bridge/lib/evm.ts | sed -n '1,140p'
else
echo "Target file not found"
fi
echo
echo "Search for wallet switching/add patterns:"
rg -n "wallet_(addEthereumChain|switchEthereumChain)|4902|request\(" src/features -g '*.ts' -g '*.tsx' || trueRepository: Trustless-Work/trustlesswork-backoffice
Length of output: 284
🌐 Web query:
EIP-3085 wallet_addEthereumChain returns selected_chain_id or wallet must select chain docs
💡 Result:
EIP-3085, which defines the wallet_addEthereumChain RPC method, does not require wallets to automatically select a newly added chain [1][2]. The specification explicitly states that the method makes no statement regarding whether a wallet should change the user's currently selected chain [2]. Whether a wallet switches to the new network is considered an implementation detail left to the wallet developer [2]. The specification clarifies that a chain must not be assumed to be automatically selected by the wallet, even if the request is successful [2]. Regarding the return value, the method returns null if the request to add the chain is successful, and it returns an error if the request fails (e.g., if the user rejects the request or validation fails) [1][3][2]. There is no requirement or standard mechanism for the method to return a selected_chain_id [1][2].
Citations:
- 1: https://eips.ethereum.org/EIPS/eip-3085
- 2: ethereum/ERCs@9c499a5
- 3: https://docs.core.app/docs/reference/wallet_addethereumchain/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target file content:"
awk '{ printf "%6d\t%s\n", NR, $0 }' src/features/cctp-bridge/lib/evm.ts | sed -n '1,130p'
echo
echo "Wallet switch/add and provider request patterns:"
rg -n "wallet_(addEthereumChain|switchEthereumChain)|4902|provider\.request|request\(" src/features -g '*.ts' -g '*.tsx' || trueRepository: Trustless-Work/trustlesswork-backoffice
Length of output: 4940
🌐 Web query:
EIP-3085 wallet_addEthereumChain must switch to added chain wallet_switchEthereumChain
💡 Result:
The RPC method wallet_addEthereumChain (EIP-3085) and the method wallet_switchEthereumChain (EIP-3326) serve distinct purposes and are not required to behave as a single atomic operation [1][2]. According to their respective specifications, wallet_addEthereumChain is used to suggest that a wallet add a new chain's configuration (such as RPC URLs, native currency, and chain ID) to its list of known networks [1][3]. Conversely, wallet_switchEthereumChain is used to request that the wallet switch its active chain to one that is already known [2][4]. Key technical clarifications regarding your query: 1. No Mandatory Requirement to Switch: There is no EIP requirement stating that wallet_addEthereumChain must automatically switch the wallet to the newly added chain [1][2]. While some wallet implementations may prompt the user to switch immediately after a successful addition as a convenience feature, this is an implementation-specific choice rather than a protocol mandate [5]. 2. Error Handling Pattern: The standard way for a decentralized application (dapp) to handle network switching is a two-step pattern [6][7]: - First, call wallet_switchEthereumChain. - If the request fails with error code 4902 (Unrecognized chain ID), it indicates the wallet does not yet know about this chain [8][2]. - The dapp should then catch this error and call wallet_addEthereumChain to register the network [6][7]. 3. Separation of Concerns: The specifications were designed with a separation of concerns [2][4]. wallet_addEthereumChain handles the provisioning of metadata, whereas wallet_switchEthereumChain is strictly focused on modifying the user's active connection [2][4]. Forcing an automatic switch during the "add" process could conflict with user intent or wallet security policies [2]. In summary, a well-behaved dapp should not rely on wallet_addEthereumChain to switch the network; instead, it should implement logic that attempts a switch first and gracefully handles the addition process only when necessary [6][7].
Citations:
- 1: https://eips.ethereum.org/EIPS/eip-3085
- 2: https://eips.ethereum.org/EIPS/eip-3326
- 3: https://docs.metamask.io/metamask-connect/evm/reference/json-rpc-api/wallet_addEthereumChain/
- 4: https://raw.githubusercontent.com/ethereum/EIPs/master/EIPS/eip-3326.md
- 5: wallet_addEthereumChain should ask users to switch chain after the network is successfully added brave/brave-browser#19291
- 6: https://wallet.page/networks
- 7: switchEthereumChain RPC call results in -32601 error code MetaMask/metamask-mobile#3035
- 8: https://docs.metamask.io/metamask-connect/evm/reference/json-rpc-api/wallet_switchEthereumChain/
Switch the wallet after adding an unknown chain.
wallet_addEthereumChain only registers the chain configuration; it does not require wallets to make that chain active. This branch calls wallet_switchEthereumChain once, then adds the chain and returns. If the wallet stays on the previous chain, the next walletClient.writeContract(...) call can submit on the wrong chain or be rejected.
Call provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: hexChainId }] }) again after wallet_addEthereumChain succeeds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/lib/evm.ts` around lines 60 - 82, After the
wallet_addEthereumChain provider.request call succeeds in the code === 4902
error handler, add a second provider.request call with method
wallet_switchEthereumChain using the same hexChainId to activate the chain in
the wallet. This ensures the wallet switches to the newly-added chain rather
than remaining on the previous chain, which would cause the next writeContract
call to execute on the wrong chain.
| const hash = await walletClient.writeContract({ | ||
| address: chain.messageTransmitter, | ||
| abi: RECEIVE_MESSAGE_ABI, | ||
| functionName: "receiveMessage", | ||
| args: [message, attestation], | ||
| }); | ||
|
|
||
| await publicClient.waitForTransactionReceipt({ hash }); | ||
| return hash; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | grep -F 'src/features/cctp-bridge/lib/evm.ts' || true
echo "== relevant file excerpt =="
if [ -f src/features/cctp-bridge/lib/evm.ts ]; then
wc -l src/features/cctp-bridge/lib/evm.ts
sed -n '1,180p' src/features/cctp-bridge/lib/evm.ts | cat -n
fi
echo "== package viem version =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -f "$f" ] && { echo "--- $f"; grep -n -i "viem" "$f" | head -20 || true; }
done
echo "== search for writeWaitReceipt pattern =="
rg -n "waitForTransactionReceipt|receiveMessage|reverted|status !==|status ==" src -g '*.ts' -g '*.tsx' | head -80Repository: Trustless-Work/trustlesswork-backoffice
Length of output: 9316
Reject a reverted transaction receipt.
waitForTransactionReceipt() only waits for the transaction to confirm; it can return a reverted receipt. Store the receipt and check receipt.status === "success" before returning the hash, or throw otherwise so the UI does not show mint completion for a failed receiveMessage.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/lib/evm.ts` around lines 112 - 120, Store the result
returned by publicClient.waitForTransactionReceipt as a receipt object, then
validate that receipt.status equals "success" before returning the hash. If the
status is not "success", throw an error to prevent the UI from showing mint
completion for a failed receiveMessage transaction.
| export const payoutPreferenceSchema = z | ||
| .object({ | ||
| destinationDomain: z.coerce.number().int(), | ||
| recipientAddress: z.string().trim().min(1, "Recipient address is required"), | ||
| }) | ||
| .superRefine((values, ctx) => { | ||
| // Accept any destination-chain address shape we support (EVM, Stellar, or | ||
| // Solana) without branching on the selected domain — the backend is the | ||
| // authority on the domain↔address pairing. The three formats can't | ||
| // overlap, so this stays unambiguous. | ||
| const isStellarAddress = isValidWallet(values.recipientAddress); | ||
| const isEvmAddress = EVM_ADDRESS_REGEX.test(values.recipientAddress); | ||
| const isSolanaAddress = SOLANA_ADDRESS_REGEX.test(values.recipientAddress); | ||
|
|
||
| if (!isStellarAddress && !isEvmAddress && !isSolanaAddress) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| path: ["recipientAddress"], | ||
| message: "Enter a valid EVM (0x...), Stellar (G...) or Solana address", | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Encode the destination-domain contract in the schema. The schema accepts unsupported integers and mismatched recipient formats. The fee hook then restores the domain type with an unchecked assertion.
src/features/cctp-bridge/schemas/payout-preference.schema.ts#L9-L29: restrict the domain literals and validate the matching address format.src/features/cctp-bridge/hooks/useFeeQuote.ts#L18-L25: consume the narrowedCctpDestinationDomainwithout an assertion.
📍 Affects 2 files
src/features/cctp-bridge/schemas/payout-preference.schema.ts#L9-L29(this comment)src/features/cctp-bridge/hooks/useFeeQuote.ts#L18-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/schemas/payout-preference.schema.ts` around lines 9
- 29, In src/features/cctp-bridge/schemas/payout-preference.schema.ts lines
9-29, restrict the destinationDomain field to valid CctpDestinationDomain
literals instead of accepting any coerced integer, and add discriminated union
validation logic in the superRefine block to ensure the recipientAddress format
(EVM, Stellar, or Solana) matches the selected domain—rejecting addresses that
are valid in general but not valid for that specific domain. In
src/features/cctp-bridge/hooks/useFeeQuote.ts lines 18-25, remove the unchecked
type assertion on the destination domain and use the narrowed
CctpDestinationDomain type directly from the validated schema result.
Source: Coding guidelines
| /** Fetches Circle's CCTP attestation for a Stellar burn (release) tx hash. */ | ||
| async getAttestation(burnTxHash: string): Promise<AttestationResponse> { | ||
| const { data } = await http.get<AttestationResponse>( | ||
| `/core/cctp/attestation/${burnTxHash}`, | ||
| ); | ||
| return data; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Validate the attestation response at the HTTP boundary. The current type permits a complete response without usable hex payloads. This state stops polling, enables the button, and then causes a silent no-op or an invalid wallet request.
src/features/cctp-bridge/services/cctp-bridge.service.ts#L73-L78: parse the external JSON with a Zod discriminated union.src/features/cctp-bridge/types/cctp-bridge.types.ts#L56-L60: requiremessageandattestationfor the complete variant.src/features/cctp-bridge/hooks/useCctpAttestation.ts#L13-L20: stop polling only after the validated complete variant is returned.src/features/cctp-bridge/hooks/useCompleteCctpMint.ts#L18-L32: remove theHexassertions and accept narrowed values.src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx#L45-L95: derive readiness from the validated complete variant.
As per coding guidelines, parse and narrow unknown external JSON at the boundary and model variant states with discriminated unions.
📍 Affects 5 files
src/features/cctp-bridge/services/cctp-bridge.service.ts#L73-L78(this comment)src/features/cctp-bridge/types/cctp-bridge.types.ts#L56-L60src/features/cctp-bridge/hooks/useCctpAttestation.ts#L13-L20src/features/cctp-bridge/hooks/useCompleteCctpMint.ts#L18-L32src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx#L45-L95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/services/cctp-bridge.service.ts` around lines 73 -
78, The CCTP attestation flow must validate external responses and narrow
complete states before use. In
src/features/cctp-bridge/services/cctp-bridge.service.ts#L73-L78, parse the HTTP
payload with a Zod discriminated union; in
src/features/cctp-bridge/types/cctp-bridge.types.ts#L56-L60, require usable
message and attestation values for the complete variant; update
useCctpAttestation in
src/features/cctp-bridge/hooks/useCctpAttestation.ts#L13-L20 to stop polling
only for that validated variant; update useCompleteCctpMint in
src/features/cctp-bridge/hooks/useCompleteCctpMint.ts#L18-L32 to remove Hex
assertions and consume narrowed values; and update CompleteCctpMintDialog in
src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx#L45-L95 to derive
readiness from the validated complete variant.
Source: Coding guidelines
| interface EscrowRef { | ||
| escrowKind: EscrowKind; | ||
| contractId: string; | ||
| /** Required for multi-release escrows — each milestone has its own receiver. */ | ||
| milestoneIndex?: number; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Model the milestone identity as a discriminated union. The optional milestoneIndex permits invalid multi-release requests and produces an /undefined URL.
src/features/cctp-bridge/types/cctp-bridge.types.ts#L6-L10: requiremilestoneIndexwhenescrowKindis"multi-release".src/features/cctp-bridge/services/cctp-bridge.service.ts#L53-L60: construct the path from the narrowed union variant.
📍 Affects 2 files
src/features/cctp-bridge/types/cctp-bridge.types.ts#L6-L10(this comment)src/features/cctp-bridge/services/cctp-bridge.service.ts#L53-L60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/types/cctp-bridge.types.ts` around lines 6 - 10, In
src/features/cctp-bridge/types/cctp-bridge.types.ts (lines 6-10), refactor
EscrowRef from a single interface with an optional milestoneIndex into a
discriminated union with two variants: one where escrowKind is "multi-release"
and milestoneIndex is required, and another where escrowKind is not
"multi-release" and milestoneIndex does not exist. Then in
src/features/cctp-bridge/services/cctp-bridge.service.ts (lines 53-60), update
the path construction logic to use the type-narrowed union variant, which will
guarantee that milestoneIndex exists when escrowKind is "multi-release" and
eliminate the possibility of undefined URL paths.
Source: Coding guidelines
| ); | ||
| const completeMint = useCompleteCctpMint(); | ||
|
|
||
| const isReady = attestation?.status === "complete"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Do not enable the action without both attestation payloads.
isReady can be true while message or attestation is absent. The button is then enabled, but its click handler silently returns.
Derive readiness from a validated complete-attestation variant. Do not use a silent guard for this invalid state.
Also applies to: 87-95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/ui/CompleteCctpMintDialog.tsx` at line 45, Update
isReady and the related action flow in CompleteCctpMintDialog so readiness
requires a complete attestation status plus both message and attestation
payloads. Base the check on the validated complete-attestation variant, and
replace the silent click-handler guard with handling that cannot enable the
action for invalid or incomplete state.
Source: Coding guidelines
| onSubmit: async (values) => { | ||
| const built = await buildPreference.mutateAsync(values); | ||
| setPendingTx(built); | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For @tanstack/react-query v5 and react-hook-form v7, does mutateAsync reject after onError runs, and does handleSubmit propagate an error thrown by its async callback?
💡 Result:
For @tanstack/react-query v5 and react-hook-form v7, the behaviors are as follows: TanStack Query v5: Yes, mutateAsync rejects after onError runs. The mutateAsync function is designed to return a promise that throws if the mutation fails [1][2]. Internally, when an error occurs, TanStack Query awaits your onError callback (if provided) before rejecting the returned promise [3]. If an error is thrown within the onError callback itself, that error will propagate and become the reason for the rejection, as the original error is caught and handled within the mutation lifecycle [4][3]. React Hook Form v7: Yes, handleSubmit propagates an error thrown by its async callback. Starting in v7.84.0 and earlier versions (following behavior changes around v7.42.0), handleSubmit no longer swallows errors thrown inside the async onSubmit callback [5][6][7]. If your async callback throws, the promise returned by handleSubmit will reject [5]. Consequently, it is recommended to wrap your async logic within a try/catch block inside the callback to manage error states (e.g., using setError) or to ensure proper cleanup if you need to handle side effects gracefully [5][8][7].
Citations:
- 1: https://tanstack.com/query/v5/docs/framework/react/guides/mutations
- 2: https://tanstack.com/query/latest/docs/framework/react/guides/mutations
- 3: https://github.com/TanStack/query/blob/6e2e37ea5fe180a6d5ee3dedf76bd72160406b2a/src/react/useMutation.ts
- 4:
isPendingremains true if error is thrown inonErrorofmutateand a query is invalidated on settle TanStack/query#9664 - 5: https://react-hook-form.com/docs/useform/handlesubmit
- 6: issue: 7.42.x – isSubmitting remains to true after error in submit function react-hook-form/react-hook-form#9821
- 7: Catch errors in handleSubmit's onValid callback and reflect state react-hook-form/react-hook-form#9940
- 8: https://www.react-hook-form.com/api/useform/handlesubmit
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'PayoutPreferenceDialog\.tsx$' . || true
echo "== outline =="
file="$(fd 'PayoutPreferenceDialog\.tsx$' . | head -n 1)"
if [ -n "${file:-}" ]; then
ast-grep outline "$file" --view compact || true
echo "== relevant lines =="
sed -n '1,150p' "$file" | cat -n
fi
echo "== package versions =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -f "$f" ] && echo "-- $f --" && (rg -n '`@tanstack/react-query`|react-hook-form' "$f" \| head -30 || true)
doneRepository: Trustless-Work/trustlesswork-backoffice
Length of output: 6502
Handle rejected mutation promises at the dialog event handlers.
mutateAsync can reject even when onError runs, and React Hook Form propagates errors thrown from async submit handlers. Wrap both await buildPreference.mutateAsync(...) and await confirmPreference.mutateAsync(...) in try/catch so rejections are caught, the dialog stays open, and the mutation toast supplies UI feedback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/cctp-bridge/ui/PayoutPreferenceDialog.tsx` around lines 99 -
102, Wrap the await buildPreference.mutateAsync(values) call in the onSubmit
handler with a try/catch block, and similarly wrap the await
confirmPreference.mutateAsync(...) call in its corresponding submit handler. The
catch blocks should not rethrow the error, allowing the dialog to remain open
while the mutation's onError handler and toast UI supply feedback to the user.
Adds the CCTP payout-preference flow: cross-chain destination, attestation and complete-mint hooks, forwarding-fee quote shown before the receiver signs, and the receiver-only payout action in the escrow UI. Updates escrow-action-policy to hide the preference once released/resolved. Adapts to develop's milestoneIndexes rename and adds viem as a direct dependency.
Summary by CodeRabbit