Skip to content

Commit 79ead81

Browse files
peachbitsclaude
andcommitted
Block imported Monero wallets from using the Edge LWS backend
Imported Monero wallets were created on the Edge LWS backend despite the business rule that imported wallets must use a full node (or a user-configured custom LWS) -- each watched wallet has an ongoing server-side scanning cost. The GUI seeded the backend wallet setting from the first option ('lws') on the edit-name scene, which then rode through to wallet creation. Centralize the rule as SpecialCurrencyInfo.checkImportedWalletSettings so the import flow and the Wallet Settings modal stay in sync: - tapping "Import Wallets" on the edit-name scene validates the chosen backend; when an override is required it shows a modal offering to continue with a full node or to open the asset's currency settings to configure a custom LWS server - the Wallet Settings modal rejects the same combination via the shared helper - a custom (non-Edge) LWS server is still allowed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fbe5ec9 commit 79ead81

9 files changed

Lines changed: 231 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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
})

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)