From 3d1344becfa18309f4e7cb47ca2cde1127f5b887 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Thu, 5 Sep 2024 15:42:39 -0700 Subject: [PATCH 1/3] Add HumanFriendlyError The purpose of this is to have an error message that we know we can safely show to the user because we know it's human readable and translated. --- .../FioDomainRegisterSelectWalletScene.tsx | 2 +- src/components/scenes/SettingsScene.tsx | 2 +- src/components/themed/FioRequestRow.tsx | 2 +- src/locales/en_US.ts | 11 +++------ src/locales/strings.ts | 5 +++- src/types/HumanFriendlyError.ts | 23 +++++++++++++++++++ 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/types/HumanFriendlyError.ts 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/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..cab4f9bb470 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1120,10 +1120,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 +1218,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 +1854,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/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 + } +} From 94144f9233bf536bc089880be895a6ba007b0578 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Thu, 5 Sep 2024 15:45:28 -0700 Subject: [PATCH 2/3] Fix Kiln error response errors --- src/locales/strings/enUS.json | 1 + .../stake-plugins/generic/util/kilnUtils.ts | 46 +++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index ab1c091d887..5de43d40483 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_stake_amount_less_then_min_s": "Amount to be staked is less than minimum of %s", "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/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 From 09bab389788f8af4a9519660aa41ebd544edf2d6 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Thu, 5 Sep 2024 15:59:25 -0700 Subject: [PATCH 3/3] Add error_amount_too_low_to_stake_s error to Cardano stake requests --- .../scenes/Staking/StakeModifyScene.tsx | 3 +++ src/locales/en_US.ts | 1 + src/locales/strings/enUS.json | 2 +- .../policyAdapters/CardanoKilnAdaptor.ts | 20 +++++++++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) 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/locales/en_US.ts b/src/locales/en_US.ts index cab4f9bb470..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', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 5de43d40483..d6b5d29bc8c 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -63,7 +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_stake_amount_less_then_min_s": "Amount to be staked 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[] = [