diff --git a/.prettierignore b/.prettierignore index ec6d3cdd..f8564104 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ package.json +test-e2e/**/*.json diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index 952fdaf9..24949435 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -20,7 +20,7 @@ import { import FeesRippleKind from 'components/FeesField/RippleKind' import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName' -import { NotEnoughBalance } from 'config/errors' +import { NotEnoughBalance, NotEnoughBalanceBecauseDestinationNotCreated } from 'config/errors' import type { WalletBridge, EditProps } from './types' type Transaction = { @@ -114,7 +114,7 @@ async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOpera } } -function isRecipientValid(currency, recipient) { +function isRecipientValid(recipient) { try { bs58check.decode(recipient) return true @@ -241,6 +241,31 @@ const getServerInfo = (map => endpointConfig => { return f() })({}) +const recipientIsNew = async (endpointConfig, recipient) => { + if (!isRecipientValid(recipient)) return false + const api = apiForEndpointConfig(endpointConfig) + try { + await api.connect() + try { + await api.getAccountInfo(recipient) + return false + } catch (e) { + if (e.message !== 'actNotFound') { + throw e + } + return true + } + } finally { + api.disconnect() + } +} + +const cacheRecipientsNew = {} +const cachedRecipientIsNew = (endpointConfig, recipient) => { + if (recipient in cacheRecipientsNew) return cacheRecipientsNew[recipient] + return (cacheRecipientsNew[recipient] = recipientIsNew(endpointConfig, recipient)) +} + const RippleJSBridge: WalletBridge = { scanAccountsOnDevice: (currency, deviceId) => Observable.create(o => { @@ -446,7 +471,7 @@ const RippleJSBridge: WalletBridge = { pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented - isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), + isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(recipient)), getRecipientWarning: () => Promise.resolve(null), createTransaction: () => ({ @@ -496,10 +521,21 @@ const RippleJSBridge: WalletBridge = { checkValidTransaction: async (a, t) => { const r = await getServerInfo(a.endpointConfig) + const reserveBaseXRP = parseAPIValue(r.validatedLedger.reserveBaseXRP) + if (t.recipient) { + if (await cachedRecipientIsNew(a.endpointConfig, t.recipient)) { + if (t.amount.lt(reserveBaseXRP)) { + const f = formatAPICurrencyXRP(reserveBaseXRP) + throw new NotEnoughBalanceBecauseDestinationNotCreated('', { + minimalAmount: `${f.currency} ${f.value}`, + }) + } + } + } if ( t.amount .plus(t.fee) - .plus(parseAPIValue(r.validatedLedger.reserveBaseXRP)) + .plus(reserveBaseXRP) .isLessThanOrEqualTo(a.balance) ) { return true @@ -513,6 +549,7 @@ const RippleJSBridge: WalletBridge = { signAndBroadcast: (a, t, deviceId) => Observable.create(o => { + delete cacheRecipientsNew[t.recipient] let cancelled = false const isCancelled = () => cancelled const onSigned = () => { diff --git a/src/config/errors.js b/src/config/errors.js index f6e4a81c..a40a3a73 100644 --- a/src/config/errors.js +++ b/src/config/errors.js @@ -30,6 +30,9 @@ export const ManagerUninstallBTCDep = createCustomErrorClass('ManagerUninstallBT export const NetworkDown = createCustomErrorClass('NetworkDown') export const NoAddressesFound = createCustomErrorClass('NoAddressesFound') export const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance') +export const NotEnoughBalanceBecauseDestinationNotCreated = createCustomErrorClass( + 'NotEnoughBalanceBecauseDestinationNotCreated', +) export const PasswordsDontMatchError = createCustomErrorClass('PasswordsDontMatch') export const PasswordIncorrectError = createCustomErrorClass('PasswordIncorrect') export const TimeoutTagged = createCustomErrorClass('TimeoutTagged') diff --git a/static/i18n/en/errors.json b/static/i18n/en/errors.json index 043c7038..8acef277 100644 --- a/static/i18n/en/errors.json +++ b/static/i18n/en/errors.json @@ -99,6 +99,9 @@ "title": "Oops, insufficient balance", "description": "Make sure the account to debit has sufficient balance" }, + "NotEnoughBalanceBecauseDestinationNotCreated": { + "title": "Recipient address is inactive. Send at least {{minimalAmount}} to activate it" + }, "PasswordsDontMatch": { "title": "Passwords don't match", "description": "Please try again" diff --git a/test-e2e/sync/sync-accounts.spec.js b/test-e2e/sync/sync-accounts.spec.js index b67c505d..d04c5c7b 100644 --- a/test-e2e/sync/sync-accounts.spec.js +++ b/test-e2e/sync/sync-accounts.spec.js @@ -46,15 +46,13 @@ function getSanitized(filePath) { const data = require(`${filePath}`) // eslint-disable-line import/no-dynamic-require const accounts = data.data.accounts.map(a => a.data) accounts.sort(ACCOUNT_SORT) - return accounts - .map(a => pick(a, ACCOUNTS_FIELDS)) - .map(a => { - a.operations.sort(OP_SORT) - return { - ...a, - operations: a.operations.map(o => pick(o, OPS_FIELDS)), - } - }) + return accounts.map(a => pick(a, ACCOUNTS_FIELDS)).map(a => { + a.operations.sort(OP_SORT) + return { + ...a, + operations: a.operations.map(o => pick(o, OPS_FIELDS)), + } + }) } function getOpHash(op) { diff --git a/test-e2e/sync/wait-sync.js b/test-e2e/sync/wait-sync.js index 79b588af..5ddb2ae6 100644 --- a/test-e2e/sync/wait-sync.js +++ b/test-e2e/sync/wait-sync.js @@ -27,7 +27,11 @@ async function waitForSync() { const areAllSync = mapped.every(account => { const diff = now - new Date(account.lastSyncDate).getTime() if (diff <= MIN_TIME_DIFF) return true - console.log(`[${account.name}] synced ${moment(account.lastSyncDate).fromNow()} (${moment(account.lastSyncDate).format('YYYY-MM-DD HH:mm:ss')})`) + console.log( + `[${account.name}] synced ${moment(account.lastSyncDate).fromNow()} (${moment( + account.lastSyncDate, + ).format('YYYY-MM-DD HH:mm:ss')})`, + ) return false }) return areAllSync