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
@@ -103,25 +93,24 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
10393 } )
10494
10595 const handlePickDestAsset = useHandler ( async ( ) => {
106- const selected = await Airship . show < string | undefined > ( bridge => (
107- < RadioListModal
96+ // Reuse the shared wallet picker, filtered to the chains Houdini can
97+ // privately route to, so the destination chain is chosen with the same
98+ // control as the source rather than a bespoke picker.
99+ const result = await Airship . show < WalletListResult > ( bridge => (
100+ < WalletListModal
108101 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- }
102+ // eslint-disable-next-line @typescript-eslint/no-deprecated
103+ navigation = { navigation as NavigationBase }
104+ headerTitle = { lstrings . houdini_ps_select_dest_asset }
105+ allowedAssets = { HOUDINI_DESTINATION_EDGE_ASSETS }
106+ showCreateWallet
119107 />
120108 ) )
121- if ( selected == null ) return
109+ if ( result ?. type !== 'wallet' ) return
110+ const selectedWallet = currencyWallets [ result . walletId ]
111+ if ( selectedWallet == null ) return
122112 const asset = HOUDINI_DESTINATION_ASSETS . find (
123- candidate =>
124- `${ candidate . displayName } (${ candidate . currencyCode } )` === selected
113+ candidate => candidate . pluginId === selectedWallet . currencyInfo . pluginId
125114 )
126115 if ( asset != null ) {
127116 setDestAsset ( asset )
@@ -183,56 +172,77 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
183172 fromWallet == null ||
184173 destAsset == null ||
185174 toAddress == null ||
186- displayAmount == null
175+ displayAmount == null ||
176+ zeroString ( displayAmount )
187177 ) {
188178 showError ( lstrings . houdini_ps_missing_fields )
189179 return
190180 }
181+
182+ // Honor the exchange-info asset disables, matching the swap flow.
183+ if (
184+ isAssetDisabled (
185+ disableAssets . source ,
186+ fromWallet . currencyInfo . pluginId ,
187+ fromTokenId
188+ )
189+ ) {
190+ showError (
191+ sprintf (
192+ lstrings . swap_token_no_enabled_exchanges_2s ,
193+ getCurrencyCode ( fromWallet , fromTokenId ) ,
194+ fromWallet . currencyInfo . displayName
195+ )
196+ )
197+ return
198+ }
199+ if (
200+ isAssetDisabled (
201+ disableAssets . destination ,
202+ destAsset . pluginId ,
203+ destAsset . tokenId
204+ )
205+ ) {
206+ showError (
207+ sprintf (
208+ lstrings . swap_token_no_enabled_exchanges_2s ,
209+ destAsset . currencyCode ,
210+ destAsset . displayName
211+ )
212+ )
213+ return
214+ }
215+
191216 setPending ( true )
192217 try {
193- const fromMultiplier = getPrimaryMultiplier (
218+ const fromMultiplier = getExchangeDenom (
194219 fromWallet . currencyConfig ,
195220 fromTokenId
196- )
221+ ) . multiplier
197222 const nativeAmount = round ( mul ( displayAmount , fromMultiplier ) , 0 )
198223
199- const toAddressInfo : EdgeSwapToAddressInfo = {
200- toPluginId : destAsset . pluginId ,
201- toAddress
224+ // Don't let the user reach confirm/approve with more than they hold.
225+ const balance = fromWallet . balanceMap . get ( fromTokenId ) ?? '0'
226+ if ( gt ( nativeAmount , balance ) ) {
227+ showError ( lstrings . exchange_insufficient_funds_below_balance )
228+ return
202229 }
203- const request : EdgeSwapRequest = {
230+
231+ const quote = await fetchHoudiniPrivateQuote ( account , {
204232 fromWallet,
205233 fromTokenId,
234+ toPluginId : destAsset . pluginId ,
206235 toTokenId : destAsset . tokenId ,
207- toAddressInfo ,
236+ toAddress ,
208237 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
238+ disablePlugins
223239 } )
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- }
233240
234241 const toConfig = account . currencyConfig [ destAsset . pluginId ]
235- const toMultiplier = getPrimaryMultiplier ( toConfig , destAsset . tokenId )
242+ const toMultiplier = getExchangeDenom (
243+ toConfig ,
244+ destAsset . tokenId
245+ ) . multiplier
236246 const fromDisplay = div ( quote . fromNativeAmount , fromMultiplier , 8 )
237247 const toDisplay = div ( quote . toNativeAmount , toMultiplier , 8 )
238248
0 commit comments