From ae3cd1dfe90fbdef35c0210c07dfa847b1e5c9bf Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Wed, 1 Jul 2026 14:43:18 -0700 Subject: [PATCH 1/3] Add wallet Sign Message flow for CEX ownership verification Adds a Sign Message option to the wallet list menu for Bitcoin-family (UTXO) wallets. The new scene shows the wallet's receive address, lets the user paste an exchange-provided message, signs it with the wallet key, and returns a copyable signature. This lets users prove control of a self-hosted wallet for CEX/CASP withdrawal checks (EU Travel Rule) without manual ID/selfie verification. --- CHANGELOG.md | 1 + src/actions/WalletListMenuActions.tsx | 9 + src/components/Main.tsx | 9 + src/components/modals/WalletListMenuModal.tsx | 29 ++++ src/components/scenes/SignMessageScene.tsx | 155 ++++++++++++++++++ src/locales/en_US.ts | 13 ++ src/locales/strings/enUS.json | 10 ++ src/types/routerTypes.tsx | 2 + 8 files changed, 228 insertions(+) create mode 100644 src/components/scenes/SignMessageScene.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cebfd37327..7656b8e18ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased (develop) +- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message. - 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. - changed: Reorganize the wallet list menu so Asset Settings is reached through Wallet Settings, and rename the Monero "Backend" card to "Server Settings". - 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. diff --git a/src/actions/WalletListMenuActions.tsx b/src/actions/WalletListMenuActions.tsx index 41347254263..aa49dca469b 100644 --- a/src/actions/WalletListMenuActions.tsx +++ b/src/actions/WalletListMenuActions.tsx @@ -40,6 +40,7 @@ export type WalletListMenuKey = | 'exportWalletTransactions' | 'getSeed' | 'manageTokens' + | 'signMessage' | 'viewXPub' | 'goToParent' | 'getRawKeys' @@ -76,6 +77,14 @@ export function walletListMenuAction( } } + case 'signMessage': { + return async (dispatch, getState) => { + navigation.navigate('signMessage', { + walletId + }) + } + } + case 'rawDelete': { return async (dispatch, getState) => { const state = getState() diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 1d67d8131e3..90b6c02013b 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -140,6 +140,7 @@ import { ReviewTriggerTestScene } from './scenes/ReviewTriggerTestScene' import { SecurityAlertsScene as SecurityAlertsSceneComponent } from './scenes/SecurityAlertsScene' import { SendScene2 as SendScene2Component } from './scenes/SendScene2' import { SettingsScene as SettingsSceneComponent } from './scenes/SettingsScene' +import { SignMessageScene as SignMessageSceneComponent } from './scenes/SignMessageScene' import { SpendingLimitsScene as SpendingLimitsSceneComponent } from './scenes/SpendingLimitsScene' import { EarnScene as EarnSceneComponent } from './scenes/Staking/EarnScene' import { StakeModifyScene as StakeModifySceneComponent } from './scenes/Staking/StakeModifyScene' @@ -283,6 +284,7 @@ const SecurityAlertsScene = ifLoggedIn(SecurityAlertsSceneComponent) const SellScene = ifLoggedIn(SellSceneComponent) const SendScene2 = ifLoggedIn(SendScene2Component) const SettingsScene = ifLoggedIn(SettingsSceneComponent) +const SignMessageScene = ifLoggedIn(SignMessageSceneComponent) const SpendingLimitsScene = ifLoggedIn(SpendingLimitsSceneComponent) const StakeModifyScene = ifLoggedIn(StakeModifySceneComponent) const StakeOptionsScene = ifLoggedIn(StakeOptionsSceneComponent) @@ -1101,6 +1103,13 @@ const EdgeAppStack: React.FC = () => { title: lstrings.title_settings }} /> + = { goToParent: 'upcircleo', manageTokens: 'plus', rawDelete: 'warning', + signMessage: 'edit', walletSettings: 'control-panel-settings', resync: 'sync', split: 'arrowsalt', @@ -115,6 +116,34 @@ export const WALLET_LIST_MENU: Array<{ label: lstrings.fragment_wallets_view_xpub, value: 'viewXPub' }, + { + pluginIds: [ + 'badcoin', + 'bitcoin', + 'bitcoincash', + 'bitcoincashtestnet', + 'bitcoingold', + 'bitcoingoldtestnet', + 'bitcoinsv', + 'bitcointestnet', + 'bitcointestnet4', + 'dash', + 'digibyte', + 'dogecoin', + 'eboost', + 'feathercoin', + 'groestlcoin', + 'litecoin', + 'qtum', + 'ravencoin', + 'smartcash', + 'ufo', + 'vertcoin', + 'zcoin' + ], + label: lstrings.fragment_wallets_sign_message, + value: 'signMessage' + }, { pluginIds: ['monero', 'piratechain', 'zcash', 'zano'], label: lstrings.fragment_wallets_view_private_view_key, diff --git a/src/components/scenes/SignMessageScene.tsx b/src/components/scenes/SignMessageScene.tsx new file mode 100644 index 00000000000..f6020c8c35d --- /dev/null +++ b/src/components/scenes/SignMessageScene.tsx @@ -0,0 +1,155 @@ +import { useQuery } from '@tanstack/react-query' +import type { EdgeCurrencyWallet } from 'edge-core-js' +import * as React from 'react' +import { View } from 'react-native' + +import { useHandler } from '../../hooks/useHandler' +import { lstrings } from '../../locales/strings' +import type { EdgeAppSceneProps } from '../../types/routerTypes' +import { SceneButtons } from '../buttons/SceneButtons' +import { EdgeCard } from '../cards/EdgeCard' +import { SceneWrapper } from '../common/SceneWrapper' +import { withWallet } from '../hoc/withWallet' +import { EdgeRow } from '../rows/EdgeRow' +import { showError } from '../services/AirshipInstance' +import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' +import { Paragraph, SmallText } from '../themed/EdgeText' +import { FilledTextInput } from '../themed/FilledTextInput' + +export interface SignMessageParams { + walletId: string +} + +interface Props extends EdgeAppSceneProps<'signMessage'> { + wallet: EdgeCurrencyWallet +} + +/** + * Lets a user sign an arbitrary message with an address they control, to prove + * self-hosted wallet ownership when withdrawing from a CEX/CASP (EU Travel + * Rule). BTC-first: the menu entry only appears for UTXO wallets whose plugin + * implements message signing. + */ +const SignMessageSceneComponent: React.FC = props => { + const { wallet } = props + + const theme = useTheme() + const styles = getStyles(theme) + + const [message, setMessage] = React.useState('') + const [signature, setSignature] = React.useState('') + const [isSigning, setIsSigning] = React.useState(false) + + // The signing address must be one the wallet owns, so we default to the + // wallet's own receive address rather than accepting an arbitrary one. + // Prefer the native segwit address (the canonical receive address the user + // hands the exchange), matching `segwitAddress ?? publicAddress` used + // elsewhere; the `publicAddress` type is the wrapped/legacy variant. + const { data: publicAddress, error: addressError } = useQuery({ + queryKey: ['signMessageAddress', wallet.id], + queryFn: async () => { + const addresses = await wallet.getAddresses({ tokenId: null }) + const receiveAddress = + addresses.find(address => address.addressType === 'segwitAddress') ?? + addresses.find(address => address.addressType === 'publicAddress') ?? + addresses[0] + if (receiveAddress == null) { + throw new Error(lstrings.sign_message_no_address_error) + } + return receiveAddress.publicAddress + } + }) + + React.useEffect(() => { + if (addressError != null) showError(addressError) + }, [addressError]) + + // Clear any prior signature when the message changes, so a stale signature + // that no longer matches the message can never be copied. + const handleChangeMessage = useHandler((text: string) => { + setMessage(text) + setSignature('') + }) + + const handleSign = useHandler(async () => { + if (publicAddress == null) { + showError(lstrings.sign_message_no_address_error) + return + } + setIsSigning(true) + try { + // `signMessage` signs the literal UTF-8 message, which is what exchanges + // verify. `signBytes` would base64-re-encode the bytes before signing and + // produce a signature over the wrong data, so it is not usable here. + // eslint-disable-next-line @typescript-eslint/no-deprecated + const signedMessage = await wallet.signMessage(message, { + otherParams: { publicAddress } + }) + setSignature(signedMessage) + } catch (error: unknown) { + showError(error) + } finally { + setIsSigning(false) + } + }) + + return ( + + + {lstrings.sign_message_instructions} + + + + + + + + {signature !== '' && ( + + + + )} + + + {lstrings.sign_message_safety_note} + + + + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + padding: theme.rem(0.5) + } +})) + +export const SignMessageScene = withWallet(SignMessageSceneComponent) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 49926310b8e..b6065f36072 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -293,6 +293,19 @@ const strings = { fragment_wallets_view_private_view_key: 'Private View Key', fragment_wallets_view_private_view_key_warning_s: `The private view key allows the receiver to see the balance in your %1$s wallet. Do not share this key unless necessary, such as for tax purposes, accounting, or similar reasons.`, fragment_wallets_view_xpub: 'View XPub Address', + fragment_wallets_sign_message: 'Sign Message', + sign_message_title: 'Sign Message', + sign_message_instructions: + 'Some exchanges ask you to prove you control this wallet by signing a message they provide. Paste the exact message below and sign it with your wallet address, then copy the signature back to the exchange.', + sign_message_address_label: 'Wallet Address', + sign_message_input_label: 'Message to Sign', + sign_message_input_placeholder: 'Paste the message from the exchange', + sign_message_sign_button: 'Sign Message', + sign_message_signature_label: 'Signature', + sign_message_safety_note: + 'Only sign messages from a service you trust. A signature proves you control this address but never reveals your private keys.', + sign_message_no_address_error: + 'Unable to load a wallet address to sign with.', fragment_wallets_pubkey_copied_title: 'XPub Address Copied', fragment_wallets_export_transactions: 'Export Transactions', fragment_wallets_rename_wallet: 'Rename Wallet', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index fe138434cfc..6240ad0460e 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -195,6 +195,16 @@ "fragment_wallets_view_private_view_key": "Private View Key", "fragment_wallets_view_private_view_key_warning_s": "The private view key allows the receiver to see the balance in your %1$s wallet. Do not share this key unless necessary, such as for tax purposes, accounting, or similar reasons.", "fragment_wallets_view_xpub": "View XPub Address", + "fragment_wallets_sign_message": "Sign Message", + "sign_message_title": "Sign Message", + "sign_message_instructions": "Some exchanges ask you to prove you control this wallet by signing a message they provide. Paste the exact message below and sign it with your wallet address, then copy the signature back to the exchange.", + "sign_message_address_label": "Wallet Address", + "sign_message_input_label": "Message to Sign", + "sign_message_input_placeholder": "Paste the message from the exchange", + "sign_message_sign_button": "Sign Message", + "sign_message_signature_label": "Signature", + "sign_message_safety_note": "Only sign messages from a service you trust. A signature proves you control this address but never reveals your private keys.", + "sign_message_no_address_error": "Unable to load a wallet address to sign with.", "fragment_wallets_pubkey_copied_title": "XPub Address Copied", "fragment_wallets_export_transactions": "Export Transactions", "fragment_wallets_rename_wallet": "Rename Wallet", diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index d7d8ec55964..0656673799e 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -58,6 +58,7 @@ import type { RampPendingParams } from '../components/scenes/RampPendingScene' import type { RampSelectOptionParams } from '../components/scenes/RampSelectOptionScene' import type { RequestParams } from '../components/scenes/RequestScene' import type { SendScene2Params } from '../components/scenes/SendScene2' +import type { SignMessageParams } from '../components/scenes/SignMessageScene' import type { EarnSceneParams } from '../components/scenes/Staking/EarnScene' import type { StakeModifyParams } from '../components/scenes/Staking/StakeModifyScene' import type { StakeOptionsParams } from '../components/scenes/Staking/StakeOptionsScene' @@ -236,6 +237,7 @@ export type EdgeAppStackParamList = {} & { send2: SendScene2Params settingsOverview: undefined settingsOverviewTab: undefined + signMessage: SignMessageParams spendingLimits: undefined stakeModify: StakeModifyParams stakeOptions: StakeOptionsParams From d59ad8d6f3a945d61c4412f4b0b8ed2a570c9849 Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Wed, 1 Jul 2026 14:54:01 -0700 Subject: [PATCH 2/3] test: add missing testIDs for maestro selectors Add signMessageInput and signMessageButton testIDs to the Sign Message scene so UI automation can drive the message field and sign action by a stable selector. --- src/components/scenes/SignMessageScene.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/scenes/SignMessageScene.tsx b/src/components/scenes/SignMessageScene.tsx index f6020c8c35d..e08b011e7ef 100644 --- a/src/components/scenes/SignMessageScene.tsx +++ b/src/components/scenes/SignMessageScene.tsx @@ -115,6 +115,7 @@ const SignMessageSceneComponent: React.FC = props => { multiline numberOfLines={4} placeholder={lstrings.sign_message_input_placeholder} + testID="signMessageInput" value={message} onChangeText={handleChangeMessage} /> @@ -138,7 +139,8 @@ const SignMessageSceneComponent: React.FC = props => { label: lstrings.sign_message_sign_button, onPress: handleSign, disabled: message === '' || publicAddress == null, - spinner: isSigning + spinner: isSigning, + testID: 'signMessageButton' }} /> From 2a248af6a91ed57c533976e8b67fe760df6104fd Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Thu, 2 Jul 2026 17:34:37 -0700 Subject: [PATCH 3/3] Let users choose the signing address in the Sign Message flow The Sign Message scene now shows the wallet's receive address in an editable field instead of a read-only row. Exchanges typically ask a user to prove control of the specific address they already provided (often a previously-used one), so the user can replace the default with that address. The wallet must control whichever address is entered; the plugin signs with the key derived from that address's stored derivation path and rejects any address it does not own, surfaced as a clear error. Editing the address clears any prior signature, and a Use default address link restores the auto-detected receive address. --- CHANGELOG.md | 2 +- src/components/scenes/SignMessageScene.tsx | 106 +++++++++++++++++---- src/locales/en_US.ts | 8 +- src/locales/strings/enUS.json | 6 +- 4 files changed, 99 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7656b8e18ba..647087ec9ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased (develop) -- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message. +- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message from a chosen wallet address (defaulting to the current receive address, or a specific address the user enters). - 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. - changed: Reorganize the wallet list menu so Asset Settings is reached through Wallet Settings, and rename the Monero "Backend" card to "Server Settings". - 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. diff --git a/src/components/scenes/SignMessageScene.tsx b/src/components/scenes/SignMessageScene.tsx index e08b011e7ef..9070cd46bbe 100644 --- a/src/components/scenes/SignMessageScene.tsx +++ b/src/components/scenes/SignMessageScene.tsx @@ -8,12 +8,13 @@ import { lstrings } from '../../locales/strings' import type { EdgeAppSceneProps } from '../../types/routerTypes' import { SceneButtons } from '../buttons/SceneButtons' import { EdgeCard } from '../cards/EdgeCard' +import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' import { EdgeRow } from '../rows/EdgeRow' import { showError } from '../services/AirshipInstance' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' -import { Paragraph, SmallText } from '../themed/EdgeText' +import { EdgeText, Paragraph, SmallText } from '../themed/EdgeText' import { FilledTextInput } from '../themed/FilledTextInput' export interface SignMessageParams { @@ -29,6 +30,13 @@ interface Props extends EdgeAppSceneProps<'signMessage'> { * self-hosted wallet ownership when withdrawing from a CEX/CASP (EU Travel * Rule). BTC-first: the menu entry only appears for UTXO wallets whose plugin * implements message signing. + * + * The signing address defaults to the wallet's current receive address, but is + * editable: an exchange usually asks the user to prove control of the specific + * address they already provided (often a previously-used one), so the user can + * replace the default with that address. The wallet must control whichever + * address is entered; the plugin signs with the key derived from that address's + * stored derivation path, and rejects any address it does not own. */ const SignMessageSceneComponent: React.FC = props => { const { wallet } = props @@ -36,16 +44,17 @@ const SignMessageSceneComponent: React.FC = props => { const theme = useTheme() const styles = getStyles(theme) + const [address, setAddress] = React.useState('') + const [addressTouched, setAddressTouched] = React.useState(false) const [message, setMessage] = React.useState('') const [signature, setSignature] = React.useState('') const [isSigning, setIsSigning] = React.useState(false) - // The signing address must be one the wallet owns, so we default to the - // wallet's own receive address rather than accepting an arbitrary one. - // Prefer the native segwit address (the canonical receive address the user - // hands the exchange), matching `segwitAddress ?? publicAddress` used - // elsewhere; the `publicAddress` type is the wrapped/legacy variant. - const { data: publicAddress, error: addressError } = useQuery({ + // Default to the wallet's own receive address. Prefer the native segwit + // address (the canonical receive address the user hands the exchange), + // matching `segwitAddress ?? publicAddress` used elsewhere; the + // `publicAddress` type is the wrapped/legacy variant. + const { data: defaultAddress, error: addressError } = useQuery({ queryKey: ['signMessageAddress', wallet.id], queryFn: async () => { const addresses = await wallet.getAddresses({ tokenId: null }) @@ -64,15 +73,33 @@ const SignMessageSceneComponent: React.FC = props => { if (addressError != null) showError(addressError) }, [addressError]) - // Clear any prior signature when the message changes, so a stale signature - // that no longer matches the message can never be copied. + // Seed the editable address with the default once it loads, unless the user + // has already typed their own address. + React.useEffect(() => { + if (!addressTouched && defaultAddress != null) setAddress(defaultAddress) + }, [addressTouched, defaultAddress]) + + // A signature is bound to both the message and the address, so clear it + // whenever either changes to prevent copying a stale signature. + const handleChangeAddress = useHandler((text: string) => { + setAddressTouched(true) + setAddress(text.trim()) + setSignature('') + }) + + const handleUseDefaultAddress = useHandler(() => { + setAddressTouched(false) + if (defaultAddress != null) setAddress(defaultAddress) + setSignature('') + }) + const handleChangeMessage = useHandler((text: string) => { setMessage(text) setSignature('') }) const handleSign = useHandler(async () => { - if (publicAddress == null) { + if (address === '') { showError(lstrings.sign_message_no_address_error) return } @@ -83,28 +110,58 @@ const SignMessageSceneComponent: React.FC = props => { // produce a signature over the wrong data, so it is not usable here. // eslint-disable-next-line @typescript-eslint/no-deprecated const signedMessage = await wallet.signMessage(message, { - otherParams: { publicAddress } + otherParams: { publicAddress: address } }) setSignature(signedMessage) } catch (error: unknown) { - showError(error) + // The plugin throws when the wallet does not own the address (or it is + // malformed). Surface a clear, actionable message for that common case. + if ( + error instanceof Error && + /data-layer address|scriptPubkey|invalid/i.test(error.message) + ) { + showError(lstrings.sign_message_address_not_owned_error) + } else { + showError(error) + } } finally { setIsSigning(false) } }) + const showUseDefault = + defaultAddress != null && address !== defaultAddress && !isSigning + return ( {lstrings.sign_message_instructions} - - - + + + {lstrings.sign_message_address_helper} + + {showUseDefault ? ( + + + {lstrings.sign_message_use_default_address} + + + ) : null} = props => { primary={{ label: lstrings.sign_message_sign_button, onPress: handleSign, - disabled: message === '' || publicAddress == null, + disabled: message === '' || address === '', spinner: isSigning, testID: 'signMessageButton' }} @@ -151,6 +208,15 @@ const SignMessageSceneComponent: React.FC = props => { const getStyles = cacheStyles((theme: Theme) => ({ container: { padding: theme.rem(0.5) + }, + useDefault: { + alignSelf: 'flex-start', + paddingHorizontal: theme.rem(0.5), + paddingBottom: theme.rem(0.5) + }, + useDefaultText: { + color: theme.iconTappable, + fontSize: theme.rem(0.75) } })) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index b6065f36072..e4f5d2078d1 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -297,7 +297,11 @@ const strings = { sign_message_title: 'Sign Message', sign_message_instructions: 'Some exchanges ask you to prove you control this wallet by signing a message they provide. Paste the exact message below and sign it with your wallet address, then copy the signature back to the exchange.', - sign_message_address_label: 'Wallet Address', + sign_message_address_label: 'Signing Address', + sign_message_address_input_placeholder: 'Enter or paste the wallet address', + sign_message_address_helper: + 'Defaults to your current receive address. To match a specific address you already gave the exchange, enter it here. This wallet must control the address.', + sign_message_use_default_address: 'Use default address', sign_message_input_label: 'Message to Sign', sign_message_input_placeholder: 'Paste the message from the exchange', sign_message_sign_button: 'Sign Message', @@ -306,6 +310,8 @@ const strings = { 'Only sign messages from a service you trust. A signature proves you control this address but never reveals your private keys.', sign_message_no_address_error: 'Unable to load a wallet address to sign with.', + sign_message_address_not_owned_error: + 'This wallet does not control that address. Enter an address that belongs to this wallet.', fragment_wallets_pubkey_copied_title: 'XPub Address Copied', fragment_wallets_export_transactions: 'Export Transactions', fragment_wallets_rename_wallet: 'Rename Wallet', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 6240ad0460e..bfaeed8fe24 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -198,13 +198,17 @@ "fragment_wallets_sign_message": "Sign Message", "sign_message_title": "Sign Message", "sign_message_instructions": "Some exchanges ask you to prove you control this wallet by signing a message they provide. Paste the exact message below and sign it with your wallet address, then copy the signature back to the exchange.", - "sign_message_address_label": "Wallet Address", + "sign_message_address_label": "Signing Address", + "sign_message_address_input_placeholder": "Enter or paste the wallet address", + "sign_message_address_helper": "Defaults to your current receive address. To match a specific address you already gave the exchange, enter it here. This wallet must control the address.", + "sign_message_use_default_address": "Use default address", "sign_message_input_label": "Message to Sign", "sign_message_input_placeholder": "Paste the message from the exchange", "sign_message_sign_button": "Sign Message", "sign_message_signature_label": "Signature", "sign_message_safety_note": "Only sign messages from a service you trust. A signature proves you control this address but never reveals your private keys.", "sign_message_no_address_error": "Unable to load a wallet address to sign with.", + "sign_message_address_not_owned_error": "This wallet does not control that address. Enter an address that belongs to this wallet.", "fragment_wallets_pubkey_copied_title": "XPub Address Copied", "fragment_wallets_export_transactions": "Export Transactions", "fragment_wallets_rename_wallet": "Rename Wallet",