1- import { div , mul , round } from 'biggystring'
2- import type {
3- EdgeCurrencyConfig ,
4- EdgeSwapQuote ,
5- EdgeSwapRequest ,
6- EdgeSwapToAddressInfo ,
7- EdgeTokenId
8- } from 'edge-core-js'
1+ import { div , gt , mul , round } from 'biggystring'
2+ import type { EdgeTokenId } from 'edge-core-js'
93import * as React from 'react'
104
115import { useHandler } from '../../hooks/useHandler'
126import { lstrings } from '../../locales/strings'
7+ import { getExchangeDenom } from '../../selectors/DenominationSelectors'
138import { useState } from '../../types/reactHooks'
149import { useSelector } from '../../types/reactRedux'
1510import type { EdgeAppSceneProps , NavigationBase } from '../../types/routerTypes'
1611import { getWalletName } from '../../util/CurrencyWalletHelpers'
1712import {
13+ fetchHoudiniPrivateQuote ,
1814 HOUDINI_DESTINATION_ASSETS ,
15+ HOUDINI_DESTINATION_EDGE_ASSETS ,
1916 type HoudiniDestinationAsset ,
2017 isValidHoudiniDestination
2118} from '../../util/houdiniPrivateSend'
@@ -24,7 +21,6 @@ import { EdgeCard } from '../cards/EdgeCard'
2421import { SceneWrapper } from '../common/SceneWrapper'
2522import { SectionHeader } from '../common/SectionHeader'
2623import { ConfirmContinueModal } from '../modals/ConfirmContinueModal'
27- import { RadioListModal } from '../modals/RadioListModal'
2824import { TextInputModal } from '../modals/TextInputModal'
2925import {
3026 WalletListModal ,
@@ -37,26 +33,10 @@ import { EdgeText } from '../themed/EdgeText'
3733interface Props extends EdgeAppSceneProps < 'houdiniPrivateSend' > { }
3834
3935/**
40- * The primary-unit multiplier for an asset, used to convert a user-entered
41- * display amount to and from a native (atomic) amount.
42- */
43- function getPrimaryMultiplier (
44- currencyConfig : EdgeCurrencyConfig ,
45- tokenId : EdgeTokenId
46- ) : string {
47- const { allTokens, currencyInfo } = currencyConfig
48- const denominations =
49- tokenId == null
50- ? currencyInfo . denominations
51- : allTokens [ tokenId ] ?. denominations ?? currencyInfo . denominations
52- return denominations [ 0 ] . multiplier
53- }
54-
55- /**
56- * A minimal prototype flow for a Houdini private send: pick a funded source
57- * wallet, pick a destination asset from the supported set, paste a destination
58- * address, get a live private quote, then create the exchange order and
59- * broadcast the on-chain deposit through core's swap-to-address path.
36+ * A Houdini private send: pick a funded source wallet, pick a destination asset
37+ * (both via the shared `WalletListModal`), paste a destination address, get a
38+ * live private quote, then create the exchange order and broadcast the on-chain
39+ * deposit through core's swap-to-address path.
6040 */
6141export const HoudiniPrivateSendScene : React . FC < Props > = props => {
6242 const { navigation } = props
@@ -67,6 +47,9 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
6747 const currencyWallets = useSelector (
6848 state => state . core . account . currencyWallets
6949 )
50+ const disablePlugins = useSelector (
51+ state => state . ui . exchangeInfo . swap . disablePlugins
52+ )
7053
7154 const [ fromWalletId , setFromWalletId ] = useState < string | undefined > (
7255 undefined
@@ -103,25 +86,24 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
10386 } )
10487
10588 const handlePickDestAsset = useHandler ( async ( ) => {
106- const selected = await Airship . show < string | undefined > ( bridge => (
107- < RadioListModal
89+ // Reuse the shared wallet picker, filtered to the chains Houdini can
90+ // privately route to, so the destination chain is chosen with the same
91+ // control as the source rather than a bespoke picker.
92+ const result = await Airship . show < WalletListResult > ( bridge => (
93+ < WalletListModal
10894 bridge = { bridge }
109- title = { lstrings . houdini_ps_select_dest_asset }
110- items = { HOUDINI_DESTINATION_ASSETS . map ( asset => ( {
111- icon : '' ,
112- name : `${ asset . displayName } (${ asset . currencyCode } )`
113- } ) ) }
114- selected = {
115- destAsset == null
116- ? undefined
117- : `${ destAsset . displayName } (${ destAsset . currencyCode } )`
118- }
95+ // eslint-disable-next-line @typescript-eslint/no-deprecated
96+ navigation = { navigation as NavigationBase }
97+ headerTitle = { lstrings . houdini_ps_select_dest_asset }
98+ allowedAssets = { HOUDINI_DESTINATION_EDGE_ASSETS }
99+ showCreateWallet
119100 />
120101 ) )
121- if ( selected == null ) return
102+ if ( result ?. type !== 'wallet' ) return
103+ const selectedWallet = currencyWallets [ result . walletId ]
104+ if ( selectedWallet == null ) return
122105 const asset = HOUDINI_DESTINATION_ASSETS . find (
123- candidate =>
124- `${ candidate . displayName } (${ candidate . currencyCode } )` === selected
106+ candidate => candidate . pluginId === selectedWallet . currencyInfo . pluginId
125107 )
126108 if ( asset != null ) {
127109 setDestAsset ( asset )
@@ -190,49 +172,34 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
190172 }
191173 setPending ( true )
192174 try {
193- const fromMultiplier = getPrimaryMultiplier (
175+ const fromMultiplier = getExchangeDenom (
194176 fromWallet . currencyConfig ,
195177 fromTokenId
196- )
178+ ) . multiplier
197179 const nativeAmount = round ( mul ( displayAmount , fromMultiplier ) , 0 )
198180
199- const toAddressInfo : EdgeSwapToAddressInfo = {
200- toPluginId : destAsset . pluginId ,
201- toAddress
181+ // Don't let the user reach confirm/approve with more than they hold.
182+ const balance = fromWallet . balanceMap . get ( fromTokenId ) ?? '0'
183+ if ( gt ( nativeAmount , balance ) ) {
184+ showError ( lstrings . exchange_insufficient_funds_below_balance )
185+ return
202186 }
203- const request : EdgeSwapRequest = {
187+
188+ const quote = await fetchHoudiniPrivateQuote ( account , {
204189 fromWallet,
205190 fromTokenId,
191+ toPluginId : destAsset . pluginId ,
206192 toTokenId : destAsset . tokenId ,
207- toAddressInfo ,
193+ toAddress ,
208194 nativeAmount,
209- quoteFor : 'from'
210- }
211-
212- // Restrict the prototype to Houdini: a swap-to-address request would
213- // otherwise fan out to every central provider, creating junk orders and
214- // burning their quotas.
215- const disabled : Record < string , true > = { }
216- for ( const pluginId of Object . keys ( account . swapConfig ) ) {
217- if ( pluginId !== 'houdini' ) disabled [ pluginId ] = true
218- }
219-
220- const quotes = await account . fetchSwapQuotes ( request , {
221- preferPluginId : 'houdini' ,
222- disabled
195+ disablePlugins
223196 } )
224- // Houdini-only by design: never fall back to another provider's quote, or
225- // the swap-to-address deposit could be routed through the wrong plugin.
226- const quote : EdgeSwapQuote | undefined = quotes . find (
227- candidate => candidate . pluginId === 'houdini'
228- )
229- if ( quote == null ) {
230- showError ( lstrings . houdini_ps_no_quote )
231- return
232- }
233197
234198 const toConfig = account . currencyConfig [ destAsset . pluginId ]
235- const toMultiplier = getPrimaryMultiplier ( toConfig , destAsset . tokenId )
199+ const toMultiplier = getExchangeDenom (
200+ toConfig ,
201+ destAsset . tokenId
202+ ) . multiplier
236203 const fromDisplay = div ( quote . fromNativeAmount , fromMultiplier , 8 )
237204 const toDisplay = div ( quote . toNativeAmount , toMultiplier , 8 )
238205
0 commit comments