Skip to content

Commit 0b75c6f

Browse files
committed
Reuse WalletListModal as the Houdini private send picker
Extract the Houdini-only swap-to-address quote construction into a shared fetchHoudiniPrivateQuote helper and drive the private send destination selection through the shared WalletListModal (filtered to Houdini's supported chains) instead of a bespoke asset picker.
1 parent 2205f69 commit 0b75c6f

3 files changed

Lines changed: 215 additions & 80 deletions

File tree

src/components/scenes/HoudiniPrivateSendScene.tsx

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
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'
93
import * as React from 'react'
4+
import { sprintf } from 'sprintf-js'
105

116
import { useHandler } from '../../hooks/useHandler'
127
import { lstrings } from '../../locales/strings'
8+
import { getExchangeDenom } from '../../selectors/DenominationSelectors'
139
import { useState } from '../../types/reactHooks'
1410
import { useSelector } from '../../types/reactRedux'
1511
import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
12+
import { getCurrencyCode } from '../../util/CurrencyInfoHelpers'
1613
import { getWalletName } from '../../util/CurrencyWalletHelpers'
1714
import {
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'
2223
import { ButtonsView } from '../buttons/ButtonsView'
2324
import { EdgeCard } from '../cards/EdgeCard'
2425
import { SceneWrapper } from '../common/SceneWrapper'
2526
import { SectionHeader } from '../common/SectionHeader'
2627
import { ConfirmContinueModal } from '../modals/ConfirmContinueModal'
27-
import { RadioListModal } from '../modals/RadioListModal'
2828
import { TextInputModal } from '../modals/TextInputModal'
2929
import {
3030
WalletListModal,
@@ -37,26 +37,10 @@ import { EdgeText } from '../themed/EdgeText'
3737
interface 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
*/
6145
export 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

src/locales/strings/enUS.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,9 @@
14461446
"houdini_ps_missing_fields": "Select a source wallet, destination asset, address, and amount first",
14471447
"houdini_ps_pick_dest_asset_first": "Select a destination asset first",
14481448
"houdini_ps_pick_source_first": "Select a source wallet first",
1449+
"houdini_swap_private_label": "Private swap",
1450+
"houdini_swap_from_amount_only": "Private swaps quote from the send amount only",
1451+
"houdini_swap_no_dest_address": "Could not get a destination address for the selected wallet",
14491452
"deposit_to_bank": "Deposit to Bank",
14501453
"your_wallets": "Your Wallets",
14511454
"pause_wallet_toast": "This wallet will no longer synchronize with the blockchain and will not detect new transactions or balance changes",

0 commit comments

Comments
 (0)