Skip to content

Commit

Permalink
Merge pull request #597 from Adamant-im/feat/eth-node-web3-client-wra…
Browse files Browse the repository at this point in the history
…pper

Chat: add Nodes offline dialog
  • Loading branch information
bludnic authored Jul 8, 2024
2 parents 3297015 + 95106d7 commit 3bcd398
Show file tree
Hide file tree
Showing 23 changed files with 318 additions and 115 deletions.
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')
} 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

0 comments on commit 3bcd398

Please sign in to comment.