Skip to content

Commit 6cd8d59

Browse files
authored
Merge pull request #6053 from EdgeApp/matthew/monero-imported-wallet-backend
Block imported Monero wallets from using the Edge LWS backend
2 parents fbe5ec9 + 48d6026 commit 6cd8d59

9 files changed

Lines changed: 239 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- added: Remote enable/disable of gift card providers via the info server's giftCardInfo config, supporting whole-provider disabling for Phaze and Bitrefill and per-brand disabling for Phaze.
66
- changed: Reorganize the wallet list menu so Asset Settings is reached through Wallet Settings, and rename the Monero "Backend" card to "Server Settings".
7+
- fixed: Prevent imported Monero wallets from using the Edge LWS backend. Choosing to import now prompts the user to continue with a full node or configure a custom LWS server, matching the wallet settings rule.
78

89
## 4.49.0 (staging)
910

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, expect, test } from '@jest/globals'
2+
3+
import { SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants'
4+
import { isMoneroEdgeLws } from '../util/monero'
5+
6+
describe('isMoneroEdgeLws', () => {
7+
test('Edge LWS when custom servers are disabled', () => {
8+
expect(
9+
isMoneroEdgeLws({
10+
enableCustomServers: false,
11+
enableCustomMonerod: false,
12+
moneroLightwalletServer: '',
13+
monerodServer: ''
14+
})
15+
).toBe(true)
16+
})
17+
18+
test('Edge LWS when the custom server points at an Edge host', () => {
19+
expect(
20+
isMoneroEdgeLws({
21+
enableCustomServers: true,
22+
enableCustomMonerod: false,
23+
moneroLightwalletServer: 'https://monerolws1.edge.app',
24+
monerodServer: ''
25+
})
26+
).toBe(true)
27+
})
28+
29+
test('custom LWS when the custom server points elsewhere', () => {
30+
expect(
31+
isMoneroEdgeLws({
32+
enableCustomServers: true,
33+
enableCustomMonerod: false,
34+
moneroLightwalletServer: 'https://my.node.example.com',
35+
monerodServer: ''
36+
})
37+
).toBe(false)
38+
})
39+
})
40+
41+
describe('monero checkImportedWalletSettings', () => {
42+
const check = SPECIAL_CURRENCY_INFO.monero.checkImportedWalletSettings
43+
if (check == null)
44+
throw new Error('monero checkImportedWalletSettings missing')
45+
46+
test('overrides lws -> monerod when using Edge LWS (default settings)', () => {
47+
const result = check(
48+
{ backend: 'lws' },
49+
{ enableCustomServers: false, moneroLightwalletServer: '' }
50+
)
51+
expect(result?.settings.backend).toBe('monerod')
52+
expect(result?.warning).toBeTruthy()
53+
})
54+
55+
test('allows lws when using a custom (non-Edge) LWS server', () => {
56+
const result = check(
57+
{ backend: 'lws' },
58+
{
59+
enableCustomServers: true,
60+
moneroLightwalletServer: 'https://my.node.example.com'
61+
}
62+
)
63+
expect(result).toBeUndefined()
64+
})
65+
66+
test('overrides lws -> monerod when the custom server is an Edge LWS host', () => {
67+
const result = check(
68+
{ backend: 'lws' },
69+
{
70+
enableCustomServers: true,
71+
moneroLightwalletServer: 'https://monerolws2.edge.app'
72+
}
73+
)
74+
expect(result?.settings.backend).toBe('monerod')
75+
})
76+
77+
test('no override when the backend is already monerod', () => {
78+
const result = check(
79+
{ backend: 'monerod' },
80+
{ enableCustomServers: false, moneroLightwalletServer: '' }
81+
)
82+
expect(result).toBeUndefined()
83+
})
84+
85+
test('treats an unset backend as needing the override under Edge LWS', () => {
86+
const result = check(
87+
{},
88+
{ enableCustomServers: false, moneroLightwalletServer: '' }
89+
)
90+
expect(result?.settings.backend).toBe('monerod')
91+
})
92+
93+
test('overrides lws -> monerod for an account with empty user settings', () => {
94+
// A never-configured account stores `{}`; it resolves to Edge LWS, so an
95+
// imported lws wallet must still be pushed to the full node.
96+
const result = check({ backend: 'lws' }, {})
97+
expect(result?.settings.backend).toBe('monerod')
98+
expect(result?.warning).toBeTruthy()
99+
})
100+
})

src/components/modals/WalletSettingsModal.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
} from '../../constants/WalletAndCurrencyConstants'
1212
import { useHandler } from '../../hooks/useHandler'
1313
import { lstrings } from '../../locales/strings'
14+
import { asMoneroUserSettings } from '../../util/monero'
1415
import { ModalButtons } from '../buttons/ModalButtons'
1516
import { EdgeCard } from '../cards/EdgeCard'
1617
import { showError } from '../services/AirshipInstance'
1718
import { SettingsHeaderRow } from '../settings/SettingsHeaderRow'
1819
import { SettingsRadioRow } from '../settings/SettingsRadioRow'
1920
import { SettingsTappableRow } from '../settings/SettingsTappableRow'
2021
import { ModalFilledTextInput } from '../themed/FilledTextInput'
21-
import { asMoneroUserSettings } from '../themed/MaybeMoneroUserSettings'
2222
import { EdgeModal } from './EdgeModal'
2323

2424
export interface WalletSettingsResult {
@@ -112,23 +112,19 @@ export const WalletSettingsModal: React.FC<Props> = props => {
112112
await wallet.renameWallet(result.name)
113113
}
114114
if (Object.keys(result.settings).length > 0) {
115-
/*
116-
Special case for Monero. Do not allow imported wallets to user Edge LWS server
117-
*/
118-
if (pluginId === 'monero') {
119-
const { enableCustomServers, moneroLightwalletServer } =
120-
asMoneroUserSettings(wallet?.currencyConfig.userSettings)
121-
if (
122-
wallet.imported &&
123-
result.settings.backend === 'lws' &&
124-
(!enableCustomServers ||
125-
/^monerolws\d+\.edge\.app$/i.test(
126-
new URL(moneroLightwalletServer).hostname
127-
))
128-
) {
129-
throw new Error(
130-
lstrings.settings_monero_edge_lws_imported_wallet_error
131-
)
115+
// Reject backend selections that are invalid for imported wallets
116+
// (e.g. Monero forbids imported wallets from using Edge's LWS server).
117+
// The rule lives in SPECIAL_CURRENCY_INFO so it stays in sync with the
118+
// wallet-import flow.
119+
if (wallet.imported) {
120+
const override = SPECIAL_CURRENCY_INFO[
121+
pluginId
122+
]?.checkImportedWalletSettings?.(
123+
result.settings,
124+
wallet.currencyConfig.userSettings ?? {}
125+
)
126+
if (override != null) {
127+
throw new Error(override.warning)
132128
}
133129
}
134130

src/components/scenes/CreateWalletEditNameScene.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,62 @@ const CreateWalletEditNameComponent: React.FC<Props> = props => {
273273
return
274274
}
275275

276+
// Validate the chosen backend for imported wallets. Some currencies (e.g.
277+
// Monero) forbid imported wallets from using Edge's own servers. If an
278+
// override is required, ask the user to either configure a custom server or
279+
// continue with the full node before letting them into the import scene.
280+
let importSettingValues = walletSettingValues
281+
const overrideItems = newWalletItemsCopy.flatMap(item => {
282+
const check =
283+
SPECIAL_CURRENCY_INFO[item.pluginId]?.checkImportedWalletSettings
284+
if (check == null) return []
285+
const override = check(
286+
walletSettingValues[item.key] ?? {},
287+
account.currencyConfig[item.pluginId]?.userSettings ?? {}
288+
)
289+
return override == null ? [] : [{ item, settings: override.settings }]
290+
})
291+
292+
if (overrideItems.length > 0) {
293+
const { pluginId } = overrideItems[0].item
294+
const proceed = await Airship.show<
295+
'useFullNode' | 'moreSettings' | undefined
296+
>(bridge => (
297+
<ButtonsModal
298+
bridge={bridge}
299+
title={lstrings.create_wallet_imported_backend_title}
300+
message={lstrings.create_wallet_imported_backend_message}
301+
buttons={{
302+
useFullNode: {
303+
label: lstrings.create_wallet_imported_backend_use_full_node
304+
},
305+
moreSettings: {
306+
label: lstrings.create_wallet_imported_backend_more_settings,
307+
onPress: async () => {
308+
navigation.navigate('currencySettings', {
309+
currencyInfo: account.currencyConfig[pluginId]?.currencyInfo
310+
})
311+
return true
312+
}
313+
}
314+
}}
315+
/>
316+
))
317+
// Only proceed when the user explicitly chooses the full node. Choosing
318+
// "More Settings" navigates to the asset's currency settings instead.
319+
if (proceed !== 'useFullNode') return
320+
321+
importSettingValues = { ...walletSettingValues }
322+
for (const { item, settings } of overrideItems) {
323+
importSettingValues[item.key] = settings
324+
}
325+
setWalletSettingValues(importSettingValues)
326+
}
327+
276328
navigation.navigate('createWalletImport', {
277329
createWalletList: [...newWalletItemsCopy, ...newTokenItems],
278330
walletNames,
279-
walletSettingValues
331+
walletSettingValues: importSettingValues
280332
})
281333
})
282334

src/components/themed/MaybeMoneroUserSettings.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { asBoolean, asObject, asOptional, asString } from 'cleaners'
21
import * as React from 'react'
32

43
import { useHandler } from '../../hooks/useHandler'
54
import { useWatch } from '../../hooks/useWatch'
65
import { lstrings } from '../../locales/strings'
76
import { useSelector } from '../../types/reactRedux'
87
import { logActivity } from '../../util/logger'
8+
import {
9+
asMoneroUserSettings,
10+
type MoneroUserSettings
11+
} from '../../util/monero'
912
import { EdgeCard } from '../cards/EdgeCard'
1013
import {
1114
type CurrencySettingProps,
@@ -17,14 +20,6 @@ import { SettingsHeaderRow } from '../settings/SettingsHeaderRow'
1720
import { SettingsRadioRow } from '../settings/SettingsRadioRow'
1821
import { SettingsSubHeader } from '../settings/SettingsSubHeader'
1922

20-
export const asMoneroUserSettings = asObject({
21-
enableCustomServers: asBoolean,
22-
enableCustomMonerod: asOptional(asBoolean, false),
23-
moneroLightwalletServer: asString,
24-
monerodServer: asOptional(asString, '')
25-
})
26-
type MoneroUserSettings = ReturnType<typeof asMoneroUserSettings>
27-
2823
type Props = CurrencySettingProps<MoneroUserSettings, undefined>
2924

3025
const MoneroUserSettingsComponent: React.FC<Props> = props => {

src/constants/WalletAndCurrencyConstants.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { gte } from 'biggystring'
2+
import { asMaybe } from 'cleaners'
3+
import type { JsonObject } from 'edge-core-js'
24
import { Platform } from 'react-native'
35

46
import { lstrings } from '../locales/strings'
57
import type { WalletConnectChainId } from '../types/types'
8+
import { asMoneroUserSettings, isMoneroEdgeLws } from '../util/monero'
69
import { removeIsoPrefix } from '../util/utils'
710

811
export const MAX_TOKEN_CODE_CHARACTERS = 7
@@ -208,6 +211,19 @@ interface SpecialCurrencyInfo {
208211
importKeyOptions?: ImportKeyOption[]
209212
defaultImportedWalletSettings?: Record<string, string>
210213
walletSettings?: WalletSetting[]
214+
/**
215+
* Some currencies restrict which backend an *imported* wallet may use.
216+
* Given a candidate set of wallet settings and the plugin's account-wide
217+
* userSettings, this returns the corrected settings plus a warning to
218+
* surface when an override is required, or undefined when the candidate
219+
* settings are allowed as-is. Centralizes the rule so the import flow (to
220+
* default + warn) and the Wallet Settings modal (to reject an invalid manual
221+
* selection) stay in sync.
222+
*/
223+
checkImportedWalletSettings?: (
224+
settings: Record<string, string>,
225+
userSettings: JsonObject
226+
) => { settings: Record<string, string>; warning: string } | undefined
211227

212228
// Flags that could move to EdgeCurrencyInfo:
213229
allowZeroTx?: boolean
@@ -401,7 +417,20 @@ export const SPECIAL_CURRENCY_INFO: Record<string, SpecialCurrencyInfo> = {
401417
inputValidation: (input: string) => /^\d+$/.test(input)
402418
}
403419
],
404-
defaultImportedWalletSettings: { backend: 'monerod' },
420+
checkImportedWalletSettings: (settings, userSettings) => {
421+
// Imported Monero wallets may not use Edge's own LWS server (each watched
422+
// wallet has an ongoing server-side scanning cost). A user-configured
423+
// custom LWS is allowed; otherwise fall back to the full node (monerod).
424+
// An unset backend defaults to 'lws' in the engine, so treat anything
425+
// that isn't an explicit 'monerod' as a candidate for the override.
426+
if (settings.backend === 'monerod') return undefined
427+
const monero = asMaybe(asMoneroUserSettings)(userSettings)
428+
if (monero != null && !isMoneroEdgeLws(monero)) return undefined
429+
return {
430+
settings: { ...settings, backend: 'monerod' },
431+
warning: lstrings.settings_monero_edge_lws_imported_wallet_error
432+
}
433+
},
405434
walletSettings: [
406435
{
407436
optionName: 'backend',

src/locales/en_US.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ const strings = {
332332
create_wallet_import_options_passphrase: 'Seed passphrase',
333333
create_wallet_import_options_passphrase_description:
334334
'A passphrase is an optional extra word or phrase you add to your recovery seed.',
335+
create_wallet_imported_backend_title: 'Choose A Different Server',
336+
create_wallet_imported_backend_message:
337+
"Edge LWS isn't supported for imported wallets. You can use a full node or a custom LWS server.",
338+
create_wallet_imported_backend_use_full_node: 'Use Full Node',
339+
create_wallet_imported_backend_more_settings: 'More Settings',
335340
create_wallet_imports_title: 'Import Wallets',
336341
create_wallet_import_all_instructions:
337342
'Enter your private seed, private key, or active key to verify and restore the associated wallet',

src/locales/strings/enUS.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@
223223
"create_wallet_import_options_birthday_height_description": "The birthday height is the network block height that your wallet will start synchronizing from.",
224224
"create_wallet_import_options_passphrase": "Seed passphrase",
225225
"create_wallet_import_options_passphrase_description": "A passphrase is an optional extra word or phrase you add to your recovery seed.",
226+
"create_wallet_imported_backend_title": "Choose A Different Server",
227+
"create_wallet_imported_backend_message": "Edge LWS isn't supported for imported wallets. You can use a full node or a custom LWS server.",
228+
"create_wallet_imported_backend_use_full_node": "Use Full Node",
229+
"create_wallet_imported_backend_more_settings": "More Settings",
226230
"create_wallet_imports_title": "Import Wallets",
227231
"create_wallet_import_all_instructions": "Enter your private seed, private key, or active key to verify and restore the associated wallet",
228232
"create_wallet_import_instructions": "Enter your private seed to verify and restore the associated wallet",

src/util/monero.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { asBoolean, asObject, asOptional, asString } from 'cleaners'
2+
3+
export const asMoneroUserSettings = asObject({
4+
enableCustomServers: asBoolean,
5+
enableCustomMonerod: asOptional(asBoolean, false),
6+
moneroLightwalletServer: asString,
7+
monerodServer: asOptional(asString, '')
8+
})
9+
export type MoneroUserSettings = ReturnType<typeof asMoneroUserSettings>
10+
11+
/**
12+
* Imported Monero wallets are not allowed to use Edge's own LWS server: each
13+
* watched wallet incurs an ongoing address-scanning cost on the server side.
14+
* A user-configured *custom* LWS server is fine. Returns true when the account
15+
* is currently pointed at an Edge-operated LWS server.
16+
*/
17+
export const isMoneroEdgeLws = (userSettings: MoneroUserSettings): boolean => {
18+
const { enableCustomServers, moneroLightwalletServer } = userSettings
19+
if (!enableCustomServers) return true
20+
try {
21+
return /^monerolws\d+\.edge\.app$/i.test(
22+
new URL(moneroLightwalletServer).hostname
23+
)
24+
} catch {
25+
// An unparseable custom URL is treated as a custom (non-Edge) server.
26+
return false
27+
}
28+
}

0 commit comments

Comments
 (0)