From 549b222bd72800e42c0e9b24224210cc1f65d8d0 Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Fri, 6 Nov 2026 14:41:23 -0800 Subject: [PATCH 1/3] Record sends to every name service The list of names this account has paid now covers every name service, keyed by service and lower-cased name, instead of FIO handles only. The FIO-only list stays readable, so sends recorded before this change still count. --- .../components/AddressTile2.test.tsx | 12 +- src/__tests__/util/FioSentAddresses.test.ts | 122 ----------- src/__tests__/util/sentNames.test.ts | 190 ++++++++++++++++++ src/components/scenes/SendScene2.tsx | 43 ++-- src/components/tiles/AddressTile2.tsx | 29 ++- src/util/FioAddressUtils.ts | 110 +--------- src/util/sentNames.ts | 162 +++++++++++++++ 7 files changed, 406 insertions(+), 262 deletions(-) delete mode 100644 src/__tests__/util/FioSentAddresses.test.ts create mode 100644 src/__tests__/util/sentNames.test.ts create mode 100644 src/util/sentNames.ts diff --git a/src/__tests__/components/AddressTile2.test.tsx b/src/__tests__/components/AddressTile2.test.tsx index ba441454dbd..4962ff0ad22 100644 --- a/src/__tests__/components/AddressTile2.test.tsx +++ b/src/__tests__/components/AddressTile2.test.tsx @@ -261,7 +261,11 @@ describe('AddressTile2 FIO address warning', () => { expect(onChangeAddress).toHaveBeenCalledTimes(1) expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ fioAddress: HANDLE, - fioPublicAddress: RESOLVED_ADDRESS, + acceptedName: { + service: 'fio', + name: HANDLE, + publicAddress: RESOLVED_ADDRESS + }, parsedUri: { publicAddress: RESOLVED_ADDRESS } }) }) @@ -335,7 +339,11 @@ describe('AddressTile2 FIO address warning', () => { ) expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ fioAddress: HANDLE, - fioPublicAddress: ENS_ADDRESS, + acceptedName: { + service: 'fio', + name: HANDLE, + publicAddress: ENS_ADDRESS + }, parsedUri: { publicAddress: ENS_ADDRESS } }) }) diff --git a/src/__tests__/util/FioSentAddresses.test.ts b/src/__tests__/util/FioSentAddresses.test.ts deleted file mode 100644 index e2111aea58a..00000000000 --- a/src/__tests__/util/FioSentAddresses.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, it } from '@jest/globals' -import { makeMemoryDisklet } from 'disklet' -import type { EdgeAccount, EdgeSpendTarget } from 'edge-core-js' - -import { - addToFioAddressCache, - addToFioSentAddresses, - type FioSentAddress, - getAcceptedFioSends, - hasSentToFioAddress, - type TileFioRecipient -} from '../../util/FioAddressUtils' - -const ALICE_ADDRESS = '0x00000000000000000000000000000000000000aa' -const ALICE_CHECKSUMMED = '0x00000000000000000000000000000000000000AA' -const OTHER_ADDRESS = '0x00000000000000000000000000000000000000cc' - -const makeAccount = (): EdgeAccount => - ({ disklet: makeMemoryDisklet() } as unknown as EdgeAccount) - -describe('FIO sent addresses', () => { - it('starts empty', async () => { - expect( - await hasSentToFioAddress(makeAccount(), 'alice@edge', ALICE_ADDRESS) - ).toBe(false) - }) - - it('matches a recorded send ignoring the handle case', async () => { - const account = makeAccount() - await addToFioSentAddresses(account, [ - { fioAddress: 'Alice@Edge', publicAddress: ALICE_ADDRESS } - ]) - expect( - await hasSentToFioAddress(account, 'alice@edge', ALICE_ADDRESS) - ).toBe(true) - expect( - await hasSentToFioAddress(account, 'ALICE@EDGE', ALICE_ADDRESS) - ).toBe(true) - }) - - it('does not match once the handle resolves somewhere else', async () => { - const account = makeAccount() - await addToFioSentAddresses(account, [ - { fioAddress: 'alice@edge', publicAddress: ALICE_ADDRESS } - ]) - expect( - await hasSentToFioAddress(account, 'alice@edge', OTHER_ADDRESS) - ).toBe(false) - }) - - it('keeps every address a handle was sent to', async () => { - const account = makeAccount() - await addToFioSentAddresses(account, [ - { fioAddress: 'alice@edge', publicAddress: ALICE_ADDRESS } - ]) - await addToFioSentAddresses(account, [ - { fioAddress: 'alice@edge', publicAddress: OTHER_ADDRESS }, - { fioAddress: 'alice@edge', publicAddress: ALICE_ADDRESS } - ]) - expect( - await hasSentToFioAddress(account, 'alice@edge', ALICE_ADDRESS) - ).toBe(true) - expect( - await hasSentToFioAddress(account, 'alice@edge', OTHER_ADDRESS) - ).toBe(true) - }) - - it('ignores handles written to the FIO address cache', async () => { - const account = makeAccount() - await addToFioAddressCache(account, ['scammer@thorchain']) - expect( - await hasSentToFioAddress(account, 'scammer@thorchain', ALICE_ADDRESS) - ).toBe(false) - }) -}) - -describe('getAcceptedFioSends', () => { - const accepted: FioSentAddress = { - fioAddress: 'alice@edge', - publicAddress: ALICE_ADDRESS - } - - const tileRecipient = (paidAddress: string): TileFioRecipient => ({ - paidAddress, - send: accepted - }) - - it('returns a handle the address tile resolved for the paid address', () => { - const target: EdgeSpendTarget = { publicAddress: ALICE_ADDRESS } - const tileRecipients = new WeakMap([[target, tileRecipient(ALICE_ADDRESS)]]) - expect(getAcceptedFioSends([target], tileRecipients)).toEqual([accepted]) - }) - - it('records the resolved address when parsing normalized the paid one', async () => { - const target: EdgeSpendTarget = { publicAddress: ALICE_CHECKSUMMED } - const tileRecipients = new WeakMap([ - [target, tileRecipient(ALICE_CHECKSUMMED)] - ]) - const account = makeAccount() - await addToFioSentAddresses( - account, - getAcceptedFioSends([target], tileRecipients) - ) - expect( - await hasSentToFioAddress(account, 'alice@edge', ALICE_ADDRESS) - ).toBe(true) - }) - - it('ignores a handle and address the caller put in otherParams', () => { - const target: EdgeSpendTarget = { - publicAddress: ALICE_ADDRESS, - otherParams: { fioAddress: 'alice@edge', fioPublicAddress: ALICE_ADDRESS } - } - expect(getAcceptedFioSends([target], new WeakMap())).toEqual([]) - }) - - it('skips a target that no longer pays the address the tile gave it', () => { - const target: EdgeSpendTarget = { publicAddress: OTHER_ADDRESS } - const tileRecipients = new WeakMap([[target, tileRecipient(ALICE_ADDRESS)]]) - expect(getAcceptedFioSends([target], tileRecipients)).toEqual([]) - }) -}) diff --git a/src/__tests__/util/sentNames.test.ts b/src/__tests__/util/sentNames.test.ts new file mode 100644 index 00000000000..a0dffffd96d --- /dev/null +++ b/src/__tests__/util/sentNames.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, it } from '@jest/globals' +import { makeMemoryDisklet } from 'disklet' +import type { EdgeAccount, EdgeSpendTarget } from 'edge-core-js' + +import { addToFioAddressCache } from '../../util/FioAddressUtils' +import { + addToSentNames, + getAcceptedNameSends, + hasSentToName, + type NameSend, + type RecipientName, + type TileNameRecipient +} from '../../util/sentNames' + +const ALICE_ADDRESS = '0x00000000000000000000000000000000000000aa' +const ALICE_CHECKSUMMED = '0x00000000000000000000000000000000000000AA' +const OTHER_ADDRESS = '0x00000000000000000000000000000000000000cc' + +const ALICE_FIO: RecipientName = { service: 'fio', name: 'alice@edge' } +const ALICE_UD: RecipientName = { service: 'unstoppable', name: 'alice.wallet' } + +const makeAccount = (): EdgeAccount => + ({ disklet: makeMemoryDisklet() } as unknown as EdgeAccount) + +describe('sent names', () => { + it('starts empty', async () => { + expect(await hasSentToName(makeAccount(), ALICE_UD, ALICE_ADDRESS)).toBe( + false + ) + }) + + it('matches a recorded send ignoring the name case', async () => { + const account = makeAccount() + await addToSentNames(account, [ + { + service: 'unstoppable', + name: 'Alice.Wallet', + publicAddress: ALICE_ADDRESS + } + ]) + expect(await hasSentToName(account, ALICE_UD, ALICE_ADDRESS)).toBe(true) + expect( + await hasSentToName( + account, + { service: 'unstoppable', name: 'ALICE.WALLET' }, + ALICE_ADDRESS + ) + ).toBe(true) + }) + + it('does not match once the name resolves somewhere else', async () => { + const account = makeAccount() + await addToSentNames(account, [ + { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + ]) + expect(await hasSentToName(account, ALICE_UD, OTHER_ADDRESS)).toBe(false) + }) + + it('matches an EVM address in either checksum case', async () => { + const account = makeAccount() + await addToSentNames(account, [ + { ...ALICE_UD, publicAddress: ALICE_CHECKSUMMED } + ]) + expect(await hasSentToName(account, ALICE_UD, ALICE_ADDRESS)).toBe(true) + + const lowerFirst = makeAccount() + await addToSentNames(lowerFirst, [ + { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + ]) + expect(await hasSentToName(lowerFirst, ALICE_UD, ALICE_CHECKSUMMED)).toBe( + true + ) + }) + + it('matches other address formats only in the same case', async () => { + const account = makeAccount() + const zcashName: RecipientName = { service: 'zns', name: 'alice.zcash' } + await addToSentNames(account, [ + { ...zcashName, publicAddress: 't1AliceAddress' } + ]) + expect(await hasSentToName(account, zcashName, 't1AliceAddress')).toBe(true) + expect(await hasSentToName(account, zcashName, 't1aliceaddress')).toBe( + false + ) + }) + + it('keeps each service apart', async () => { + const account = makeAccount() + await addToSentNames(account, [ + { service: 'ens', name: 'alice.eth', publicAddress: ALICE_ADDRESS } + ]) + expect( + await hasSentToName( + account, + { service: 'plugin', name: 'alice.eth' }, + ALICE_ADDRESS + ) + ).toBe(false) + }) + + it('keeps every address a name was sent to', async () => { + const account = makeAccount() + await addToSentNames(account, [ + { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + ]) + await addToSentNames(account, [ + { ...ALICE_UD, publicAddress: OTHER_ADDRESS }, + { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + ]) + expect(await hasSentToName(account, ALICE_UD, ALICE_ADDRESS)).toBe(true) + expect(await hasSentToName(account, ALICE_UD, OTHER_ADDRESS)).toBe(true) + }) + + it('reads FIO sends recorded in the older FIO-only list', async () => { + const account = makeAccount() + await account.disklet.setText( + 'FioSentAddresses.json', + JSON.stringify({ addresses: { 'alice@edge': [ALICE_ADDRESS] } }) + ) + expect(await hasSentToName(account, ALICE_FIO, ALICE_ADDRESS)).toBe(true) + expect( + await hasSentToName( + account, + { service: 'zano', name: 'alice@edge' }, + ALICE_ADDRESS + ) + ).toBe(false) + + // A new send keeps the older entries: + await addToSentNames(account, [ + { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + ]) + await account.disklet.delete('FioSentAddresses.json') + expect(await hasSentToName(account, ALICE_FIO, ALICE_ADDRESS)).toBe(true) + }) + + it('ignores handles written to the FIO address cache', async () => { + const account = makeAccount() + await addToFioAddressCache(account, ['scammer@thorchain']) + expect( + await hasSentToName( + account, + { service: 'fio', name: 'scammer@thorchain' }, + ALICE_ADDRESS + ) + ).toBe(false) + }) +}) + +describe('getAcceptedNameSends', () => { + const accepted: NameSend = { ...ALICE_UD, publicAddress: ALICE_ADDRESS } + + const tileRecipient = (paidAddress: string): TileNameRecipient => ({ + paidAddress, + send: accepted + }) + + it('returns a name the address tile resolved for the paid address', () => { + const target: EdgeSpendTarget = { publicAddress: ALICE_ADDRESS } + const tileRecipients = new WeakMap([[target, tileRecipient(ALICE_ADDRESS)]]) + expect(getAcceptedNameSends([target], tileRecipients)).toEqual([accepted]) + }) + + it('records the resolved address when parsing normalized the paid one', async () => { + const target: EdgeSpendTarget = { publicAddress: ALICE_CHECKSUMMED } + const tileRecipients = new WeakMap([ + [target, tileRecipient(ALICE_CHECKSUMMED)] + ]) + const account = makeAccount() + await addToSentNames( + account, + getAcceptedNameSends([target], tileRecipients) + ) + expect(await hasSentToName(account, ALICE_UD, ALICE_ADDRESS)).toBe(true) + }) + + it('ignores a name and address the caller put in otherParams', () => { + const target: EdgeSpendTarget = { + publicAddress: ALICE_ADDRESS, + otherParams: { fioAddress: 'alice@edge', fioPublicAddress: ALICE_ADDRESS } + } + expect(getAcceptedNameSends([target], new WeakMap())).toEqual([]) + }) + + it('skips a target that no longer pays the address the tile gave it', () => { + const target: EdgeSpendTarget = { publicAddress: OTHER_ADDRESS } + const tileRecipients = new WeakMap([[target, tileRecipient(ALICE_ADDRESS)]]) + expect(getAcceptedNameSends([target], tileRecipients)).toEqual([]) + }) +}) diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 16950034b4c..06d3637dc20 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -77,14 +77,11 @@ import { import { getWalletName } from '../../util/CurrencyWalletHelpers' import { addToFioAddressCache, - addToFioSentAddresses, checkRecordSendFee, FIO_FEE_EXCEEDS_SUPPLIED_MAXIMUM, FIO_NO_BUNDLED_ERR_CODE, FioError, - getAcceptedFioSends, - recordSend, - type TileFioRecipient + recordSend } from '../../util/FioAddressUtils' import { detectHoudiniChains, @@ -124,6 +121,11 @@ import { parseOwnNetworkPayment, peekPaymentUri } from '../../util/paymentUri' +import { + addToSentNames, + getAcceptedNameSends, + type TileNameRecipient +} from '../../util/sentNames' import { disableAssetsCover, getStealthDisableAssets, @@ -298,11 +300,11 @@ const SendComponent: React.FC = props => { const needsScrollToEnd = React.useRef(false) /** - * The FIO handle the address tile resolved for each spend target, kept out - * of `otherParams`, which the scene's caller can fill. + * The name the address tile resolved for each spend target, kept out of + * `otherParams`, which the scene's caller can fill. */ - const tileFioRecipients = React.useRef( - new WeakMap() + const tileNameRecipients = React.useRef( + new WeakMap() ) const makeSpendCounter = React.useRef(0) const scrollViewRef = React.useRef(null) @@ -1012,7 +1014,7 @@ const SendComponent: React.FC = props => { addressEntryMethod, parsedUri, fioAddress, - fioPublicAddress, + acceptedName, alias, resolvedName, crossChainNativeAmount, @@ -1153,13 +1155,13 @@ const SendComponent: React.FC = props => { zanoAlias: alias, resolvedName } - if (fioAddress != null && fioPublicAddress != null) { - tileFioRecipients.current.set(spendTarget, { + if (acceptedName != null) { + tileNameRecipients.current.set(spendTarget, { paidAddress: spendTarget.publicAddress, - send: { fioAddress, publicAddress: fioPublicAddress } + send: acceptedName }) } else { - tileFioRecipients.current.delete(spendTarget) + tileNameRecipients.current.delete(spendTarget) } // We can assume the spendTarget object came from the Component spendInfo so simply resetting the spendInfo @@ -3363,13 +3365,13 @@ const SendComponent: React.FC = props => { playSendSound().catch((error: unknown) => { console.log(error) // Fail quietly }) - // The quote pays the spend target, so a handle the user accepted in + // The quote pays the spend target, so a name the user accepted in // the address tile counts as sent to, the same as a plain send: - addToFioSentAddresses( + addToSentNames( account, - getAcceptedFioSends( + getAcceptedNameSends( spendInfo.spendTargets, - tileFioRecipients.current + tileNameRecipients.current ) ).catch((error: unknown) => { console.log(error) // The swap went through either way @@ -3560,9 +3562,12 @@ const SendComponent: React.FC = props => { } } await addToFioAddressCache(account, payeeFioAddresses) - await addToFioSentAddresses( + await addToSentNames( account, - getAcceptedFioSends(spendInfo.spendTargets, tileFioRecipients.current) + getAcceptedNameSends( + spendInfo.spendTargets, + tileNameRecipients.current + ) ).catch((error: unknown) => { console.log(error) // The send went through either way }) diff --git a/src/components/tiles/AddressTile2.tsx b/src/components/tiles/AddressTile2.tsx index dd4d52d9ce4..885554012ea 100644 --- a/src/components/tiles/AddressTile2.tsx +++ b/src/components/tiles/AddressTile2.tsx @@ -30,13 +30,11 @@ import { isOwnEvmAddress } from '../../util/CurrencyInfoHelpers' import { parseDeepLink } from '../../util/DeepLinkParser' -import { - checkPubAddress, - hasSentToFioAddress -} from '../../util/FioAddressUtils' +import { checkPubAddress } from '../../util/FioAddressUtils' import { type NameService, reverseLookupName } from '../../util/nameServices' import { type CrossChainPayment, withStatedAsset } from '../../util/paymentUri' import { resolveName } from '../../util/resolveName' +import { hasSentToName, type NameSend } from '../../util/sentNames' import { isEmail } from '../../util/utils' import { isZnsName, resolveZnsName } from '../../util/zns' import { EdgeAnim } from '../common/EdgeAnim' @@ -59,11 +57,11 @@ export type AddressEntryMethod = 'scan' | 'other' export interface ChangeAddressResult { fioAddress?: string /** - * The public address `fioAddress` resolved to when the user entered it, - * after resolving any ENS name the handle points to. Only set for a handle - * resolved here, never for one a request prefilled. + * The name the user entered and accepted, with the public address it + * resolved to, after resolving any ENS name a FIO handle points to. Only + * set for a name resolved here, never for one a request prefilled. */ - fioPublicAddress?: string + acceptedName?: NameSend parsedUri?: EdgeParsedUri addressEntryMethod: AddressEntryMethod alias?: string @@ -554,7 +552,14 @@ export const AddressTile2 = React.forwardRef( // set address await onChangeAddress({ fioAddress, - fioPublicAddress, + acceptedName: + fioAddress != null && fioPublicAddress != null + ? { + service: 'fio', + name: fioAddress, + publicAddress: fioPublicAddress + } + : undefined, parsedUri, addressEntryMethod, alias: zanoAlias, @@ -908,7 +913,11 @@ const hasInteractedWithFioAddress = async ( ownFioAddresses: FioAddress[] ): Promise => ownFioAddresses.some(own => own.name.toLowerCase() === fioAddress) || - (await hasSentToFioAddress(account, fioAddress, publicAddress)) + (await hasSentToName( + account, + { service: 'fio', name: fioAddress }, + publicAddress + )) /** * True when the part of a FIO address before the @ reads like a wallet diff --git a/src/util/FioAddressUtils.ts b/src/util/FioAddressUtils.ts index 528e3ba631b..2d407e9cfbc 100644 --- a/src/util/FioAddressUtils.ts +++ b/src/util/FioAddressUtils.ts @@ -1,12 +1,5 @@ import { div } from 'biggystring' -import { - asArray, - asJSON, - asMaybe, - asNumber, - asObject, - asString -} from 'cleaners' +import { asMaybe, asNumber, asObject, asString } from 'cleaners' import type { Disklet } from 'disklet' import type { EdgeAccount, @@ -14,7 +7,6 @@ import type { EdgeCurrencyWallet, EdgeDenomination, EdgeSpendInfo, - EdgeSpendTarget, EdgeTransaction } from 'edge-core-js' import { sprintf } from 'sprintf-js' @@ -36,7 +28,6 @@ import { DECIMAL_PRECISION, truncateDecimals } from './utils' const CONNECTED_WALLETS = 'ConnectedWallets.json' const FIO_ADDRESS_CACHE = 'FioAddressCache.json' -const FIO_SENT_ADDRESSES = 'FioSentAddresses.json' const FIO_EXPIRED_CHECK = 'FioExpiredCheck.json' const MONTH = 1000 * 60 * 60 * 24 * 30 const DEFAULT_BUNDLE_SET_VALUE = 1 @@ -662,105 +653,6 @@ export const getFioAddressCache = async ( } } -/** - * FIO addresses this account has sent funds to, each mapped to the public - * addresses it resolved to at the time, keyed in lower case. Unlike the FIO - * address cache, which also records handles from incoming requests and - * received transactions, only sends whose handle the user typed or scanned - * and accepted write here, so a stranger cannot add a handle to it. - */ -const asFioSentAddresses = asJSON( - asObject({ addresses: asMaybe(asObject(asArray(asString)), {}) }) -) - -/** A send to a FIO address, with the public address the handle resolved to. */ -export interface FioSentAddress { - fioAddress: string - publicAddress: string -} - -/** - * A FIO handle the address tile resolved and the user accepted for one spend - * target. `paidAddress` is the address the tile gave the target, which can be - * a normalized form of the resolved `send.publicAddress` (an EVM checksum, - * for one). - */ -export interface TileFioRecipient { - paidAddress: string | undefined - send: FioSentAddress -} - -const getFioSentAddresses = async ( - account: EdgeAccount -): Promise> => { - try { - const text = await account.disklet.getText(FIO_SENT_ADDRESSES) - return asFioSentAddresses(text).addresses - } catch (error: unknown) { - // A missing or unreadable file means no sends are recorded yet: - return {} - } -} - -/** - * True when this account has sent to the FIO address while it resolved to - * this public address. A handle whose owner has since pointed it somewhere - * else counts as new. - */ -export const hasSentToFioAddress = async ( - account: EdgeAccount, - fioAddress: string, - publicAddress: string -): Promise => { - const sent = await getFioSentAddresses(account) - return sent[fioAddress.toLowerCase()]?.includes(publicAddress) ?? false -} - -/** - * The FIO sends a broadcast made that the user accepted in the address tile. - * `tileRecipients` holds what the tile resolved for each target; a target's - * `otherParams` are not consulted, because a caller such as an Edge Provider - * `requestSpend` can fill them with anything. A target counts only while it - * still pays the address the tile gave it. The recorded address is the one - * FIO resolved, the same form the address tile checks against later. - */ -export const getAcceptedFioSends = ( - spendTargets: EdgeSpendTarget[], - tileRecipients: WeakMap -): FioSentAddress[] => { - const sends: FioSentAddress[] = [] - for (const target of spendTargets) { - const tileRecipient = tileRecipients.get(target) - if ( - tileRecipient?.paidAddress != null && - tileRecipient.paidAddress === target.publicAddress - ) { - sends.push(tileRecipient.send) - } - } - return sends -} - -export const addToFioSentAddresses = async ( - account: EdgeAccount, - sends: FioSentAddress[] -): Promise => { - const sent = await getFioSentAddresses(account) - let changed = false - for (const { fioAddress, publicAddress } of sends) { - const key = fioAddress.toLowerCase() - const publicAddresses = sent[key] ?? [] - if (publicAddresses.includes(publicAddress)) continue - sent[key] = [...publicAddresses, publicAddress] - changed = true - } - if (!changed) return - await account.disklet.setText( - FIO_SENT_ADDRESSES, - JSON.stringify({ addresses: sent }) - ) -} - export const checkRecordSendFee = async ( fioWallet: EdgeCurrencyWallet | null, fioAddress: string diff --git a/src/util/sentNames.ts b/src/util/sentNames.ts new file mode 100644 index 00000000000..191c2b8d6b8 --- /dev/null +++ b/src/util/sentNames.ts @@ -0,0 +1,162 @@ +import { asArray, asJSON, asMaybe, asObject, asString } from 'cleaners' +import type { EdgeAccount, EdgeSpendTarget } from 'edge-core-js' + +/** + * The services that turn a name the user enters into an address. `plugin` + * covers any other name the wallet's own currency plugin resolves. + */ +export type RecipientNameService = + | 'fio' + | 'ens' + | 'unstoppable' + | 'zns' + | 'zano' + | 'plugin' + +/** A name the user typed, scanned or picked, with the service it belongs to. */ +export interface RecipientName { + service: RecipientNameService + name: string +} + +/** A send to a name, with the public address the name resolved to. */ +export interface NameSend extends RecipientName { + publicAddress: string +} + +/** + * A name the address tile resolved and the user accepted for one spend + * target. `paidAddress` is the address the tile gave the target, which can be + * a normalized form of the resolved `send.publicAddress` (an EVM checksum, + * for one). + */ +export interface TileNameRecipient { + paidAddress: string | undefined + send: NameSend +} + +const SENT_NAMES = 'SentNames.json' +const LEGACY_FIO_SENT_ADDRESSES = 'FioSentAddresses.json' + +/** + * Names this account has sent funds to, by service, each mapped to the public + * addresses it resolved to at the time, keyed in lower case. Only sends whose + * name the user entered and accepted write here, so a stranger cannot add a + * name to it through an incoming request or transaction. + */ +const asSentNames = asJSON( + asObject({ + names: asMaybe(asObject(asObject(asArray(asString))), {}) + }) +) + +/** The FIO-only list this one replaced, keyed by lower-cased FIO address. */ +const asLegacyFioSentAddresses = asJSON( + asObject({ addresses: asMaybe(asObject(asArray(asString)), {}) }) +) + +type SentNames = Record> + +const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/ + +/** + * True when both strings are the same public address. EVM addresses are + * hex, and their mixed-case checksum is optional: a name can resolve to the + * checksummed form on one path and the lower-case form on another. Other + * address formats can be case-sensitive, so they must match exactly. + */ +const isSameAddress = (known: string, address: string): boolean => + known === address || + (EVM_ADDRESS.test(known) && known.toLowerCase() === address.toLowerCase()) + +const includesAddress = (addresses: string[], address: string): boolean => + addresses.some(known => isSameAddress(known, address)) + +const readText = async ( + account: EdgeAccount, + path: string +): Promise => + await account.disklet.getText(path).catch(() => undefined) + +const getSentNames = async (account: EdgeAccount): Promise => { + const text = await readText(account, SENT_NAMES) + // A copy, since the cleaner's fallback object is shared between reads: + const sent: SentNames = { ...asMaybe(asSentNames)(text)?.names } + + // FIO sends recorded before this list covered every service: + const legacyText = await readText(account, LEGACY_FIO_SENT_ADDRESSES) + const legacy = asMaybe(asLegacyFioSentAddresses)(legacyText)?.addresses + if (legacy != null) { + const fio = { ...sent.fio } + for (const [name, publicAddresses] of Object.entries(legacy)) { + const known = fio[name] ?? [] + fio[name] = [ + ...known, + ...publicAddresses.filter(address => !includesAddress(known, address)) + ] + } + sent.fio = fio + } + return sent +} + +/** + * True when this account has sent to the name while it resolved to this + * public address. A name whose owner has since pointed it somewhere else + * counts as new. + */ +export const hasSentToName = async ( + account: EdgeAccount, + recipient: RecipientName, + publicAddress: string +): Promise => { + const sent = await getSentNames(account) + const publicAddresses = + sent[recipient.service]?.[recipient.name.toLowerCase()] + return ( + publicAddresses != null && includesAddress(publicAddresses, publicAddress) + ) +} + +/** + * The name sends a broadcast made that the user accepted in the address tile. + * `tileRecipients` holds what the tile resolved for each target; a target's + * `otherParams` are not consulted, because a caller such as an Edge Provider + * `requestSpend` can fill them with anything. A target counts only while it + * still pays the address the tile gave it. The recorded address is the one + * the name resolved to, the same form the address tile checks against later. + */ +export const getAcceptedNameSends = ( + spendTargets: EdgeSpendTarget[], + tileRecipients: WeakMap +): NameSend[] => { + const sends: NameSend[] = [] + for (const target of spendTargets) { + const tileRecipient = tileRecipients.get(target) + if ( + tileRecipient?.paidAddress != null && + tileRecipient.paidAddress === target.publicAddress + ) { + sends.push(tileRecipient.send) + } + } + return sends +} + +export const addToSentNames = async ( + account: EdgeAccount, + sends: NameSend[] +): Promise => { + const sent = await getSentNames(account) + let changed = false + for (const { service, name, publicAddress } of sends) { + const key = name.toLowerCase() + const names = sent[service] ?? {} + const publicAddresses = names[key] ?? [] + if (includesAddress(publicAddresses, publicAddress)) continue + sent[service] = { ...names, [key]: [...publicAddresses, publicAddress] } + changed = true + } + if (!changed) return + await account.disklet.setText(SENT_NAMES, JSON.stringify({ names: sent })) +} From 53478d95cd9b65b3a2b28c208fdede04603950d5 Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Fri, 6 Nov 2026 14:44:43 -0800 Subject: [PATCH 2/3] Return the resolved name from the address modal The modal now resolves with the picked address and, when it turned a name into that address, the name and its service (unstoppable, ens, zns or the wallet's own plugin). The send tile needs that to tell a name from an address. A resolution now only counts for the input that started it. Before, an address resolved for an earlier name stayed in state and was submitted for any later input the resolver did not touch. --- src/components/modals/AddressModal.tsx | 65 ++++++++++++------- .../Fio/FioRequestConfirmationScene.tsx | 45 +++++++------ src/components/scenes/RequestScene.tsx | 20 +++--- src/components/themed/SendFromFioRows.tsx | 22 ++++--- src/components/tiles/AddressTile2.tsx | 8 +-- 5 files changed, 93 insertions(+), 67 deletions(-) diff --git a/src/components/modals/AddressModal.tsx b/src/components/modals/AddressModal.tsx index 897ca2fef77..10501fe98a4 100644 --- a/src/components/modals/AddressModal.tsx +++ b/src/components/modals/AddressModal.tsx @@ -30,6 +30,7 @@ import { } from '../../util/FioAddressUtils' import { reverseLookupName } from '../../util/nameServices' import { resolveName } from '../../util/resolveName' +import type { RecipientName, RecipientNameService } from '../../util/sentNames' import { isZnsName, resolveZnsName } from '../../util/zns' import { EdgeButton } from '../buttons/EdgeButton' import { EdgeTouchableWithoutFeedback } from '../common/EdgeTouchableWithoutFeedback' @@ -44,8 +45,17 @@ import { UnscaledText } from '../text/UnscaledText' import { ModalFilledTextInput } from '../themed/FilledTextInput' import { EdgeModal } from './EdgeModal' +/** + * The address the user picked. When the modal resolved a name to that + * address, `name` holds the name and its service. + */ +export interface AddressModalResult { + address: string + name?: RecipientName +} + interface OwnProps { - bridge: AirshipBridge + bridge: AirshipBridge // eslint-disable-next-line react/no-unused-prop-types walletId: string currencyCode: string @@ -71,7 +81,8 @@ interface State { uri: string validLabel: string | undefined errorLabel: string | undefined - cryptoAddress?: string + /** The last name resolved, with the input that named it. */ + resolved?: { input: string; address: string; name: RecipientName } fioAddresses: FioAddresses filteredFioAddresses: string[] showSpinner: boolean @@ -93,7 +104,7 @@ export class AddressModalComponent extends React.Component { this.state = { uri: '', validLabel: undefined, - cryptoAddress: undefined, + resolved: undefined, errorLabel: undefined, fioAddresses: { addresses: {} }, filteredFioAddresses: [], @@ -302,6 +313,7 @@ export class AddressModalComponent extends React.Component { this.props.coreWallet, name ).catch(() => undefined) + let service: RecipientNameService = 'plugin' if ( this.checkIfUnstoppableDomain(name) && globalKeys.UNSTOPPABLE_DOMAINS_API_KEY != null @@ -313,20 +325,26 @@ export class AddressModalComponent extends React.Component { this.props.coreWallet.currencyInfo.pluginId ] ) + service = 'unstoppable' } else if (this.checkIfEnsDomain(name)) { address = await this.fetchEnsAddress(name) + service = 'ens' } else if ( this.checkIfZnsName(name) && this.props.coreWallet.currencyInfo.pluginId === 'zcash' ) { address = await this.fetchZnsAddress(name) + service = 'zns' } if (address == null) { throw new ResolutionError('UnsupportedDomain', { domain: name }) } - this.setState({ cryptoAddress: address, validLabel: address }) + this.setState({ + resolved: { input: name, address, name: { service, name } }, + validLabel: address + }) } catch (err: unknown) { - this.setState({ cryptoAddress: undefined, validLabel: undefined }) + this.setState({ resolved: undefined, validLabel: undefined }) if (err instanceof ResolutionError) { const message = sprintf(lstrings[err.code], name, currencyTicker) @@ -464,31 +482,28 @@ export class AddressModalComponent extends React.Component { } handleSubmit = (): void => { - const { uri, cryptoAddress, errorLabel } = this.state - const { coreWallet } = this.props + const { uri, resolved, errorLabel } = this.state + const { bridge, coreWallet } = this.props + if (errorLabel != null) return - let submitData = cryptoAddress ?? uri // Preserve Zano alias inputs (e.g., "@alias") so the caller can capture // and persist the alias in transaction metadata while still resolving it // before parsing the URI. - if ( - coreWallet.currencyInfo.pluginId === 'zano' && - typeof uri === 'string' && - uri.startsWith('@') - ) { - submitData = uri + const isZanoAlias = + coreWallet.currencyInfo.pluginId === 'zano' && uri.startsWith('@') + // Same idea for ZNS (.zcash) names on Zcash: return the original name so + // the caller can capture it and persist it in transaction metadata. + const isZnsInput = + coreWallet.currencyInfo.pluginId === 'zcash' && isZnsName(uri) + + // A resolution only counts for the input that started it, so text the + // user changed afterwards, or a slower lookup for earlier text, never + // swaps in an address the current input did not name: + if (!isZanoAlias && !isZnsInput && resolved?.input === uri) { + bridge.resolve({ address: resolved.address, name: resolved.name }) + return } - // Same idea for ZNS (.zcash) names on Zcash — return the original name so - // the caller can capture znsName and persist it in transaction metadata. - if ( - coreWallet.currencyInfo.pluginId === 'zcash' && - typeof uri === 'string' && - isZnsName(uri) - ) { - submitData = uri - } - if (errorLabel != null) return - this.props.bridge.resolve(submitData) + bridge.resolve({ address: uri }) } handleClose = (): void => { diff --git a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx index 0f9f91fb0a3..98931bcbb18 100644 --- a/src/components/scenes/Fio/FioRequestConfirmationScene.tsx +++ b/src/components/scenes/Fio/FioRequestConfirmationScene.tsx @@ -33,7 +33,10 @@ import { EdgeCard } from '../../cards/EdgeCard' import { SceneWrapper } from '../../common/SceneWrapper' import { withWallet } from '../../hoc/withWallet' import { SceneContainer } from '../../layout/SceneContainer' -import { AddressModal } from '../../modals/AddressModal' +import { + AddressModal, + type AddressModalResult +} from '../../modals/AddressModal' import { ButtonsModal } from '../../modals/ButtonsModal' import { TextInputModal } from '../../modals/TextInputModal' import { EdgeRow } from '../../rows/EdgeRow' @@ -302,15 +305,17 @@ export class FioRequestConfirmationConnected extends React.Component< openFioAddressFromModal = async (): Promise => { const { currencyCode, fioPlugin, wallet } = this.props const { walletAddresses } = this.state - const fioAddressFrom = await Airship.show(bridge => ( - - )) + const fioAddressFrom = ( + await Airship.show(bridge => ( + + )) + )?.address if (fioAddressFrom == null) return if (fioPlugin != null) { const accountExists = @@ -349,15 +354,17 @@ export class FioRequestConfirmationConnected extends React.Component< const { fioPlugin, wallet, currencyCode } = this.props this.setState({ settingFioAddressTo: true }) - const fioAddressTo = await Airship.show(bridge => ( - - )) + const fioAddressTo = ( + await Airship.show(bridge => ( + + )) + )?.address if (fioAddressTo == null) { this.showError() return diff --git a/src/components/scenes/RequestScene.tsx b/src/components/scenes/RequestScene.tsx index 4d2ffbca17b..0c14cff9d60 100644 --- a/src/components/scenes/RequestScene.tsx +++ b/src/components/scenes/RequestScene.tsx @@ -61,7 +61,7 @@ import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' import { ChatBubblesIcon, ChevronRightIcon } from '../icons/ThemedIcons' -import { AddressModal } from '../modals/AddressModal' +import { AddressModal, type AddressModalResult } from '../modals/AddressModal' import { ButtonsModal } from '../modals/ButtonsModal' import { WalletListModal, @@ -741,14 +741,16 @@ export class RequestSceneComponent extends React.Component< return } - const fioAddressTo = await Airship.show(bridge => ( - - )) + const fioAddressTo = ( + await Airship.show(bridge => ( + + )) + )?.address if (fioAddressTo != null) { navigation.navigate('fioRequestConfirmation', { amounts: this.state.amounts, diff --git a/src/components/themed/SendFromFioRows.tsx b/src/components/themed/SendFromFioRows.tsx index 8d0095f0812..dac057e19c1 100644 --- a/src/components/themed/SendFromFioRows.tsx +++ b/src/components/themed/SendFromFioRows.tsx @@ -14,7 +14,7 @@ import { FIO_NO_BUNDLED_ERR_CODE } from '../../util/FioAddressUtils' import { SectionView } from '../layout/SectionView' -import { AddressModal } from '../modals/AddressModal' +import { AddressModal, type AddressModalResult } from '../modals/AddressModal' import { ButtonsModal } from '../modals/ButtonsModal' import { TextInputModal } from '../modals/TextInputModal' import { EdgeRow } from '../rows/EdgeRow' @@ -157,15 +157,17 @@ export class SendFromFioRowsComponent extends React.PureComponent< selectAddress = async (): Promise => { const { currencyCode, coreWallet } = this.props - const response = await Airship.show(bridge => ( - - )) + const response = ( + await Airship.show(bridge => ( + + )) + )?.address if (response != null && response !== '') { await this.setFioAddress(response) } diff --git a/src/components/tiles/AddressTile2.tsx b/src/components/tiles/AddressTile2.tsx index 885554012ea..33b60917ad4 100644 --- a/src/components/tiles/AddressTile2.tsx +++ b/src/components/tiles/AddressTile2.tsx @@ -39,7 +39,7 @@ import { isEmail } from '../../util/utils' import { isZnsName, resolveZnsName } from '../../util/zns' import { EdgeAnim } from '../common/EdgeAnim' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' -import { AddressModal } from '../modals/AddressModal' +import { AddressModal, type AddressModalResult } from '../modals/AddressModal' import { showFullScreenSpinner } from '../modals/AirshipFullScreenSpinner' import { ConfirmContinueModal } from '../modals/ConfirmContinueModal' import { @@ -649,7 +649,7 @@ export const AddressTile2 = React.forwardRef( ) : lstrings.scan_address_modal_title - Airship.show(bridge => ( + Airship.show(bridge => ( )) .then(async result => { - if (result != null && result !== '') { - await changeAddress(result, 'other') + if (result != null && result.address !== '') { + await changeAddress(result.address, 'other') } }) .catch((error: unknown) => { From 7ce55ced2b77eef53cab5647f5dfca12c1002bad Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Fri, 6 Nov 2026 14:49:48 -0800 Subject: [PATCH 3/3] Warn before the first send to any name Names from every service (FIO, ENS, Unstoppable Domains, ZcashNames, Zano aliases, and names the wallet's own plugin resolves) now go through one gate in the address tile. It warns when this account has never paid the name at the address it resolves to now, and still skips the account's own FIO handles. The copy is service-neutral: it says the input is a name, not an address, names the service, coin and resolved address, and says a name never converts, bridges or routes funds. The FIO-only heuristic that guessed whether a handle copied an address is gone. --- CHANGELOG.md | 1 + .../components/AddressTile2.test.tsx | 435 +++++++++++++----- src/components/tiles/AddressTile2.tsx | 212 +++++---- src/locales/en_US.ts | 16 +- src/locales/strings/de.json | 9 +- src/locales/strings/enUS.json | 9 +- src/locales/strings/es.json | 9 +- src/locales/strings/esMX.json | 9 +- src/locales/strings/fr.json | 9 +- src/locales/strings/it.json | 9 +- src/locales/strings/ja.json | 9 +- src/locales/strings/kaa.json | 9 +- src/locales/strings/ko.json | 9 +- src/locales/strings/pt.json | 9 +- src/locales/strings/ru.json | 9 +- src/locales/strings/vi.json | 9 +- src/locales/strings/zh.json | 9 +- 17 files changed, 490 insertions(+), 291 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea74eb21598..04400a2576e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased (develop) +- fixed: Warn before the first send to any ENS, Unstoppable Domains, Zano or ZcashNames name, not only FIO handles - fixed: Exchange info, gift card info, Houdini chain, and Zcash migration status refreshes running twice per interval ## 4.53.0 (staging) diff --git a/src/__tests__/components/AddressTile2.test.tsx b/src/__tests__/components/AddressTile2.test.tsx index 4962ff0ad22..9033dd79da0 100644 --- a/src/__tests__/components/AddressTile2.test.tsx +++ b/src/__tests__/components/AddressTile2.test.tsx @@ -5,6 +5,10 @@ import type * as EthersModule from 'ethers' import * as React from 'react' import { sprintf } from 'sprintf-js' +import { + AddressModal, + type AddressModalResult +} from '../../components/modals/AddressModal' import { Airship } from '../../components/services/AirshipInstance' import { AddressTile2, @@ -24,6 +28,14 @@ jest.mock('../../util/nameServices', () => ({ reverseLookupName: async () => null })) +// ZcashNames resolve over the network: +const mockZnsNames: Record = {} +jest.mock('../../util/zns', () => ({ + ...jest.requireActual('../../util/zns'), + resolveZnsName: async (name: string) => + mockZnsNames[name.toLowerCase()] ?? null +})) + // A FIO handle can resolve to an ENS name, which resolves over the network: const mockEnsNames: Record = {} jest.mock('ethers', () => { @@ -126,23 +138,33 @@ describe('AddressTile2 "Myself" picker', () => { }) }) -describe('AddressTile2 FIO address warning', () => { +describe('AddressTile2 name warning', () => { const HANDLE = 'bc1qdeposit@thorchain' - const DEPOSIT = 'osmo1qxyzv9lcmrsxyhk8zwfm5snntmqz5r3g7xyzab' - const DEPOSIT_HANDLE = `${DEPOSIT}@solibc` const RESOLVED_ADDRESS = '0x00000000000000000000000000000000000000bb' const ENS_NAME = 'payee.eth' const ENS_ADDRESS = '0x00000000000000000000000000000000000000dd' + const UD_NAME = 'alice.wallet' + const UD_ADDRESS = '0x00000000000000000000000000000000000000ee' + const ZANO_ALIAS = '@alice' + const ZANO_ADDRESS = 'ZxZanoAddressForAlice' + const ZNS_NAME = 'alice.zcash' + const ZNS_ADDRESS = 'u1zcashaddressforalice' mockEnsNames[ENS_NAME] = ENS_ADDRESS + mockZnsNames[ZNS_NAME] = ZNS_ADDRESS interface EnterOptions { approved?: boolean cache?: Record input?: string + /** Picked in the address modal instead of entered through the ref. */ + modalResult?: AddressModalResult ownHandles?: string[] + pluginId?: string resolves?: boolean resolvesTo?: string + /** The FIO-only list older versions wrote. */ sent?: Record + sentNames?: Record> } const enterAddress = async ( @@ -156,14 +178,22 @@ describe('AddressTile2 FIO address warning', () => { approved = true, cache = {}, input = HANDLE, + modalResult, ownHandles = [], + pluginId = 'ethereum', resolves = true, resolvesTo = RESOLVED_ADDRESS, - sent = {} + sent = {}, + sentNames = {} } = options const wallet = { ...makeWallet('source', async () => []), + currencyInfo: { ...ethConfig.currencyInfo, pluginId }, + otherMethods: { + resolveName: async (name: string) => + name === ZANO_ALIAS ? ZANO_ADDRESS : undefined + }, parseUri: async (text: string) => ({ publicAddress: text }) } as unknown as EdgeCurrencyWallet const fioConfig = { @@ -184,7 +214,12 @@ describe('AddressTile2 FIO address warning', () => { const onChangeAddress = jest.fn< (result: ChangeAddressResult) => Promise >(async () => {}) - const showSpy = jest.spyOn(Airship, 'show').mockResolvedValue(approved) + const showSpy = jest + .spyOn(Airship, 'show') + .mockImplementation(async renderModal => { + const modal = renderModal(null as never) as React.ReactElement + return modal.type === AddressModal ? modalResult : approved + }) const rootState: FakeState = { core: { @@ -192,10 +227,14 @@ describe('AddressTile2 FIO address warning', () => { currencyConfig: { ethereum: ethConfig, fio: fioConfig }, currencyWallets: { source: wallet }, disklet: { - getText: async (path: string) => - JSON.stringify({ + getText: async (path: string) => { + if (path === 'SentNames.json') { + return JSON.stringify({ names: sentNames }) + } + return JSON.stringify({ addresses: path === 'FioSentAddresses.json' ? sent : cache }) + } }, watch: () => () => {} } @@ -227,9 +266,16 @@ describe('AddressTile2 FIO address warning', () => { /> ) - await act(async () => { - await ref.current?.onChangeAddress(input) - }) + if (modalResult != null) { + fireEvent.press(rendered.getByTestId('addressTileEnter')) + await waitFor(() => { + expect(onChangeAddress).toHaveBeenCalled() + }) + } else { + await act(async () => { + await ref.current?.onChangeAddress(input) + }) + } rendered.unmount() const warningModals = showSpy.mock.calls @@ -242,8 +288,7 @@ describe('AddressTile2 FIO address warning', () => { }> ) .filter( - modal => - modal.props.title === lstrings.warning_sending_to_fio_address_title + modal => modal.props.title === lstrings.warning_sending_to_name_title ) showSpy.mockRestore() return { @@ -255,146 +300,284 @@ describe('AddressTile2 FIO address warning', () => { } } - it('warns for a handle this account has never interacted with', async () => { - const { onChangeAddress, warnings } = await enterAddress({}) - expect(warnings).toBe(1) - expect(onChangeAddress).toHaveBeenCalledTimes(1) - expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ - fioAddress: HANDLE, - acceptedName: { - service: 'fio', - name: HANDLE, - publicAddress: RESOLVED_ADDRESS - }, - parsedUri: { publicAddress: RESOLVED_ADDRESS } + describe('FIO handles', () => { + it('warns for a handle this account has never interacted with', async () => { + const { onChangeAddress, warnings } = await enterAddress({}) + expect(warnings).toBe(1) + expect(onChangeAddress).toHaveBeenCalledTimes(1) + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + fioAddress: HANDLE, + acceptedName: { + service: 'fio', + name: HANDLE, + publicAddress: RESOLVED_ADDRESS + }, + parsedUri: { publicAddress: RESOLVED_ADDRESS } + }) }) - }) - it('names the coin, the resolved address, and the address the handle copies', async () => { - const { warningText } = await enterAddress({ input: DEPOSIT_HANDLE }) - expect(warningText).toHaveLength(1) - expect(warningText[0]).toContain( - sprintf( - lstrings.warning_sending_to_fio_address_resolves_3s, - DEPOSIT_HANDLE, - 'ETH', - RESOLVED_ADDRESS + it('names the name, its service, the coin, and the resolved address', async () => { + const { warningText } = await enterAddress({}) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + HANDLE, + 'FIO', + 'ETH', + RESOLVED_ADDRESS + ) ) - ) - expect(warningText[0]).toContain( - sprintf(lstrings.warning_sending_to_fio_address_not_name_1s, DEPOSIT) - ) - expect(warningText[0]).toContain( - lstrings.warning_sending_to_fio_address_owner - ) - }) - - it('leaves out the "will not go to" sentence for a plain name', async () => { - const { warningText } = await enterAddress({ input: 'alice@edge' }) - expect(warningText).toHaveLength(1) - expect(warningText[0]).toContain( - lstrings.warning_sending_to_fio_address_no_routing - ) - expect(warningText[0]).not.toContain( - sprintf(lstrings.warning_sending_to_fio_address_not_name_1s, 'alice') - ) - }) + expect(warningText[0]).toContain( + lstrings.warning_sending_to_name_no_routing + ) + expect(warningText[0]).toContain(lstrings.warning_sending_to_name_owner) + }) - it('keeps the recipient unset when the warning is declined', async () => { - const { onChangeAddress, warnings } = await enterAddress({ - approved: false + it('keeps the recipient unset when the warning is declined', async () => { + const { onChangeAddress, warnings } = await enterAddress({ + approved: false + }) + expect(warnings).toBe(1) + expect(onChangeAddress).not.toHaveBeenCalled() }) - expect(warnings).toBe(1) - expect(onChangeAddress).not.toHaveBeenCalled() - }) - it('skips the warning for a handle this account sent to, ignoring case', async () => { - const { onChangeAddress, warnings } = await enterAddress({ - input: 'BC1QDeposit@thorchain', - sent: { [HANDLE]: [RESOLVED_ADDRESS] } + it('skips the warning for a handle in the older FIO-only list, ignoring case', async () => { + const { onChangeAddress, warnings } = await enterAddress({ + input: 'BC1QDeposit@thorchain', + sent: { [HANDLE]: [RESOLVED_ADDRESS] } + }) + expect(warnings).toBe(0) + expect(onChangeAddress).toHaveBeenCalledTimes(1) }) - expect(warnings).toBe(0) - expect(onChangeAddress).toHaveBeenCalledTimes(1) - }) - it('warns for a sent-to handle that now resolves to another address', async () => { - const { warnings } = await enterAddress({ - sent: { [HANDLE]: ['0x00000000000000000000000000000000000000cc'] } + it('skips the warning for a handle in the sent names list', async () => { + const { warnings } = await enterAddress({ + sentNames: { fio: { [HANDLE]: [RESOLVED_ADDRESS] } } + }) + expect(warnings).toBe(0) }) - expect(warnings).toBe(1) - }) - it('accepts and shows the address an ENS name from the handle points to', async () => { - const { onChangeAddress, warningText } = await enterAddress({ - resolvesTo: ENS_NAME + it('warns for a sent-to handle that now resolves to another address', async () => { + const { warnings } = await enterAddress({ + sent: { [HANDLE]: ['0x00000000000000000000000000000000000000cc'] } + }) + expect(warnings).toBe(1) }) - expect(warningText).toHaveLength(1) - expect(warningText[0]).toContain( - sprintf( - lstrings.warning_sending_to_fio_address_resolves_3s, - HANDLE, - 'ETH', - ENS_ADDRESS + + it('accepts and shows the address an ENS name from the handle points to', async () => { + const { onChangeAddress, warningText } = await enterAddress({ + resolvesTo: ENS_NAME + }) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + HANDLE, + 'FIO', + 'ETH', + ENS_ADDRESS + ) ) - ) - expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ - fioAddress: HANDLE, - acceptedName: { - service: 'fio', - name: HANDLE, - publicAddress: ENS_ADDRESS - }, - parsedUri: { publicAddress: ENS_ADDRESS } + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + fioAddress: HANDLE, + acceptedName: { + service: 'fio', + name: HANDLE, + publicAddress: ENS_ADDRESS + }, + parsedUri: { publicAddress: ENS_ADDRESS } + }) + }) + + it('warns once the ENS name from a sent-to handle points elsewhere', async () => { + const { warnings } = await enterAddress({ + resolvesTo: ENS_NAME, + sent: { [HANDLE]: [ENS_NAME, RESOLVED_ADDRESS] } + }) + expect(warnings).toBe(1) }) - }) - it('warns once the ENS name from a sent-to handle points elsewhere', async () => { - const { warnings } = await enterAddress({ - resolvesTo: ENS_NAME, - sent: { [HANDLE]: [ENS_NAME, RESOLVED_ADDRESS] } + it('skips the warning when the ENS name still points to the sent address', async () => { + const { warnings } = await enterAddress({ + resolvesTo: ENS_NAME, + sent: { [HANDLE]: [ENS_ADDRESS] } + }) + expect(warnings).toBe(0) }) - expect(warnings).toBe(1) - }) - it('skips the warning when the ENS name still points to the sent address', async () => { - const { warnings } = await enterAddress({ - resolvesTo: ENS_NAME, - sent: { [HANDLE]: [ENS_ADDRESS] } + it('warns for a handle that only came from an incoming request', async () => { + const { warnings } = await enterAddress({ cache: { [HANDLE]: true } }) + expect(warnings).toBe(1) + }) + + it("skips the warning for the account's own handle", async () => { + const { onChangeAddress, warnings } = await enterAddress({ + ownHandles: ['BC1QDeposit@thorchain'] + }) + expect(warnings).toBe(0) + expect(onChangeAddress).toHaveBeenCalledTimes(1) }) - expect(warnings).toBe(0) - }) - it('warns for a handle that only came from an incoming request', async () => { - const { warnings } = await enterAddress({ cache: { [HANDLE]: true } }) - expect(warnings).toBe(1) + it.each(['bc1qdeposit@other', 'someoneelse@thorchain'])( + 'warns for %s when only a different own handle matches part of it', + async input => { + const { warnings } = await enterAddress({ + input, + ownHandles: ['BC1QDeposit@thorchain'] + }) + expect(warnings).toBe(1) + } + ) }) - it("skips the warning for the account's own handle", async () => { - const { onChangeAddress, warnings } = await enterAddress({ - ownHandles: ['BC1QDeposit@thorchain'] + describe('other name services', () => { + it('warns for an ENS name typed into the tile', async () => { + const { onChangeAddress, warningText } = await enterAddress({ + input: ENS_NAME, + resolves: false + }) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + ENS_NAME, + 'ENS', + 'ETH', + ENS_ADDRESS + ) + ) + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + acceptedName: { + service: 'ens', + name: ENS_NAME, + publicAddress: ENS_ADDRESS + }, + parsedUri: { publicAddress: ENS_ADDRESS }, + resolvedName: { name: ENS_NAME, service: 'ens' } + }) }) - expect(warnings).toBe(0) - expect(onChangeAddress).toHaveBeenCalledTimes(1) - }) - it.each(['bc1qdeposit@other', 'someoneelse@thorchain'])( - 'warns for %s when only a different own handle matches part of it', - async input => { + it('skips the warning for an ENS name this account sent to', async () => { const { warnings } = await enterAddress({ - input, - ownHandles: ['BC1QDeposit@thorchain'] + input: ENS_NAME, + resolves: false, + sentNames: { ens: { [ENS_NAME]: [ENS_ADDRESS] } } }) - expect(warnings).toBe(1) - } - ) + expect(warnings).toBe(0) + }) + + it('warns for an Unstoppable Domains name the address modal resolved', async () => { + const { onChangeAddress, warningText } = await enterAddress({ + modalResult: { + address: UD_ADDRESS, + name: { service: 'unstoppable', name: UD_NAME } + }, + resolves: false + }) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + UD_NAME, + 'Unstoppable Domains', + 'ETH', + UD_ADDRESS + ) + ) + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + acceptedName: { + service: 'unstoppable', + name: UD_NAME, + publicAddress: UD_ADDRESS + }, + parsedUri: { publicAddress: UD_ADDRESS }, + resolvedName: { name: UD_NAME, service: 'unstoppable' } + }) + }) + + it('skips the warning for a modal name this account sent to', async () => { + const { onChangeAddress, warnings } = await enterAddress({ + modalResult: { + address: UD_ADDRESS, + name: { service: 'unstoppable', name: UD_NAME } + }, + resolves: false, + sentNames: { unstoppable: { [UD_NAME]: [UD_ADDRESS] } } + }) + expect(warnings).toBe(0) + expect(onChangeAddress).toHaveBeenCalledTimes(1) + }) + + it('does not warn for an address picked in the modal', async () => { + const { onChangeAddress, warnings } = await enterAddress({ + modalResult: { address: UD_ADDRESS }, + resolves: false + }) + expect(warnings).toBe(0) + expect(onChangeAddress.mock.calls[0][0].acceptedName).toBeUndefined() + }) + + it('warns for a Zano alias', async () => { + const { onChangeAddress, warningText } = await enterAddress({ + input: ZANO_ALIAS, + pluginId: 'zano', + resolves: false + }) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + ZANO_ALIAS, + 'Zano', + 'ETH', + ZANO_ADDRESS + ) + ) + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + acceptedName: { + service: 'zano', + name: ZANO_ALIAS, + publicAddress: ZANO_ADDRESS + }, + alias: ZANO_ALIAS + }) + }) - it('does not warn for input that is not a FIO handle', async () => { - const { onChangeAddress, warnings } = await enterAddress({ - input: RESOLVED_ADDRESS, - resolves: false + it('warns for a ZcashNames name', async () => { + const { onChangeAddress, warningText } = await enterAddress({ + input: 'Alice.zcash', + pluginId: 'zcash', + resolves: false + }) + expect(warningText).toHaveLength(1) + expect(warningText[0]).toContain( + sprintf( + lstrings.warning_sending_to_name_resolves_4s, + ZNS_NAME, + 'ZcashNames', + 'ETH', + ZNS_ADDRESS + ) + ) + expect(onChangeAddress.mock.calls[0][0]).toMatchObject({ + acceptedName: { + service: 'zns', + name: ZNS_NAME, + publicAddress: ZNS_ADDRESS + } + }) + }) + + it('does not warn for input that is not a name', async () => { + const { onChangeAddress, warnings } = await enterAddress({ + input: RESOLVED_ADDRESS, + resolves: false + }) + expect(warnings).toBe(0) + expect(onChangeAddress).toHaveBeenCalledTimes(1) + expect(onChangeAddress.mock.calls[0][0].acceptedName).toBeUndefined() }) - expect(warnings).toBe(0) - expect(onChangeAddress).toHaveBeenCalledTimes(1) }) }) diff --git a/src/components/tiles/AddressTile2.tsx b/src/components/tiles/AddressTile2.tsx index 33b60917ad4..dbdbaa94e8c 100644 --- a/src/components/tiles/AddressTile2.tsx +++ b/src/components/tiles/AddressTile2.tsx @@ -1,7 +1,6 @@ import Clipboard from '@react-native-clipboard/clipboard' import { asMaybe, asObject, asString } from 'cleaners' import type { - EdgeAccount, EdgeAddress, EdgeCurrencyWallet, EdgeParsedUri, @@ -34,7 +33,12 @@ import { checkPubAddress } from '../../util/FioAddressUtils' import { type NameService, reverseLookupName } from '../../util/nameServices' import { type CrossChainPayment, withStatedAsset } from '../../util/paymentUri' import { resolveName } from '../../util/resolveName' -import { hasSentToName, type NameSend } from '../../util/sentNames' +import { + hasSentToName, + type NameSend, + type RecipientName, + type RecipientNameService +} from '../../util/sentNames' import { isEmail } from '../../util/utils' import { isZnsName, resolveZnsName } from '../../util/zns' import { EdgeAnim } from '../common/EdgeAnim' @@ -292,11 +296,65 @@ export const AddressTile2 = React.forwardRef( } ) + /** + * Anyone can register a name and point it at any address. Scammers tell + * users to send to one, such as a FIO handle like "@thorchain" appended to + * the user's own exchange deposit address, and resolving it pays the + * scammer. A name this account has not paid at this address before needs + * consent. The address the send pays, not the name, is what the user + * accepts and what gets recorded once sent, since the owner can repoint + * the name at any time. + * + * Resolves false when the user declined, and otherwise the accepted name + * (undefined when no name was entered). Leaves the spinner off. + */ + const confirmNameSend = useHandler( + async ( + enteredName: RecipientName | undefined, + address: string + ): Promise => { + const acceptedName: NameSend | undefined = + enteredName == null + ? undefined + : { ...enteredName, publicAddress: address } + setLoading(false) + if ( + acceptedName == null || + isOwnFioAddress(acceptedName, ownFioAddresses) || + (await hasSentToName(account, acceptedName, address)) + ) { + return acceptedName + } + const approved = await Airship.show(bridge => ( + + + {sprintf( + lstrings.warning_sending_to_name_resolves_4s, + acceptedName.name, + getNameServiceName(acceptedName.service, coreWallet), + currencyCode, + acceptedName.publicAddress + )} + + {lstrings.warning_sending_to_name_no_routing} + + )) + return approved ? acceptedName : false + } + ) + const changeAddress = useHandler( async ( address: string, addressEntryMethod: AddressEntryMethod, - walletAddresses?: EdgeAddress[] + walletAddresses?: EdgeAddress[], + modalName?: RecipientName ) => { if (address == null || address.trim() === '') return @@ -311,7 +369,15 @@ export const AddressTile2 = React.forwardRef( await rejectUnparsedAddress(address, addressEntryMethod) return } + // A name the address modal resolved still needs the same consent + // as one resolved for this wallet's own chain: + const acceptedName = await confirmNameSend( + modalName, + payment.publicAddress + ) + if (acceptedName === false) return await onChangeAddress({ + acceptedName, parsedUri: { publicAddress: payment.publicAddress }, addressEntryMethod, crossChainNativeAmount: payment.nativeAmount, @@ -327,7 +393,15 @@ export const AddressTile2 = React.forwardRef( let zanoAlias: string | undefined let resolvedName: { name: string; service: NameService } | undefined let fioAddress - let fioPublicAddress: string | undefined + // The name the user entered, once it resolved to `address`. The + // address modal resolves some names itself and passes them in: + let enteredName: RecipientName | undefined = modalName + if ( + modalName?.service === 'ens' || + modalName?.service === 'unstoppable' + ) { + resolvedName = { name: modalName.name, service: modalName.service } + } if (fioPlugin != null) { try { const publicAddress = await checkPubAddress( @@ -337,7 +411,7 @@ export const AddressTile2 = React.forwardRef( currencyCode ) fioAddress = address.toLowerCase() - fioPublicAddress = publicAddress + enteredName = { service: 'fio', name: fioAddress } address = publicAddress } catch (e: unknown) { const invalidCode = @@ -366,63 +440,15 @@ export const AddressTile2 = React.forwardRef( const resolvedAddress = await ethersProvider.resolveName(address) if (resolvedAddress != null) { resolvedName = { name: enteredInput, service: 'ens' } + // A FIO handle that points at an ENS name stays the name + // the user entered: + enteredName ??= { service: 'ens', name: enteredInput } address = resolvedAddress } } catch (_) {} } } - // A handle can resolve to an ENS name the owner can repoint at any - // time, so the address the send pays, not the name, is what the - // user accepts below and what gets recorded once it is sent: - if (fioAddress != null) fioPublicAddress = address - - // Scammers tell users to append a handle they registered, such as - // "@thorchain", to the user's own exchange deposit address. Resolving - // it pays the scammer, so a handle new to this account needs consent. - if ( - fioAddress != null && - fioPublicAddress != null && - !(await hasInteractedWithFioAddress( - account, - fioAddress, - fioPublicAddress, - ownFioAddresses - )) - ) { - setLoading(false) - const handleAddress = address - const namePart = enteredInput.split('@')[0] - const approved = await Airship.show(bridge => ( - - - {sprintf( - lstrings.warning_sending_to_fio_address_resolves_3s, - enteredInput, - currencyCode, - handleAddress - )} - - - {looksLikeAddress(namePart) - ? sprintf( - lstrings.warning_sending_to_fio_address_not_name_1s, - namePart - ) - : lstrings.warning_sending_to_fio_address_no_routing} - - - )) - if (!approved) return - setLoading(true) - } - // Check if this is an email for Tron USDT and show warning for potential // PIX send if ( @@ -454,7 +480,10 @@ export const AddressTile2 = React.forwardRef( zanoAlias = enteredInput try { const resolved = await resolveName(coreWallet, enteredInput) - if (resolved != null) address = resolved + if (resolved != null) { + enteredName = { service: 'zano', name: enteredInput } + address = resolved + } } catch (_) {} } @@ -470,11 +499,16 @@ export const AddressTile2 = React.forwardRef( name: enteredInput.toLowerCase(), service: 'zns' } + enteredName = { service: 'zns', name: resolvedName.name } address = resolved } } catch (_) {} } + const acceptedName = await confirmNameSend(enteredName, address) + if (acceptedName === false) return + setLoading(true) + try { const parsedUri: EdgeParsedUri & { paymentProtocolUrl?: string } = withStatedAsset( @@ -552,14 +586,7 @@ export const AddressTile2 = React.forwardRef( // set address await onChangeAddress({ fioAddress, - acceptedName: - fioAddress != null && fioPublicAddress != null - ? { - service: 'fio', - name: fioAddress, - publicAddress: fioPublicAddress - } - : undefined, + acceptedName, parsedUri, addressEntryMethod, alias: zanoAlias, @@ -659,7 +686,7 @@ export const AddressTile2 = React.forwardRef( )) .then(async result => { if (result != null && result.address !== '') { - await changeAddress(result.address, 'other') + await changeAddress(result.address, 'other', undefined, result.name) } }) .catch((error: unknown) => { @@ -900,29 +927,32 @@ const getStyles = cacheStyles((theme: Theme) => ({ } })) -/** - * True when the handle belongs to this account, or this account has sent - * funds to it while it resolved to the same public address. Handles that only - * arrived through incoming requests or received transactions do not count, - * since anyone can put them there. - */ -const hasInteractedWithFioAddress = async ( - account: EdgeAccount, - fioAddress: string, - publicAddress: string, +/** True for one of this account's own FIO handles. */ +const isOwnFioAddress = ( + recipient: RecipientName, ownFioAddresses: FioAddress[] -): Promise => - ownFioAddresses.some(own => own.name.toLowerCase() === fioAddress) || - (await hasSentToName( - account, - { service: 'fio', name: fioAddress }, - publicAddress - )) - -/** - * True when the part of a FIO address before the @ reads like a wallet - * address rather than a person's name: letters and digits only, at least as - * long as the shortest common address formats. - */ -const looksLikeAddress = (namePart: string): boolean => - /^[a-zA-Z0-9]{25,}$/.test(namePart) +): boolean => + recipient.service === 'fio' && + ownFioAddresses.some( + own => own.name.toLowerCase() === recipient.name.toLowerCase() + ) + +/** The service's own name, which no locale translates. */ +const NAME_SERVICE_NAMES: Record< + Exclude, + string +> = { + ens: 'ENS', + fio: 'FIO', + unstoppable: 'Unstoppable Domains', + zano: 'Zano', + zns: 'ZcashNames' +} + +const getNameServiceName = ( + service: RecipientNameService, + coreWallet: EdgeCurrencyWallet +): string => + service === 'plugin' + ? coreWallet.currencyInfo.displayName + : NAME_SERVICE_NAMES[service] diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 4ba6461f43d..f92e6feb8c5 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -158,15 +158,13 @@ const strings = { warning_sending_pix_to_email_title: 'Sending PIX payment to email address', warning_sending_pix_to_email_body: 'You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.', - warning_sending_to_fio_address_title: 'Sending to a FIO Address', - warning_sending_to_fio_address_resolves_3s: - '%1$s is a FIO address. Your %2$s will go to the address its owner registered:\n\n%3$s', - warning_sending_to_fio_address_not_name_1s: - "It will not go to %1$s. A FIO address does not route, bridge, swap, or convert funds to another network. Anyone can register one, including names that contain someone else's address.", - warning_sending_to_fio_address_no_routing: - "A FIO address does not route, bridge, swap, or convert funds to another network. Anyone can register one, including names that contain someone else's address.", - warning_sending_to_fio_address_owner: - 'Only continue if you know who owns this FIO address. Otherwise, treat this as a scam. Sent funds cannot be recovered.', + warning_sending_to_name_title: 'Sending to a Name', + warning_sending_to_name_resolves_4s: + '%1$s is a name registered with %2$s, not an address. Your %3$s will go to the address its owner set:\n\n%4$s', + warning_sending_to_name_no_routing: + "A name never converts, bridges, or routes funds to another network. Anyone can register one, including names that contain someone else's address.", + warning_sending_to_name_owner: + 'Only continue if you know who owns this name. Otherwise, treat this as a scam. Sent funds cannot be recovered.', warning_earn: 'Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.', diff --git a/src/locales/strings/de.json b/src/locales/strings/de.json index 7a97089399d..75b3a8d05e1 100644 --- a/src/locales/strings/de.json +++ b/src/locales/strings/de.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Senden an eine FIO-Adresse", - "warning_sending_to_fio_address_resolves_3s": "%1$s ist eine FIO-Adresse. Ihre %2$s gehen an die Adresse, die der Inhaber registriert hat:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Sie gehen nicht an %1$s. Eine FIO-Adresse leitet Gelder nicht in ein anderes Netzwerk weiter, überträgt sie nicht per Bridge und tauscht oder konvertiert sie nicht. Jeder kann eine registrieren, auch Namen, die die Adresse einer anderen Person enthalten.", - "warning_sending_to_fio_address_no_routing": "Eine FIO-Adresse leitet Gelder nicht in ein anderes Netzwerk weiter, überträgt sie nicht per Bridge und tauscht oder konvertiert sie nicht. Jeder kann eine registrieren, auch Namen, die die Adresse einer anderen Person enthalten.", - "warning_sending_to_fio_address_owner": "Fahren Sie nur fort, wenn Sie wissen, wem diese FIO-Adresse gehört. Andernfalls betrachten Sie dies als Betrug. Gesendete Gelder können nicht zurückgeholt werden.", + "warning_sending_to_name_title": "Senden an einen Namen", + "warning_sending_to_name_resolves_4s": "%1$s ist ein bei %2$s registrierter Name, keine Adresse. Ihre %3$s gehen an die Adresse, die der Inhaber festgelegt hat:\n\n%4$s", + "warning_sending_to_name_no_routing": "Ein Name konvertiert Gelder nie, überträgt sie nicht per Bridge und leitet sie nicht in ein anderes Netzwerk weiter. Jeder kann einen registrieren, auch Namen, die die Adresse einer anderen Person enthalten.", + "warning_sending_to_name_owner": "Fahren Sie nur fort, wenn Sie wissen, wem dieser Name gehört. Andernfalls betrachten Sie dies als Betrug. Gesendete Gelder können nicht zurückgeholt werden.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Achtung!", "alert_dropdown_warning": "Warnung! ", diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 2263dcb9d22..fc6ac99e809 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Sending to a FIO Address", - "warning_sending_to_fio_address_resolves_3s": "%1$s is a FIO address. Your %2$s will go to the address its owner registered:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "It will not go to %1$s. A FIO address does not route, bridge, swap, or convert funds to another network. Anyone can register one, including names that contain someone else's address.", - "warning_sending_to_fio_address_no_routing": "A FIO address does not route, bridge, swap, or convert funds to another network. Anyone can register one, including names that contain someone else's address.", - "warning_sending_to_fio_address_owner": "Only continue if you know who owns this FIO address. Otherwise, treat this as a scam. Sent funds cannot be recovered.", + "warning_sending_to_name_title": "Sending to a Name", + "warning_sending_to_name_resolves_4s": "%1$s is a name registered with %2$s, not an address. Your %3$s will go to the address its owner set:\n\n%4$s", + "warning_sending_to_name_no_routing": "A name never converts, bridges, or routes funds to another network. Anyone can register one, including names that contain someone else's address.", + "warning_sending_to_name_owner": "Only continue if you know who owns this name. Otherwise, treat this as a scam. Sent funds cannot be recovered.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alert! ", "alert_dropdown_warning": "Warning! ", diff --git a/src/locales/strings/es.json b/src/locales/strings/es.json index 1894125b65e..e7f2d63d7f0 100644 --- a/src/locales/strings/es.json +++ b/src/locales/strings/es.json @@ -88,11 +88,10 @@ "warning_battery_saver": "¡Se detectó el ahorro de batería! Es posible que los saldos no se actualicen. Para una mejor experiencia, por favor desactiva el modo de ahorro de batería.", "warning_sending_pix_to_email_title": "Enviando pago PIX a la dirección de correo electrónico", "warning_sending_pix_to_email_body": "Escaneaste o ingresaste una dirección de correo electrónico que es una dirección de pago PIX válida en Brasil. Al continuar, se creará un pago PIX de USDT en Tron a la dirección de correo electrónico proporcionada.", - "warning_sending_to_fio_address_title": "Envío a una dirección FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s es una dirección FIO. Tus %2$s irán a la dirección que registró su propietario:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "No irán a %1$s. Una dirección FIO no redirige, no transfiere por puente, no intercambia ni convierte fondos a otra red. Cualquiera puede registrar una, incluso con nombres que contienen la dirección de otra persona.", - "warning_sending_to_fio_address_no_routing": "Una dirección FIO no redirige, no transfiere por puente, no intercambia ni convierte fondos a otra red. Cualquiera puede registrar una, incluso con nombres que contienen la dirección de otra persona.", - "warning_sending_to_fio_address_owner": "Continúa solo si sabes quién es el propietario de esta dirección FIO. De lo contrario, considéralo una estafa. Los fondos enviados no se pueden recuperar.", + "warning_sending_to_name_title": "Envío a un nombre", + "warning_sending_to_name_resolves_4s": "%1$s es un nombre registrado en %2$s, no una dirección. Tus %3$s irán a la dirección que configuró su propietario:\n\n%4$s", + "warning_sending_to_name_no_routing": "Un nombre nunca convierte, transfiere mediante puente ni enruta fondos a otra red. Cualquiera puede registrar uno, incluso nombres que contienen la dirección de otra persona.", + "warning_sending_to_name_owner": "Continúa solo si sabes quién es el propietario de este nombre. De lo contrario, considéralo una estafa. Los fondos enviados no se pueden recuperar.", "warning_earn": "El staking de criptomonedas implica riesgos (pérdida de fondos, fallos de contratos inteligentes, volatilidad y bloqueo de activos). Investiga bien cada opción de Earn.", "alert_dropdown_alert": "¡Alerta! ", "alert_dropdown_warning": "¡Advertencia! ", diff --git a/src/locales/strings/esMX.json b/src/locales/strings/esMX.json index 25ecc78d882..349f3875f84 100644 --- a/src/locales/strings/esMX.json +++ b/src/locales/strings/esMX.json @@ -88,11 +88,10 @@ "warning_battery_saver": "¡Se detectó ahorro de batería! Es posible que los saldos no se actualicen. Para disfrutar de la mejor experiencia, desactive el modo de ahorro de batería.", "warning_sending_pix_to_email_title": "Envío pago de PIX a dirección de correo electrónico", "warning_sending_pix_to_email_body": "Escaneó o ingresó una dirección de correo electrónico que es una dirección de pago PIX válida de Brasil. Si continúa, se creará un pago PIX de Tron USDT a la dirección de correo electrónico proporcionada.", - "warning_sending_to_fio_address_title": "Envío a una dirección FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s es una dirección FIO. Tus %2$s se enviarán a la dirección que registró su propietario:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "No se enviarán a %1$s. Una dirección FIO no redirige, no transfiere por puente, no intercambia ni convierte fondos a otra red. Cualquiera puede registrar una, incluso con nombres que contienen la dirección de otra persona.", - "warning_sending_to_fio_address_no_routing": "Una dirección FIO no redirige, no transfiere por puente, no intercambia ni convierte fondos a otra red. Cualquiera puede registrar una, incluso con nombres que contienen la dirección de otra persona.", - "warning_sending_to_fio_address_owner": "Continúa solo si sabes quién es el dueño de esta dirección FIO. De lo contrario, considéralo una estafa. Los fondos enviados no se pueden recuperar.", + "warning_sending_to_name_title": "Envío a un nombre", + "warning_sending_to_name_resolves_4s": "%1$s es un nombre registrado en %2$s, no una dirección. Tus %3$s se enviarán a la dirección que configuró su dueño:\n\n%4$s", + "warning_sending_to_name_no_routing": "Un nombre nunca convierte, transfiere mediante puente ni enruta fondos a otra red. Cualquiera puede registrar uno, incluso nombres que contienen la dirección de otra persona.", + "warning_sending_to_name_owner": "Continúa solo si sabes quién es el dueño de este nombre. De lo contrario, considéralo una estafa. Los fondos enviados no se pueden recuperar.", "warning_earn": "El staking de criptomonedas implica riesgos (pérdida de fondos, fallos de contratos inteligentes, volatilidad y bloqueo de activos). Investiga bien cada opción de Earn.", "alert_dropdown_alert": "¡Alerta! ", "alert_dropdown_warning": "¡Advertencia! ", diff --git a/src/locales/strings/fr.json b/src/locales/strings/fr.json index ec468609353..811ea03d65c 100644 --- a/src/locales/strings/fr.json +++ b/src/locales/strings/fr.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Envoi vers une adresse FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s est une adresse FIO. Vos %2$s seront envoyés à l'adresse enregistrée par son propriétaire :\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Ils ne seront pas envoyés à %1$s. Une adresse FIO ne redirige pas, ne transfère pas par pont, n'échange pas et ne convertit pas les fonds vers un autre réseau. N'importe qui peut en enregistrer une, y compris avec un nom qui contient l'adresse de quelqu'un d'autre.", - "warning_sending_to_fio_address_no_routing": "Une adresse FIO ne redirige pas, ne transfère pas par pont, n'échange pas et ne convertit pas les fonds vers un autre réseau. N'importe qui peut en enregistrer une, y compris avec un nom qui contient l'adresse de quelqu'un d'autre.", - "warning_sending_to_fio_address_owner": "Ne continuez que si vous savez à qui appartient cette adresse FIO. Sinon, considérez qu'il s'agit d'une arnaque. Les fonds envoyés ne peuvent pas être récupérés.", + "warning_sending_to_name_title": "Envoi vers un nom", + "warning_sending_to_name_resolves_4s": "%1$s est un nom enregistré auprès de %2$s, pas une adresse. Vos %3$s seront envoyés à l'adresse définie par son propriétaire :\n\n%4$s", + "warning_sending_to_name_no_routing": "Un nom ne convertit jamais les fonds, ne les fait pas passer par un pont et ne les achemine pas vers un autre réseau. N'importe qui peut en enregistrer un, y compris des noms qui contiennent l'adresse de quelqu'un d'autre.", + "warning_sending_to_name_owner": "Ne continuez que si vous savez à qui appartient ce nom. Sinon, considérez qu'il s'agit d'une arnaque. Les fonds envoyés ne peuvent pas être récupérés.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alert! ", "alert_dropdown_warning": "Warning! ", diff --git a/src/locales/strings/it.json b/src/locales/strings/it.json index d4578374439..abd4c88c16d 100644 --- a/src/locales/strings/it.json +++ b/src/locales/strings/it.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Risparmio batteria rilevato! I bilanci potrebbero non essere aggiornati. Per una migliore esperienza, si prega di disattivare la modalità risparmio batteria.", "warning_sending_pix_to_email_title": "Invio pagamento PIX all'indirizzo email", "warning_sending_pix_to_email_body": "Hai scansionato o inserito un indirizzo email che è un indirizzo di pagamento PIX Brasile valido. Continuando, verrrà creasto un pagamento PIX di Tron USDT all'indirizzo email fornito.", - "warning_sending_to_fio_address_title": "Invio a un indirizzo FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s è un indirizzo FIO. I tuoi %2$s andranno all'indirizzo registrato dal suo proprietario:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Non andranno a %1$s. Un indirizzo FIO non instrada, non trasferisce tramite bridge, non scambia e non converte fondi verso un'altra rete. Chiunque può registrarne uno, anche con nomi che contengono l'indirizzo di qualcun altro.", - "warning_sending_to_fio_address_no_routing": "Un indirizzo FIO non instrada, non trasferisce tramite bridge, non scambia e non converte fondi verso un'altra rete. Chiunque può registrarne uno, anche con nomi che contengono l'indirizzo di qualcun altro.", - "warning_sending_to_fio_address_owner": "Continua solo se sai chi possiede questo indirizzo FIO. Altrimenti, consideralo una truffa. I fondi inviati non possono essere recuperati.", + "warning_sending_to_name_title": "Invio a un nome", + "warning_sending_to_name_resolves_4s": "%1$s è un nome registrato su %2$s, non un indirizzo. I tuoi %3$s andranno all'indirizzo impostato dal suo proprietario:\n\n%4$s", + "warning_sending_to_name_no_routing": "Un nome non converte mai i fondi, non li trasferisce tramite bridge e non li instrada verso un'altra rete. Chiunque può registrarne uno, anche nomi che contengono l'indirizzo di un'altra persona.", + "warning_sending_to_name_owner": "Continua solo se sai chi possiede questo nome. Altrimenti, consideralo una truffa. I fondi inviati non possono essere recuperati.", "warning_earn": "Mettere in stake le cripto comporta rischi, tra cui la perdita dei fondi, fallimenti di smart contract, perdite indotte dalla volatilità e periodi di blocco degli asset. Fai le tue ricerche per ogni opzione di stake disponibile in Guadagna.", "alert_dropdown_alert": "Avviso! ", "alert_dropdown_warning": "Attenzione! ", diff --git a/src/locales/strings/ja.json b/src/locales/strings/ja.json index 401f124fbac..66cb101a5ea 100644 --- a/src/locales/strings/ja.json +++ b/src/locales/strings/ja.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "FIOアドレスへの送金", - "warning_sending_to_fio_address_resolves_3s": "%1$s はFIOアドレスです。%2$s は、その所有者が登録したアドレスに送られます:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "%1$s には送られません。FIOアドレスは、資金を別のネットワークへルーティング、ブリッジ、スワップ、変換することはありません。FIOアドレスは誰でも登録でき、他人のアドレスを含む名前も登録できます。", - "warning_sending_to_fio_address_no_routing": "FIOアドレスは、資金を別のネットワークへルーティング、ブリッジ、スワップ、変換することはありません。FIOアドレスは誰でも登録でき、他人のアドレスを含む名前も登録できます。", - "warning_sending_to_fio_address_owner": "このFIOアドレスの所有者を知っている場合にのみ続行してください。そうでない場合は詐欺とみなしてください。送金した資金は取り戻せません。", + "warning_sending_to_name_title": "名前への送金", + "warning_sending_to_name_resolves_4s": "%1$sは%2$sに登録された名前で、アドレスではありません。%3$sは、その所有者が設定したアドレスに送られます:\n\n%4$s", + "warning_sending_to_name_no_routing": "名前が資金を変換したり、ブリッジしたり、別のネットワークへルーティングしたりすることはありません。名前は誰でも登録でき、他人のアドレスを含む名前も登録できます。", + "warning_sending_to_name_owner": "この名前の所有者を知っている場合にのみ続行してください。そうでない場合は詐欺として扱ってください。送金した資金は取り戻せません。", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "警告! ", "alert_dropdown_warning": "注意! ", diff --git a/src/locales/strings/kaa.json b/src/locales/strings/kaa.json index 69b8616e2da..7c38fb76087 100644 --- a/src/locales/strings/kaa.json +++ b/src/locales/strings/kaa.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "FIO mánziline jiberiw", - "warning_sending_to_fio_address_resolves_3s": "%1$s bul FIO mánzili. Sizdiń %2$s onıń iyesi dizimnen ótkergen mánzilge jiberiledi:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Olar %1$s mánziline jiberilmeydi. FIO mánzili qarjılardı basqa tarmaqqa jóneltpeydi, kópir arqalı ótkermeydi, almastırmaydı hám konvertaciyalamaydı. Onı hár kim dizimnen ótkere aladı, sonıń ishinde basqa birewdiń mánzili bar atlardı da.", - "warning_sending_to_fio_address_no_routing": "FIO mánzili qarjılardı basqa tarmaqqa jóneltpeydi, kópir arqalı ótkermeydi, almastırmaydı hám konvertaciyalamaydı. Onı hár kim dizimnen ótkere aladı, sonıń ishinde basqa birewdiń mánzili bar atlardı da.", - "warning_sending_to_fio_address_owner": "Bul FIO mánziliniń iyesi kim ekenin bilseńiz ǵana dawam etiń. Bolmasa, bunı alayaqlıq dep esaplań. Jiberilgen qarjılardı qaytarıp alıw múmkin emes.", + "warning_sending_to_name_title": "Atqa jiberiw", + "warning_sending_to_name_resolves_4s": "%1$s bul %2$s arqalı dizimnen ótkerilgen at, mánzil emes. Sizdiń %3$s onıń iyesi belgilegen mánzilge jiberiledi:\n\n%4$s", + "warning_sending_to_name_no_routing": "At hesh qashan qarjılardı konvertaciyalamaydı, kópir arqalı ótkermeydi hám basqa tarmaqqa jóneltpeydi. Onı hár kim dizimnen ótkere aladı, sonıń ishinde basqa birewdiń mánzili bar atlardı da.", + "warning_sending_to_name_owner": "Bul attıń iyesi kim ekenin bilseńiz ǵana dawam etiń. Bolmasa, bunı alayaqlıq dep esaplań. Jiberilgen qarjılardı qaytarıp alıw múmkin emes.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Dıqqat! ", "alert_dropdown_warning": "Dıqqat! ", diff --git a/src/locales/strings/ko.json b/src/locales/strings/ko.json index 2bbb916992a..cfcefb39bff 100644 --- a/src/locales/strings/ko.json +++ b/src/locales/strings/ko.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "FIO 주소로 보내기", - "warning_sending_to_fio_address_resolves_3s": "%1$s은(는) FIO 주소입니다. %2$s은(는) 소유자가 등록한 주소로 전송됩니다:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "%1$s(으)로 전송되지 않습니다. FIO 주소는 자금을 다른 네트워크로 라우팅, 브리지, 스왑 또는 변환하지 않습니다. 누구나 FIO 주소를 등록할 수 있으며, 다른 사람의 주소가 포함된 이름도 등록할 수 있습니다.", - "warning_sending_to_fio_address_no_routing": "FIO 주소는 자금을 다른 네트워크로 라우팅, 브리지, 스왑 또는 변환하지 않습니다. 누구나 FIO 주소를 등록할 수 있으며, 다른 사람의 주소가 포함된 이름도 등록할 수 있습니다.", - "warning_sending_to_fio_address_owner": "이 FIO 주소의 소유자를 아는 경우에만 계속하세요. 그렇지 않다면 사기로 간주하세요. 보낸 자금은 되찾을 수 없습니다.", + "warning_sending_to_name_title": "이름으로 보내기", + "warning_sending_to_name_resolves_4s": "%1$s은(는) %2$s에 등록된 이름이며 주소가 아닙니다. %3$s은(는) 소유자가 설정한 주소로 전송됩니다:\n\n%4$s", + "warning_sending_to_name_no_routing": "이름은 자금을 변환하거나 브리지하거나 다른 네트워크로 라우팅하지 않습니다. 누구나 이름을 등록할 수 있으며, 다른 사람의 주소가 포함된 이름도 등록할 수 있습니다.", + "warning_sending_to_name_owner": "이 이름의 소유자를 아는 경우에만 계속하세요. 그렇지 않다면 사기로 간주하세요. 보낸 자금은 되찾을 수 없습니다.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alert! ", "alert_dropdown_warning": "Warning! ", diff --git a/src/locales/strings/pt.json b/src/locales/strings/pt.json index 3740beb6805..99ac20cffb0 100644 --- a/src/locales/strings/pt.json +++ b/src/locales/strings/pt.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Enviando para um endereço FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s é um endereço FIO. Seus %2$s irão para o endereço que o proprietário registrou:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Eles não irão para %1$s. Um endereço FIO não roteia, não faz ponte, não troca nem converte fundos para outra rede. Qualquer pessoa pode registrar um, inclusive com nomes que contêm o endereço de outra pessoa.", - "warning_sending_to_fio_address_no_routing": "Um endereço FIO não roteia, não faz ponte, não troca nem converte fundos para outra rede. Qualquer pessoa pode registrar um, inclusive com nomes que contêm o endereço de outra pessoa.", - "warning_sending_to_fio_address_owner": "Só continue se você souber quem é o dono deste endereço FIO. Caso contrário, trate isso como um golpe. Fundos enviados não podem ser recuperados.", + "warning_sending_to_name_title": "Enviando para um nome", + "warning_sending_to_name_resolves_4s": "%1$s é um nome registrado no %2$s, não um endereço. Seus %3$s irão para o endereço que o dono definiu:\n\n%4$s", + "warning_sending_to_name_no_routing": "Um nome nunca converte, faz bridge ou encaminha fundos para outra rede. Qualquer pessoa pode registrar um, inclusive nomes que contêm o endereço de outra pessoa.", + "warning_sending_to_name_owner": "Só continue se você souber quem é o dono deste nome. Caso contrário, trate isso como um golpe. Fundos enviados não podem ser recuperados.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alerta! ", "alert_dropdown_warning": "Atenção! ", diff --git a/src/locales/strings/ru.json b/src/locales/strings/ru.json index 6241e480bfe..a4759984114 100644 --- a/src/locales/strings/ru.json +++ b/src/locales/strings/ru.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Отправка на адрес FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s является адресом FIO. Ваши %2$s будут отправлены на адрес, который зарегистрировал его владелец:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Средства не будут отправлены на %1$s. Адрес FIO не перенаправляет средства в другую сеть, не переводит их через мост, не обменивает и не конвертирует. Зарегистрировать адрес FIO может кто угодно, в том числе с именем, содержащим чужой адрес.", - "warning_sending_to_fio_address_no_routing": "Адрес FIO не перенаправляет средства в другую сеть, не переводит их через мост, не обменивает и не конвертирует. Зарегистрировать адрес FIO может кто угодно, в том числе с именем, содержащим чужой адрес.", - "warning_sending_to_fio_address_owner": "Продолжайте, только если вы знаете, кому принадлежит этот адрес FIO. В противном случае считайте это мошенничеством. Отправленные средства невозможно вернуть.", + "warning_sending_to_name_title": "Отправка на имя", + "warning_sending_to_name_resolves_4s": "%1$s является именем, зарегистрированным в %2$s, а не адресом. Ваши %3$s будут отправлены на адрес, указанный его владельцем:\n\n%4$s", + "warning_sending_to_name_no_routing": "Имя никогда не конвертирует средства, не переводит их через мост и не направляет в другую сеть. Зарегистрировать имя может кто угодно, в том числе имя, содержащее чужой адрес.", + "warning_sending_to_name_owner": "Продолжайте, только если знаете, кому принадлежит это имя. В противном случае считайте это мошенничеством. Отправленные средства невозможно вернуть.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Внимание! ", "alert_dropdown_warning": "Внимание! ", diff --git a/src/locales/strings/vi.json b/src/locales/strings/vi.json index 5450e4753a2..8184368e5d7 100644 --- a/src/locales/strings/vi.json +++ b/src/locales/strings/vi.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "Gửi đến địa chỉ FIO", - "warning_sending_to_fio_address_resolves_3s": "%1$s là một địa chỉ FIO. %2$s của bạn sẽ được gửi đến địa chỉ mà chủ sở hữu của nó đã đăng ký:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "Tiền sẽ không được gửi đến %1$s. Địa chỉ FIO không định tuyến, chuyển qua cầu nối, hoán đổi hay chuyển đổi tiền sang mạng khác. Bất kỳ ai cũng có thể đăng ký một địa chỉ FIO, kể cả những tên chứa địa chỉ của người khác.", - "warning_sending_to_fio_address_no_routing": "Địa chỉ FIO không định tuyến, chuyển qua cầu nối, hoán đổi hay chuyển đổi tiền sang mạng khác. Bất kỳ ai cũng có thể đăng ký một địa chỉ FIO, kể cả những tên chứa địa chỉ của người khác.", - "warning_sending_to_fio_address_owner": "Chỉ tiếp tục nếu bạn biết ai sở hữu địa chỉ FIO này. Nếu không, hãy coi đây là lừa đảo. Không thể khôi phục tiền đã gửi.", + "warning_sending_to_name_title": "Gửi đến một tên", + "warning_sending_to_name_resolves_4s": "%1$s là một tên đã đăng ký với %2$s, không phải địa chỉ. %3$s của bạn sẽ được gửi đến địa chỉ mà chủ sở hữu đã thiết lập:\n\n%4$s", + "warning_sending_to_name_no_routing": "Một tên không bao giờ chuyển đổi, chuyển qua cầu nối hay định tuyến tiền sang mạng khác. Bất kỳ ai cũng có thể đăng ký một tên, kể cả những tên chứa địa chỉ của người khác.", + "warning_sending_to_name_owner": "Chỉ tiếp tục nếu bạn biết ai sở hữu tên này. Nếu không, hãy coi đây là lừa đảo. Tiền đã gửi không thể khôi phục.", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alert! ", "alert_dropdown_warning": "Warning! ", diff --git a/src/locales/strings/zh.json b/src/locales/strings/zh.json index 7219fc9951b..fec74615d30 100644 --- a/src/locales/strings/zh.json +++ b/src/locales/strings/zh.json @@ -88,11 +88,10 @@ "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", "warning_sending_pix_to_email_body": "You scanned or entered an email address which is a valid Brazil PIX payment address. Continuing will create a PIX payment of Tron USDT to the email address provided.", - "warning_sending_to_fio_address_title": "发送到 FIO 地址", - "warning_sending_to_fio_address_resolves_3s": "%1$s 是一个 FIO 地址。您的 %2$s 将发送到其所有者注册的地址:\n\n%3$s", - "warning_sending_to_fio_address_not_name_1s": "资金不会发送到 %1$s。FIO 地址不会将资金路由、跨链桥接、兑换或转换到其他网络。任何人都可以注册 FIO 地址,包括含有他人地址的名称。", - "warning_sending_to_fio_address_no_routing": "FIO 地址不会将资金路由、跨链桥接、兑换或转换到其他网络。任何人都可以注册 FIO 地址,包括含有他人地址的名称。", - "warning_sending_to_fio_address_owner": "只有在您知道此 FIO 地址的所有者时才继续。否则,请将其视为诈骗。已发送的资金无法追回。", + "warning_sending_to_name_title": "发送到名称", + "warning_sending_to_name_resolves_4s": "%1$s 是在 %2$s 注册的名称,不是地址。您的 %3$s 将发送到其所有者设置的地址:\n\n%4$s", + "warning_sending_to_name_no_routing": "名称绝不会将资金转换、跨链桥接或路由到其他网络。任何人都可以注册名称,包括含有他人地址的名称。", + "warning_sending_to_name_owner": "只有在您知道此名称归谁所有时才继续。否则,请将其视为诈骗。已发送的资金无法追回。", "warning_earn": "Staking cryptocurrency carries risks including loss of funds, smart contract failures, volatility-induced losses, and asset lock periods. Please research each Earn option thoroughly.", "alert_dropdown_alert": "Alert! ", "alert_dropdown_warning": "Warning! ",