Skip to content

Commit 2dfb65b

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 2dfb65b

3 files changed

Lines changed: 117 additions & 80 deletions

File tree

src/components/scenes/HoudiniPrivateSendScene.tsx

Lines changed: 32 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { div, mul, round } from 'biggystring'
2-
import type {
3-
EdgeCurrencyConfig,
4-
EdgeSwapQuote,
5-
EdgeSwapRequest,
6-
EdgeSwapToAddressInfo,
7-
EdgeTokenId
8-
} from 'edge-core-js'
2+
import type { EdgeTokenId } from 'edge-core-js'
93
import * as React from 'react'
104

115
import { useHandler } from '../../hooks/useHandler'
126
import { lstrings } from '../../locales/strings'
7+
import { getExchangeDenom } from '../../selectors/DenominationSelectors'
138
import { useState } from '../../types/reactHooks'
149
import { useSelector } from '../../types/reactRedux'
1510
import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
1611
import { getWalletName } from '../../util/CurrencyWalletHelpers'
1712
import {
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'
2421
import { SceneWrapper } from '../common/SceneWrapper'
2522
import { SectionHeader } from '../common/SectionHeader'
2623
import { ConfirmContinueModal } from '../modals/ConfirmContinueModal'
27-
import { RadioListModal } from '../modals/RadioListModal'
2824
import { TextInputModal } from '../modals/TextInputModal'
2925
import {
3026
WalletListModal,
@@ -37,26 +33,10 @@ import { EdgeText } from '../themed/EdgeText'
3733
interface 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
*/
6141
export const HoudiniPrivateSendScene: React.FC<Props> = props => {
6242
const { navigation } = props
@@ -103,25 +83,24 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
10383
})
10484

10585
const handlePickDestAsset = useHandler(async () => {
106-
const selected = await Airship.show<string | undefined>(bridge => (
107-
<RadioListModal
86+
// Reuse the shared wallet picker, filtered to the chains Houdini can
87+
// privately route to, so the destination chain is chosen with the same
88+
// control as the source rather than a bespoke picker.
89+
const result = await Airship.show<WalletListResult>(bridge => (
90+
<WalletListModal
10891
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-
}
92+
// eslint-disable-next-line @typescript-eslint/no-deprecated
93+
navigation={navigation as NavigationBase}
94+
headerTitle={lstrings.houdini_ps_select_dest_asset}
95+
allowedAssets={HOUDINI_DESTINATION_EDGE_ASSETS}
96+
showCreateWallet
11997
/>
12098
))
121-
if (selected == null) return
99+
if (result?.type !== 'wallet') return
100+
const selectedWallet = currencyWallets[result.walletId]
101+
if (selectedWallet == null) return
122102
const asset = HOUDINI_DESTINATION_ASSETS.find(
123-
candidate =>
124-
`${candidate.displayName} (${candidate.currencyCode})` === selected
103+
candidate => candidate.pluginId === selectedWallet.currencyInfo.pluginId
125104
)
126105
if (asset != null) {
127106
setDestAsset(asset)
@@ -190,49 +169,26 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
190169
}
191170
setPending(true)
192171
try {
193-
const fromMultiplier = getPrimaryMultiplier(
172+
const fromMultiplier = getExchangeDenom(
194173
fromWallet.currencyConfig,
195174
fromTokenId
196-
)
175+
).multiplier
197176
const nativeAmount = round(mul(displayAmount, fromMultiplier), 0)
198177

199-
const toAddressInfo: EdgeSwapToAddressInfo = {
200-
toPluginId: destAsset.pluginId,
201-
toAddress
202-
}
203-
const request: EdgeSwapRequest = {
178+
const quote = await fetchHoudiniPrivateQuote(account, {
204179
fromWallet,
205180
fromTokenId,
181+
toPluginId: destAsset.pluginId,
206182
toTokenId: destAsset.tokenId,
207-
toAddressInfo,
208-
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
183+
toAddress,
184+
nativeAmount
223185
})
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-
}
233186

234187
const toConfig = account.currencyConfig[destAsset.pluginId]
235-
const toMultiplier = getPrimaryMultiplier(toConfig, destAsset.tokenId)
188+
const toMultiplier = getExchangeDenom(
189+
toConfig,
190+
destAsset.tokenId
191+
).multiplier
236192
const fromDisplay = div(quote.fromNativeAmount, fromMultiplier, 8)
237193
const toDisplay = div(quote.toNativeAmount, toMultiplier, 8)
238194

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",

src/util/houdiniPrivateSend.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import type { EdgeTokenId } from 'edge-core-js'
1+
import type {
2+
EdgeAccount,
3+
EdgeCurrencyWallet,
4+
EdgeSwapQuote,
5+
EdgeSwapRequest,
6+
EdgeSwapToAddressInfo,
7+
EdgeTokenId
8+
} from 'edge-core-js'
9+
10+
import { lstrings } from '../locales/strings'
11+
import type { EdgeAsset } from '../types/types'
212

313
/**
414
* A destination asset Houdini can privately route a swap to, paired with the
@@ -64,15 +74,16 @@ export const HOUDINI_DESTINATION_ASSETS: HoudiniDestinationAsset[] = [
6474
tokenId: null,
6575
currencyCode: 'DASH',
6676
displayName: 'Dash',
67-
addressValidation: /^[X|7][0-9A-Za-z]{33}$/
77+
addressValidation: /^[X7][0-9A-Za-z]{33}$/
6878
},
6979
{
7080
pluginId: 'solana',
7181
tokenId: null,
7282
currencyCode: 'SOL',
7383
displayName: 'Solana',
74-
addressValidation:
75-
/^[1-9A-HJ-NP-SU-Za-hj-np-su-z][1-9A-HJ-NP-Za-km-z]{31,43}$/
84+
// Base58, 32-44 chars; every position uses the full Base58 alphabet
85+
// (excludes 0, O, I, l).
86+
addressValidation: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/
7687
},
7788
{
7889
pluginId: 'tron',
@@ -118,6 +129,16 @@ export const HOUDINI_DESTINATION_ASSETS: HoudiniDestinationAsset[] = [
118129
}
119130
]
120131

132+
/**
133+
* The Houdini destination chains expressed as `EdgeAsset`s, for filtering the
134+
* shared `WalletListModal` down to the assets Houdini can privately route to.
135+
*/
136+
export const HOUDINI_DESTINATION_EDGE_ASSETS: EdgeAsset[] =
137+
HOUDINI_DESTINATION_ASSETS.map(asset => ({
138+
pluginId: asset.pluginId,
139+
tokenId: asset.tokenId
140+
}))
141+
121142
/**
122143
* Validate a pasted destination address against the asset's Houdini regex.
123144
*/
@@ -127,3 +148,60 @@ export function isValidHoudiniDestination(
127148
): boolean {
128149
return asset.addressValidation.test(address.trim())
129150
}
151+
152+
export interface HoudiniPrivateQuoteParams {
153+
fromWallet: EdgeCurrencyWallet
154+
fromTokenId: EdgeTokenId
155+
toPluginId: string
156+
toTokenId: EdgeTokenId
157+
toAddress: string
158+
nativeAmount: string
159+
}
160+
161+
/**
162+
* Build a Houdini swap-to-address request and fetch a Houdini-only private
163+
* quote. Restricting the request to Houdini keeps a swap-to-address quote from
164+
* fanning out to every central provider (which would create junk orders and
165+
* burn their quotas) and guarantees the on-chain deposit is routed through the
166+
* private path rather than another provider's.
167+
*/
168+
export async function fetchHoudiniPrivateQuote(
169+
account: EdgeAccount,
170+
params: HoudiniPrivateQuoteParams
171+
): Promise<EdgeSwapQuote> {
172+
const {
173+
fromWallet,
174+
fromTokenId,
175+
toPluginId,
176+
toTokenId,
177+
toAddress,
178+
nativeAmount
179+
} = params
180+
181+
// `toAddressInfo` carries only what the request does not already hold: the
182+
// address itself and the destination plugin (the token is on the request).
183+
const toAddressInfo: EdgeSwapToAddressInfo = { toPluginId, toAddress }
184+
const request: EdgeSwapRequest = {
185+
fromWallet,
186+
fromTokenId,
187+
toTokenId,
188+
toAddressInfo,
189+
nativeAmount,
190+
quoteFor: 'from'
191+
}
192+
193+
const disabled: Record<string, true> = {}
194+
for (const pluginId of Object.keys(account.swapConfig)) {
195+
if (pluginId !== 'houdini') disabled[pluginId] = true
196+
}
197+
198+
const quotes = await account.fetchSwapQuotes(request, {
199+
preferPluginId: 'houdini',
200+
disabled
201+
})
202+
const quote = quotes.find(candidate => candidate.pluginId === 'houdini')
203+
if (quote == null) {
204+
throw new Error(lstrings.houdini_ps_no_quote)
205+
}
206+
return quote
207+
}

0 commit comments

Comments
 (0)