Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat: add Nodes offline dialog #597

Merged
merged 12 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/components/LoginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -98,8 +100,14 @@ export default defineComponent({
emit('login')
})
.catch((err) => {
if (isAxiosError(err)) {
emit('error', 'login.invalid_passphrase')
bludnic marked this conversation as resolved.
Show resolved Hide resolved
} 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()
Expand Down
132 changes: 132 additions & 0 deletions src/components/NodesOfflineDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<template>
<v-dialog v-model="showDialog" width="500" :class="className">
<v-card>
<v-card-title class="a-text-header">
{{ t('chats.nodes_offline_dialog.title') }}
</v-card-title>

<v-divider class="a-divider" />

<v-card-text :class="`${className}__card-text`">
<div
:class="`${className}__disclaimer a-text-regular-enlarged`"
v-html="t('chats.nodes_offline_dialog.text', { coin: nodeType.toUpperCase() })"
></div>
</v-card-text>

<v-col cols="12" :class="[`${className}__btn-block`, 'text-center']">
<v-btn
:class="[`${className}__btn-free-tokens`, 'a-btn-primary']"
to="/options/nodes"
variant="text"
prepend-icon="mdi-open-in-new"
>
<div :class="`${className}__btn-text`">
{{ t('chats.nodes_offline_dialog.open_nodes_button') }}
</div>
</v-btn>
</v-col>
</v-card>
</v-dialog>
</template>

<script lang="ts">
import { NodeStatusResult } from '@/lib/nodes/abstract.node'
import { computed, PropType, ref, watch } from 'vue'
import { NodeType } from '@/lib/nodes/types'
import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'

const className = 'all-nodes-disabled-dialog'
const classes = {
root: className
}

export default {
props: {
nodeType: {
type: String as PropType<NodeType>,
required: true
}
},
emits: ['update:modelValue'],
setup() {
const { t } = useI18n()
const store = useStore()

const showDialog = ref(false)

const nodes = computed<NodeStatusResult[]>(() => store.getters['nodes/adm'])
const isOnline = computed<boolean>(() => store.getters['isOnline'])
const className = 'nodes-offline-dialog'

const offlineNodesStatus = computed(() => {
return {
allOffline: nodes.value.every(
(node) =>
!(
node.online &&
node.active &&
!node.outOfSync &&
node.hasMinNodeVersion &&
node.hasSupportedProtocol
)
),
hasDisabled: nodes.value.some((node) => !node.active)
}
})

watch(
offlineNodesStatus,
(value) => {
showDialog.value = value.allOffline && value.hasDisabled && isOnline.value
},
{
deep: true,
immediate: true
}
)

return {
t,
nodes,
classes,
offlineNodesStatus,
showDialog,
className
}
}
}
</script>
<style lang="scss" scoped>
@import 'vuetify/_settings.scss';
@import '@/assets/styles/settings/_colors.scss';

.nodes-offline-dialog {
&__card-text {
padding: 16px !important;
}
&__disclaimer {
margin-top: 10px;
}
&__btn {
margin-top: 15px;
margin-bottom: 20px;
}
&__btn-icon {
margin-right: 8px;
}
&__btn-block {
padding: 0 0 30px 0;
text-align: center;
}
}

.v-theme--dark {
.nodes-offline-dialog {
&__disclaimer {
color: map-get($shades, 'white');
}
}
}
</style>
2 changes: 1 addition & 1 deletion src/components/SendFundsForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,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) {
Expand Down
8 changes: 4 additions & 4 deletions src/components/nodes/hooks/useNodeStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ function getNodeStatusDetail(
return null
}

if (!node.hasMinNodeVersion) {
if (!node.hasSupportedProtocol) {
return {
text: t('nodes.unsupported_reason_api_version')
text: t('nodes.unsupported_reason_protocol')
}
} else if (!node.hasSupportedProtocol) {
} else if (!node.hasMinNodeVersion) {
return {
text: t('nodes.unsupported_reason_protocol')
text: t('nodes.unsupported_reason_api_version')
}
} else if (node.online) {
return {
Expand Down
10 changes: 2 additions & 8 deletions src/lib/bitcoin/bitcoin-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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)
}
}
8 changes: 2 additions & 6 deletions src/lib/bitcoin/dash-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,14 @@ 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
})
}

_invokeMany(calls) {
return dash
.getClient()
.post('/', calls)
.then((response) => response.data)
return dash.useClient((client) => client.post('/', calls)).then((response) => response.data)
}
}
8 changes: 2 additions & 6 deletions src/lib/bitcoin/doge-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
53 changes: 36 additions & 17 deletions src/lib/nodes/abstract.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,27 @@ export abstract class Client<N extends Node> {
}
}

// 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()

if (!node) {
console.warn(`${this.type}: No online nodes at the moment`)
return this.getNode().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<T>(cb: (client: N['client']) => T) {
const node = this.getNode()

return node.client
return cb(node.client)
}

/**
Expand Down Expand Up @@ -106,20 +116,19 @@ export abstract class Client<N extends Node> {
* @returns {ApiNode}
*/
protected getRandomNode() {
const onlineNodes = this.nodes.filter((x) => x.online && x.active && !x.outOfSync)
const onlineNodes = this.nodes.filter(this.isActiveNode)
return onlineNodes[Math.floor(Math.random() * onlineNodes.length)]
}

/**
* Returns the fastest node.
*/
protected getFastestNode() {
return this.nodes.reduce((fastest, current) => {
if (!current.online || !current.active || current.outOfSync) {
return fastest
}
return !fastest || fastest.ping > current.ping ? current : fastest
})
const onlineNodes = this.nodes.filter(this.isActiveNode)
if (onlineNodes.length === 0) return undefined
return onlineNodes.reduce((fastest, current) =>
current.ping < fastest.ping ? current : fastest
)
}

protected getNode() {
Expand All @@ -128,7 +137,7 @@ export abstract class Client<N extends 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')
throw new AllNodesOfflineError(this.type)
}

return node
Expand All @@ -138,7 +147,7 @@ export abstract class Client<N extends Node> {
* Throws an error if all the nodes are offline.
*/
assertAnyNodeOnline() {
const onlineNodes = this.nodes.filter((x) => x.online && x.active && !x.outOfSync)
const onlineNodes = this.nodes.filter(this.isActiveNode)

if (onlineNodes.length === 0) {
throw new AllNodesOfflineError(this.type)
Expand All @@ -163,4 +172,14 @@ export abstract class Client<N extends Node> {
node.outOfSync = !nodesInSync.nodes.includes(node)
}
}

protected isActiveNode(node: Node) {
return (
node.online &&
node.active &&
!node.outOfSync &&
node.hasMinNodeVersion() &&
node.hasSupportedProtocol
)
}
}
32 changes: 25 additions & 7 deletions src/lib/nodes/adm/AdmClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { isNodeOfflineError } from '@/lib/nodes/utils/errors'
import { AdmNode, Payload, RequestConfig } from './AdmNode'
import { Client } from '../abstract.client'

const CHECK_ONLINE_NODE_INTERVAL = 10000

/**
* Provides methods for calling the ADAMANT API.
*
Expand Down Expand Up @@ -41,13 +43,7 @@ export class AdmClient extends Client<AdmNode> {
* @param {RequestConfig} config request config
*/
async request<P extends Payload = Payload, R = any>(config: RequestConfig<P>): Promise<R> {
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 = await this.fetchAvailableNode()

return node.request(config).catch((error) => {
if (isNodeOfflineError(error)) {
Expand All @@ -59,4 +55,26 @@ export class AdmClient extends Client<AdmNode> {
throw error
})
}

async fetchAvailableNode() {
const node = this.useFastest ? this.getFastestNode() : this.getRandomNode()
if (node) {
return node
}

return await new Promise<AdmNode>((resolve) => {
const ticker = setInterval(() => {
let node
try {
node = this.useFastest ? this.getFastestNode() : this.getRandomNode()
if (node) {
clearInterval(ticker)
resolve(node)
}
} catch (e) {
console.error(e)
}
}, CHECK_ONLINE_NODE_INTERVAL)
})
}
}
Loading