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'
4+ import { sprintf } from 'sprintf-js'
105
116import { useHandler } from '../../hooks/useHandler'
127import { lstrings } from '../../locales/strings'
8+ import { getExchangeDenom } from '../../selectors/DenominationSelectors'
139import { useState } from '../../types/reactHooks'
1410import { useSelector } from '../../types/reactRedux'
1511import type { EdgeAppSceneProps , NavigationBase } from '../../types/routerTypes'
12+ import { getCurrencyCode } from '../../util/CurrencyInfoHelpers'
1613import { getWalletName } from '../../util/CurrencyWalletHelpers'
1714import {
15+ fetchHoudiniPrivateQuote ,
1816 HOUDINI_DESTINATION_ASSETS ,
17+ HOUDINI_DESTINATION_EDGE_ASSETS ,
1918 type HoudiniDestinationAsset ,
19+ isAssetDisabled ,
2020 isValidHoudiniDestination
2121} from '../../util/houdiniPrivateSend'
22+ import { zeroString } from '../../util/utils'
2223import { ButtonsView } from '../buttons/ButtonsView'
2324import { EdgeCard } from '../cards/EdgeCard'
2425import { SceneWrapper } from '../common/SceneWrapper'
2526import { SectionHeader } from '../common/SectionHeader'
2627import { ConfirmContinueModal } from '../modals/ConfirmContinueModal'
27- import { RadioListModal } from '../modals/RadioListModal'
2828import { TextInputModal } from '../modals/TextInputModal'
2929import {
3030 WalletListModal ,
@@ -37,26 +37,10 @@ import { EdgeText } from '../themed/EdgeText'
3737interface Props extends EdgeAppSceneProps < 'houdiniPrivateSend' > { }
3838
3939/**
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.
40+ * A Houdini private send: pick a funded source wallet, pick a destination asset
41+ * (both via the shared `WalletListModal`), paste a destination address, get a
42+ * live private quote, then create the exchange order and broadcast the on-chain
43+ * deposit through core's swap-to-address path.
6044 */
6145export const HoudiniPrivateSendScene : React . FC < Props > = props => {
6246 const { navigation } = props
@@ -67,6 +51,12 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
6751 const currencyWallets = useSelector (
6852 state => state . core . account . currencyWallets
6953 )
54+ const disablePlugins = useSelector (
55+ state => state . ui . exchangeInfo . swap . disablePlugins
56+ )
57+ const disableAssets = useSelector (
58+ state => state . ui . exchangeInfo . swap . disableAssets
59+ )
7060
7161 const [ fromWalletId , setFromWalletId ] = useState < string | undefined > (
7262 undefined
@@ -85,6 +75,7 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
8575 fromWalletId != null ? currencyWallets [ fromWalletId ] : undefined
8676
8777 const handlePickSource = useHandler ( async ( ) => {
78+ if ( pending ) return
8879 const result = await Airship . show < WalletListResult > ( bridge => (
8980 < WalletListModal
9081 bridge = { bridge }
@@ -103,25 +94,25 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
10394 } )
10495
10596 const handlePickDestAsset = useHandler ( async ( ) => {
106- const selected = await Airship . show < string | undefined > ( bridge => (
107- < RadioListModal
97+ if ( pending ) return
98+ // Reuse the shared wallet picker, filtered to the chains Houdini can
99+ // privately route to, so the destination chain is chosen with the same
100+ // control as the source rather than a bespoke picker.
101+ const result = await Airship . show < WalletListResult > ( bridge => (
102+ < WalletListModal
108103 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- }
104+ // eslint-disable-next-line @typescript-eslint/no-deprecated
105+ navigation = { navigation as NavigationBase }
106+ headerTitle = { lstrings . houdini_ps_select_dest_asset }
107+ allowedAssets = { HOUDINI_DESTINATION_EDGE_ASSETS }
108+ showCreateWallet
119109 />
120110 ) )
121- if ( selected == null ) return
111+ if ( result ?. type !== 'wallet' ) return
112+ const selectedWallet = currencyWallets [ result . walletId ]
113+ if ( selectedWallet == null ) return
122114 const asset = HOUDINI_DESTINATION_ASSETS . find (
123- candidate =>
124- `${ candidate . displayName } (${ candidate . currencyCode } )` === selected
115+ candidate => candidate . pluginId === selectedWallet . currencyInfo . pluginId
125116 )
126117 if ( asset != null ) {
127118 setDestAsset ( asset )
@@ -131,6 +122,7 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
131122 } )
132123
133124 const handleEnterAddress = useHandler ( async ( ) => {
125+ if ( pending ) return
134126 if ( destAsset == null ) {
135127 showError ( lstrings . houdini_ps_pick_dest_asset_first )
136128 return
@@ -158,6 +150,7 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
158150 } )
159151
160152 const handleEnterAmount = useHandler ( async ( ) => {
153+ if ( pending ) return
161154 if ( fromWallet == null ) {
162155 showError ( lstrings . houdini_ps_pick_source_first )
163156 return
@@ -183,56 +176,77 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
183176 fromWallet == null ||
184177 destAsset == null ||
185178 toAddress == null ||
186- displayAmount == null
179+ displayAmount == null ||
180+ zeroString ( displayAmount )
187181 ) {
188182 showError ( lstrings . houdini_ps_missing_fields )
189183 return
190184 }
185+
186+ // Honor the exchange-info asset disables, matching the swap flow.
187+ if (
188+ isAssetDisabled (
189+ disableAssets . source ,
190+ fromWallet . currencyInfo . pluginId ,
191+ fromTokenId
192+ )
193+ ) {
194+ showError (
195+ sprintf (
196+ lstrings . swap_token_no_enabled_exchanges_2s ,
197+ getCurrencyCode ( fromWallet , fromTokenId ) ,
198+ fromWallet . currencyInfo . displayName
199+ )
200+ )
201+ return
202+ }
203+ if (
204+ isAssetDisabled (
205+ disableAssets . destination ,
206+ destAsset . pluginId ,
207+ destAsset . tokenId
208+ )
209+ ) {
210+ showError (
211+ sprintf (
212+ lstrings . swap_token_no_enabled_exchanges_2s ,
213+ destAsset . currencyCode ,
214+ destAsset . displayName
215+ )
216+ )
217+ return
218+ }
219+
191220 setPending ( true )
192221 try {
193- const fromMultiplier = getPrimaryMultiplier (
222+ const fromMultiplier = getExchangeDenom (
194223 fromWallet . currencyConfig ,
195224 fromTokenId
196- )
225+ ) . multiplier
197226 const nativeAmount = round ( mul ( displayAmount , fromMultiplier ) , 0 )
198227
199- const toAddressInfo : EdgeSwapToAddressInfo = {
200- toPluginId : destAsset . pluginId ,
201- toAddress
228+ // Don't let the user reach confirm/approve with more than they hold.
229+ const balance = fromWallet . balanceMap . get ( fromTokenId ) ?? '0'
230+ if ( gt ( nativeAmount , balance ) ) {
231+ showError ( lstrings . exchange_insufficient_funds_below_balance )
232+ return
202233 }
203- const request : EdgeSwapRequest = {
234+
235+ const quote = await fetchHoudiniPrivateQuote ( account , {
204236 fromWallet,
205237 fromTokenId,
238+ toPluginId : destAsset . pluginId ,
206239 toTokenId : destAsset . tokenId ,
207- toAddressInfo ,
240+ toAddress ,
208241 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
242+ disablePlugins
223243 } )
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- }
233244
234245 const toConfig = account . currencyConfig [ destAsset . pluginId ]
235- const toMultiplier = getPrimaryMultiplier ( toConfig , destAsset . tokenId )
246+ const toMultiplier = getExchangeDenom (
247+ toConfig ,
248+ destAsset . tokenId
249+ ) . multiplier
236250 const fromDisplay = div ( quote . fromNativeAmount , fromMultiplier , 8 )
237251 const toDisplay = div ( quote . toNativeAmount , toMultiplier , 8 )
238252
0 commit comments