Skip unfunded-destination warning for Soroban on Search address screen#2778
Conversation
PR #2720 removed the misleading "Blockaid unfunded destination" warning for pure Soroban custom tokens on the transaction Review step. The same warning still appeared one step earlier on the Search address screen (SendTo). This change consolidates the rule into a single helper so both surfaces agree by construction. - Add popup/helpers/sendWarnings.ts with two pure helpers: - shouldCheckUnfundedDestinationWarning (qualitative gate: classic asset + classic destination + non-collectible) - shouldShowAccountDoesntExistWarning (compound: gate + strict isFunded === false, matching Review-step semantics where isFunded is boolean | null) - SendTo/index.tsx: pull asset/isCollectible from transactionDataSelector and use the compound helper; delete the dead shouldAccountDoesntExistWarning export and unused baseReserve. - useSimulateTxData.tsx: refactor getExpectedToFailReason in-place to delegate the qualitative gate to the shared helper; thread destination and isCollectible (already available in the hook) into the single call site without widening the hook's public API. - Tests: - New sendWarnings.test.ts covers native, classic, SAC, pure Soroban, collectible, contract destination, null/undefined funding, and the early-return query-param hydration edge case. - useSimulateTxData.test.ts updated for the new params and gains collectible / contract-destination cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR aligns the “unfunded destination” warning behavior between the Send flow’s Search address (SendTo) step and the Review step by introducing a shared predicate that disables the warning for Soroban-only transfers (contract-issuer assets), collectibles, and contract destinations.
Changes:
- Added shared helpers in
popup/helpers/sendWarnings.tsto centralize when the unfunded-destination warning rule should apply, plus a SendTo-specific compound predicate that requiresisFunded === false. - Updated
SendToto use the shared helper and to avoid warning on unknown funding state (null/undefined). - Refactored
getExpectedToFailReasonto delegate the qualitative gate to the shared helper and updated/expanded unit tests accordingly.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/helpers/sendWarnings.ts | New shared predicates to gate unfunded-destination warnings consistently across screens. |
| extension/src/popup/helpers/tests/sendWarnings.test.ts | Unit tests covering classic vs Soroban/collectible/contract-destination cases and funding-state semantics. |
| extension/src/popup/components/send/SendTo/index.tsx | Uses the new helper to suppress misleading warnings on Search address step and aligns strict isFunded === false behavior. |
| extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx | Refactors getExpectedToFailReason to use the shared qualitative gate and accounts for collectibles/destination contract IDs. |
| extension/src/popup/components/send/SendAmount/hooks/tests/useSimulateTxData.test.ts | Updates existing tests for new params and adds coverage for collectible + contract-destination behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,71 @@ | |||
| import { isContractId } from "popup/helpers/soroban"; | |||
Addressed Copilot PR review suggestion: sendWarnings.ts only uses the isContractId predicate, so importing from the lightweight @shared source directly avoids pulling in the larger popup/helpers/soroban module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
leofelix077
left a comment
There was a problem hiding this comment.
Verified behavior, unit tests and other E2E tests for regression. All look fine
There's one E2E test that is failing, but also failing on master, due to wrong mocks (Send token payment to C address), but works fine when doing manually. Checking it separately
xlm-and-collectibles.mov
custom-token.mov
Screen.Recording.2026-05-15.at.12.57.01.mov
#2778) (#2781) * Skip unfunded-destination warning for Soroban on Search address screen PR #2720 removed the misleading "Blockaid unfunded destination" warning for pure Soroban custom tokens on the transaction Review step. The same warning still appeared one step earlier on the Search address screen (SendTo). This change consolidates the rule into a single helper so both surfaces agree by construction. - Add popup/helpers/sendWarnings.ts with two pure helpers: - shouldCheckUnfundedDestinationWarning (qualitative gate: classic asset + classic destination + non-collectible) - shouldShowAccountDoesntExistWarning (compound: gate + strict isFunded === false, matching Review-step semantics where isFunded is boolean | null) - SendTo/index.tsx: pull asset/isCollectible from transactionDataSelector and use the compound helper; delete the dead shouldAccountDoesntExistWarning export and unused baseReserve. - useSimulateTxData.tsx: refactor getExpectedToFailReason in-place to delegate the qualitative gate to the shared helper; thread destination and isCollectible (already available in the hook) into the single call site without widening the hook's public API. - Tests: - New sendWarnings.test.ts covers native, classic, SAC, pure Soroban, collectible, contract destination, null/undefined funding, and the early-return query-param hydration edge case. - useSimulateTxData.test.ts updated for the new params and gains collectible / contract-destination cases. * Import isContractId from @shared/api/helpers/soroban in sendWarnings.ts Addressed Copilot PR review suggestion: sendWarnings.ts only uses the isContractId predicate, so importing from the lightweight @shared source directly avoids pulling in the larger popup/helpers/soroban module. --------- Co-authored-by: Jake Urban <10968980+JakeUrban@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* v5.41.0 * Trigger CI * Skip unfunded-destination warning for Soroban on Search address screen (#2778) (#2781) * Skip unfunded-destination warning for Soroban on Search address screen PR #2720 removed the misleading "Blockaid unfunded destination" warning for pure Soroban custom tokens on the transaction Review step. The same warning still appeared one step earlier on the Search address screen (SendTo). This change consolidates the rule into a single helper so both surfaces agree by construction. - Add popup/helpers/sendWarnings.ts with two pure helpers: - shouldCheckUnfundedDestinationWarning (qualitative gate: classic asset + classic destination + non-collectible) - shouldShowAccountDoesntExistWarning (compound: gate + strict isFunded === false, matching Review-step semantics where isFunded is boolean | null) - SendTo/index.tsx: pull asset/isCollectible from transactionDataSelector and use the compound helper; delete the dead shouldAccountDoesntExistWarning export and unused baseReserve. - useSimulateTxData.tsx: refactor getExpectedToFailReason in-place to delegate the qualitative gate to the shared helper; thread destination and isCollectible (already available in the hook) into the single call site without widening the hook's public API. - Tests: - New sendWarnings.test.ts covers native, classic, SAC, pure Soroban, collectible, contract destination, null/undefined funding, and the early-return query-param hydration edge case. - useSimulateTxData.test.ts updated for the new params and gains collectible / contract-destination cases. * Import isContractId from @shared/api/helpers/soroban in sendWarnings.ts Addressed Copilot PR review suggestion: sendWarnings.ts only uses the isContractId predicate, so importing from the lightweight @shared source directly avoids pulling in the larger popup/helpers/soroban module. --------- Co-authored-by: Jake Urban <10968980+JakeUrban@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Cássio Marcos Goulart <cassiomgoulart@gmail.com> Co-authored-by: Cássio Marcos Goulart <3228151+CassioMG@users.noreply.github.com> Co-authored-by: Jake Urban <10968980+JakeUrban@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Problem
PR #2720 removed the misleading "Blockaid unfunded destination" warning for
pure Soroban custom tokens on the transaction Review step. The same
warning still appears one step earlier on the Search address screen
(
extension/src/popup/components/send/SendTo), which is what issue #2777tracks. Freighter Mobile already addresses both screens.
Root cause
Two surfaces independently decide whether to show the unfunded-destination
warning:
SendTo/index.tsx:282-285) renders<AccountDoesntExistWarning />based purely on!destinationBalances.isFunded,with no asset-side check at all.
SendAmount/hooks/useSimulateTxData.tsx:94-123—getExpectedToFailReason) checksassetCanonical !== "native"andisContractId(issuer), but doesn't considerisCollectibleorcontract destinations.
The exported helper
shouldAccountDoesntExistWarning(isFunded, assetID, amount)in
SendTo/index.tsx:51-58is dead code (verified via repo-wide grep) andencodes a third, incomplete version of the rule.
Proposed change
1. New shared helper
Create
extension/src/popup/helpers/sendWarnings.ts:isContractIdis the same predicate already used ingetExpectedToFailReason, so the two surfaces will agree by construction.The qualitative/quantitative split (helper answers "does the rule apply?";
caller decides "is the destination actually unfunded enough to fail?") is
deliberate. The native ≥ 1 XLM create-account threshold lives where the
amount is finalized —
getExpectedToFailReason's native branch — and theSendTo screen never needs it (asset is fixed, amount is not yet known).
This addresses the naming concern by making the helper's semantics
unambiguous: it is a precondition gate, not a "this send will fail"
predicate.
2. Use the helper in
SendTo/index.tsxassetandisCollectiblefrom the existingtransactionDataSelector(already imported and used for
destination/federationAddress).!isFundedcheck with a single compoundhelper call (see Tests, below, for why the
=== falseand thequalitative gate are co-located):
validatedAddressis the right thing to pass: federation domains areresolved to G-addresses there, and
useSendToDataonly fetchesdestinationBalanceswhen the resolved address is non-contract anyway —but passing it through the helper keeps the rule self-consistent at the
read site.
Note the change from
!destinationBalances.isFundedtodestinationBalances?.isFunded === false:isFundedisboolean | null(@shared/api/types/backend-api.ts:10-13), and Review-step logic only warns on=== false(useSimulateTxData.tsx:105-123). Aligning Search with that strict check means an unknown/null funding state (e.g. mid-fetch or fetch failure) no longer triggers a misleading warning. (Critic round 2.)shouldAccountDoesntExistWarningexport and its basereserve constant. They are unused (grep confirms). Leaving a stale,
near-correct helper next to the new one is a footgun for the next
person tracing this logic. The min-1-XLM check it encoded for native
sends already lives in
getExpectedToFailReason's native branch.3. Refactor
getExpectedToFailReason— in-place, no API surface changePer critic feedback (round 1, item 1): do not thread new args through
popup/views/Send/index.tsx. The hook already has what it needs:useSimulateTxDataalready acceptsdestinationas a hook arg(
useSimulateTxData.tsx:394-405).useSimulateTxDataalready destructures fromtransactionDataSelector(
useSimulateTxData.tsx:410-412); addisCollectibleto that destructure.getExpectedToFailReason(...)call inside the hook(
useSimulateTxData.tsx:472-477) gains two locally-scoped parameters(
destination,isCollectible).Send/index.tsxis untouched; the hook signature is untouched.Inside
getExpectedToFailReason, replace the bespokeassetCanonical !== "native"+ issuer parsing with:4. Tests
Unit tests for
shouldCheckUnfundedDestinationWarningcovering:isCollectible: true(regardless of asset) → falseassetCanonical(initial-state edge case beforeuseSendQueryParamsresolves) → defaults to "treat as classic" →document why this is safe (the warning would only render once
destinationBalances loads, which gates on a real address, and the
default redux value for
assetis"native"pertransactionSubmission.ts:488, so empty-string is not a realruntime path; covered as a defensive case).
SendTowarning gate as a unit-testable helper. Theextension/src/popup/views/__tests__/SendPayment.test.tsxintegrationsuite is currently
describe.skip("Send", ...)(line 128, verifiedround 3) — anything added there would not run. Rather than unskip an
unrelated suite or stand up an ad-hoc integration harness for one
boolean, extract the wrapping
=== false+ helper composition into asecond pure helper alongside
shouldCheckUnfundedDestinationWarningin
sendWarnings.ts:The
SendToJSX then collapses to:This puts the strict-
=== falserule in the same module as thequalitative gate (one place to update) and makes the integration cases
pure unit tests in
extension/src/popup/helpers/__tests__/sendWarnings.test.ts:asset: "native",isCollectible: false,isFunded: false) + non-contract G-destination → true.asset: "TOKEN:CABC...XYZ",isCollectible: false,isFunded: false) + G-destination → false.isCollectible: true,asset: "native"—reproduces the early-return query-param hydration in
useSendQueryParams.ts:64-85wheresaveIsCollectible(true)runsbut the asset destructure path is short-circuited) +
isFunded: false→ false.destination: "C...") regardless of asset +isFunded: false→ false. (Also documents thatuseSendToData.tsx:92-93already skips balance fetch for contractdestinations, so this is belt-and-braces.)
isFunded: null, e.g. mid-fetch or backendfallback) with classic asset + non-contract destination → false
(regression guard for the strict-
=== falsechange, per criticround 2).
isFunded: undefined(no balances loaded yet) with classic asset →false (defensive, matches the optional-chaining at the call site).
isFunded: truewith classic asset → false (sanity).Update
useSimulateTxData.test.ts's existinggetExpectedToFailReasoncases to pass the newdestination/isCollectibleparams, and add a collectible case (collectible flagtrue with classic asset / unfunded G-destination → returns
null).5. Out of scope (file as follow-ups if confirmed)
recipient flow — none called out in Skip unfunded-destination warning for Soroban on Search address screen #2777, each touches separate UI.
buildUnfundedContextshape(which feeds Blockaid result merging on mobile). Extension's Review
step uses a simpler "expected to fail reason" string; cross-platform
convergence is a separate refactor.
@shared/helpers/for cross-submodulereuse. Mobile's helper is shaped differently (returns
UnfundedDestinationContext); a shared module would have to bridgeboth shapes, which is out of scope for a one-screen bug fix.
Verification
Per
freighter/AGENTS.md(also stored in agent memory): from repo rootrun
yarn test:ciandyarn build:extension. Manually exercise theSendTo screen in the extension dev build with (a) a classic asset →
unfunded G-address (warning shown) and (b) a pure Soroban custom token →
unfunded G-address (warning hidden).
Closes #2777