Skip to content

Commit c204165

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 c204165

3 files changed

Lines changed: 125 additions & 75 deletions

File tree

src/components/scenes/HoudiniPrivateSendScene.tsx

Lines changed: 26 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
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'
@@ -15,7 +9,10 @@ import { useSelector } from '../../types/reactRedux'
159
import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
1610
import { getWalletName } from '../../util/CurrencyWalletHelpers'
1711
import {
12+
fetchHoudiniPrivateQuote,
13+
getPrimaryMultiplier,
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)
@@ -196,40 +175,14 @@ export const HoudiniPrivateSendScene: React.FC<Props> = props => {
196175
)
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]
235188
const toMultiplier = getPrimaryMultiplier(toConfig, destAsset.tokenId)

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: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import type { EdgeTokenId } from 'edge-core-js'
1+
import type {
2+
EdgeAccount,
3+
EdgeCurrencyConfig,
4+
EdgeCurrencyWallet,
5+
EdgeSwapQuote,
6+
EdgeSwapRequest,
7+
EdgeSwapToAddressInfo,
8+
EdgeTokenId
9+
} from 'edge-core-js'
10+
11+
import { lstrings } from '../locales/strings'
12+
import type { EdgeAsset } from '../types/types'
213

314
/**
415
* A destination asset Houdini can privately route a swap to, paired with the
@@ -64,7 +75,7 @@ export const HOUDINI_DESTINATION_ASSETS: HoudiniDestinationAsset[] = [
6475
tokenId: null,
6576
currencyCode: 'DASH',
6677
displayName: 'Dash',
67-
addressValidation: /^[X|7][0-9A-Za-z]{33}$/
78+
addressValidation: /^[X7][0-9A-Za-z]{33}$/
6879
},
6980
{
7081
pluginId: 'solana',
@@ -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,76 @@ export function isValidHoudiniDestination(
127148
): boolean {
128149
return asset.addressValidation.test(address.trim())
129150
}
151+
152+
/**
153+
* The primary-unit multiplier for an asset, used to convert a user-entered
154+
* display amount to and from a native (atomic) amount.
155+
*/
156+
export function getPrimaryMultiplier(
157+
currencyConfig: EdgeCurrencyConfig,
158+
tokenId: EdgeTokenId
159+
): string {
160+
const { allTokens, currencyInfo } = currencyConfig
161+
const denominations =
162+
tokenId == null
163+
? currencyInfo.denominations
164+
: allTokens[tokenId]?.denominations ?? currencyInfo.denominations
165+
return denominations[0].multiplier
166+
}
167+
168+
export interface HoudiniPrivateQuoteParams {
169+
fromWallet: EdgeCurrencyWallet
170+
fromTokenId: EdgeTokenId
171+
toPluginId: string
172+
toTokenId: EdgeTokenId
173+
toAddress: string
174+
nativeAmount: string
175+
}
176+
177+
/**
178+
* Build a Houdini swap-to-address request and fetch a Houdini-only private
179+
* quote. Restricting the request to Houdini keeps a swap-to-address quote from
180+
* fanning out to every central provider (which would create junk orders and
181+
* burn their quotas) and guarantees the on-chain deposit is routed through the
182+
* private path rather than another provider's.
183+
*/
184+
export async function fetchHoudiniPrivateQuote(
185+
account: EdgeAccount,
186+
params: HoudiniPrivateQuoteParams
187+
): Promise<EdgeSwapQuote> {
188+
const {
189+
fromWallet,
190+
fromTokenId,
191+
toPluginId,
192+
toTokenId,
193+
toAddress,
194+
nativeAmount
195+
} = params
196+
197+
// `toAddressInfo` carries only what the request does not already hold: the
198+
// address itself and the destination plugin (the token is on the request).
199+
const toAddressInfo: EdgeSwapToAddressInfo = { toPluginId, toAddress }
200+
const request: EdgeSwapRequest = {
201+
fromWallet,
202+
fromTokenId,
203+
toTokenId,
204+
toAddressInfo,
205+
nativeAmount,
206+
quoteFor: 'from'
207+
}
208+
209+
const disabled: Record<string, true> = {}
210+
for (const pluginId of Object.keys(account.swapConfig)) {
211+
if (pluginId !== 'houdini') disabled[pluginId] = true
212+
}
213+
214+
const quotes = await account.fetchSwapQuotes(request, {
215+
preferPluginId: 'houdini',
216+
disabled
217+
})
218+
const quote = quotes.find(candidate => candidate.pluginId === 'houdini')
219+
if (quote == null) {
220+
throw new Error(lstrings.houdini_ps_no_quote)
221+
}
222+
return quote
223+
}

0 commit comments

Comments
 (0)