Adding table tbl_fee

This table used to store majority fee of monero nettype.
By calculating majority fee via "cron" every 300s, the function to
get majority fee for nettypes can be done with single query.

The frontend majority static data in the frontend removed and
now use `/api/v1/fees` endpoint to get majority fee value.

Note: Don't know if it works well with `onload` method or not. Let see.
This commit is contained in:
ditatompel 2024-05-31 16:28:21 +07:00
parent 55f6af1f22
commit 48fe09c1cb
No known key found for this signature in database
GPG key ID: 31D3D06D77950979
6 changed files with 159 additions and 72 deletions

View file

@ -6,26 +6,6 @@ export async function load() {
title: 'Public Monero Remote Nodes List', title: 'Public Monero Remote Nodes List',
description: 'List of public Monero remote nodes that you can use with your favourite Monero wallet. You can filter by country, protocol, or CORS capable nodes.', description: 'List of public Monero remote nodes that you can use with your favourite Monero wallet. You can filter by country, protocol, or CORS capable nodes.',
keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc' keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc'
}, }
/**
* Array containing network fees.
* For now, I use static data to reduce the amount of API calls.
* See the values from `/api/v1/fees`
* @type {{ nettype: string, estimate_fee: number }[]}
*/
netFees: [
{
nettype: 'mainnet',
estimate_fee: 20000
},
{
nettype: 'stagenet',
estimate_fee: 56000
},
{
nettype: 'testnet',
estimate_fee: 20000
}
]
}; };
} }

View file

@ -1,7 +1,7 @@
<script> <script>
import { DataHandler } from '@vincjo/datatables/remote'; import { DataHandler } from '@vincjo/datatables/remote';
import { format, formatDistance } from 'date-fns'; import { format, formatDistance } from 'date-fns';
import { loadData, loadCountries } from './api-handler'; import { loadData, loadFees, loadCountries } from './api-handler';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { import {
DtSrRowsPerPage, DtSrRowsPerPage,
@ -30,28 +30,34 @@
/** @type {{total_nodes: number, cc: string, name: string}[]} */ /** @type {{total_nodes: number, cc: string, name: string}[]} */
let countries = []; let countries = [];
let fees = [];
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 }); const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
let rows = handler.getRows(); let rows = handler.getRows();
/** @type {Object.<string, number>} */ /** @type {Object.<string, number>} */
let majorityFee = data.netFees.reduce( let majorityFee;
/**
* @param {Object.<string, number>} o
* @param {{ nettype: string, estimate_fee: number }} key
* @returns {Object.<string, number>}
*/
(o, key) => ({
...o,
[key.nettype]: key.estimate_fee
}),
{}
);
onMount(() => { onMount(() => {
loadFees().then((data) => {
fees = data;
majorityFee = fees.reduce(
/**
* @param {Object.<string, number>} o
* @param {{ nettype: string, estimate_fee: number }} key
* @returns {Object.<string, number>}
*/
(o, key) => ({
...o,
[key.nettype]: key.estimate_fee
}),
{}
);
});
loadCountries().then((data) => { loadCountries().then((data) => {
countries = data; countries = data;
}); });
handler.onChange((state) => loadData(state)); handler.onChange((state) => loadData(state));
handler.invalidate(); handler.invalidate();
}); });

View file

@ -1,6 +1,9 @@
import { apiUri } from '$lib/utils/common'; import { apiUri } from '$lib/utils/common';
/** @param {import('@vincjo/datatables/remote/state')} state */ /**
* @typedef {import('@vincjo/datatables/remote').State} State
* @param {State} state - The state object from the data table.
*/
export const loadData = async (state) => { export const loadData = async (state) => {
const response = await fetch(apiUri(`/api/v1/nodes?${getParams(state)}`)); const response = await fetch(apiUri(`/api/v1/nodes?${getParams(state)}`));
const json = await response.json(); const json = await response.json();
@ -14,6 +17,13 @@ export const loadCountries = async () => {
return json.data ?? []; return json.data ?? [];
}; };
export const loadFees = async () => {
const response = await fetch(apiUri('/api/v1/fees'));
const json = await response.json();
return json.data ?? [];
};
/** @param {State} state - The state object from the data table. */
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => { const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
let params = `page=${pageNumber}&limit=${rowsPerPage}`; let params = `page=${pageNumber}&limit=${rowsPerPage}`;

View file

@ -147,6 +147,9 @@ func (r *CronRepo) execCron(slug string) {
case "delete_old_probe_logs": case "delete_old_probe_logs":
slog.Info(fmt.Sprintf("[CRON] Start running task: %s", slug)) slog.Info(fmt.Sprintf("[CRON] Start running task: %s", slug))
r.deleteOldProbeLogs() r.deleteOldProbeLogs()
case "calculate_majority_fee":
slog.Info(fmt.Sprintf("[CRON] Start running task: %s", slug))
r.calculateMajorityFee()
} }
} }
@ -159,3 +162,48 @@ func (r *CronRepo) deleteOldProbeLogs() {
slog.Error(fmt.Sprintf("[CRON] Failed to delete old probe logs: %s", err)) slog.Error(fmt.Sprintf("[CRON] Failed to delete old probe logs: %s", err))
} }
} }
func (r *CronRepo) calculateMajorityFee() {
netTypes := [3]string{"mainnet", "stagenet", "testnet"}
for _, net := range netTypes {
row, err := r.db.Query(`
SELECT
COUNT(id) AS node_count,
nettype,
estimate_fee
FROM
tbl_node
WHERE
nettype = ?
GROUP BY
estimate_fee
ORDER BY
node_count DESC
LIMIT 1`, net)
if err != nil {
slog.Error(fmt.Sprintf("[CRON] Failed to calculate majority fee: %s", err))
}
defer row.Close()
var (
nettype string
estimateFee int
nodeCount int
)
for row.Next() {
err = row.Scan(&nodeCount, &nettype, &estimateFee)
if err != nil {
slog.Error(fmt.Sprintf("[CRON] Failed to calculate majority fee: %s", err))
continue
}
query := `UPDATE tbl_fee SET estimate_fee = ?, node_count = ? WHERE nettype = ?`
_, err = r.db.Exec(query, estimateFee, nodeCount, nettype)
if err != nil {
slog.Error(fmt.Sprintf("[CRON] Failed to update majority fee: %s", err))
continue
}
}
}
}

View file

@ -7,7 +7,7 @@ import (
type migrateFn func(*DB) error type migrateFn func(*DB) error
var dbMigrate = [...]migrateFn{v1} var dbMigrate = [...]migrateFn{v1, v2}
func MigrateDb(db *DB) error { func MigrateDb(db *DB) error {
version := getSchemaVersion(db) version := getSchemaVersion(db)
@ -193,3 +193,61 @@ func v1(db *DB) error {
return nil return nil
} }
func v2(db *DB) error {
slog.Debug("[DB] Migrating database schema version 2")
// table: tbl_fee
slog.Debug("[DB] Creating table: tbl_fee")
_, err := db.Exec(`
CREATE TABLE tbl_fee (
nettype VARCHAR(100) NOT NULL DEFAULT '',
estimate_fee INT(9) UNSIGNED NOT NULL DEFAULT 0,
node_count INT(9) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (nettype)
)`)
if err != nil {
return err
}
slog.Debug("[DB] Adding default fee to table: tbl_fee")
_, err = db.Exec(`
INSERT INTO tbl_fee (
nettype,
estimate_fee,
node_count
) VALUES (
'mainnet',
0,
0
), (
'stagenet',
0,
0
), (
'testnet',
0,
0
);`)
if err != nil {
return err
}
slog.Debug("[DB] Adding majority fee cron jobs to table: tbl_cron")
_, err = db.Exec(`
INSERT INTO tbl_cron (
title,
slug,
description,
run_every
) VALUES (
'Calculate majority fee',
'calculate_majority_fee',
'Calculate majority Monero fee',
300
);`)
if err != nil {
return err
}
return nil
}

View file

@ -19,7 +19,7 @@ type MoneroRepository interface {
Node(id int) (Node, error) Node(id int) (Node, error)
Add(protocol string, host string, port uint) error Add(protocol string, host string, port uint) error
Nodes(QueryNodes) (Nodes, error) Nodes(QueryNodes) (Nodes, error)
NetFees() []NetFee NetFees() []*NetFee
Countries() ([]Countries, error) Countries() ([]Countries, error)
GiveJob(acceptTor int) (Node, error) GiveJob(acceptTor int) (Node, error)
ProcessJob(report ProbeReport, proberId int64) error ProcessJob(report ProbeReport, proberId int64) error
@ -80,13 +80,6 @@ func (r *MoneroRepo) Node(id int) (Node, error) {
return node, err return node, err
} }
// Nodes represents a list of nodes
type Nodes struct {
TotalRows int `json:"total_rows"`
RowsPerPage int `json:"rows_per_page"`
Items []*Node `json:"items"`
}
// QueryNodes represents database query parameters // QueryNodes represents database query parameters
type QueryNodes struct { type QueryNodes struct {
Host string Host string
@ -160,6 +153,13 @@ func (q QueryNodes) toSQL() (args []interface{}, where, sortBy, sortDirection st
return args, where, sortBy, sortDirection return args, where, sortBy, sortDirection
} }
// Nodes represents a list of nodes
type Nodes struct {
TotalRows int `json:"total_rows"`
RowsPerPage int `json:"rows_per_page"`
Items []*Node `json:"items"`
}
// Get nodes from database // Get nodes from database
func (r *MoneroRepo) Nodes(q QueryNodes) (Nodes, error) { func (r *MoneroRepo) Nodes(q QueryNodes) (Nodes, error) {
args, where, sortBy, sortDirection := q.toSQL() args, where, sortBy, sortDirection := q.toSQL()
@ -310,35 +310,20 @@ type NetFee struct {
NodeCount int `json:"node_count" db:"node_count"` NodeCount int `json:"node_count" db:"node_count"`
} }
// Get majority net fee from database // Get majority net fee from table tbl_fee
func (r *MoneroRepo) NetFees() []NetFee { func (r *MoneroRepo) NetFees() []*NetFee {
// TODO: Create in-memory cache for this var netFees []*NetFee
netTypes := [3]string{"mainnet", "stagenet", "testnet"} err := r.db.Select(&netFees, `
netFees := []NetFee{} SELECT
nettype,
for _, net := range netTypes { estimate_fee,
fees := NetFee{} node_count
err := r.db.Get(&fees, ` FROM
SELECT tbl_fee
COUNT(id) AS node_count, `)
nettype, if err != nil {
estimate_fee slog.Error(fmt.Sprintf("[MONERO] Failed to get net fees: %s", err))
FROM
tbl_node
WHERE
nettype = ?
GROUP BY
estimate_fee
ORDER BY
node_count DESC
LIMIT 1`, net)
if err != nil {
fmt.Println("WARN:", err.Error())
continue
}
netFees = append(netFees, fees)
} }
return netFees return netFees
} }