From de929b4f7e790b635d67815d0ca895c387d36c08 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 9 Feb 2024 00:28:49 +0000 Subject: [PATCH 1/5] feat: handle login errors --- src/components/LoginForm.vue | 10 +++++++++- src/components/SendFundsForm.vue | 2 +- src/lib/nodes/abstract.client.ts | 2 +- src/lib/nodes/adm/AdmClient.ts | 8 +------- src/lib/nodes/utils/errors.ts | 14 ++++++++++++-- src/locales/en.json | 5 ++++- src/locales/ru.json | 5 ++++- src/locales/zh.json | 5 ++++- 8 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/components/LoginForm.vue b/src/components/LoginForm.vue index 90ed4842d..1d448ac86 100644 --- a/src/components/LoginForm.vue +++ b/src/components/LoginForm.vue @@ -55,6 +55,8 @@ import { validateMnemonic } from 'bip39' import { computed, ref, defineComponent } from 'vue' import { useStore } from 'vuex' +import { isAxiosError } from 'axios' +import { isAllNodesOfflineError } from '@/lib/nodes/utils/errors' export default defineComponent({ props: { @@ -98,8 +100,14 @@ export default defineComponent({ emit('login') }) .catch((err) => { + if (isAxiosError(err)) { + emit('error', 'login.invalid_passphrase') + } else if (isAllNodesOfflineError(err)) { + emit('error', 'errors.all_nodes_offline') + } else { + emit('error', 'errors.something_went_wrong') + } console.log(err) - emit('error', 'login.invalid_passphrase') }) .finally(() => { antiFreeze() diff --git a/src/components/SendFundsForm.vue b/src/components/SendFundsForm.vue index 236817ce0..cfb8cece4 100644 --- a/src/components/SendFundsForm.vue +++ b/src/components/SendFundsForm.vue @@ -746,7 +746,7 @@ export default { } else if (/Invalid JSON RPC Response/i.test(message)) { message = this.$t('transfer.error_unknown') } else if (error instanceof AllNodesOfflineError) { - message = this.$t('transfer.error_all_nodes_offline', { + message = this.$t('errors.all_nodes_offline', { crypto: error.nodeLabel.toUpperCase() }) } else if (error instanceof PendingTransactionError) { diff --git a/src/lib/nodes/abstract.client.ts b/src/lib/nodes/abstract.client.ts index 62ff6f81c..b376b9687 100644 --- a/src/lib/nodes/abstract.client.ts +++ b/src/lib/nodes/abstract.client.ts @@ -129,7 +129,7 @@ export abstract class Client { // All nodes seem to be offline: let's refresh the statuses this.checkHealth() // But there's nothing we can do right now - throw new Error('No online nodes at the moment') + throw new AllNodesOfflineError(this.type) } return node diff --git a/src/lib/nodes/adm/AdmClient.ts b/src/lib/nodes/adm/AdmClient.ts index c68afabb6..63d258e1a 100644 --- a/src/lib/nodes/adm/AdmClient.ts +++ b/src/lib/nodes/adm/AdmClient.ts @@ -41,13 +41,7 @@ export class AdmClient extends Client { * @param {RequestConfig} config request config */ async request

(config: RequestConfig

): Promise { - const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() - if (!node) { - // All nodes seem to be offline: let's refresh the statuses - this.checkHealth() - // But there's nothing we can do right now - return Promise.reject(new Error('No online nodes at the moment')) - } + const node = this.getNode() return node.request(config).catch((error) => { if (isNodeOfflineError(error)) { diff --git a/src/lib/nodes/utils/errors.ts b/src/lib/nodes/utils/errors.ts index 52eebfb39..3190f1758 100644 --- a/src/lib/nodes/utils/errors.ts +++ b/src/lib/nodes/utils/errors.ts @@ -1,10 +1,15 @@ import { NodeType } from '@/lib/nodes/types' +const CODES = { + NODE_OFFLINE: 'NODE_OFFLINE', + ALL_NODES_OFFLINE: 'ALL_NODES_OFFLINE' +} as const + /** * Custom error to indicate that the endpoint is not available */ export class NodeOfflineError extends Error { - code = 'NODE_OFFLINE' + code = CODES.NODE_OFFLINE constructor() { super('Node is offline') @@ -16,7 +21,7 @@ export class NodeOfflineError extends Error { } export function isNodeOfflineError(error: Error): error is NodeOfflineError { - return (error as NodeOfflineError).code === 'NODE_OFFLINE' + return (error as NodeOfflineError).code === CODES.NODE_OFFLINE } /** @@ -29,6 +34,7 @@ export class TransactionNotFound extends Error { } export class AllNodesOfflineError extends Error { + code = CODES.ALL_NODES_OFFLINE nodeLabel: NodeType // to distinct eth-node from eth-indexer it may be better to use TNodeLabel constructor(label: NodeType) { @@ -37,3 +43,7 @@ export class AllNodesOfflineError extends Error { this.nodeLabel = label } } + +export function isAllNodesOfflineError(error: Error): error is AllNodesOfflineError { + return (error as AllNodesOfflineError).code === CODES.ALL_NODES_OFFLINE +} diff --git a/src/locales/en.json b/src/locales/en.json index 8a615e01b..71444466c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -86,6 +86,10 @@ "online": "Online—Connection is set" }, "error": "Error", + "errors": { + "something_went_wrong": "Something went wrong. Check the console for details", + "all_nodes_offline": "All {crypto} nodes are offline. Try again later" + }, "home": { "balance": "Balance", "buy_tokens_anonymously": "Anonymously on website", @@ -312,7 +316,6 @@ "error_chat_fee": "To send {crypto} via chat, you need 0.001 ADM", "error_dust_amount": "Dust amount—Send more tokens", "recipient_minimum_balance": "Recipient must have at least 0.05 LSK—Send more tokens", - "error_all_nodes_offline": "All {crypto} nodes are offline. Try again later", "error_precision": "Too high precision—Set less decimal places", "error_erc20_fee": "You need a {fee} to pay a transfer fee", "error_field_is_required": "Field is required", diff --git a/src/locales/ru.json b/src/locales/ru.json index c53eff8bb..3146a61e9 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -86,6 +86,10 @@ "online": "Подключение восстановлено" }, "error": "Ошибка", + "errors": { + "something_went_wrong": "Что-то пошло не так. Детали ошибки в консоли", + "all_nodes_offline": "Все {crypto} ноды недоступны. Попробуйте позже" + }, "home": { "balance": "Баланс", "buy_tokens_anonymously": "Анонимно на сайте", @@ -313,7 +317,6 @@ "error_chat_fee": "Чтобы отправить {crypto} в чате, вам также нужно 0.001 ADM", "error_dust_amount": "Мелочь — Отправьте больше токенов", "recipient_minimum_balance": "На кошельке получателя должно быть больше 0.05 LSK — Отправьте больше токенов", - "error_all_nodes_offline": "Все {crypto} ноды недоступны. Попробуйте позже", "error_precision": "Уменьшите количество знаков после запятой", "error_erc20_fee": "Вам также нужно {fee}, чтобы оплатить комиссию", "error_field_is_required": "Поле не заполнено", diff --git a/src/locales/zh.json b/src/locales/zh.json index 835f42a2d..2ddc951fa 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -86,6 +86,10 @@ "online": "联机--已设置连接" }, "error": "错误", + "errors": { + "something_went_wrong": "Something went wrong. Check the console for details", + "all_nodes_offline": "All {crypto} nodes are offline. Try again later" + }, "home": { "balance": "余额", "buy_tokens_anonymously": "在网站上匿名", @@ -312,7 +316,6 @@ "error_chat_fee": "要通过聊天发送 {crypto},您需要 0.001 ADM", "error_dust_amount": "无存量--发送更多代币", "recipient_minimum_balance": "收件人必须至少有 0.05 LSK--发送更多令牌", - "error_all_nodes_offline": "All {crypto} nodes are offline. Try again later", "error_precision": "精度过高--设置小数位数较少", "error_erc20_fee": "您需要{费}来支付转会费", "error_field_is_required": "字段是必需的", From 8e88d9b09a3b1c307153834fe0a5976777ce19c3 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 9 Feb 2024 01:32:41 +0000 Subject: [PATCH 2/5] fix(nodes): handle `AllNodesOffline` error --- src/lib/bitcoin/bitcoin-api.js | 10 ++----- src/lib/bitcoin/dash-api.js | 8 ++---- src/lib/bitcoin/doge-api.js | 8 ++---- src/lib/nodes/abstract.client.ts | 26 ++++++++++++++----- src/lib/nodes/eth/EthClient.ts | 26 +++++++++++-------- src/lib/nodes/lsk-indexer/LskIndexerClient.ts | 8 +----- src/lib/nodes/lsk/LskClient.ts | 8 +----- src/store/modules/erc20/erc20-actions.js | 5 ++-- .../modules/eth-base/eth-base-actions.js | 9 +++---- src/store/modules/eth/actions.js | 18 ++++++------- src/types/utils.ts | 3 +++ 11 files changed, 58 insertions(+), 71 deletions(-) create mode 100644 src/types/utils.ts diff --git a/src/lib/bitcoin/bitcoin-api.js b/src/lib/bitcoin/bitcoin-api.js index b989361f2..1575de881 100644 --- a/src/lib/bitcoin/bitcoin-api.js +++ b/src/lib/bitcoin/bitcoin-api.js @@ -28,10 +28,7 @@ export default class BitcoinApi extends BtcBaseApi { /** @override */ sendTransaction(txHex) { - return btc - .getClient() - .post('/tx', txHex) - .then((response) => response.data) + return btc.useClient((client) => client.post('/tx', txHex)).then((response) => response.data) } /** @override */ @@ -87,9 +84,6 @@ export default class BitcoinApi extends BtcBaseApi { /** Executes a GET request to the API */ _get(url, params) { - return btc - .getClient() - .get(url, { params }) - .then((response) => response.data) + return btc.useClient((client) => client.get(url, { params })).then((response) => response.data) } } diff --git a/src/lib/bitcoin/dash-api.js b/src/lib/bitcoin/dash-api.js index f3489631e..4efd9eb26 100644 --- a/src/lib/bitcoin/dash-api.js +++ b/src/lib/bitcoin/dash-api.js @@ -95,8 +95,7 @@ export default class DashApi extends BtcBaseApi { */ _invoke(method, params) { return dash - .getClient() - .post('/', { method, params }) + .useClient((client) => client.post('/', { method, params })) .then(({ data }) => { if (data.error) throw new DashApiError(method, data.error) return data.result @@ -104,9 +103,6 @@ export default class DashApi extends BtcBaseApi { } _invokeMany(calls) { - return dash - .getClient() - .post('/', calls) - .then((response) => response.data) + return dash.useClient((client) => client.post('/', calls)).then((response) => response.data) } } diff --git a/src/lib/bitcoin/doge-api.js b/src/lib/bitcoin/doge-api.js index cc7860037..6352a1750 100644 --- a/src/lib/bitcoin/doge-api.js +++ b/src/lib/bitcoin/doge-api.js @@ -150,17 +150,13 @@ export default class DogeApi extends BtcBaseApi { /** Executes a GET request to the DOGE API */ _get(url, params) { - return doge - .getClient() - .get(url, { params }) - .then((response) => response.data) + return doge.useClient((client) => client.get(url, { params })).then((response) => response.data) } /** Executes a POST request to the DOGE API */ _post(url, data) { return doge - .getClient() - .post(url, qs.stringify(data), POST_CONFIG) + .useClient((client) => client.post(url, qs.stringify(data), POST_CONFIG)) .then((response) => response.data) } diff --git a/src/lib/nodes/abstract.client.ts b/src/lib/nodes/abstract.client.ts index b376b9687..24e392d5a 100644 --- a/src/lib/nodes/abstract.client.ts +++ b/src/lib/nodes/abstract.client.ts @@ -56,17 +56,29 @@ export abstract class Client { } } + // Use with caution: + // This method can throw an error if there are no online nodes. + // Better use "useClient()" method. getClient(): N['client'] { - const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() + const node = this.getNode() - if (!node) { - console.warn(`${this.type}: No online nodes at the moment`) + return node.client + } - // Return a random one from the full list hopefully is online - return this.nodes[Math.floor(Math.random() * this.nodes.length)].client - } + /** + * Invokes a client method. + * + * eth + * .useClient((client) => client.getTransactionCount(this.$store.state.eth.address)) + * .then(res => console.log("res", res)) + * .catch(err => console.log("err", err)) + * + * @param cb + */ + async useClient(cb: (client: N['client']) => T) { + const node = this.getNode() - return node.client + return cb(node.client) } /** diff --git a/src/lib/nodes/eth/EthClient.ts b/src/lib/nodes/eth/EthClient.ts index 2c03d109e..92f773eaf 100644 --- a/src/lib/nodes/eth/EthClient.ts +++ b/src/lib/nodes/eth/EthClient.ts @@ -58,20 +58,24 @@ export class EthClient extends Client { sendSignedTransaction(...args: Parameters): Promise { return new Promise((resolve, reject) => { - this.getNode() - .client.sendSignedTransaction(...args) - .on('transactionHash', (hash) => { - if (typeof hash === 'string') { - resolve(hash) - } else { - resolve(bytesToHex(hash)) - } - }) - .on('error', reject) + try { + this.getNode() + .client.sendSignedTransaction(...args) + .on('transactionHash', (hash) => { + if (typeof hash === 'string') { + resolve(hash) + } else { + resolve(bytesToHex(hash)) + } + }) + .on('error', reject) + } catch (err) { + reject(err) + } }) } - getNonce(address: string) { + async getNonce(address: string) { return this.getNode().client.getTransactionCount(address) } } diff --git a/src/lib/nodes/lsk-indexer/LskIndexerClient.ts b/src/lib/nodes/lsk-indexer/LskIndexerClient.ts index d4cc205b3..7d759ba0a 100644 --- a/src/lib/nodes/lsk-indexer/LskIndexerClient.ts +++ b/src/lib/nodes/lsk-indexer/LskIndexerClient.ts @@ -21,13 +21,7 @@ export class LskIndexerClient extends Client { params?: Endpoints[E]['params'], axiosRequestConfig?: AxiosRequestConfig ): Promise { - const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() - if (!node) { - // All nodes seem to be offline: let's refresh the statuses - this.checkHealth() - // But there's nothing we can do right now - throw new Error('No online nodes at the moment') - } + const node = this.getNode() return node.request(endpoint, params, axiosRequestConfig) } diff --git a/src/lib/nodes/lsk/LskClient.ts b/src/lib/nodes/lsk/LskClient.ts index b57afe878..a451f643c 100644 --- a/src/lib/nodes/lsk/LskClient.ts +++ b/src/lib/nodes/lsk/LskClient.ts @@ -17,13 +17,7 @@ export class LskClient extends Client { method: M, params?: RpcResults[M]['params'] ): Promise { - const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() - if (!node) { - // All nodes seem to be offline: let's refresh the statuses - this.checkHealth() - // But there's nothing we can do right now - throw new Error('No online nodes at the moment') - } + const node = this.getNode() return node.invoke(method, params) } diff --git a/src/store/modules/erc20/erc20-actions.js b/src/store/modules/erc20/erc20-actions.js index 72c266ccb..e330ac154 100644 --- a/src/store/modules/erc20/erc20-actions.js +++ b/src/store/modules/erc20/erc20-actions.js @@ -16,7 +16,7 @@ const abiDecoder = new AbiDecoder(Erc20) const initTransaction = async (api, context, ethAddress, amount, nonce, increaseFee) => { const contract = new EthContract(Erc20, context.state.contractAddress) - const gasPrice = await api.getClient().getGasPrice() + const gasPrice = await api.useClient((client) => client.getGasPrice()) const transaction = { from: context.state.address, @@ -30,8 +30,7 @@ const initTransaction = async (api, context, ethAddress, amount, nonce, increase } const gasLimit = await api - .getClient() - .estimateGas(transaction) + .useClient((client) => client.estimateGas(transaction)) .catch(() => BigInt(DEFAULT_ERC20_TRANSFER_GAS_LIMIT)) transaction.gasLimit = increaseFee ? ethUtils.increaseFee(gasLimit) : gasLimit diff --git a/src/store/modules/eth-base/eth-base-actions.js b/src/store/modules/eth-base/eth-base-actions.js index 2bdf722d8..c40508b9c 100644 --- a/src/store/modules/eth-base/eth-base-actions.js +++ b/src/store/modules/eth-base/eth-base-actions.js @@ -189,8 +189,7 @@ export default function createActions(config) { if (!transaction) return void api - .getClient() - .getBlock(payload.blockNumber) + .useClient((client) => client.getBlock(payload.blockNumber)) .then((block) => { // Converting from BigInt into Number must be safe const timestamp = BigNumber(block.timestamp.toString()).multipliedBy(1000).toNumber() @@ -238,8 +237,7 @@ export default function createActions(config) { ) void api - .getClient() - .getTransaction(payload.hash) + .useClient((client) => client.getTransaction(payload.hash)) .then((tx) => { const isFinalized = tx.blockNumber !== undefined @@ -324,8 +322,7 @@ export default function createActions(config) { ) void api - .getClient() - .getTransactionReceipt(payload.hash) + .useClient((client) => client.getTransactionReceipt(payload.hash)) .then((tx) => { let replay = true diff --git a/src/store/modules/eth/actions.js b/src/store/modules/eth/actions.js index f259f32a3..b70925b10 100644 --- a/src/store/modules/eth/actions.js +++ b/src/store/modules/eth/actions.js @@ -18,7 +18,7 @@ function storeEthAddress(context) { } const initTransaction = async (api, context, ethAddress, amount, nonce, increaseFee) => { - const gasPrice = await api.getClient().getGasPrice() + const gasPrice = await api.useClient((client) => client.getGasPrice()) const transaction = { from: context.state.address, @@ -29,8 +29,7 @@ const initTransaction = async (api, context, ethAddress, amount, nonce, increase } const gasLimit = await api - .getClient() - .estimateGas(transaction) + .useClient((client) => client.estimateGas(transaction)) .catch(() => BigInt(DEFAULT_ETH_TRANSFER_GAS_LIMIT)) transaction.gasLimit = increaseFee ? utils.increaseFee(gasLimit) : gasLimit @@ -59,7 +58,9 @@ const createSpecificActions = (api) => ({ } try { - const rawBalance = await api.getClient().getBalance(state.address, 'latest') + const rawBalance = await api.useClient((client) => + client.getBalance(state.address, 'latest') + ) const balance = Number(utils.toEther(rawBalance.toString())) commit('balance', balance) @@ -80,8 +81,7 @@ const createSpecificActions = (api) => ({ // Balance void api - .getClient() - .getBalance(context.state.address, 'latest') + .useClient((client) => client.getBalance(context.state.address, 'latest')) .then((balance) => { context.commit('balance', Number(utils.toEther(balance.toString()))) context.commit('setBalanceStatus', FetchStatus.Success) @@ -89,8 +89,7 @@ const createSpecificActions = (api) => ({ // Current gas price void api - .getClient() - .getGasPrice() + .useClient((client) => client.getGasPrice()) .then((price) => { context.commit('gasPrice', { gasPrice: Number(price), @@ -100,8 +99,7 @@ const createSpecificActions = (api) => ({ // Current block number void api - .getClient() - .getBlockNumber() + .useClient((client) => client.getBlockNumber()) .then((number) => { context.commit('blockNumber', Number(number)) }) diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 000000000..22b6b7e91 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,3 @@ +export type ExtractMethods = { + [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never +} From 0bbba2f6287904e4f1dad2b061750cf671dde64d Mon Sep 17 00:00:00 2001 From: bludnic Date: Sat, 10 Feb 2024 23:28:30 +0000 Subject: [PATCH 3/5] feat(Chat): add NodesOfflineDialog --- src/components/NodesOfflineDialog.vue | 67 +++++++++++++++++++++++++++ src/locales/en.json | 7 ++- src/locales/ru.json | 7 ++- src/locales/zh.json | 7 ++- src/views/Chat.vue | 4 ++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/components/NodesOfflineDialog.vue diff --git a/src/components/NodesOfflineDialog.vue b/src/components/NodesOfflineDialog.vue new file mode 100644 index 000000000..efdeea9b5 --- /dev/null +++ b/src/components/NodesOfflineDialog.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/locales/en.json b/src/locales/en.json index 71444466c..4b89ade84 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -79,7 +79,12 @@ "you_reacted": "Reacted", "partner_reacted": "Reacted", "you_removed_reaction": "Removed reaction", - "partner_removed_reaction": "Removed reaction" + "partner_removed_reaction": "Removed reaction", + "nodes_offline_dialog": { + "title": "Nodes are offline", + "text": "All {coin} nodes appear to be offline or possibly disabled. To continue use the Messenger please review the Nodes list and enable any nodes that are found to be disabled.", + "open_nodes_button": "Open Nodes page" + } }, "connection": { "offline": "Offline—No connection", diff --git a/src/locales/ru.json b/src/locales/ru.json index 3146a61e9..d81c760db 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -79,7 +79,12 @@ "you_reacted": "Отреагировали", "partner_reacted": "Отреагировал", "you_removed_reaction": "Убрали реакцию", - "partner_removed_reaction": "Убрал реакцию" + "partner_removed_reaction": "Убрал реакцию", + "nodes_offline_dialog": { + "title": "Узлы недоступны", + "text": "Кажется все узлы {coin} недоступны, или возможно отключены. Перейдите на страницу Узлов и включите узлы, если они отключены.", + "open_nodes_button": "Перейти в Список узлов" + } }, "connection": { "offline": "Нет подключения к Интернету", diff --git a/src/locales/zh.json b/src/locales/zh.json index 2ddc951fa..825fcb4e3 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -79,7 +79,12 @@ "you_reacted": "Reacted", "partner_reacted": "已反应", "you_removed_reaction": "已删除反应", - "partner_removed_reaction": "已删除的反应" + "partner_removed_reaction": "已删除的反应", + "nodes_offline_dialog": { + "title": "Nodes are offline", + "text": "All {coin} nodes appear to be offline or possibly disabled. To continue use the Messenger please review the Nodes list and enable any nodes that are found to be disabled.", + "open_nodes_button": "Open Nodes page" + } }, "connection": { "offline": "Offline—无连接", diff --git a/src/views/Chat.vue b/src/views/Chat.vue index c18de16e2..59edf1cf7 100644 --- a/src/views/Chat.vue +++ b/src/views/Chat.vue @@ -17,10 +17,13 @@ + + +