enable sending to federated address#821
Conversation
There was a problem hiding this comment.
Pull request overview
Enables sending to Stellar federation addresses (user*domain) by preserving the original federation address for display while using the resolved G... public key for transaction building.
Changes:
- Added
federationAddressto the transaction settings store and propagated it through send flow screens for display and recents. - Updated send UI (amount/review/processing) to show federation address when available.
- Added a new E2E flow to validate federation address send on mainnet and added input search debounce.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ducks/transactionSettings.ts | Adds federationAddress state + setter to track original federation address separately from resolved recipient key. |
| src/components/screens/SendScreen/screens/TransactionProcessingScreen.tsx | Uses federation address (when present) when saving “recent addresses” after a send completes. |
| src/components/screens/SendScreen/screens/TransactionAmountScreen.tsx | Displays federation address in the recipient row when present. |
| src/components/screens/SendScreen/components/SendReviewBottomSheet.tsx | Displays federation address + resolved/truncated G... key in the review sheet. |
| src/components/screens/SendScreen/SendSearchContacts.tsx | Adds debounced search; attempts to store resolved address for transactions while keeping federation for display. |
| e2e/flows/transactions/SendFederatedAddress.yaml | Adds an E2E flow covering federation resolution and a complete send. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * @param {string} address - The recipient address | ||
| * @param {string} address - The recipient address (resolved G... public key) | ||
| */ | ||
| saveRecipientAddress: (address) => set({ recipientAddress: address }), |
There was a problem hiding this comment.
saveRecipientAddress updates recipientAddress but leaves federationAddress untouched. This can lead to stale federation UI (and recent address saving) when other entry points set the recipient without also clearing federation (e.g., TransactionAmountScreen clears recipient on mount but not federation). Consider clearing federationAddress inside saveRecipientAddress (or providing a single setter that updates both) so state can’t become inconsistent.
| saveRecipientAddress: (address) => set({ recipientAddress: address }), | |
| saveRecipientAddress: (address) => | |
| set({ recipientAddress: address, federationAddress: "" }), |
| } else if (transactionHash) { | ||
| setStatus(TransactionStatus.SENT); | ||
| addRecentAddress(recipientAddress); | ||
| addRecentAddress(federationAddress || recipientAddress); | ||
| } else if (isContractAddress && !isSubmitting) { |
There was a problem hiding this comment.
Saving federationAddress into recents will break re-sending from the Recent list: loadRecentAddresses persists only the raw string, and selecting a recent federated address currently bypasses searchAddress/federation resolution, so the transaction flow can end up with an unresolved user*domain in recipientAddress. Consider storing the resolved G... address in recents (optionally alongside the federation address for display), or ensure selecting a recent federated address triggers resolution before continuing.
| (text: string) => { | ||
| setAddress(text); | ||
| // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
| searchAddress(text); | ||
|
|
||
| if (searchDebounceRef.current) { | ||
| clearTimeout(searchDebounceRef.current); | ||
| } | ||
|
|
There was a problem hiding this comment.
With the new debounce, the send recipient store state isn’t cleared until the delayed searchAddress(text) runs. That means searchError / isValidDestination from a previous search can remain visible for ~300ms while the user is typing. Consider clearing the relevant send-recipient state immediately on input change (e.g., add an action like clearSearchState() / setSearching(true) and call it here) so the UI reflects the new input right away.
…/freighter-mobile into lf-enable-send-federated-address
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| searchError: null, | ||
| isValidDestination: false, | ||
| isDestinationFunded: null, | ||
| isSearching: true, |
There was a problem hiding this comment.
clearSearchState sets isSearching: true and leaves destinationAddress/federationAddress untouched. If the pending debounced searchAddress call is cancelled (e.g., on unmount), the store can be left permanently “searching” with stale destination data. Consider resetting isSearching to false here (or introducing a separate debouncing flag in the component) and clearing destinationAddress/federationAddress to keep the store internally consistent.
| searchError: null, | |
| isValidDestination: false, | |
| isDestinationFunded: null, | |
| isSearching: true, | |
| destinationAddress: "", | |
| federationAddress: "", | |
| searchError: null, | |
| isValidDestination: false, | |
| isDestinationFunded: null, | |
| isSearching: false, |
| searchAddress: mockSearchAddress, | ||
| addRecentAddress: mockAddRecentAddress, | ||
| setDestinationAddress: mockSetDestinationAddress, | ||
| clearSearchState: jest.fn(), | ||
| resetSendRecipient: mockReset, |
There was a problem hiding this comment.
The SendSearchContacts tests’ useSendRecipientStore mock only adds clearSearchState, but the component now also reads isSearching, isValidDestination, and isDestinationFunded from the store. The mock should provide realistic defaults for these fields to avoid the component taking different render paths in tests vs production. Additionally, the module mock for helpers/stellar should export isFederationAddress now that the component imports/uses it; otherwise any test that triggers handleContactPress will throw. Finally, since search is now debounced via setTimeout, assertions that searchAddress was called may need fake timers / advancing timers to avoid flakiness.
…/freighter-mobile into lf-enable-send-federated-address
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {searchResults.length > 0 ? ( | ||
| <SearchSuggestionsList | ||
| suggestions={searchResults} | ||
| onContactPress={handleContactPress} | ||
| onContactPress={(contactAddress, name) => { | ||
| handleContactPress(contactAddress, name); | ||
| }} | ||
| /> | ||
| ) : ( | ||
| recentAddresses.length > 0 && ( | ||
| <RecentContactsList | ||
| transactions={recentAddresses} | ||
| onContactPress={handleContactPress} | ||
| onContactPress={(contactAddress, name) => { | ||
| handleContactPress(contactAddress, name); | ||
| }} |
There was a problem hiding this comment.
handleContactPress is async, but it’s invoked from the list callbacks without awaiting/voiding the returned promise. With @typescript-eslint/no-floating-promises enabled elsewhere in this file, this is likely to trigger linting and can also hide unhandled rejections. Prefer onContactPress={(a, n) => void handleContactPress(a, n)} (or make the wrapper async and await).
|
@CassioMG I opened a separate issue for the extension for the memo type inference bug listed on the analysis stellar/freighter#2717 / https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md |
ContactType was defined and set throughout the code but never consumed for business logic - only checked in test assertions. Removed the enum, the type property from Contact interface, and all assignments. Per code review feedback: property was write-only with no actual usage.
Resolved conflicts in: - SendReviewBottomSheet.tsx: combined imports (added computeTotalFeeXlm, isSorobanTransaction from soroban helpers) - TransactionAmountScreen.tsx: combined imports (added isSorobanTransaction) - TransactionTokenScreen.tsx: kept ContactRow for recipient display (part of federated address feature) All conflicts resolved to preserve federated address functionality while integrating main's fee breakdown and Soroban transaction improvements.
| if (memoType === "id" && /^\d+$/.test(memo)) { | ||
| // Memo.id validates numeric range (0..2^64-1); wrap to avoid crash on out-of-range values | ||
| try { | ||
| transactionBuilder.addMemo(Memo.id(memo)); | ||
| } catch { | ||
| transactionBuilder.addMemo(Memo.text(memo)); | ||
| } | ||
| } else if (memoType === "hash") { | ||
| try { | ||
| const hashBytes = Buffer.from(memo, "base64"); | ||
| if (hashBytes.length === 32) { | ||
| transactionBuilder.addMemo(Memo.hash(hashBytes)); | ||
| } else { | ||
| transactionBuilder.addMemo(Memo.text(memo)); | ||
| } | ||
| } catch { | ||
| transactionBuilder.addMemo(Memo.text(memo)); | ||
| } | ||
| } else { | ||
| transactionBuilder.addMemo(Memo.text(memo)); | ||
| } |
There was a problem hiding this comment.
Silent memo-type downgrade
When Memo.id(memo) validation fails (non-numeric, > 2^64−1), the catch falls through to Memo.text(memo) at line 406. Same shape for memo_type:"hash" with invalid base64 (lines 414, 417) and memo_type:"return" (bare else at line 420).
For a federation handle that points to a custodial/exchange address with memo_type:"id", the receiving exchange routes deposits by memo type. A silent downgrade to text means the transaction succeeds on-chain but the funds aren't routed to the user's sub-account — they land in the omnibus address, and recovery is manual support / KYC / possibly weeks. I'd rather have the wallet fail-loud than rewrite the memo type.
Sketch (not prescriptive): logger.warn("buildPaymentTransaction", "Federation memo failed validation", { memoType, error }) to capture a Sentry breadcrumb, then surface an Alert — something like "We couldn't process the memo returned by the federation server. The recipient may not credit this transfer correctly. Please verify the address or contact the recipient." — and abort the send.
For reference, the extension's parallel PR stellar/freighter#2744 ships a federationMemo.ts helper that validates id / hash / text against SEP-0002, throws on failure, and surfaces a generic user-facing error while Sentry-capturing the diagnostic detail. Not saying we should port it wholesale — just one already-designed pattern in case it fits.
…/freighter-mobile into lf-enable-send-federated-address
Closes #807
tested on iOS Device - Build 1.13.25 (1776460485)
Untitled.mp4
Checklist
PR structure
Testing
Release