2024-05-22 15:45:38 +00:00
|
|
|
package monero
|
2024-05-04 10:24:47 +00:00
|
|
|
|
|
|
|
import (
|
2024-05-06 18:08:01 +00:00
|
|
|
"database/sql"
|
2024-05-04 10:24:47 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2024-05-04 11:52:47 +00:00
|
|
|
"fmt"
|
2024-05-23 22:37:27 +00:00
|
|
|
"log/slog"
|
2024-05-04 10:24:47 +00:00
|
|
|
"net"
|
2024-05-04 11:52:47 +00:00
|
|
|
"slices"
|
2024-05-04 10:24:47 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2024-07-06 18:28:44 +00:00
|
|
|
|
|
|
|
"github.com/ditatompel/xmr-remote-nodes/internal/database"
|
2024-09-09 11:21:03 +00:00
|
|
|
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
|
2024-05-04 11:52:47 +00:00
|
|
|
"github.com/jmoiron/sqlx/types"
|
2024-05-04 10:24:47 +00:00
|
|
|
)
|
|
|
|
|
2024-07-06 20:13:11 +00:00
|
|
|
type moneroRepo struct {
|
2024-05-04 10:24:47 +00:00
|
|
|
db *database.DB
|
|
|
|
}
|
|
|
|
|
2024-07-06 20:13:11 +00:00
|
|
|
func New() *moneroRepo {
|
|
|
|
return &moneroRepo{db: database.GetDB()}
|
2024-05-04 10:24:47 +00:00
|
|
|
}
|
|
|
|
|
2024-05-26 22:13:03 +00:00
|
|
|
// Node represents a single remote node
|
2024-05-22 15:45:38 +00:00
|
|
|
type Node struct {
|
|
|
|
ID uint `json:"id,omitempty" db:"id"`
|
2024-05-04 11:52:47 +00:00
|
|
|
Hostname string `json:"hostname" db:"hostname"`
|
2024-05-22 15:45:38 +00:00
|
|
|
IP string `json:"ip" db:"ip_addr"`
|
2024-05-04 11:52:47 +00:00
|
|
|
Port uint `json:"port" db:"port"`
|
|
|
|
Protocol string `json:"protocol" db:"protocol"`
|
|
|
|
IsTor bool `json:"is_tor" db:"is_tor"`
|
|
|
|
IsAvailable bool `json:"is_available" db:"is_available"`
|
2024-05-22 15:45:38 +00:00
|
|
|
Nettype string `json:"nettype" db:"nettype"`
|
2024-05-04 15:53:03 +00:00
|
|
|
Height uint `json:"height" db:"height"`
|
2024-05-04 11:52:47 +00:00
|
|
|
AdjustedTime uint `json:"adjusted_time" db:"adjusted_time"`
|
|
|
|
DatabaseSize uint `json:"database_size" db:"database_size"`
|
|
|
|
Difficulty uint `json:"difficulty" db:"difficulty"`
|
2024-05-04 15:53:03 +00:00
|
|
|
Version string `json:"version" db:"version"`
|
|
|
|
Status string `json:"status,omitempty"`
|
2024-05-04 18:42:47 +00:00
|
|
|
Uptime float64 `json:"uptime" db:"uptime"`
|
2024-05-04 11:52:47 +00:00
|
|
|
EstimateFee uint `json:"estimate_fee" db:"estimate_fee"`
|
2024-05-22 15:45:38 +00:00
|
|
|
ASN uint `json:"asn" db:"asn"`
|
|
|
|
ASNName string `json:"asn_name" db:"asn_name"`
|
2024-05-04 11:52:47 +00:00
|
|
|
CountryCode string `json:"cc" db:"country"`
|
|
|
|
CountryName string `json:"country_name" db:"country_name"`
|
|
|
|
City string `json:"city" db:"city"`
|
2024-05-22 15:45:38 +00:00
|
|
|
Latitude float64 `json:"latitude" db:"lat"`
|
|
|
|
Longitude float64 `json:"longitude" db:"lon"`
|
2024-05-04 11:52:47 +00:00
|
|
|
DateEntered uint `json:"date_entered,omitempty" db:"date_entered"`
|
|
|
|
LastChecked uint `json:"last_checked" db:"last_checked"`
|
|
|
|
FailedCount uint `json:"failed_count,omitempty" db:"failed_count"`
|
|
|
|
LastCheckStatus types.JSONText `json:"last_check_statuses" db:"last_check_status"`
|
2024-05-22 15:45:38 +00:00
|
|
|
CORSCapable bool `json:"cors" db:"cors_capable"`
|
2024-09-05 17:08:59 +00:00
|
|
|
IPv6Only bool `json:"ipv6_only" db:"ipv6_only"`
|
2024-09-11 18:13:30 +00:00
|
|
|
IPAddresses string `json:"ip_addresses" db:"ip_addresses"`
|
2024-05-04 11:52:47 +00:00
|
|
|
}
|
|
|
|
|
2024-05-26 22:13:03 +00:00
|
|
|
// Get node from database by id
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) Node(id int) (Node, error) {
|
2024-05-22 15:45:38 +00:00
|
|
|
var node Node
|
2024-05-30 12:19:03 +00:00
|
|
|
err := r.db.Get(&node, `SELECT * FROM tbl_node WHERE id = ?`, id)
|
2024-05-06 18:08:01 +00:00
|
|
|
if err != nil && err != sql.ErrNoRows {
|
2024-05-30 12:41:03 +00:00
|
|
|
slog.Error(err.Error())
|
2024-05-06 18:08:01 +00:00
|
|
|
return node, errors.New("Can't get node information")
|
|
|
|
}
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
return node, errors.New("Node not found")
|
|
|
|
}
|
|
|
|
return node, err
|
|
|
|
}
|
|
|
|
|
2024-05-26 23:38:11 +00:00
|
|
|
// QueryNodes represents database query parameters
|
2024-05-26 22:13:03 +00:00
|
|
|
type QueryNodes struct {
|
|
|
|
Host string
|
2024-05-30 12:19:03 +00:00
|
|
|
Nettype string // Can be "any", mainnet, stagenet, testnet. Default: "any"
|
|
|
|
Protocol string // Can be "any", tor, http, https. Default: "any"
|
2024-05-26 22:13:03 +00:00
|
|
|
CC string // 2 letter country code
|
|
|
|
Status int
|
|
|
|
CORS int
|
|
|
|
|
|
|
|
// pagination
|
2024-05-04 11:52:47 +00:00
|
|
|
RowsPerPage int
|
|
|
|
Page int
|
|
|
|
SortBy string
|
|
|
|
SortDirection string
|
|
|
|
}
|
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
// toSQL generates SQL query from query parameters
|
2024-08-05 11:27:00 +00:00
|
|
|
func (q *QueryNodes) toSQL() (args []interface{}, where string) {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq := []string{}
|
2024-05-04 11:52:47 +00:00
|
|
|
|
|
|
|
if q.Host != "" {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "(hostname LIKE ? OR ip_addr LIKE ?)")
|
|
|
|
args = append(args, "%"+q.Host+"%", "%"+q.Host+"%")
|
2024-05-04 11:52:47 +00:00
|
|
|
}
|
2024-07-29 15:30:03 +00:00
|
|
|
if slices.Contains([]string{"mainnet", "stagenet", "testnet"}, q.Nettype) {
|
|
|
|
wq = append(wq, "nettype = ?")
|
|
|
|
args = append(args, q.Nettype)
|
2024-05-06 07:33:13 +00:00
|
|
|
}
|
2024-05-30 12:19:03 +00:00
|
|
|
if q.Protocol != "any" && slices.Contains([]string{"tor", "http", "https"}, q.Protocol) {
|
2024-05-06 07:33:13 +00:00
|
|
|
if q.Protocol == "tor" {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "is_tor = ?")
|
|
|
|
args = append(args, 1)
|
2024-05-06 07:33:13 +00:00
|
|
|
} else {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "(protocol = ? AND is_tor = ?)")
|
|
|
|
args = append(args, q.Protocol, 0)
|
2024-05-06 07:33:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if q.CC != "any" {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "country = ?")
|
2024-05-06 07:33:13 +00:00
|
|
|
if q.CC == "UNKNOWN" {
|
2024-05-30 12:19:03 +00:00
|
|
|
args = append(args, "")
|
2024-05-06 07:33:13 +00:00
|
|
|
} else {
|
2024-05-30 12:19:03 +00:00
|
|
|
args = append(args, q.CC)
|
2024-05-06 07:33:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if q.Status != -1 {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "is_available = ?")
|
|
|
|
args = append(args, q.Status)
|
2024-05-06 07:33:13 +00:00
|
|
|
}
|
2024-05-22 15:45:38 +00:00
|
|
|
if q.CORS != -1 {
|
2024-05-30 12:19:03 +00:00
|
|
|
wq = append(wq, "cors_capable = ?")
|
|
|
|
args = append(args, q.CORS)
|
2024-05-06 07:33:13 +00:00
|
|
|
}
|
2024-05-04 11:52:47 +00:00
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
if len(wq) > 0 {
|
|
|
|
where = "WHERE " + strings.Join(wq, " AND ")
|
2024-05-04 11:52:47 +00:00
|
|
|
}
|
|
|
|
|
2024-08-05 11:27:00 +00:00
|
|
|
if !slices.Contains([]string{"last_checked", "uptime"}, q.SortBy) {
|
|
|
|
q.SortBy = "last_checked"
|
2024-05-30 12:19:03 +00:00
|
|
|
}
|
2024-08-05 11:27:00 +00:00
|
|
|
if q.SortDirection != "asc" {
|
|
|
|
q.SortDirection = "DESC"
|
2024-05-30 12:19:03 +00:00
|
|
|
}
|
2024-05-04 11:52:47 +00:00
|
|
|
|
2024-08-05 11:27:00 +00:00
|
|
|
return args, where
|
2024-05-30 12:19:03 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 09:28:21 +00:00
|
|
|
// Nodes represents a list of nodes
|
|
|
|
type Nodes struct {
|
|
|
|
TotalRows int `json:"total_rows"`
|
|
|
|
RowsPerPage int `json:"rows_per_page"`
|
|
|
|
Items []*Node `json:"items"`
|
|
|
|
}
|
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
// Get nodes from database
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) {
|
2024-08-05 11:27:00 +00:00
|
|
|
args, where := q.toSQL()
|
2024-05-30 12:19:03 +00:00
|
|
|
|
|
|
|
var nodes Nodes
|
|
|
|
|
2024-06-03 06:18:52 +00:00
|
|
|
nodes.RowsPerPage = q.RowsPerPage
|
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
qTotal := fmt.Sprintf(`
|
2024-05-19 21:22:58 +00:00
|
|
|
SELECT
|
|
|
|
COUNT(id) AS total_rows
|
|
|
|
FROM
|
|
|
|
tbl_node
|
|
|
|
%s`, where)
|
2024-05-04 11:52:47 +00:00
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
err := r.db.QueryRow(qTotal, args...).Scan(&nodes.TotalRows)
|
2024-05-04 11:52:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return nodes, err
|
|
|
|
}
|
2024-05-30 12:19:03 +00:00
|
|
|
args = append(args, q.RowsPerPage, (q.Page-1)*q.RowsPerPage)
|
2024-05-04 11:52:47 +00:00
|
|
|
|
2024-05-19 21:22:58 +00:00
|
|
|
query := fmt.Sprintf(`
|
|
|
|
SELECT
|
2024-05-30 12:19:03 +00:00
|
|
|
*
|
2024-05-19 21:22:58 +00:00
|
|
|
FROM
|
|
|
|
tbl_node
|
2024-08-05 11:27:00 +00:00
|
|
|
%s
|
2024-05-19 21:22:58 +00:00
|
|
|
ORDER BY
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
LIMIT ?
|
2024-08-05 11:27:00 +00:00
|
|
|
OFFSET ?`, where, q.SortBy, q.SortDirection)
|
2024-05-30 12:19:03 +00:00
|
|
|
err = r.db.Select(&nodes.Items, query, args...)
|
2024-05-04 11:52:47 +00:00
|
|
|
|
2024-05-30 12:19:03 +00:00
|
|
|
return nodes, err
|
2024-05-04 11:52:47 +00:00
|
|
|
}
|
|
|
|
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) Add(protocol string, hostname string, port uint) error {
|
2024-05-04 10:24:47 +00:00
|
|
|
if protocol != "http" && protocol != "https" {
|
|
|
|
return errors.New("Invalid protocol, must one of or HTTP/HTTPS")
|
|
|
|
}
|
|
|
|
|
|
|
|
if port > 65535 || port < 1 {
|
|
|
|
return errors.New("Invalid port number")
|
|
|
|
}
|
|
|
|
|
|
|
|
is_tor := false
|
|
|
|
if strings.HasSuffix(hostname, ".onion") {
|
|
|
|
is_tor = true
|
|
|
|
}
|
|
|
|
|
2024-09-09 11:21:03 +00:00
|
|
|
ipAddr := ""
|
2024-09-11 18:13:30 +00:00
|
|
|
ips := ""
|
2024-09-09 11:21:03 +00:00
|
|
|
ipv6_only := false
|
2024-09-05 17:08:59 +00:00
|
|
|
|
2024-05-04 10:24:47 +00:00
|
|
|
if !is_tor {
|
|
|
|
hostIps, err := net.LookupIP(hostname)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-09-09 11:21:03 +00:00
|
|
|
ipv6_only = ip.IsIPv6Only(hostIps)
|
2024-09-05 17:08:59 +00:00
|
|
|
|
|
|
|
hostIp := hostIps[0]
|
2024-05-04 10:24:47 +00:00
|
|
|
if hostIp.IsPrivate() {
|
|
|
|
return errors.New("IP address is private")
|
|
|
|
}
|
|
|
|
if hostIp.IsLoopback() {
|
|
|
|
return errors.New("IP address is loopback address")
|
|
|
|
}
|
|
|
|
|
2024-09-09 11:21:03 +00:00
|
|
|
ipAddr = hostIp.String()
|
2024-09-11 18:13:30 +00:00
|
|
|
ips = ip.SliceToString(hostIps)
|
2024-09-16 15:35:14 +00:00
|
|
|
} else {
|
|
|
|
if strings.HasPrefix(hostname, "http://") || strings.HasPrefix(hostname, "https://") {
|
|
|
|
return errors.New("Don't start hostname with http:// or https://, just put your hostname")
|
|
|
|
}
|
2024-05-04 10:24:47 +00:00
|
|
|
}
|
|
|
|
|
2024-07-06 20:13:11 +00:00
|
|
|
row, err := r.db.Query(`
|
2024-05-19 21:22:58 +00:00
|
|
|
SELECT
|
|
|
|
id
|
|
|
|
FROM
|
|
|
|
tbl_node
|
|
|
|
WHERE
|
|
|
|
protocol = ?
|
|
|
|
AND hostname = ?
|
|
|
|
AND port = ?
|
|
|
|
LIMIT 1`, protocol, hostname, port)
|
2024-05-04 10:24:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer row.Close()
|
|
|
|
|
|
|
|
if row.Next() {
|
|
|
|
return errors.New("Node already monitored")
|
|
|
|
}
|
|
|
|
statusDb, _ := json.Marshal([5]int{2, 2, 2, 2, 2})
|
2024-07-06 20:13:11 +00:00
|
|
|
_, err = r.db.Exec(`
|
2024-05-19 21:22:58 +00:00
|
|
|
INSERT INTO tbl_node (
|
|
|
|
protocol,
|
|
|
|
hostname,
|
|
|
|
port,
|
|
|
|
is_tor,
|
|
|
|
nettype,
|
|
|
|
ip_addr,
|
|
|
|
lat,
|
|
|
|
lon,
|
|
|
|
date_entered,
|
|
|
|
last_checked,
|
2024-09-05 17:08:59 +00:00
|
|
|
last_check_status,
|
2024-09-11 18:13:30 +00:00
|
|
|
ip_addresses,
|
2024-09-05 17:08:59 +00:00
|
|
|
ipv6_only
|
2024-05-19 21:22:58 +00:00
|
|
|
) VALUES (
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
|
|
|
?,
|
2024-09-05 17:08:59 +00:00
|
|
|
?,
|
2024-09-11 18:13:30 +00:00
|
|
|
?,
|
2024-05-19 21:22:58 +00:00
|
|
|
?
|
|
|
|
)`,
|
|
|
|
protocol,
|
|
|
|
hostname,
|
|
|
|
port,
|
|
|
|
is_tor,
|
|
|
|
"",
|
2024-09-09 11:21:03 +00:00
|
|
|
ipAddr,
|
2024-05-19 21:22:58 +00:00
|
|
|
0,
|
|
|
|
0,
|
|
|
|
time.Now().Unix(),
|
|
|
|
0,
|
2024-09-05 17:08:59 +00:00
|
|
|
string(statusDb),
|
2024-09-11 18:13:30 +00:00
|
|
|
ips,
|
2024-09-05 17:08:59 +00:00
|
|
|
ipv6_only)
|
2024-05-04 10:24:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2024-05-04 12:27:21 +00:00
|
|
|
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) Delete(id uint) error {
|
2024-05-31 06:04:53 +00:00
|
|
|
if _, err := r.db.Exec(`DELETE FROM tbl_node WHERE id = ?`, id); err != nil {
|
2024-05-06 10:45:18 +00:00
|
|
|
return err
|
|
|
|
}
|
2024-05-31 06:04:53 +00:00
|
|
|
if _, err := r.db.Exec(`DELETE FROM tbl_probe_log WHERE node_id = ?`, id); err != nil {
|
2024-05-06 10:45:18 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-05-06 06:19:48 +00:00
|
|
|
type NetFee struct {
|
|
|
|
Nettype string `json:"nettype" db:"nettype"`
|
|
|
|
EstimateFee uint `json:"estimate_fee" db:"estimate_fee"`
|
|
|
|
NodeCount int `json:"node_count" db:"node_count"`
|
|
|
|
}
|
|
|
|
|
2024-05-31 09:28:21 +00:00
|
|
|
// Get majority net fee from table tbl_fee
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) NetFees() []NetFee {
|
2024-06-07 17:37:26 +00:00
|
|
|
var netFees []NetFee
|
2024-05-31 09:28:21 +00:00
|
|
|
err := r.db.Select(&netFees, `
|
|
|
|
SELECT
|
|
|
|
nettype,
|
|
|
|
estimate_fee,
|
|
|
|
node_count
|
|
|
|
FROM
|
|
|
|
tbl_fee
|
|
|
|
`)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error(fmt.Sprintf("[MONERO] Failed to get net fees: %s", err))
|
2024-05-06 06:19:48 +00:00
|
|
|
}
|
|
|
|
return netFees
|
|
|
|
}
|
2024-05-06 06:35:15 +00:00
|
|
|
|
2024-06-07 17:53:28 +00:00
|
|
|
// Countries represents list of countries
|
2024-05-22 15:45:38 +00:00
|
|
|
type Countries struct {
|
2024-05-06 06:35:15 +00:00
|
|
|
TotalNodes int `json:"total_nodes" db:"total_nodes"`
|
2024-05-22 15:45:38 +00:00
|
|
|
CC string `json:"cc" db:"country"` // country code
|
2024-05-06 06:35:15 +00:00
|
|
|
Name string `json:"name" db:"country_name"`
|
|
|
|
}
|
|
|
|
|
2024-06-07 17:53:28 +00:00
|
|
|
// Get list of countries (count by nodes)
|
2024-07-06 20:13:11 +00:00
|
|
|
func (r *moneroRepo) Countries() ([]Countries, error) {
|
2024-06-07 17:53:28 +00:00
|
|
|
var c []Countries
|
|
|
|
err := r.db.Select(&c, `
|
2024-05-19 21:22:58 +00:00
|
|
|
SELECT
|
|
|
|
COUNT(id) AS total_nodes,
|
|
|
|
country,
|
|
|
|
country_name
|
|
|
|
FROM
|
|
|
|
tbl_node
|
|
|
|
GROUP BY
|
|
|
|
country
|
|
|
|
ORDER BY
|
|
|
|
country ASC`)
|
2024-06-07 17:53:28 +00:00
|
|
|
return c, err
|
2024-05-06 06:35:15 +00:00
|
|
|
}
|