diff --git a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx index 1d70d821cdc..f8c995ef4ce 100644 --- a/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx +++ b/src/components/scenes/Fio/FioDomainRegisterSelectWalletScene.tsx @@ -201,7 +201,7 @@ class FioDomainRegisterSelectWallet extends React.PureComponent { const [defaultLogLevel, setDefaultLogLevel] = React.useState(logSettings.defaultLogLevel) const [disableAnim, setDisableAnim] = useState(getDeviceSettings().disableAnimations) const [forceLightAccountCreate, setForceLightAccountCreate] = useState(getDeviceSettings().forceLightAccountCreate) - const [touchIdText, setTouchIdText] = React.useState(lstrings.settings_button_use_touchID) + const [touchIdText, setTouchIdText] = React.useState(lstrings.settings_button_use_touchID) const iconSize = theme.rem(1.25) const isLightAccount = username == null diff --git a/src/components/scenes/Staking/StakeModifyScene.tsx b/src/components/scenes/Staking/StakeModifyScene.tsx index d91133410a9..77ee72f148f 100644 --- a/src/components/scenes/Staking/StakeModifyScene.tsx +++ b/src/components/scenes/Staking/StakeModifyScene.tsx @@ -17,6 +17,7 @@ import { StakePosition } from '../../../plugins/stake-plugins/types' import { getExchangeDenomByCurrencyCode } from '../../../selectors/DenominationSelectors' +import { HumanFriendlyError } from '../../../types/HumanFriendlyError' import { useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { getCurrencyIconUris } from '../../../util/CdnUris' @@ -169,6 +170,8 @@ const StakeModifySceneComponent = (props: Props) => { setErrorMessage(errMessage) } else if (err instanceof InsufficientFundsError) { setErrorMessage(lstrings.exchange_insufficient_funds_title) + } else if (err instanceof HumanFriendlyError) { + setErrorMessage(err.message) } else { showError(err) setErrorMessage(lstrings.unknown_error_occurred_fragment) diff --git a/src/components/themed/FioRequestRow.tsx b/src/components/themed/FioRequestRow.tsx index 33b74da5317..e63a8a6265b 100644 --- a/src/components/themed/FioRequestRow.tsx +++ b/src/components/themed/FioRequestRow.tsx @@ -74,7 +74,7 @@ class FioRequestRowComponent extends React.PureComponent { const styles = getStyles(theme) let statusStyle = styles.requestPartialConfirmation - let label = lstrings.fragment_wallet_unconfirmed + let label: string = lstrings.fragment_wallet_unconfirmed if (status === 'sent_to_blockchain') { statusStyle = styles.requestDetailsReceivedTx label = lstrings.fragment_request_subtitle diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 68ae8abf8f0..dedc7eb697b 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -100,6 +100,7 @@ const strings = { error_paymentprotocol_no_payment_option: 'No currencies available for this Payment Protocol invoice. Accepted currencies: %s', error_paymentprotocol_tx_verification_failed: 'Payment Protocol transaction verification mismatch', error_spend_amount_less_then_min_s: 'Spend amount is less than minimum of %s', + error_amount_too_low_to_stake_s: 'The amount %s is too low to stake successfully', // Warning messages: warning_low_fee_selected: 'Low Fee Selected', @@ -1120,10 +1121,7 @@ const strings = { price_change_buy_sell_trade: 'Would you like to buy, sell, or exchange %1$s?', // Update notices update_notice_deprecate_electrum_servers_title: 'Blockbook Upgrade', - update_notice_deprecate_electrum_servers_message: - '%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.' + - '\n\n' + - 'NOTE: If you had custom nodes enabled, those wallets will not sync until corrected.', + update_notice_deprecate_electrum_servers_message: `%s no longer uses Electrum Servers. If you would like to continue to use CUSTOM NODES, please input Blockbook compatible addresses.\n\nNOTE: If you had custom nodes enabled, those wallets will not sync until corrected.`, error_boundary_title: 'Oops!', error_boundary_message_s: @@ -1221,9 +1219,7 @@ const strings = { loan_breakdown_title: 'Loan Breakdown', loan_close_swap_warning: "Closing your loan will liquidate some of the deposited collateral if you do not have enough balance to repay the remaining principal and interest on your loan. The remaining collateral will be deposited back to your wallet.\n\nLiquidation most likely will incur a higer capital cost, if remaining principal isn't repaid.", - loan_close_loan_no_tx_needed_message: - `There appears to be no principal to repay nor collateral to withdraw.\n\n` + - `No transactions are required to close your account, however the account may re-appear after closing if there are pending on-chain transactions.`, + loan_close_loan_no_tx_needed_message: `There appears to be no principal to repay nor collateral to withdraw.\n\nNo transactions are required to close your account, however the account may re-appear after closing if there are pending on-chain transactions.`, loan_close_loan_title: 'Close Loan', loan_close_multiple_asset_error: 'Closing loans with multiple debt assets and/or deposited collateral assets is not supported.\n\nPlease specify funding sources to repay loans with using Repay.', @@ -1859,7 +1855,7 @@ const strings = { auto_log_off_failed_message_s: 'Failed to auto-logoff: %s', contacts_load_failed_message_s: 'Failed to load contacts: %s' -} +} as const // eslint-disable-next-line import/no-default-export export default strings diff --git a/src/locales/strings.ts b/src/locales/strings.ts index 6d5368243f3..dd46d5cb469 100644 --- a/src/locales/strings.ts +++ b/src/locales/strings.ts @@ -15,7 +15,10 @@ import zh from './strings/zh.json' const allLocales = { en, de, ru, es, esMX, it, pt, ja, fr, ko, vi, zh } -export const lstrings = { ...en } +export const lstrings = { ...en } as const +export type LStrings = typeof lstrings +export type LStringsKey = keyof LStrings +export type LStringsValues = LStrings[LStringsKey] // Set the language at boot: const [firstLocale] = getLocales() diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index ab1c091d887..d6b5d29bc8c 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -63,6 +63,7 @@ "error_paymentprotocol_no_payment_option": "No currencies available for this Payment Protocol invoice. Accepted currencies: %s", "error_paymentprotocol_tx_verification_failed": "Payment Protocol transaction verification mismatch", "error_spend_amount_less_then_min_s": "Spend amount is less than minimum of %s", + "error_amount_too_low_to_stake_s": "The amount %s is too low to stake successfully", "warning_low_fee_selected": "Low Fee Selected", "warning_custom_fee_selected": "Custom Fee Selected", "warning_low_or_custom_fee": "Using a low fee may increase the amount of time it takes for your transaction to confirm. In rare instances your transaction can fail.", diff --git a/src/plugins/stake-plugins/generic/policyAdapters/CardanoKilnAdaptor.ts b/src/plugins/stake-plugins/generic/policyAdapters/CardanoKilnAdaptor.ts index c008c0daccc..a41533b1c18 100644 --- a/src/plugins/stake-plugins/generic/policyAdapters/CardanoKilnAdaptor.ts +++ b/src/plugins/stake-plugins/generic/policyAdapters/CardanoKilnAdaptor.ts @@ -1,11 +1,13 @@ import { eq, gt, sub } from 'biggystring' import { EdgeCurrencyWallet, EdgeTransaction } from 'edge-core-js' +import { lstrings } from '../../../../locales/strings' +import { HumanFriendlyError } from '../../../../types/HumanFriendlyError' import { infoServerData } from '../../../../util/network' import { AssetId, ChangeQuote, PositionAllocation, QuoteAllocation, StakePosition } from '../../types' import { asInfoServerResponse } from '../../util/internalTypes' import { StakePolicyConfig } from '../types' -import { makeKilnApi } from '../util/kilnUtils' +import { KilnError, makeKilnApi } from '../util/kilnUtils' import { StakePolicyAdapter } from './types' export interface CardanoPooledKilnAdapterConfig { @@ -71,7 +73,21 @@ export const makeCardanoKilnAdapter = (policyConfig: StakePolicyConfig { + if (error instanceof Error) return error + throw error + }) + if (result instanceof KilnError) { + if (/Value \d+ less than the minimum UTXO value \d+/.test(result.error)) { + const displayBalance = await wallet.nativeToDenomination(walletBalance, wallet.currencyInfo.currencyCode) + throw new HumanFriendlyError(lstrings.error_amount_too_low_to_stake_s, `${displayBalance} ${wallet.currencyInfo.currencyCode}`) + } + } + if (result instanceof Error) { + throw result + } + + const stakeTransaction = result const edgeTx: EdgeTransaction = await wallet.otherMethods.decodeStakingTx(stakeTransaction.unsigned_tx_serialized) const allocations: QuoteAllocation[] = [ diff --git a/src/plugins/stake-plugins/generic/util/kilnUtils.ts b/src/plugins/stake-plugins/generic/util/kilnUtils.ts index 875d680fa69..b5092f7f7a8 100644 --- a/src/plugins/stake-plugins/generic/util/kilnUtils.ts +++ b/src/plugins/stake-plugins/generic/util/kilnUtils.ts @@ -1,4 +1,17 @@ -import { asArray, asEither, asMaybe, asObject, asString, asValue, Cleaner } from 'cleaners' +import { asArray, asJSON, asMaybe, asObject, asString, asValue, Cleaner } from 'cleaners' + +export class KilnError extends Error { + name: string + message: string + error: string + + constructor(message: string, error: string) { + super(message) + this.name = 'KilnError' + this.message = message + this.error = error + } +} export interface KilnApi { adaGetStakes: (params: { @@ -28,6 +41,8 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { const res = await fetch(url, opts) if (!res.ok) { const message = await res.text() + const errorResponse = asMaybe(asKilnErrorResponse)(message) + if (errorResponse != null) throw new KilnError(errorResponse.message, errorResponse.error) throw new Error(`Kiln fetch error: ${message}`) } const json = await res.json() @@ -62,7 +77,6 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { // eslint-disable-next-line @typescript-eslint/no-base-to-string const raw = await fetchKiln(`/v1/ada/stakes?${query.toString()}`) const response = asKilnResponse(asArray(asAdaStake))(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) return response.data }, @@ -77,7 +91,6 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { }) }) const response = asKilnResponse(asAdaStakeTransaction)(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) return response.data }, @@ -90,7 +103,6 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { }) }) const response = asKilnResponse(asAdaUnstakeTransaction)(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) return response.data }, @@ -104,7 +116,6 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { }) }) const response = asKilnResponse(asAdaUnstakeTransaction)(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) return response.data }, @@ -112,14 +123,12 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { async ethGetOnChainStakes(address) { const raw = await fetchKiln(`/v1/eth/onchain/v2/stakes?wallets=${address}`) const response = asKilnResponse(asArray(asEthOnChainStake))(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) return response.data }, // https://docs.api.kiln.fi/reference/getethonchainv2operations async ethGetOnChainOperations(address) { const raw = await fetchKiln(`/v1/eth/onchain/v2/operations?wallets=${address}`) const response = asKilnResponse(asArray(asMaybe(asExitOperation)))(raw) - if ('message' in response) throw new Error('Kiln error: ' + response.message) const filteredOps = response.data.filter((op): op is ExitOperation => op != null) return filteredOps } @@ -135,15 +144,22 @@ export const makeKilnApi = (baseUrl: string, apiKey: string): KilnApi => { export interface KilnResponse { data: T } + const asKilnResponse = (asT: Cleaner) => - asEither( - asObject({ - data: asT - }), - asObject({ - message: asString - }) - ) + asObject({ + data: asT + }) + +export interface KilnErrorResponse { + error: string + message: string +} +const asKilnErrorResponse = asJSON( + asObject({ + error: asString, + message: asString + }) +) // // Ada diff --git a/src/types/HumanFriendlyError.ts b/src/types/HumanFriendlyError.ts new file mode 100644 index 00000000000..9d84e9a0f5a --- /dev/null +++ b/src/types/HumanFriendlyError.ts @@ -0,0 +1,23 @@ +import { sprintf } from 'sprintf-js' + +import { LStringsValues } from '../locales/strings' + +/** + * Error class that is meant to be used for errors that are meant to be shown + * to the user. The error message must be an lstrings value along with any + * sprintf arguments. + * + * This error type is strictly a GUI error type because of it's dependency + * on lstrings. + */ +export class HumanFriendlyError extends Error { + name: string + message: string + + constructor(format: LStringsValues, ...args: any[]) { + const message = sprintf(format, ...args) + super(message) + this.name = 'HumanFriendlyError' + this.message = message + } +}