From 5705b8e91b38a7b0359a40dc3f5e2fcc37837bed Mon Sep 17 00:00:00 2001 From: everoddandeven Date: Tue, 8 Oct 2024 22:45:24 +0200 Subject: [PATCH] Network methods implementation Peers methods implementation Bans methods implementation --- .../services/daemon/daemon-data.service.ts | 56 ++++++++- .../core/services/daemon/daemon.service.ts | 39 ++++++- src/app/pages/bans/bans.component.html | 100 +++++++++++++--- src/app/pages/bans/bans.component.ts | 108 ++++++++++++------ src/app/pages/network/network.component.html | 86 ++++++++++++++ src/app/pages/network/network.component.ts | 66 ++++++++++- src/app/pages/peers/peers.component.html | 33 +++++- src/app/pages/peers/peers.component.ts | 41 ++++++- .../transactions/transactions.component.html | 66 ++++++++++- .../transactions/transactions.component.ts | 100 +++++++++++++--- src/common/PeerInfo.ts | 35 ++++++ src/common/SpentKeyImage.ts | 16 +++ src/common/TxPool.ts | 24 ++++ src/common/UnconfirmedTx.ts | 90 +++++++++++++++ src/common/index.ts | 4 + src/common/request/GetPeerListRequest.ts | 11 ++ .../request/GetTransactionPoolRequest.ts | 11 ++ src/common/request/index.ts | 2 + 18 files changed, 804 insertions(+), 84 deletions(-) create mode 100644 src/common/PeerInfo.ts create mode 100644 src/common/SpentKeyImage.ts create mode 100644 src/common/TxPool.ts create mode 100644 src/common/UnconfirmedTx.ts create mode 100644 src/common/request/GetPeerListRequest.ts create mode 100644 src/common/request/GetTransactionPoolRequest.ts diff --git a/src/app/core/services/daemon/daemon-data.service.ts b/src/app/core/services/daemon/daemon-data.service.ts index c7f02e3..6215fa5 100644 --- a/src/app/core/services/daemon/daemon-data.service.ts +++ b/src/app/core/services/daemon/daemon-data.service.ts @@ -1,6 +1,6 @@ import { EventEmitter, Injectable } from '@angular/core'; import { DaemonService } from './daemon.service'; -import { BlockCount, BlockHeader, Chain, CoreIsBusyError, DaemonInfo, MinerData, MiningStatus, NetStats, NetStatsHistory, PublicNode, SyncInfo } from '../../../../common'; +import { BlockCount, BlockHeader, Chain, Connection, CoreIsBusyError, DaemonInfo, MinerData, MiningStatus, NetStats, NetStatsHistory, PeerInfo, PublicNode, SyncInfo, TxPool } from '../../../../common'; @Injectable({ providedIn: 'root' @@ -12,6 +12,7 @@ export class DaemonDataService { private _refreshing: boolean = false; private _firstRefresh: boolean = true; private _lastRefresh: number = Date.now(); + private _lastRefreshHeight: number = -1; private _daemonRunning: boolean = false; private _daemonRestarting: boolean = false; @@ -48,6 +49,15 @@ export class DaemonDataService { private _publicNodes: PublicNode[] = []; private _gettingPublicNodes: boolean = false; + private _transactionPool?: TxPool; + private _gettingTransactionPool: boolean = false; + + private _connections: Connection[] = []; + private _gettingConnections: boolean = false; + + private _peerList: PeerInfo[] = []; + private _gettingPeerList: boolean = false; + public readonly syncStart: EventEmitter = new EventEmitter(); public readonly syncEnd: EventEmitter = new EventEmitter(); public readonly syncError: EventEmitter = new EventEmitter(); @@ -182,6 +192,30 @@ export class DaemonDataService { return this._gettingPublicNodes; } + public get transactionPool(): TxPool | undefined { + return this._transactionPool; + } + + public get gettingTransactionPool(): boolean { + return this._gettingTransactionPool; + } + + public get connections(): Connection[] { + return this._connections; + } + + public get gettingConnections(): boolean { + return this._gettingConnections; + } + + public get peerList(): PeerInfo[] { + return this._peerList; + } + + public get gettingPeerList(): boolean { + return this._gettingPeerList; + } + public setRefreshTimeout(ms: number = 5000): void { this.refreshTimeoutMs = ms; } @@ -324,12 +358,25 @@ export class DaemonDataService { await this.refreshMinerData(); + this._gettingPeerList = true; + this._peerList = await this.daemonService.getPeerList(); + this._gettingPeerList = false; + this._gettingPublicNodes = true; - this._publicNodes = await this.daemonService.getPublicNodes(true, true); - this._gettingPublicNodes = false; + if (this._daemonInfo.synchronized && this._daemonInfo.txPoolSize > 0) { + this._gettingTransactionPool = true; + this._transactionPool = await this.daemonService.getTransactionPool(); + this._gettingTransactionPool = false; + } + + this._gettingConnections = true; + this._connections = await this.daemonService.getConnections(); + this._gettingConnections = false; + + this._lastRefreshHeight = this._daemonInfo.heightWithoutBootstrap; this._lastRefresh = Date.now(); } catch(error) { console.error(error); @@ -341,6 +388,9 @@ export class DaemonDataService { this._gettingAltChains = false; this._gettingNetStats = false; this._gettingPublicNodes = false; + this._gettingTransactionPool = false; + this._gettingConnections = false; + this._gettingPeerList = false; this.syncError.emit(error); diff --git a/src/app/core/services/daemon/daemon.service.ts b/src/app/core/services/daemon/daemon.service.ts index bac2244..1b8b13f 100644 --- a/src/app/core/services/daemon/daemon.service.ts +++ b/src/app/core/services/daemon/daemon.service.ts @@ -41,7 +41,9 @@ import { SetBootstrapDaemonRequest, SetLogLevelRequest, SetLogHashRateRequest, - SetLogCategoriesRequest + SetLogCategoriesRequest, + GetTransactionPoolRequest, + GetPeerListRequest } from '../../../../common/request'; import { BlockTemplate } from '../../../../common/BlockTemplate'; import { GeneratedBlocks } from '../../../../common/GeneratedBlocks'; @@ -76,6 +78,7 @@ import { TxInfo } from '../../../../common/TxInfo'; import { DaemonSettings } from '../../../../common/DaemonSettings'; import { MethodNotFoundError } from '../../../../common/error/MethodNotFoundError'; import { openDB, IDBPDatabase } from "idb" +import { PeerInfo, TxPool } from '../../../../common'; @Injectable({ providedIn: 'root' @@ -411,7 +414,12 @@ export class DaemonService { public async getConnections(): Promise { const response = await this.callRpc(new GetConnectionsRequest()); - const connections: any[] = response.connections; + + if (!response.result || !response.result.connections) { + return []; + } + + const connections: any[] = response.result.connections; const result: Connection[] = []; connections.forEach((connection: any) => result.push(Connection.parse(connection))) @@ -797,6 +805,23 @@ export class DaemonService { return NetStats.parse(response); } + public async getPeerList(): Promise { + const response = await this.callRpc(new GetPeerListRequest()); + + if (response.status != 'OK') { + throw new Error(response.status); + } + + const peerList: PeerInfo[] = []; + const whiteList: any[] | undefined = response.white_list + const grayList: any[] | undefined = response.gray_list; + + if (whiteList) whiteList.forEach((white: any) => peerList.push(PeerInfo.parse(white, 'white'))); + if (grayList) grayList.forEach((gray: any) => peerList.push(PeerInfo.parse(gray, 'gray'))); + + return peerList; + } + public async getPublicNodes(whites: boolean = true, grays: boolean = false, includeBlocked: boolean = false): Promise { const response = await this.callRpc(new GetPublicNodesRequest(whites, grays, includeBlocked)); @@ -810,6 +835,16 @@ export class DaemonService { return nodes; } + public async getTransactionPool(): Promise { + const response = await this.callRpc(new GetTransactionPoolRequest()); + + if (response.status != 'OK') { + throw new Error(response.status); + } + + return TxPool.parse(response); + } + public async getTransactionPoolHashes(): Promise { const response = await this.callRpc(new GetTransactionPoolHashesRequest()); diff --git a/src/app/pages/bans/bans.component.html b/src/app/pages/bans/bans.component.html index 7baea7e..4c6f764 100644 --- a/src/app/pages/bans/bans.component.html +++ b/src/app/pages/bans/bans.component.html @@ -11,21 +11,91 @@ -
- - - - - - - - -
HostIpSeconds
+
+
+

List of banned IPs

+
+ + + + + + + + +
HostIpSeconds
+ +
+ + + + +
+ +
+

Ban another node by IP

+ + +
+
+ + + + +
+
+ +
+ + +
+ Invalid bans provided. +
+ List of nodes to ban +
+
+ +
+
+
+ +
+ + + + +
+
diff --git a/src/app/pages/bans/bans.component.ts b/src/app/pages/bans/bans.component.ts index b5eca76..5fa073d 100644 --- a/src/app/pages/bans/bans.component.ts +++ b/src/app/pages/bans/bans.component.ts @@ -1,8 +1,9 @@ import { AfterViewInit, Component, NgZone } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; import { NavbarService } from '../../shared/components/navbar/navbar.service'; import { DaemonService } from '../../core/services/daemon/daemon.service'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; +import { DaemonDataService } from '../../core/services'; +import { Ban } from '../../../common'; @Component({ @@ -15,61 +16,85 @@ export class BansComponent implements AfterViewInit { new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', true, 'Overview', true), new NavbarLink('pills-set-bans-tab', '#pills-set-bans', 'pills-set-bans', false, 'Set Bans', true) ]; - public daemonRunning: boolean = false; + + public refreshingBansTable: boolean = false; + + public get daemonRunning(): boolean { + return this.daemonData.running; + } + + public get daemonStopping(): boolean { + return this.daemonData.stopping; + } + public get daemonChangingStatus(): boolean { return this.daemonService.stopping || this.daemonService.starting; } - constructor(private router: Router, private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) { - this.router.events.subscribe((event) => { - if (event instanceof NavigationEnd) { - if (event.url != '/bans') return; - this.onNavigationEnd(); + public settingBans: boolean = false; + public setBansBansJsonString: string = ''; + public setBansSuccess: boolean = false; + public setBansError: string = ''; + + public get validBans(): boolean { + try { + const bans: any[] = JSON.parse(this.setBansBansJsonString); + + if (!Array.isArray(bans)) { + return false; } - }) - this.daemonService.isRunning().then((running) => { - this.daemonRunning = running - }); - - this.daemonService.onDaemonStatusChanged.subscribe((running: boolean) => { - this.daemonRunning = running; - - if (running) this.load(); - }); + bans.forEach((ban) => Ban.parse(ban)); + return true; + } + catch(error) { + return false; + } } - ngAfterViewInit(): void { - this.navbarService.removeLinks(); + private get bans(): Ban[] { + if (!this.validBans) { + return []; + } + const bans: Ban[] = []; + const rawBans: any[] = []; - console.log('BansComponent AFTER VIEW INIT'); + rawBans.forEach((rawBan) => bans.push(Ban.parse(rawBan))); + + return bans; + } + + constructor(private daemonData: DaemonDataService, private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) { + } + + public ngAfterViewInit(): void { + this.navbarService.setLinks(this.navbarLinks); this.ngZone.run(() => { - //const $ = require('jquery'); - //const bootstrapTable = require('bootstrap-table'); - const $table = $('#bansTable'); - $table.bootstrapTable({ - - }); + $table.bootstrapTable({}); $table.bootstrapTable('refreshOptions', { classes: 'table table-bordered table-hover table-dark table-striped' }); $table.bootstrapTable('showLoading'); - this.load(); + this.refreshBansTable(); }); } - private onNavigationEnd(): void { - this.navbarService.removeLinks(); - } - - private async load(): Promise { + public async refreshBansTable(): Promise { const $table = $('#bansTable'); + let _bans: Ban[] = []; + + try { + _bans = await this.daemonService.getBans(); + } + catch (error) { + console.error(error); + _bans = []; + } - const _bans = await this.daemonService.getBans(); const bans: any[] = []; _bans.forEach((ban) => bans.push({ @@ -82,8 +107,21 @@ export class BansComponent implements AfterViewInit { } - public async setBans() { - + public async setBans(): Promise { + this.settingBans = true; + + try { + await this.daemonService.setBans(...this.bans); + this.setBansError = ''; + this.setBansSuccess = true; + } + catch (error) { + console.error(error); + this.setBansSuccess = false; + this.setBansError = `${error}`; + } + + this.settingBans = false; } } diff --git a/src/app/pages/network/network.component.html b/src/app/pages/network/network.component.html index 5d84aa7..777685e 100644 --- a/src/app/pages/network/network.component.html +++ b/src/app/pages/network/network.component.html @@ -12,6 +12,7 @@
+

Bytes In

@@ -19,6 +20,91 @@

Bytes Out

+ +
+

Information about incoming and outgoing connections to your node

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
IDPeer IDAddressHostIPPortStateAvg DownloadAvg UploadCurrent DownloadCurrent UploadHeightIncomingLive TimeRecv CountRecv Idle TimeSend CountSend Idle TimeSupport Flags
+
+
+ +
+

Set daemon bandwidth limits

+
+ + + + + +
+
+
+ +
+ + + Download limit in kBytes per second (-1 reset to default, 0 don't change the current limit) +
+ +
+ + + Upload limit in kBytes per second (-1 reset to default, 0 don't change the current limit) +
+ +
+
+
+ +
+ + + + +
+ + +
diff --git a/src/app/pages/network/network.component.ts b/src/app/pages/network/network.component.ts index 06fee3b..df6d630 100644 --- a/src/app/pages/network/network.component.ts +++ b/src/app/pages/network/network.component.ts @@ -1,16 +1,17 @@ -import { AfterViewInit, Component } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy } from '@angular/core'; import { NavbarService } from '../../shared/components/navbar/navbar.service'; import { DaemonDataService, DaemonService } from '../../core/services'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; import { Chart, ChartData } from 'chart.js/auto' import { NetStatsHistoryEntry } from '../../../common'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-network', templateUrl: './network.component.html', styleUrl: './network.component.scss' }) -export class NetworkComponent implements AfterViewInit { +export class NetworkComponent implements AfterViewInit, OnDestroy { public readonly navbarLinks: NavbarLink[]; private netStatsBytesInChart?: Chart; @@ -24,15 +25,29 @@ export class NetworkComponent implements AfterViewInit { return this.daemonData.stopping; } + public limiting: boolean = false; + public limitUp: number = 0; + public limitDown: number = 0; + public setLimitResult?: { limitUp: number, limitDown: number }; + public setLimitSuccess: boolean = false; + public setLimitError: string = ''; + + private subscriptions: Subscription[] = []; + constructor(private navbarService: NavbarService, private daemonService: DaemonService, private daemonData: DaemonDataService) { this.navbarLinks = [ new NavbarLink('pills-net-stats-tab', '#pills-net-stats', 'pills-net-stats', false, 'Statistics'), - new NavbarLink('pills-limits-tab', '#pills-limits', 'pills-limits', false, 'Limits') + new NavbarLink('pills-connections-tab', '#pills-connections', 'connections', false, 'Connetions'), + new NavbarLink('pills-limits-tab', '#pills-limit', 'pills-limit', false, 'Limit') ]; - this.daemonData.netStatsRefreshEnd.subscribe(() => { + this.subscriptions.push(this.daemonData.netStatsRefreshEnd.subscribe(() => { this.refreshNetStatsHistory(); - }); + })); + + this.subscriptions.push(this.daemonData.syncEnd.subscribe(() => { + this.loadConnectionsTable(); + })); this.daemonService.onDaemonStatusChanged.subscribe((running: boolean) => { if (!running) { @@ -47,6 +62,29 @@ export class NetworkComponent implements AfterViewInit { public ngAfterViewInit(): void { this.navbarService.setLinks(this.navbarLinks); this.initNetStatsHistoryChart(); + this.initConnectionsTable(); + } + + public ngOnDestroy(): void { + this.subscriptions.forEach((sub) => { + sub.unsubscribe(); + }); + + this.subscriptions = []; + } + + private initConnectionsTable(): void { + const $table = $('#connectionsTable'); + $table.bootstrapTable({}); + $table.bootstrapTable('refreshOptions', { + classes: 'table table-bordered table-hover table-dark table-striped' + }); + } + + private loadConnectionsTable(): void { + const $table = $('#connectionsTable'); + + $table.bootstrapTable('load', this.daemonData.connections); } private buildChartBytesInData(): ChartData { @@ -159,4 +197,22 @@ export class NetworkComponent implements AfterViewInit { } } + + public async setLimit(): Promise { + this.limiting = true; + + try { + this.setLimitResult = await this.daemonService.setLimit(this.limitUp, this.limitDown); + this.setLimitSuccess = true; + this.setLimitError = ''; + } + catch (error) { + console.error(error); + this.setLimitResult = undefined; + this.setLimitSuccess = false; + this.setLimitError = `${error}`; + } + + this.limiting = false; + } } \ No newline at end of file diff --git a/src/app/pages/peers/peers.component.html b/src/app/pages/peers/peers.component.html index f8eaba6..a3f3609 100644 --- a/src/app/pages/peers/peers.component.html +++ b/src/app/pages/peers/peers.component.html @@ -12,7 +12,38 @@
-
+
+

List of known peers

+
+ + + + + + + + + + + +
HostIDIPLast SeenPortType
+
+ + +
+ + + + +
+ +

Public peer information

{ const $publicNodesTable = $('#publicNodesTable'); + const $peerListTable = $('#peerListTable'); + $publicNodesTable.bootstrapTable({}); $publicNodesTable.bootstrapTable('refreshOptions', { classes: 'table table-bordered table-hover table-dark table-striped' }); + $peerListTable.bootstrapTable({}); + $peerListTable.bootstrapTable('refreshOptions', { + classes: 'table table-bordered table-hover table-dark table-striped' + }); + $publicNodesTable.bootstrapTable('load', this.daemonData.publicNodes); - + $peerListTable.bootstrapTable('load', this.daemonData.peerList); + const sub = this.daemonData.syncEnd.subscribe(() => { $publicNodesTable.bootstrapTable('load', this.daemonData.publicNodes); + //$peerListTable.bootstrapTable('load', this.daemonData.peerList); }); this.subscriptions.push(sub); }); } + public async refreshPeerListTable(): Promise { + this.refreshingPeerList = true; + + try { + await new Promise((resolve, reject) => { + setTimeout(() => { + this.ngZone.run(() => { + try { + const $peerListTable = $('#peerListTable'); + $peerListTable.bootstrapTable('load', this.daemonData.peerList); + resolve(); + } + catch(error) { + reject(error); + } + }); + }, 1000); + }); + } + catch(error) { + console.error(error); + } + + this.refreshingPeerList = false; + } + public ngOnDestroy(): void { this.subscriptions.forEach((sub) => { sub.unsubscribe(); diff --git a/src/app/pages/transactions/transactions.component.html b/src/app/pages/transactions/transactions.component.html index 1302350..bc38482 100644 --- a/src/app/pages/transactions/transactions.component.html +++ b/src/app/pages/transactions/transactions.component.html @@ -11,9 +11,63 @@ -
+
+ +
+

Information about valid transactions seen by the node but not yet mined into a block, as well as spent key image information for the txpool in the node's memory

+
+ +
Transactions
+
+
+ + + + + + + + + + + + + + + + + + +
ID HashRelayedBlob SizeDo Not RelayDouble Spend SeenFeeKept By BlockLast Failed HeightLast Failed Id HashLast Relayed TimeMax Used Block HeightMax Used Block Id HashReceive TimeTx Blob
+
+ +
+ +
Spent Key Images
+
+ + + + + + +
ID Hash
+
+ +
+ +
-
+
@@ -95,12 +150,12 @@
Stop relaying transaction to other nodes
-
+
@@ -152,7 +207,7 @@
-
+

Flush a list of transaction IDs

@@ -180,7 +235,7 @@
-
+

Flush a list of bad transaction IDs from cache

@@ -207,6 +262,7 @@
+
diff --git a/src/app/pages/transactions/transactions.component.ts b/src/app/pages/transactions/transactions.component.ts index 08956fb..9acbb6f 100644 --- a/src/app/pages/transactions/transactions.component.ts +++ b/src/app/pages/transactions/transactions.component.ts @@ -1,16 +1,20 @@ -import { AfterViewInit, Component, NgZone } from '@angular/core'; +import { AfterViewInit, Component, NgZone, OnDestroy } from '@angular/core'; import { DaemonService } from '../../core/services/daemon/daemon.service'; import { NavbarService } from '../../shared/components/navbar/navbar.service'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; import { TxBacklogEntry } from '../../../common/TxBacklogEntry'; import { SimpleBootstrapCard } from '../../shared/utils'; +import { DaemonDataService } from '../../core/services'; +import { table } from 'console'; +import { SpentKeyImage, UnconfirmedTx } from '../../../common'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-transactions', templateUrl: './transactions.component.html', styleUrl: './transactions.component.scss' }) -export class TransactionsComponent implements AfterViewInit { +export class TransactionsComponent implements AfterViewInit, OnDestroy { public readonly navbarLinks: NavbarLink[]; public canRelay: boolean; @@ -29,7 +33,13 @@ export class TransactionsComponent implements AfterViewInit { return this.txIdsJsonString != ''; } - public daemonRunning: boolean = false; + public get daemonRunning(): boolean { + return this.daemonData.running; + } + + public get daemonStopping(): boolean { + return this.daemonData.stopping; + } public rawTxJsonString: string = ''; public sendRawTxDoNotRelay: boolean = false; @@ -37,9 +47,28 @@ export class TransactionsComponent implements AfterViewInit { public sendRawTxError: string = ''; public sendingRawTx: boolean = false; - constructor(private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) { + private get unconfirmedTxs(): UnconfirmedTx[] { + if (!this.daemonData.transactionPool) { + return []; + } + + return this.daemonData.transactionPool.transactions; + } + + private get spentKeyImages(): SpentKeyImage[] { + if (!this.daemonData.transactionPool) { + return []; + } + + return this.daemonData.transactionPool.spentKeyImages; + } + + private subscriptions: Subscription[] = []; + + constructor(private daemonData: DaemonDataService, private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) { this.navbarLinks = [ - new NavbarLink('pills-relay-tx-tab', '#pills-relay-tx', 'pills-relay-tx', true, 'Relay Tx'), + new NavbarLink('pills-tx-pool-tab', '#pills-tx-pool', 'pills-tx-pool', true, 'Pool'), + new NavbarLink('pills-relay-tx-tab', '#pills-relay-tx', 'pills-relay-tx', false, 'Relay Tx'), new NavbarLink('pills-send-raw-tx-tab', '#pills-send-raw-tx', 'pills-send-raw-tx', false, 'Send Raw Tx'), new NavbarLink('pills-tx-backlog-tab', '#pills-tx-backlog', 'pills-tx-backlog', false, 'Tx Backlog'), new NavbarLink('pills-coinbase-tx-sum-tab', '#pills-coinbase-tx-sum', 'pills-coinbase-tx-sum', false, 'Coinbase Tx Sum'), @@ -51,26 +80,63 @@ export class TransactionsComponent implements AfterViewInit { this.txPoolBacklog = []; this.canRelay = false; - this.daemonService.onDaemonStatusChanged.subscribe((running) => { - this.ngZone.run(() => { - this.daemonRunning = running; - }); - }); - this.daemonService.isRunning().then((value: boolean) => { - this.ngZone.run(() => { - this.daemonRunning = value; - }); - }); } - ngAfterViewInit(): void { + public ngAfterViewInit(): void { + this.ngZone.run(() => { this.navbarService.setLinks(this.navbarLinks); + + this.initTables(); + this.subscriptions.push(this.daemonData.syncEnd.subscribe(() => { + this.refreshTables(); + })); + this.load().then(() => { this.navbarService.enableLinks(); }).catch((error) => { console.error(error); this.navbarService.disableLinks(); - }) + }); + }); + } + + public ngOnDestroy(): void { + this.subscriptions.forEach((sub) => { + sub.unsubscribe(); + }); + + this.subscriptions = []; + } + + private initTable(id: string): void { + const $table = $(`#${id}`); + + $table.bootstrapTable({}); + $table.bootstrapTable('refreshOptions', { + classes: 'table table-bordered table-hover table-dark table-striped' + }); + } + + private initTables(): void { + this.initTable('spentKeyImagesTable'); + this.initTable('transactionsTable'); + } + + private loadTransactionsTable(): void { + const $table = $('#transactionsTable'); + + $table.bootstrapTable('load', this.unconfirmedTxs); + } + + private loadSpentKeyImagesTable(): void { + const $table = $('#spentKeyImagesTable'); + + $table.bootstrapTable('load', this.spentKeyImages); + } + + private refreshTables(): void { + this.loadSpentKeyImagesTable(); + this.loadTransactionsTable(); } private async load(): Promise { diff --git a/src/common/PeerInfo.ts b/src/common/PeerInfo.ts new file mode 100644 index 0000000..7bae07b --- /dev/null +++ b/src/common/PeerInfo.ts @@ -0,0 +1,35 @@ +export class PeerInfo { + public readonly type: 'white' | 'gray'; + public readonly host: number; + public readonly id: string; + public readonly ip: number; + public readonly lastSeen: number; + public readonly port: number; + + constructor(type: 'white' | 'gray', host: number, id: string, ip: number, lastSeen: number, port: number) { + this.type = type; + this.host = host; + this.id = id; + this.ip = ip; + this.lastSeen = lastSeen; + this.port = port; + } + + public static parse(peerInfo: any, type: 'white' | 'gray'): PeerInfo { + const host = peerInfo.host; + const id = peerInfo.id; + const ip = peerInfo.ip; + const lastSeen = peerInfo.last_seen; + const port = peerInfo.port; + + return new PeerInfo(type, host, id, ip, lastSeen, port); + } +} + +/** + * host - unsigned int; IP address in integer format +id - string; Peer id +ip - unsigned int; IP address in integer format +last_seen - unsigned int; unix time at which the peer has been seen for the last time +port - unsigned int; TCP port the peer is using to connect to monero network. + */ \ No newline at end of file diff --git a/src/common/SpentKeyImage.ts b/src/common/SpentKeyImage.ts new file mode 100644 index 0000000..5c388cd --- /dev/null +++ b/src/common/SpentKeyImage.ts @@ -0,0 +1,16 @@ +export class SpentKeyImage { + public readonly idHash: string; + public readonly txsHashes: string[]; + + constructor(idHash: string, txsHashes: string[]) { + this.idHash = idHash; + this.txsHashes = txsHashes; + } + + public static parse(spentKeyImage: any): SpentKeyImage { + const idHash = spentKeyImage.id_hash; + const txsHashes = spentKeyImage.txs_hashes; + + return new SpentKeyImage(idHash, txsHashes); + } +} \ No newline at end of file diff --git a/src/common/TxPool.ts b/src/common/TxPool.ts new file mode 100644 index 0000000..fa85238 --- /dev/null +++ b/src/common/TxPool.ts @@ -0,0 +1,24 @@ +import { SpentKeyImage } from "./SpentKeyImage"; +import { UnconfirmedTx } from "./UnconfirmedTx"; + +export class TxPool { + public readonly transactions: UnconfirmedTx[]; + public readonly spentKeyImages: SpentKeyImage[]; + + constructor(transactions: UnconfirmedTx[], spentKeyImages: SpentKeyImage[]) { + this.transactions = transactions; + this.spentKeyImages = spentKeyImages; + } + + public static parse(txPool: any): TxPool { + const _transactions: any[] | undefined = txPool.transactions + const _spentKeyImages: any[] | undefined = txPool.spent_key_images; + const transactions: UnconfirmedTx[] = []; + const spentKeyImages: SpentKeyImage[] = []; + + if (_transactions) _transactions.forEach((tx: any) => transactions.push(UnconfirmedTx.parse(tx))); + if (_spentKeyImages) _spentKeyImages.forEach((ski: any) => spentKeyImages.push(SpentKeyImage.parse(ski))); + + return new TxPool(transactions, spentKeyImages); + } +} \ No newline at end of file diff --git a/src/common/UnconfirmedTx.ts b/src/common/UnconfirmedTx.ts new file mode 100644 index 0000000..77c96d8 --- /dev/null +++ b/src/common/UnconfirmedTx.ts @@ -0,0 +1,90 @@ + +export class UnconfirmedTx { + public readonly blobSize: number; + public readonly doNotRelay: boolean; + public readonly doubleSpendSeen: boolean; + public readonly fee: number; + public readonly idHash: string; + public readonly keptByBlock: boolean; + public readonly lastFailedHeight: number; + public readonly lastFailedIdHash: string; + public readonly lastRelayedTime: number; + public readonly maxUsedBlockHeight: number; + public readonly maxUsedBlockIdHash: string; + public readonly receiveTime: number; + public readonly relayed: boolean; + public readonly txBlob: string; + + + constructor( + blobSize: number, + doNotRelay: boolean, + doubleSpendSeen: boolean, + fee: number, + idHash: string, + keptByBlock: boolean, + lastFailedHeight: number, + lastFailedIdHash: string, + lastRelayedTime: number, + maxUsedBlockHeight: number, + maxUsedBlockIdHash: string, + receiveTime: number, + relayed: boolean, + txBlob: string + ) { + this.blobSize = blobSize; + this.doNotRelay = doNotRelay; + this.doubleSpendSeen = doubleSpendSeen; + this.fee = fee; + this.idHash = idHash; + this.keptByBlock = keptByBlock; + this.lastFailedHeight = lastFailedHeight; + this.lastFailedIdHash = lastFailedIdHash; + this.lastRelayedTime = lastRelayedTime; + this.maxUsedBlockHeight = maxUsedBlockHeight; + this.maxUsedBlockIdHash = maxUsedBlockIdHash; + this.receiveTime = receiveTime; + this.relayed = relayed; + this.txBlob = txBlob; + } + + public static parse(unconfirmedTx: any): UnconfirmedTx { + const blobSize = unconfirmedTx.blob_size; + const doNotRelay = unconfirmedTx.do_not_relay; + const doubleSpendSeen = unconfirmedTx.double_spend_seen; + const fee = unconfirmedTx.fee; + const idHash = unconfirmedTx.id_hash; + const keptByBlock = unconfirmedTx.kept_by_block; + const lastFailedHeight = unconfirmedTx.last_failed_height; + const lastFailedIdHash = unconfirmedTx.last_failed_id_hash; + const lastRelayedTime = unconfirmedTx.last_relayed_time; + const maxUsedBlockHeight = unconfirmedTx.max_used_block_height; + const maxUsedBlockIdHash = unconfirmedTx.max_used_block_id_hash; + const receiveTime = unconfirmedTx.receive_time; + const relayed = unconfirmedTx.relayed; + const txBlob = unconfirmedTx.tx_blob; + + return new UnconfirmedTx( + blobSize, doNotRelay, doubleSpendSeen, fee, idHash, keptByBlock, + lastFailedHeight, lastFailedIdHash, lastRelayedTime, maxUsedBlockHeight, + maxUsedBlockIdHash, receiveTime, relayed, txBlob + ); + } +} + +/** + * blob_size - unsigned int; The size of the full transaction blob. +do_not_relay; boolean; States if this transaction should not be relayed +double_spend_seen - boolean; States if this transaction has been seen as double spend. +fee - unsigned int; The amount of the mining fee included in the transaction, in atomic units. +id_hash - string; The transaction ID hash. +kept_by_block - boolean; States if the tx was included in a block at least once (true) or not (false). +last_failed_height - unsigned int; If the transaction validation has previously failed, this tells at what height that occurred. +last_failed_id_hash - string; Like the previous, this tells the previous transaction ID hash. +last_relayed_time - unsigned int; Last unix time at which the transaction has been relayed. +max_used_block_height - unsigned int; Tells the height of the most recent block with an output used in this transaction. +max_used_block_id_hash - string; Tells the hash of the most recent block with an output used in this transaction. +receive_time - unsigned int; The Unix time that the transaction was first seen on the network by the node. +relayed - boolean; States if this transaction has been relayed +tx_blob - unsigned int; Hexadecimal blob representing the transaction. + */ \ No newline at end of file diff --git a/src/common/index.ts b/src/common/index.ts index 14ec46b..c2b6af4 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -26,6 +26,7 @@ export { OutKey } from './OutKey'; export { Output } from './Output'; export { OutputDistribution } from './OutputDistribution'; export { Peer } from './Peer'; +export { PeerInfo } from './PeerInfo'; export { PublicNode } from './PublicNode'; export { Span } from './Span'; export { SyncInfo } from './SyncInfo'; @@ -34,6 +35,9 @@ export { TxInfo } from './TxInfo'; export { UpdateInfo } from './UpdateInfo'; export { LogCategoryLevel, LogCategories } from './LogCategories'; export { NetStatsHistory, NetStatsHistoryEntry } from './NetStatsHistory'; +export { UnconfirmedTx } from './UnconfirmedTx'; +export { SpentKeyImage } from './SpentKeyImage'; +export { TxPool } from './TxPool'; export * from './error'; export * from './request'; diff --git a/src/common/request/GetPeerListRequest.ts b/src/common/request/GetPeerListRequest.ts new file mode 100644 index 0000000..17ed253 --- /dev/null +++ b/src/common/request/GetPeerListRequest.ts @@ -0,0 +1,11 @@ +import { RPCRequest } from "./RPCRequest"; + +export class GetPeerListRequest extends RPCRequest { + public override readonly method: 'get_peer_list' = 'get_peer_list'; + public override readonly restricted: boolean = true; + + public override toDictionary(): { [key: string]: any; } { + return {}; + } + +} \ No newline at end of file diff --git a/src/common/request/GetTransactionPoolRequest.ts b/src/common/request/GetTransactionPoolRequest.ts new file mode 100644 index 0000000..22a13a1 --- /dev/null +++ b/src/common/request/GetTransactionPoolRequest.ts @@ -0,0 +1,11 @@ +import { RPCRequest } from "./RPCRequest"; + +export class GetTransactionPoolRequest extends RPCRequest { + public override readonly method: 'get_transaction_pool' = 'get_transaction_pool'; + public override readonly restricted: boolean = false; + + public override toDictionary(): { [key: string]: any; } { + return {}; + } + +} \ No newline at end of file diff --git a/src/common/request/index.ts b/src/common/request/index.ts index 8f49b1e..1533139 100644 --- a/src/common/request/index.ts +++ b/src/common/request/index.ts @@ -57,6 +57,8 @@ export { SetBootstrapDaemonRequest } from "./SetBootstrapDaemonRequest"; export { SetLogLevelRequest } from "./SetLogLevelRequest"; export { SetLogHashRateRequest } from "./SetLogHashRateRequest"; export { SetLogCategoriesRequest } from "./SetLogCategoriesRequest"; +export { GetTransactionPoolRequest } from "./GetTransactionPoolRequest"; +export { GetPeerListRequest } from "./GetPeerListRequest"; /** * Restricted requests