Skip to content

Support token destinations for Stealth Send and Swap - #6253

Open
j0ntz wants to merge 8 commits into
developfrom
jon/houdini-token-destinations
Open

j0ntz wants to merge 8 commits into
developfrom
jon/houdini-token-destinations

Conversation

@j0ntz

@j0ntz j0ntz commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Technical Design Document

stealth-send-swap.md

Description

Asana task

A swap-send or Stealth Send can pay out only a chain's coin, because the app has no list of the tokens HoudiniSwap routes. This reads that list from the info server rollup (houdiniAssets, served by EdgeApp/edge-info-server#166) and lets the send scene pay out a token, such as TRX to USDT on Tron or SOL to USDC on Solana.

  • houdiniChains.ts reads houdiniAssets as served, keyed by Edge pluginId and tokenId. No contract address is matched in the app. memoNeeded, addressValidation and each asset's hasSelfPrivate come from the served data. The baked-in Houdini chain table is gone.
  • evmChainId is Edge data that Houdini does not serve. It stays in a small EDGE_CHAIN_DATA table. URI schemes are matched by Edge pluginId only (schemeNamesChain).
  • A served address pattern that does not compile, or that matches text that is no address (Houdini serves ^.*$ for some chains), drops its chain. EDGE_CHAIN_DATA replaces 7 served patterns that reject real addresses or accept wrong ones: cardano, dash, ecash, hedera, monero, pivx and solana. It also pins memoNeeded true for the six chains whose deposits are credited only by tag (cosmoshub, hedera, ripple, stellar, thorchainrune and ton). A served false cannot drop a destination tag. Edge's Solana pattern requires both letter cases. A 42-character bc1 or cashaddr made only of base58 characters is not offered Solana.
  • Stealth Send and Stealth Swap stay hidden until the rollup carries the field. network.ts exposes onInfoRollup, notified by both rollup paths (the fetched rollup and the signed one applied on a warm launch), and Services reloads Houdini's list on each. A rollup that lands after login shows Stealth at once instead of after the next 10-minute poll.
  • The send scene tracks a recipient asset (plugin id and token id). makeSwapSendRequest builds the swap request with the real toTokenId.
  • pickKeepsRecipient and getWalletSwitchReset decide when a pick or a source wallet switch keeps the entered address, with unit tests next to the other helpers.
  • getMemoCarry moves a memo into the Destination Tag row when a swap-send starts and back into the plain memo when it ends. Toggling Stealth or picking the source asset back on a tag chain keeps the tag. getWalletSwitchMemoCarry does the same for a source wallet switch that keeps the address with Stealth armed.
  • The "Recipient receives" picker lists each served chain coin followed by that chain's served tokens. Every row is the asset name over " Network", with the currency code on the right and no radio button (layout D from the task). It is its own modal, RecipientAssetListModal, built on ListModal. RadioListModal loses its search prop and filter, since this scene was their only caller.
  • The "Myself" wallet picker also lists served token wallets, in the existing wallet row format. Every pick goes through the scene, and picking the source asset clears a destination picked earlier while keeping the entered amount. A token wallet pick reads that token's own receive address. The pick logic lives in getSelfTransferPick, keepEnteredAmount and isReceiveSideGuaranteed, with unit tests. It still leaves out the source wallet: a same-chain token payout to yourself needs a second wallet on that chain.
  • On the Exchange scene, both wallet pickers list only served assets while Stealth Swap is on. With it off they are unchanged. A Stealth Swap checks Houdini's own bans on both sides before quoting and shows the "no enabled exchanges" message for a banned asset. A banned destination the account cannot describe (a plugin it lacks, or a token missing from its token list) is still refused, named by its ids.
  • An info-server ban on the provider removes that destination from these pickers.
  • The send scene's Exchange Rate row shows the rate without the swap provider's name.
  • parseCrossChainPayment takes the payout's tokenId: a payment code naming that token is accepted with its amount in the token's units, another token's code is refused, and a coin code on a token payout keeps its address but not its amount. Asked for ETH, the Ethereum parser throws on a USDT transfer code. When that first ask throws on a payment URI, parseAsPayout asks again with the payout token's code. Bare text is never asked as a token, since Tron reads USDT text as a PIX lookup. A token payout on the wallet's own chain reads a payment URI with the payout token's code in handleUnparsedAddress. An ETH wallet paying out USDT therefore takes a USDT transfer invoice with its payee and a guaranteed USDT receive amount.
  • A plain or same-asset send keeps a payment code's amount only when the parse names no asset or names the asset being sent. A coin value= no longer sizes a token spend.
  • A "Myself" pick on another asset, an address that adopts another chain, and an address that switches back to the wallet's own network all keep the entered amount (keepEnteredAmount).

Review notes:

  • Undecided label: the send scene's own "Recipient receives" row shows a pick as "Tether (USDT)" over "Tron Network", the same pieces as a picker row. The task left that label open. This is the suggested default.
  • The first commit on this branch, "Apply provider asset bans to swap-sends", is the commit of EdgeApp/edge-react-gui#6243. Review from the second commit on.
  • A payment URI's amount sets the side whose asset it states (getUriAmountSide). An amount another chain's parser read, or one stated in the payout token on the sending chain, sets the receive amount. One stated in the sent asset sets the send amount. Any other asset sets neither.
  • Stealth Send from a token source now pays out that token when Houdini serves it, and is unavailable for a token Houdini does not serve. Before, it paid out the chain coin.

Tested on an iOS simulator, with the app pointed at a local info server that served the live Houdini list:

  • TRX to USDT on Tron to a pasted address: sent, ended on the success scene.
  • TRX to USDT on Tron through "Myself": live quote.
  • SOL to USDC on Solana through "Myself": sent 0.105 SOL, ended on the success scene, and 11.137809 USDC arrived in the second Solana wallet.
  • SOL to USDC on Solana to a pasted address: live quote.
  • USDT to USDT Stealth Send on Tron: sent, ended on the success scene.
  • An injected info-server ban on USDC on Solana as a destination: the row is gone from the picker.
  • Exchange scene wallet pickers with Stealth Swap off and on.
  • A source wallet switch on the send scene with USDT on Tron picked as the payout: the pick and the recipient address are cleared. The same switch on a plain TRX send keeps the address.
  • A ban on USDT on Tron for this provider, injected after the pick: entering an amount shows No enabled exchanges support USDT (on Tron) and no quote is fetched. For this frame the app's two info poll intervals were shortened to 5 seconds as an uncommitted edit, because the shipped intervals are 5 and 10 minutes.
  • After the review rework, on the served houdiniAssets list: the picker's served token rows and a Stealth USDT payout quote on Tron. From a Tron wallet with Tether on Tron picked, "Myself" and the TRX row of a second Tron wallet reverted the payout to TRX as a plain send. With BTC banned as a Houdini source and Stealth Swap on, BTC to ETH showed the no-enabled-exchanges toast; with Stealth Swap off the same pair quoted through Maya Protocol. These were quotes only, with no funds sent.
  • The fixes from the Cursor review rounds, re-driven at 68d28e3 with quotes only and no funds sent:
    • "Myself" from a Solana wallet to the USDC wallet of a second Solana wallet: the recipient row reads USDC on Solana with that token wallet's own receive address.
    • From a Tron wallet with Tether on Tron picked, "Myself" and the TRX row of a second Tron wallet: the payout reverts to a plain TRX send to that wallet.
    • The kept amount on a same-asset "Myself" pick: 40 TRX stays. The amount row needs an address first, and clearing the address clears the amount. The scene showed the amount row before any address as an uncommitted edit for these two frames.
    • A Cosmos Hub send with memo 104729: arming Stealth Send moves it into the Destination Tag row, and disarming moves it back to the memo.
    • A Houdini-only ban on USDT on Tron as a destination, with Stealth Swap on: BTC to USDT on Tron shows No enabled exchanges support USDT (on Tron), and the receiving picker drops that wallet's USDT row while keeping its TRX row.
    • The same ban on the send scene, with the scene's asset description forced empty as an uncommitted edit: the message names the token by its contract id, No enabled exchanges support TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (on Tron).
    • With the served list setting memoNeeded false for Cosmos Hub: a Stealth Send of 20 ATOM with memo 104729 keeps the Destination Tag row.
    • A Stealth Send of 20 ATOM from Cosmos Hub with Destination Tag 104729, then a "Send from Wallet" switch to a second Cosmos Hub wallet: the address and amount stay, Stealth Send turns off, and 104729 moves into the Transaction Memo row.
    • From a Tron wallet, a pasted bc1 address is adopted as Bitcoin with no network picker and no Solana candidate.
    • With the app holding the list from login, the served list was emptied and the app's 10-minute info poll ran: Stealth Send from Tron to BTC is still offered after toggling it off and on.
  • The "Myself" picks after their move into getSelfTransferPick, re-driven at 0b2218d with quotes only and no funds sent:
    • From My Solana, "Myself" and the USDC row of My Solana 2: the recipient row reads USDC on Solana with that wallet's address, as a swap-send.
    • From My Solana, "Myself" and the SOL row of My Solana 2: a plain SOL send to that wallet, with no swap.
    • With USDC on Solana picked in "Recipient receives" and no address yet, "Myself" and the SOL row of My Solana 2: the payout ends and the scene is a plain SOL send to that wallet. The same result after a "Myself" USDC pick with 0.12 SOL entered, once the address is removed; removing the address also clears the amount. No amount reaches the pick.
  • At 9507981, with quotes only and no funds sent:
    • A warm launch with the rollup cached: Stealth Swap is offered without waiting for the info poll.
    • With an amount entered, clearing the address resets the amount. A later "Myself" pick starts with no amount. The kept amount on an adopt is therefore not reachable by tapping, and the scene tests below cover it.
    • With Ethereum picked in "Recipient receives" on a Bitcoin wallet, entering a bc1 address switches back to a plain Bitcoin send with Stealth off.
  • At fe37002, with quotes only and no funds sent:
    • A 25 USDT invoice on Ethereum, entered on a Bitcoin wallet with Tether on Ethereum picked: refused before the parser retry, and accepted after it with the address and a guaranteed 25 USDT. No quote followed: Houdini returned no route for that pair.
    • On a USDC on Ethereum wallet with Stealth on, an ETH value= code fills the address and leaves the amount at 0 USDC.
    • The kept amount on a "Myself" pick, with the scene's address gate on the amount row removed by hand for these two frames: 0.0002 BTC entered with Ethereum picked, then "Myself" and My Ether, kept 0.0002 BTC and quoted about 0.0065 ETH.
  • At 0186e0a, with quotes only and no funds sent:
    • A 25 USDT transfer invoice entered on an Ethereum wallet with Tether on Ethereum picked: accepted with the payee and a guaranteed 25 USDT. Houdini returned no route at that size, since the invoice is under its $25 floor.
  • Unit tests cover the houdiniChains.ts helpers, RecipientAssetListModal, and in stealthSwap.test.ts the Houdini bans on both sides. SwapCreateScene.test.tsx checks that a Houdini-only ban on either side blocks a Stealth Swap and not a plain swap, and AddressTile2.test.tsx checks that a token row in the "Myself" picker reads that token's receive address. SendScene2.recipientNetwork.ui.test.tsx covers the own-network switch with Stealth on (a destination tag from an XRP code survives, a token with no private route to itself is refused, a source whose private route came back unavailable is refused), the entered amount kept through a "Myself" pick, an adopting address and a switch back, a coin code on a token wallet leaving the amount empty, and a USDT invoice accepted on an ETH wallet paying out USDT. paymentUri.test.ts covers payout-token codes, including a parser that reads a token invoice only when asked for that token, and keysStore.test.ts covers the warm refresh notifying rollup listeners.

Screenshots 01 to 23 were taken before the rebase onto develop, 24 to 28 after it. Screenshots 29 to 31 show the wallet switch. Screenshot 32 shows the ban message. Screenshots 33 to 36 show the SOL to USDC send through "Myself". The last batch (2026-10-14, at 7af7eec) is the review rework: 11 and 12 the served token rows and Stealth USDT quote, 13 and 14 the "Myself" reset, 15 and 16 the Stealth Swap source ban and its control. The batch at 68d28e3 carries 11, 12, 15 and 16 forward and adds 17 to 26 for the Cursor review fixes; 19, 20 and 26 were taken with the uncommitted edits named above. Screenshots 27 to 29 show the last three items of that list. Screenshots 30 and 31, at 0b2218d, show the Cosmos Hub wallet switch before and after. Screenshots 32 to 36, also at 0b2218d, show the three "Myself" picks in that order. Screenshots 37 to 39, at 9507981, show the warm-launch Stealth Swap, the amount reset after clearing the address, and the switch back to the wallet's own network; they are carried to fe37002. Screenshots 40 and 41 show the USDT invoice before and after the parser retry, 42 and 43 the USDC Stealth send and its empty amount after the value= code, and 44 and 45 the kept amount with the gate removed. Screenshots 46 and 47, at 0186e0a, show the USDT invoice on the Ethereum wallet entered and accepted.

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

  • EdgeApp/edge-react-gui#6243: its commit is the first commit of this branch.
  • EdgeApp/edge-info-server#166: serves houdiniAssets. Until it deploys with houdiniApiKey and houdiniApiSecret set, Stealth Send and Stealth Swap stay hidden. It must reach production before this app release.

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Tested on an iOS simulator only (a large-screen iPhone).

Test evidence

8a28b76
2026-10-10
🪓 Info server poll intervals shortened to 5 seconds (uncommitted) so a ban injected after the pick reaches the scene

1. recipient picker rows

2. recipient picker search

3. recipient row

5. success

6. sol usdc picker search

7. sol usdc recipient row

8. sol usdc quote

9. myself picker

10. myself picker search

11. myself recipient usdc

12. myself sol usdc quote

13. injected ban usdc solana absent

14. exchange from stealth off

15. exchange to stealth off

16. exchange to create stealth off

17. exchange stealth swap on

18. exchange from stealth on

19. exchange to stealth on

20. exchange to create stealth on

21. usdt usdt recipient row

22. usdt usdt stealth quote

23. usdt usdt stealth success

24. rebased recipient picker rows

25. rebased recipient picker search tron

26. tron myself picker my tron 2

27. tron myself provider minimum

28. tron myself trx usdt quote

29. token payout before wallet switch

30. token payout after wallet switch

31. same asset switch keeps address

32. 🪓 ban message names chain

33. myself sol usdc quote 0105

34. myself sol usdc success

35. myself sol usdc tx details

36. my solana 2 usdc received
68d28e3
2026-10-14
🪓 19-20: showed the You send row before an address was entered (SendScene2 amount-row gate); 26: forced destDescription undefined so the ban message falls back to asset ids. Both reverted.

11. served token rows in recipient picker

12. stealth usdt payout quote

15. stealth swap houdini source ban refused

16. stealth off control quote

17. myself token receive address

18. myself same asset after token

19. 🪓 amount before myself

20. 🪓 myself keeps amount

21. plain memo before stealth

22. stealth carries memo to tag

23. stealth off tag back to memo

24. stealth swap dest ban refused

25. stealth swap picker drops banned dest

26. 🪓 dest ban fallback ids

27. memo floor tag row stays

28. bc1 address adopts bitcoin no solana

29. list kept after empty poll stealth offered
0b2218d
2026-10-14

30. wallet switch tag before

31. wallet switch tag after

32. myself adopt usdc

33. myself same asset plain send

34. token picked no address

35. myself source asset ends swap

36. myself source asset after myself token
0186e0a
Filter Stealth Swap pickers to served assets
2026-10-16
🪓 amount row shown before any address (SendScene2 swap You-send gate removed, reverted)

37. warm launch stealth swap shown

38. clear then myself amount reset

39. own network switch back plain send

40. token invoice refused before fix

41. token invoice accepted

42. usdc stealth on

43. usdc coin value amount empty

44. 🪓 amount before myself

45. 🪓 myself keeps amount

46. same chain usdt invoice entered

47. same chain usdt invoice accepted

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx

@j0ntz j0ntz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch was self-reviewed before the PR opened (code-review at high effort, then curated).

Fixed before review (1)
  • src/docs/stealth-send-swap.md:113: the captions of two code blocks quoted from edge-core-js pointed at edge-react-gui blobs. They link to edge-core-js. Folded into "Load Houdini's token list from the info server".
Rejected in curation (8)
  • src/components/scenes/SendScene2.tsx:1003: a same-chain swap-send drops the amount of a scanned or pasted payment URI. Rejected: on the same chain the URI is parsed with the source asset's code, so its amount is not in the token's units. Dropping it costs a retype and never sends a wrong amount. Stated in the PR description.
  • src/components/modals/RecipientAssetListModal.tsx:60: the new rows have no highlight for the selected asset. Rejected: the task's layout D says "nothing marking the current pick".
  • src/actions/HoudiniTokensActions.ts:20: a later rollup without houdiniTokens replaces the loaded list with {}. Rejected: the server omits the field only before its first complete fetch, and after that a failed fetch keeps serving the last list.
  • src/components/scenes/SwapCreateScene.tsx:306: with Stealth Swap on, the pickers hide every token while the list is empty. Rejected: the spec says to show only served tokens, and chain coins stay listed.
  • src/util/houdiniChains.ts:426: matching lowercases contract addresses on both sides for every chain. Rejected: the spec asks for a lowercase match. A mismatch hides a token and never lists a wrong one. USDT on Tron and USDC on Solana match on the simulator.
  • src/actions/HoudiniTokensActions.ts:22: one malformed entry makes the cleaner throw, and the stored list stays. Rejected: both ends of the format are ours, and a stale list is better than an empty one.
  • src/components/scenes/SendScene2.tsx:2029: the swap provider's name is gone from the Exchange Rate row. Rejected: requested by the task owner during the run.
  • src/actions/HoudiniTokensActions.ts:21: every refresh dispatches a new object when the list is unchanged. Rejected: the refresh runs every 10 minutes and two memos recompute.

@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from 302cf2e to d83e976 Compare October 10, 2026 00:58

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx Outdated
Comment thread src/components/scenes/SwapCreateScene.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from d83e976 to 8a28b76 Compare October 10, 2026 01:29

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data from the server. Per the review on EdgeApp/edge-info-server#166, the server would serve Houdini's chain and per-asset data keyed by Edge pluginId and tokenId. This PR can then read it as is, with no contract-address matching (inline comment on getTokenFlag). memoNeeded, addressValidation and the coin's hasSelfPrivate would also come from the served data, and HOUDINI_CHAINS would keep only what Houdini does not provide. Open points for the rework:

  • evmChainId and the URI scheme names are Edge data, not Houdini's. Either the server's chain table carries them, or a small table stays here.
  • Before the rollup loads, or while the server runs without a Houdini key, there would be no chains at all. Stealth Send could stay hidden until the data arrives, or the app could ship a fallback copy.
  • addressValidation arrives as a regex string, and the scene uses it to guess which chain a pasted address belongs to. A pattern that fails to compile should mark its chain unavailable, and a permissive one would match every paste. Houdini reports ^.*$ for the IBC chains (see the comment in edge-exchange-plugins src/mappings/houdini.ts), which is why they are excluded today.

Behavior and tests. The three Warnings and four Suggestions inline apply to the current code and carry over to the rework.

Comment thread src/util/houdiniChains.ts Outdated
Comment thread src/components/scenes/SwapCreateScene.tsx
Comment thread src/components/scenes/SendScene2.tsx Outdated
Comment thread src/components/tiles/AddressTile2.tsx Outdated
Comment thread src/locales/en_US.ts Outdated
Comment thread src/components/scenes/SendScene2.tsx
Comment thread src/docs/stealth-send-swap.md Outdated
Comment thread CHANGELOG.md Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 2 times, most recently from afa9855 to 7af7eec Compare October 14, 2026 07:05
@j0ntz

j0ntz commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Reworked to read houdiniAssets from EdgeApp/edge-info-server#166 as served, keyed by pluginId and tokenId, with memoNeeded, addressValidation and each asset's hasSelfPrivate taken from it. On the open points:

  • evmChainId stays in a small EDGE_CHAIN_DATA table in houdiniChains.ts. URI schemes are matched by Edge pluginId only (schemeNamesChain), so no scheme table is needed.
  • Stealth Send and Stealth Swap stay hidden until the rollup carries the field (houdiniChains.size > 0), with no fallback copy. Production needs houdiniApiKey and houdiniApiSecret set on the info server before the app release.
  • A served pattern that does not compile, or that matches text that is no address (Houdini's ^.*$), drops its chain. EDGE_CHAIN_DATA also replaces 7 served patterns that reject real addresses or accept wrong ones: cardano, dash, ecash, hedera, monero, pivx and solana.

The inline Warnings and Suggestions are answered on their threads.

Comment thread src/components/scenes/SendScene2.tsx

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx
Comment thread src/util/houdiniChains.ts
Comment thread src/util/houdiniChains.ts Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 3 times, most recently from 178407c to cbd1838 Compare October 14, 2026 08:04

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from cbd1838 to df73664 Compare October 14, 2026 08:28

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/tiles/AddressTile2.tsx
Comment thread src/components/scenes/SendScene2.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from df73664 to 025be96 Compare October 14, 2026 08:51

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from 025be96 to ce7a7ff Compare October 14, 2026 09:08

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from ce7a7ff to 0aa582d Compare October 14, 2026 09:26

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx Outdated
Comment thread src/util/houdiniChains.ts Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from 0aa582d to 68d28e3 Compare October 14, 2026 09:49

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier asks and the Bugbot fixes all check out at 68d28e3b20: tsc is clean and all 1150 jest tests pass. The rest is inline. The Warning on the wallet switch describes a destination tag that gets dropped; it predates this PR but sits in the switch the PR reworks. The other two Warnings ask for tests on logic that broke during this PR.

Suggestion: after the autosquash, "Load Houdini's token list from the info server" keeps its original body, which says the server serves houdiniTokens and the app matches tokens by contract address. Both are gone, so that message needs rewording when the fixups are squashed.

Comment thread src/components/scenes/SendScene2.tsx
Comment thread src/components/scenes/SendScene2.tsx Outdated
Comment thread src/components/scenes/SwapCreateScene.tsx
Comment thread src/docs/stealth-send-swap.md Outdated
Comment thread src/components/scenes/SendScene2.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 2 times, most recently from bacb300 to 0b2218d Compare October 14, 2026 21:33
@j0ntz

j0ntz commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

The five inline threads are answered with their fixups (9830e64, 0b2218d, 14ab129). The reword of "Load Houdini's token list from the info server" happens at landing, when the fixups fold into it; the commit tool here cannot carry an amend! commit for that subject. The new body will read:

The info server serves Houdini's chains and assets as houdiniAssets in its rollup, keyed by Edge pluginId and tokenId. Read the field from the raw rollup with our own cleaner (asHoudiniChains) and keep it in state.ui.houdiniChains; a later rollup without the field keeps the loaded list.

The info server serves the tokens Houdini can route as `houdiniTokens`
in its rollup. Read the field from the raw rollup with a cleaner of our
own, keep it in redux, and match it to Edge tokens by contract address.
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from 0b2218d to d9052c7 Compare October 15, 2026 23:15
@j0ntz

j0ntz commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto develop with #6242 in, head d9052c7. The reconcile was substantial, so please look at the rebased head before it lands.

  • Message: your wording is now the body of "Pay out a token from the send scene".
  • HoudiniChainsActions.ts comment: it now says asHoudiniChains drops a malformed chain and keeps the rest, where the rollup's cleaner would reject them all.
  • handleChangeAddress: keeps Explain why the send scene declines an address #6242's own-network switch (adoptOwnNetworkDestination, now clearing pickedRecipientAsset) alongside this branch's destinationCleared and isReceiveSideGuaranteed.
  • HOUDINI_CHAINS rejection: detection and isFormatOf go through getHoudiniChain(houdiniChains, id), so a chain the info server does not serve is skipped.
  • getOwnNetworkBlock: drops sourceTokenId and reads the source asset's own hasSelfPrivate, coin or token. The probe key stays ${pluginId}:${tokenId}->.
  • isTokenAmount: replaced by getUriAmountSide, which places a same-chain amount by the asset the URI states: the payout asset sets the receive side, the sent asset sets the send side, any other asset sets neither. This changes behavior: a coin sent to a same-chain token keeps the URI amount on the send side, where the old rule priced the token payout in the coin's smallest unit. Explain why the send scene declines an address #6242's tests are ported (8 getUriAmountSide cases; the recipient-network UI tests seed ui.houdiniChains).

The design doc has a Phase 40 entry for the reconcile. tsc and eslint are clean, and jest passes 1260 of 1260 tests in 134 suites.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx
Comment thread src/components/scenes/SendScene2.tsx
Comment thread src/components/modals/RecipientAssetListModal.tsx

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the rebase against both parents (develop at e79f5e9 with #6242, and the approved 0b2218d): the own-network switch, getHoudiniChain(houdiniChains, ...) in detection and isFormatOf, getOwnNetworkBlock on the source asset, the ${sourceKey}->${sourceKey} probe key and getUriAmountSide all read right, and #6242's tests are ported. tsc clean, jest 1260/1260, no new eslint warnings. One ask inline.

For the next rebase: #6241 removed the third parameter of makeStealthSwapRequestOptions, which SendScene2.tsx:3937 still passes.

Comment thread src/components/scenes/SendScene2.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from d9052c7 to 9068d1a Compare October 16, 2026 19:25
@j0ntz

j0ntz commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

The inline ask is answered in a2158bc (three Stealth-on scene tests, each failing under the mutations it targets). The makeStealthSwapRequestOptions third argument at SendScene2.tsx:3937 comes out in the rebase onto develop at land time, since this branch's base still predates #6241.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 2 times, most recently from f570671 to 5326298 Compare October 16, 2026 19:52

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/actions/HoudiniChainsActions.ts
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from 5326298 to 66c4d78 Compare October 16, 2026 20:09

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/util/paymentUri.ts
Comment thread src/components/scenes/SendScene2.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 3 times, most recently from 2a735c6 to 0b5c90f Compare October 16, 2026 20:44

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/components/scenes/SendScene2.tsx
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch 4 times, most recently from d27ef0a to fe37002 Compare October 16, 2026 22:13

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fe37002. Configure here.

Comment thread src/components/scenes/SendScene2.tsx
j0ntz added 6 commits October 16, 2026 15:59
The design doc now says a plain or same-asset send keeps a payment code's amount only when the parse names no asset or names the asset being sent, and records that change as Phase 41.

Stamp the design doc with the fingerprint of the code tree it documents (f8a62bfec8c19efdeddd44f2473e1ef7bbdc3aa8)

Reload Houdini's list whenever an info rollup lands, answering Bugbot's finding that the Stealth list can stay empty after the rollup arrives.

On a warm launch the signed rollup is applied in the background, after Services mounts. The refresher's mount run read no houdiniAssets, wrote an empty list, and the next run was 10 minutes later, so Stealth stayed hidden until then. network.ts now exposes onInfoRollup, notified by both rollup paths (fetchPublicRollup and the signed applyPublicRollup in keysStore), and Services reloads the list on each notification. A keysStore test proves the warm refresh notifies listeners with the rollup that carries houdiniAssets.

Document the rollup listener that reloads Houdini's list: the body explains why the refresher's mount run can beat the warm-launch rollup and how onInfoRollup closes the gap, the testing list adds the keysStore case, and Phase 42 records the change.

Stamp the design doc with the fingerprint of the code tree it documents (3399e0238bc1ef0d6f9f922b0f907b621f103bc5)

Document payout-token codes and the kept amount on adopt

The TDD now covers parseCrossChainPayment's payout tokenId, the entered
amount surviving a Myself pick or an adopting address, the new tests,
and Phase 43.

Stamp the design doc with the fingerprint of the code tree it documents (89eb5e47f12ead4aed1d56a0df4bec989fc55daa)

Pin the TDD's code captions to the pushed head

The captions pointed at a local commit that the push rewrote, so every
code link returned a 404.

State that the kept amount on adopt is not reachable by tapping

A sim drive (0.0005 BTC code, clear icon, Myself, an ETH wallet) adopted
the ETH address and showed 0 BTC: every route back to an empty address
tile runs handleResetSendTransaction, which zeroes the amount, and the
amount tile renders only once an address is set. The Myself paragraph and
Phase 43 now say the adopt keep guards the handler contract and is pinned
by the unit cases. Captions re-pin to the pushed head 2a735c6.

Record the own-network amount keep in the design doc

Notes that a bare own-network address keeps the entered amount through
keepEnteredAmount, adds the switch-back scene case to the Testing counts,
and logs the Bugbot finding under Phase 43.

Stamp the design doc with the fingerprint of the code tree it documents (859bf13522cf8102db52fba42e115d7832723c0a)

Repin code captions to the pushed head

The own-network keep moved SendScene2 and its doc lines, so the captions now cite 708912c, the pushed tip that carries that code.

Stamp the design doc with the fingerprint of the code tree it documents (3cc795d423cf53e2c1b27d5f52d421aa64170f20)

Design doc: a token invoice on a same-chain token payout is read with the payout token's code (Payment URI amounts), the new scene case and counts in Testing, and Phase 45.

Stamp the design doc with the fingerprint of the code tree it documents (c3131a1886cb4ca10e9f379f1f8e207738a9dd2d)

Fixup-for: auto
Houdini's routes come from the info server's `houdiniAssets` field,
cleaned by `asHoudiniChains`, where they came from `houdiniTokens` and
`asHoudiniTokens`. Each chain, keyed by pluginId, carries its memo
flag, address pattern and coin, plus the tokens Houdini serves on it by
tokenId. That list takes over from the hard-coded `HOUDINI_CHAINS`
table and from matching tokens by contract address.

A swap-send tracks a recipient asset (plugin and token) where it tracked
a chain, so the quote asks for the picked token and same-asset compares
both. The recipient picker and the Myself wallet picker list each chain
coin followed by the tokens Houdini serves on it, less the destinations
the info server withdrew.
Test the own-network switch with Stealth on

Covers peachbits' ask on the reconcile: a destination tag read from an
XRP code survives the switch back to the wallet's network, a token with
no private route to itself is refused with the self-private reason, and
a source whose same-asset private route came back unavailable is
refused with the route reason. Each test fails under the mutation the
review named for it.

Type the fake XRP settings in full

The fake XRP wallet's settings left out customFeeSettings, which the
fake plugin's settings type requires; tsc rejected the test file.

Fixup-for: human
Drop a URI amount stated in another asset on a plain or same-asset send

Answers Bugbot's finding on 9068d1a: with Stealth on and no destination
picked, a token send treats a payment code as a plain send, so an EIP-681
coin value (which withStatedAsset marks as the coin) was written into the
token amount. A coin code for 0.002 ETH on a USDT wallet filled
2,000,000,000 USDT. The plain branch now keeps a code's amount only when
the parse states the source asset, and a scene test pins it.

Keep the entered amount when adopting a payout

adoptDestination handed the scene a bare address, and the receive-side
branch of handleChangeAddress set the send amount from the address alone, so
a "Myself" pick on another asset, or an address that adopts another chain,
cleared the amount the user had entered. The adopt resets the guaranteed
side to send, and that amount is in the asset being sent, so a destination
that arrives with no amount of its own now keeps it through
keepEnteredAmount, stated in the source asset.

Accept a payment code for the payout token

parseCrossChainPayment refused every token code, a rule left from coin-only
payouts, so a USDT invoice scanned for a USDT payout was called an invalid
address. It now takes the payout's tokenId: a code naming that token is
accepted with its amount, in the token's units; another token's code is
still refused; and a coin code on a token payout keeps its address but not
its amount.

Keep the entered amount through the own-network switch

Answers Bugbot's finding on 0b5c90f: adoptOwnNetworkDestination handed
the bare own-chain parse to handleChangeAddress, whose plain branch writes
the parse's nativeAmount (undefined for a bare address) over the amount
already entered. It now wraps the parse in keepEnteredAmount, as the
self-transfer pick and adoptDestination do, so a code's own amount still
wins and a bare address keeps the source-asset amount. A scene case drives
a BTC code, an Ethereum switch, then a bare BTC address.

Retry a token payout's invoice with the token's own code: the ETH parser throws on a transfer code asked as ETH, so the earlier fix for Bugbot's token-invoice finding never accepted one on a real chain

Read a token invoice with the payout token's own code when the payout is a token on the wallet's own chain. The wallet parsed it with the coin's code, which an EVM parser refuses for a token transfer, so an ETH-to-USDT send declined a USDT invoice (Bugbot: same-chain token invoices rejected). The stated amount prices the receive side. Scene test covers it.

Fixup-for: auto
The send scene's Exchange Rate row listed the swap provider under the
rate. The row now shows the rate and the price impact only.
With Stealth Swap on, the Exchange scene wallet pickers list only the
chain coins and tokens Houdini serves, and the receiving picker also
drops the destinations the info server withdrew. With it off they are
unfiltered.
@j0ntz
j0ntz force-pushed the jon/houdini-token-destinations branch from fe37002 to 0186e0a Compare October 16, 2026 23:00

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository because its GitHub organization is not linked to your Claude organization.

To turn it back on, one person needs both roles: owner of the GitHub organization (or the account owner, for a personal repository) and admin or owner of your Claude organization. If that is not you, pass this on to someone who is.

Open Organization settings, Git providers and click Link next to the GitHub organization's name. If it is not listed, click Enable GitHub integration or Connect your GitHub first, whichever the page shows.

New pull requests are then reviewed as before. To get a review on one that is already open, someone with write access can comment @claude review.

Tip: disable this comment in your organization's Code Review settings.

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.

2 participants