Skip to content
Open
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4cc61a5
feat(lib/nodes/abstract.node.ts): request a node by IP if domain is n…
graycraft Apr 20, 2025
64e79a0
feat(lib/nodes/): request a node by IP if domain is not available
graycraft Apr 20, 2025
3828ade
feat(lib/nodes/eth/EthNode.ts): request a node by IP if domain is not…
graycraft Apr 21, 2025
4ce93cb
chore(src/lib/nodes/): import `NodeInfo` interface instead of explici…
graycraft Apr 22, 2025
259d74c
chore(src/lib/nodes/): rename `this.alt_ip` to `this.altIp`
graycraft Apr 22, 2025
9a5e3af
refactor(src/lib/nodes/): remove mapping of endpoint properties
graycraft May 12, 2025
8b50ec2
fix(src/lib/nodes/): add `baseURL` detection for `get` requests
graycraft May 13, 2025
32674e8
chore(src/lib/nodes/): add `console.info` for debugging domain-IP swi…
graycraft Jun 7, 2025
df7ac0d
refactor(src/lib/nodes/eth/Eth{Client,Node}.ts): create 2 clients in …
graycraft Jun 11, 2025
086cec5
refactor(src/lib/nodes/): split Eth node clients to main and alt
graycraft Jun 11, 2025
f3578ad
refactor(src/lib/nodes/): move base URL detection logic to `getBaseUR…
graycraft Jun 11, 2025
1ab8aa2
refactor(src/lib/nodes/abstract.node.ts): simplify domain-IP switch l…
graycraft Jun 17, 2025
c94387c
refactor(src/lib/nodes/): rename property to `preferDomain` and inver…
graycraft Jun 17, 2025
8010ea0
refactor(src/lib/nodes/abstract.node.ts): rename property to `availab…
graycraft Jun 17, 2025
1a78ec5
fix(src/{lib/nodes/,store/modules/{erc20,eth}}): create a new client …
graycraft Jun 17, 2025
4be85c4
perf(src/lib/nodes/eth/EthNode.ts): return cached clients
graycraft Jun 18, 2025
9f6bc2e
refactor(src/lib/nodes/eth/EthNode.ts): return main client if `altIp`…
graycraft Jun 20, 2025
2e7236e
refactor(src/lib/nodes/eth/EthClient.ts): rename `getClientInstance` …
graycraft Jun 20, 2025
c011741
refactor(src/lib/nodes): do not check node URL on client initialization
graycraft Jun 20, 2025
b574708
refactor(src/lib/nodes/eth-indexer/EthIndexer.ts): do not check node …
graycraft Jun 20, 2025
d24aa2b
chore(src/lib/nodes): remove type definition of `baseURL`
graycraft Jun 20, 2025
69e7556
refactor(src/lib/nodes): move `getBaseURL` method to the abstract nod…
graycraft Jun 20, 2025
d4e96b0
refactor(src/lib/nodes/{abstract.client.ts,eth/EthClient.ts}): move `…
graycraft Jun 21, 2025
9d7df2b
chore(src/lib/nodes/abstract.node.ts): return `this` context of node …
graycraft Jun 22, 2025
caa984f
test(src/lib/nodes/__tests__/): add tests for Ethereum nodes
graycraft Jun 22, 2025
7527cee
Merge branch 'dev' into feature/alternative-ip
graycraft Jun 29, 2025
463d175
Merge branch 'dev' into feature/alternative-ip
graycraft Aug 5, 2025
417afe1
feat(src/lib/nodes/abstract.node.ts): update URL components of a node…
graycraft Aug 18, 2025
7fbe7a0
chore(src/lib/nodes/abstract.node.ts): debug additional parameters fo…
graycraft Aug 18, 2025
abedafd
chore(src/lib/nodes/abstract.node.ts): debug messages for `startHealt…
graycraft Aug 18, 2025
b63eb13
docs(src/lib/nodes/abstract.node.ts): do not return node instance
graycraft Aug 19, 2025
ce031ae
fix(src/lib/nodes/abstract.node.ts): satisfy URL constructor requirem…
graycraft Aug 24, 2025
ae08a9d
fix(src/lib/nodes/abstract.node.ts): update health check logic in acc…
graycraft Aug 28, 2025
4afc731
chore(src/lib/nodes/abstract.node.ts): print healthcheck messages ver…
graycraft Aug 28, 2025
c16535e
fix(src/lib/nodes/abstract.node.ts): healthcheck messages text
graycraft Aug 28, 2025
0cbb116
fix(src/lib/nodes/abstract.node.ts): `firstDomainAttempt` property sw…
graycraft Aug 28, 2025
85fb7a1
fix(src/lib/nodes/abstract.node.ts): perform healthcheck only if a no…
graycraft Aug 28, 2025
a777fb3
fix(src/lib/nodes/abstract.node.ts): update `hasSupportedProtocol` af…
graycraft Aug 28, 2025
49b06ff
fix(src/lib/nodes/abstract.node.ts): enable `hasSupportedProtocol` if…
graycraft Aug 28, 2025
c9f1239
Merge branch 'dev' into feature/alternative-ip
graycraft Sep 1, 2025
0c6d1c4
chore(release-notes.md): add entry with PR #768 to improvements
graycraft Sep 1, 2025
da80374
fix(src/lib/nodes/abstract.node.ts): count connections and check prot…
graycraft Sep 2, 2025
68051ec
fix(src/lib/nodes/abstract.node.ts): check alt IP if defined and prot…
graycraft Sep 2, 2025
f7edc8c
fix(src/lib/nodes/abstract.node.ts): `connectionCount` and `preferDom…
graycraft Sep 2, 2025
d95c97d
fix(src/lib/nodes/abstract.node.ts): `this.altIp` instead of `checkAl…
graycraft Sep 2, 2025
3d54318
Merge remote-tracking branch 'origin/dev' into feature/alternative-ip
S-FrontendDev Sep 5, 2025
ee29448
feat(src/lib/nodes/abstract.node.ts): reset HealthCheck status if a n…
graycraft Sep 6, 2025
aed69d5
refactor(src/lib/nodes/abstract.node.ts): rename `connectionCount` to…
graycraft Sep 6, 2025
c989b17
fix(src/lib/nodes/abstract.node.ts): increment `healthcheckCount` if …
graycraft Sep 6, 2025
1b771fe
chore(src/lib/nodes/abstract.node.ts): output alternative IP and URL …
graycraft Sep 6, 2025
56fc4c6
chore(src/lib/nodes/abstract.node.ts): remove debugging message from …
graycraft Sep 6, 2025
4b29aa7
fix(src/lib/nodes/abstract.node.ts): reset `healthcheckInProgress` af…
graycraft Sep 6, 2025
baab6f5
fix(src/lib/nodes/abstract.node.ts): reset `height` and `outOfSync` a…
graycraft Sep 7, 2025
37528e3
fix(src/lib/nodes/abstract.node.ts): set `outOfSync` to `false`, not …
graycraft Sep 7, 2025
49e04b8
fix(src/lib/nodes/abstract.node.ts): not reset `healthcheckInProgress…
graycraft Sep 7, 2025
49d9756
fix(src/lib/nodes/abstract.node.ts): check health if a node is online…
graycraft Sep 9, 2025
2fb2dce
fix(src/lib/nodes/abstract.node.ts): behavior according to the revise…
graycraft Sep 14, 2025
d166a47
fix(src/lib/nodes/abstract.node.ts): detecting insecure requests supp…
graycraft Sep 14, 2025
c32fb90
feat(src/lib/nodes/abstract.node.ts): mark a blocked HTTP node as off…
graycraft Sep 14, 2025
8622105
fix(src/lib/nodes/abstract.node.ts): invert logic of `isHttpBlocked` …
graycraft Sep 15, 2025
aea613c
Merge remote-tracking branch 'origin/dev' into feature/alternative-ip
S-FrontendDev Sep 15, 2025
ad6abeb
style(src/lib/nodes/abstract.node.ts): add trailing commas
graycraft Sep 18, 2025
bdda8b5
feat(src/lib/nodes/abstract.node.ts): if connection lost repeat URL/I…
graycraft Sep 18, 2025
eee2431
chore(src/lib/nodes/abstract.node.ts): log message about successful c…
graycraft Sep 27, 2025
100cc3f
fix(src/lib/nodes/abstract.node.ts): persistently save connection typ…
graycraft Sep 28, 2025
8993697
fix(src/lib/nodes/abstract.node.ts): temporaly consider node offline …
graycraft Oct 8, 2025
1c3ec3d
feat(store/modules/dev-tools): add Vuex store for ADAMANT Development…
graycraft Oct 25, 2025
6571301
feat(store): add Vuex store for ADAMANT Development Tools
graycraft Oct 25, 2025
57b56e3
feat(store/types/root-state): define `devTools` in root state
graycraft Oct 25, 2025
edcaed8
feat(store/plugins/localStorage): save `devTools` state to local storage
graycraft Oct 25, 2025
06e400a
feat(src/locales/{en,ru}.json): add translation for `dev_screens.logg…
graycraft Oct 25, 2025
d855d1f
feat(views/devScreens/DevScreens): add dropdown for verbose logging
graycraft Oct 25, 2025
d36c88e
Merge branch 'feat/log-verbosity' into feature/alternative-ip
graycraft Oct 27, 2025
4f50736
test(abstract.node.ts): output status update messages at debug level,…
graycraft Oct 27, 2025
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
1 change: 1 addition & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All notable changes in this release are listed below.
- Release notes file added [#853](https://github.com/Adamant-im/adamant-im/pull/853) — [@S-FrontendDev](https://github.com/S-FrontendDev)

### Improvements
- Use secondary URL with alternate IP if a node is unavailable by primary URL [#768](https://github.com/Adamant-im/adamant-im/pull/768) — [@graycraft](https://github.com/graycraft)
- APK name changed in GitHub workflow [#839](https://github.com/Adamant-im/adamant-im/pull/839) — [@S-FrontendDev](https://github.com/S-FrontendDev)
- Wallets UI updated for better usability [#846](https://github.com/Adamant-im/adamant-im/pull/846) — [@Linhead](https://github.com/Linhead), [@adamant-al](https://github.com/adamant-al)
- ESLint updated to improve code quality [#849](https://github.com/Adamant-im/adamant-im/pull/849) — [@graycraft](https://github.com/graycraft)
Expand Down
2 changes: 1 addition & 1 deletion src/config/utils/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NodeInfo } from '@/types/wallets'
import type { NodeInfo } from '@/types/wallets'
import { BlockchainSymbol } from './types'
import config from '../index'

Expand Down
95 changes: 95 additions & 0 deletions src/lib/nodes/__tests__/eth/EthNode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest'

import { EthNode } from '../../eth/EthNode.ts'

const nodeByDomain = new EthNode({
alt_ip: 'http://95.216.114.252:44099',
url: 'https://ethnode2.adamant.im'
}),
nodeByIP = new EthNode({
alt_ip: 'http://95.216.114.252:44099',
url: 'https://aaa.adamant.im'
}),
nodeOffline = new EthNode({
alt_ip: 'http://111.111.111.111:44099',
url: 'https://aaa.adamant.im'
})

describe('client', () => {
describe('by domain', () => {
it('get block number', async () => {
expect(await nodeByDomain.client().getBlockNumber()).toBeTypeOf('bigint')
})
})
describe('by IP', () => {
it('get block number', async () => {
try {
await nodeByIP.client().getBlockNumber()
} catch (error) {
expect(error).toEqual({
message:
'request to https://aaa.adamant.im/ failed, reason: getaddrinfo ENOTFOUND aaa.adamant.im',
type: 'system',
errno: 'ENOTFOUND',
code: 'ENOTFOUND'
})
}
})
})
describe('offline', () => {
it('get block number', async () => {
try {
await nodeOffline.client().getBlockNumber()
} catch (error) {
expect(error).toEqual({
message:
'request to https://aaa.adamant.im/ failed, reason: getaddrinfo ENOTFOUND aaa.adamant.im',
type: 'system',
errno: 'ENOTFOUND',
code: 'ENOTFOUND'
})
}
})
})
})
describe('node', () => {
describe('by domain', () => {
it('start healthcheck', async () => {
const node = await nodeByDomain.startHealthcheck()

expect(node.altIp).toBe('http://95.216.114.252:44099')
expect(node.altIpAvailable).toBe(false)
expect(node.availableByDomain).toBe(true)
expect(node.hostname).toBe('ethnode2.adamant.im')
expect(node.online).toBe(true)
expect(node.preferDomain).toBe(true)
expect(node.url).toBe('https://ethnode2.adamant.im')
})
})
describe('by IP', () => {
it('start healthcheck', async () => {
const node = await nodeByIP.startHealthcheck()

expect(node.altIp).toBe('http://95.216.114.252:44099')
expect(node.altIpAvailable).toBe(false)
expect(node.availableByDomain).toBe(false)
expect(node.hostname).toBe('aaa.adamant.im')
expect(node.online).toBe(true)
expect(node.preferDomain).toBe(false)
expect(node.url).toBe('https://aaa.adamant.im')
})
})
describe('offline', () => {
it('start healthcheck', async () => {
const node = await nodeOffline.startHealthcheck()

expect(node.altIp).toBe('http://111.111.111.111:44099')
expect(node.altIpAvailable).toBe(false)
expect(node.availableByDomain).toBe(false)
expect(node.hostname).toBe('aaa.adamant.im')
expect(node.online).toBe(true)
expect(node.preferDomain).toBe(false)
expect(node.url).toBe('https://aaa.adamant.im')
})
})
})
7 changes: 0 additions & 7 deletions src/lib/nodes/abstract.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,6 @@ 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'] {
return this.getNode().client
}

/**
* Invokes a client method.
*
Expand Down
139 changes: 127 additions & 12 deletions src/lib/nodes/abstract.node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { NodeOfflineError } from '@/lib/nodes/utils/errors.ts'
import type { NodeInfo } from '@/types/wallets/index.ts'
import { getHealthCheckInterval } from './utils/getHealthcheckConfig'
import { TNodeLabel } from './constants'
import { HealthcheckInterval, HealthcheckResult, NodeKind, NodeStatus, NodeType } from './types'
Expand Down Expand Up @@ -29,6 +31,10 @@ export abstract class Node<C = unknown> {
*/
wsPort = '36668'

/**
* Node alternative IP
*/
altIp?: string
/**
* Node base URL
*/
Expand All @@ -41,6 +47,10 @@ export abstract class Node<C = unknown> {
* Node port like 36666 for http nodes (default)
*/
port: string
/**
* Indicates that a node URL protocol is the same as page URL.
*/
hasSupportedProtocol = true
/**
* Node hostname like bid.adamant.im or 23.226.231.225
*/
Expand All @@ -53,13 +63,16 @@ export abstract class Node<C = unknown> {
* If Socket port like :36668 needed for connection
*/
wsPortNeeded = false
hasSupportedProtocol = true

// Healthcheck related params
/** Healthcheck related params. */
/**
* Indicates whether node is available.
*/
online = true
/**
* Indicates whether prefer a node with alternative IP or not
*/
preferDomain = true
/**
* Node ping estimation
*/
Expand Down Expand Up @@ -92,18 +105,22 @@ export abstract class Node<C = unknown> {
onStatusChangeCallback?: (nodeStatus: ReturnType<typeof this.getStatus>) => void

timer?: NodeJS.Timeout
healthcheckCount = 0
healthCheckInterval: HealthcheckInterval = 'normal'
client: C
healthcheckInProgress = false

constructor(
url: string,
endpoint: NodeInfo,
type: NodeType,
kind: NodeKind,
label: TNodeLabel,
version = '',
minNodeVersion = ''
minNodeVersion = '',
) {
const { alt_ip, url } = endpoint

this.altIp = alt_ip
this.url = url
this.type = type
this.label = label
Expand All @@ -113,7 +130,7 @@ export abstract class Node<C = unknown> {
this.hostname = new URL(url).hostname
this.minNodeVersion = minNodeVersion
this.version = version
this.hasSupportedProtocol = !(this.protocol === 'http:' && appProtocol === 'https:')
this.hasSupportedProtocol = this.isHttpAllowed(this.protocol)
this.active = nodesStorage.isActive(url)

this.client = this.buildClient()
Expand All @@ -127,16 +144,65 @@ export abstract class Node<C = unknown> {
clearInterval(this.timer)

if (this.active && !this.healthcheckInProgress) {
const protocol = new URL(this.url).protocol

try {
this.healthcheckInProgress = true
const health = await this.checkHealth()

this.height = health.height
this.ping = health.ping
const { height, ping } = await this.checkHealth()

if (!this.healthcheckCount) {
console.info(
`[HealthCheck] Connection via ${this.getBaseURL(this)} succeeded (URL: ${this.url}${this.altIp ? ', IP: ' + this.altIp : ''}).`,
)
}

/**
* HTTP request might be fulfilled in HTTPS environment if a user allowed insecure content for the site in own Site settings (desktop browsers).
*/
if (protocol === 'http:') this.hasSupportedProtocol = true

this.healthcheckCount++
this.height = height
this.online = true
} catch {
this.online = false
this.ping = ping

console.info(
`[HealthCheck] Node status updated for ${this.getBaseURL(this)}. Height: ${height}. Ping: ${ping}. Count: ${this.healthcheckCount}. Node is online.`,
)
} catch (error) {
const code = (error as NodeOfflineError).code ?? 'unknown'

console.info(
`[HealthCheck] Connection via ${this.getBaseURL(this)} failed (URL: ${this.url}${this.altIp ? ', IP: ' + this.altIp : ''}). ${code ? `Error code: ${code}` : ''}.`,
)

if (this.preferDomain) {
if (!this.altIp) {
if (protocol === 'https:' || this.isHttpAllowed(protocol)) this.online = false

console.info(
`[HealthCheck] Alternative IP is not defined for ${this.getBaseURL(this)}. Node is offline.`,
)
}
if (this.healthcheckCount < 1) {
this.preferDomain = false
} else {
this.online = false
}
} else {
if (protocol === 'https:' || this.isHttpAllowed(protocol)) this.online = false

console.info(
`[HealthCheck] Node is not reachable by URL ${this.url}${this.altIp ? ' and by alternative IP ' + this.altIp : ''}. Node is offline.`,
)

if (this.healthcheckCount < 2) {
if (this.altIp) this.preferDomain = true
}
}
} finally {
this.updateURL()
this.healthcheckInProgress = false
}

Expand All @@ -145,8 +211,10 @@ export abstract class Node<C = unknown> {

this.timer = setTimeout(
() => this.startHealthcheck(),
getHealthCheckInterval(this.label, this.online ? this.healthCheckInterval : 'crucial')
getHealthCheckInterval(this.label, this.online ? this.healthCheckInterval : 'crucial'),
)

return this
}

updateHealthCheckInterval(interval: HealthcheckInterval) {
Expand All @@ -162,8 +230,20 @@ export abstract class Node<C = unknown> {
this.onStatusChangeCallback = callback
}

/**
* Get base URL for requests depending on availability of a node's domain.
* @param { Node } node A node instance.
* @returns { string } Base URL.
*/
getBaseURL(node: Node): string {
const baseURL = node.preferDomain ? node.url : (node.altIp ?? node.url)

return baseURL
}

getStatus() {
return {
alt_ip: this.altIp,
url: this.url,
port: this.port,
hostname: this.hostname,
Expand All @@ -184,7 +264,7 @@ export abstract class Node<C = unknown> {
status: this.getNodeStatus(),
type: this.type,
label: this.label,
formattedHeight: this.formatHeight(this.height)
formattedHeight: this.formatHeight(this.height),
}
}

Expand Down Expand Up @@ -217,6 +297,18 @@ export abstract class Node<C = unknown> {
return this.version >= this.minNodeVersion
}

/**
* Can not be sure, whether HTTP nodes are allowed or blocked in HTTPS environment, until an actual request is fulfilled.
* Therefore, only assume HTTP is not allowed in HTTPS environment in advance here.
* @param { "http:" | "https:" } protocol Data transfer protocol.
* @returns { boolean } Whether a HTTP node is allowed or not.
*/
isHttpAllowed(protocol: string): boolean {
const blocked = !(protocol === 'http:' && appProtocol === 'https:')

return blocked
}

protected abstract checkHealth(): Promise<HealthcheckResult>
protected abstract buildClient(): C

Expand All @@ -226,11 +318,34 @@ export abstract class Node<C = unknown> {
toggleNode(active: boolean) {
this.active = active

/** Reset properties for HealthCheck to default if a node enabled again. */
if (active) {
this.healthcheckCount = 0
this.online = true
this.preferDomain = true
}

nodesStorage.saveActive(this.url, active)

return this.getStatus()
}

/**
* Update URL components depending on availability of a node by URL with domain.
* `altIp` and `url` always contain 'http:' or 'https:' in address.
*/
updateURL() {
const baseURL = this.getBaseURL(this)

/** New URL constructor requires `baseURL` to be a valid URL, otherwise throws TypeError. */
if (baseURL) {
this.hostname = new URL(baseURL).hostname
this.port = new URL(baseURL).port
this.protocol = new URL(baseURL).protocol as HttpProtocol
}
this.wsProtocol = this.protocol === 'https:' ? 'wss:' : 'ws:'
}

displayVersion() {
return this.version ? `v${this.version}` : ''
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/nodes/adm/AdmClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RegisterChatMessageTransaction
} from '@/lib/schema/client'
import { NODE_LABELS } from '@/lib/nodes/constants'
import type { NodeInfo } from '@/types/wallets'
import { AdmNode, Payload, RequestConfig } from './AdmNode'
import { Client } from '../abstract.client'

Expand All @@ -16,7 +17,7 @@ import { Client } from '../abstract.client'
* is not available at the moment.
*/
export class AdmClient extends Client<AdmNode> {
constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') {
constructor(endpoints: NodeInfo[] = [], minNodeVersion = '0.0.0') {
super('adm', 'node', NODE_LABELS.AdmNode)
this.nodes = endpoints.map((endpoint) => new AdmNode(endpoint, minNodeVersion))
this.minNodeVersion = minNodeVersion
Expand Down
Loading