mirror of
https://github.com/ditatompel/xmr-remote-nodes.git
synced 2025-01-08 05:52:10 +07:00
Christian Ditaputratama
f0a10208e2
DEPRECATED: Using int value for CORS is deprecated, please use "on" to filter CORS capable nodes. Leave CORS empty to disable CORS filter.
323 lines
11 KiB
Text
323 lines
11 KiB
Text
package views
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/ditatompel/xmr-remote-nodes/internal/ip"
|
|
"github.com/ditatompel/xmr-remote-nodes/internal/monero"
|
|
"github.com/ditatompel/xmr-remote-nodes/internal/paging"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var nettypes = []string{"mainnet", "stagenet", "testnet"}
|
|
var protocols = []string{"tor", "http", "https"}
|
|
|
|
type nodeStatus struct {
|
|
Code int
|
|
Text string
|
|
}
|
|
|
|
var nodeStatuses = []nodeStatus{
|
|
{-1, "ANY"},
|
|
{1, "Online"},
|
|
{0, "Offline"},
|
|
}
|
|
|
|
templ RemoteNodes(data monero.Nodes, countries []monero.Countries, q monero.QueryNodes, p paging.Pagination) {
|
|
<!-- Hero -->
|
|
<section class="relative overflow-hidden pt-6">
|
|
<!-- Gradients -->
|
|
<div aria-hidden="true" class="flex absolute -top-96 start-1/2 transform -translate-x-1/2">
|
|
<div class="bg-gradient-to-r blur-3xl w-[25rem] h-[44rem] rotate-[-60deg] transform -translate-x-[10rem] from-amber-800/30 to-orange-800/40"></div>
|
|
<div class="bg-gradient-to-tl blur-3xl w-[90rem] h-[50rem] rounded-fulls origin-top-left -rotate-12 -translate-x-[15rem] from-orange-900/60 via-orange-900/40 to-amber-900/80"></div>
|
|
</div>
|
|
<!-- End Gradients -->
|
|
<div class="relative z-10">
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16">
|
|
<div class="text-center">
|
|
<!-- Title -->
|
|
<div class="mt-5">
|
|
<h1 class="block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200">Public Monero Remote Nodes List</h1>
|
|
</div>
|
|
<!-- End Title -->
|
|
<div class="mt-5">
|
|
<p class="text-lg text-neutral-300"><strong>Monero remote node</strong> is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.</p>
|
|
</div>
|
|
<hr class="mt-6"/>
|
|
</div>
|
|
<div class="max-w-3xl text-center mx-auto mt-8 prose prose-invert">
|
|
<p>Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.</p>
|
|
<p>Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the <strong>Monero community suggests to <span class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<!-- End Hero -->
|
|
<div class="flex flex-col max-w-6xl mx-auto mb-10">
|
|
<div class="min-w-full inline-block align-middle">
|
|
@TableNodes(data, countries, q, p)
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ TableNodes(data monero.Nodes, countries []monero.Countries, q monero.QueryNodes, p paging.Pagination) {
|
|
<div id="tbl_nodes" class="bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden">
|
|
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700">
|
|
@DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q)
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="dt">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Host:Port</th>
|
|
<th scope="col">Nettype</th>
|
|
<th scope="col">Protocol</th>
|
|
<th scope="col">Country</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Estimate Fee</th>
|
|
<th scope="col">Uptime</th>
|
|
<th scope="col">Check</th>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<input
|
|
type="text"
|
|
id="host"
|
|
name="host"
|
|
value={ fmt.Sprintf("%s", q.Host) }
|
|
autocomplete="off"
|
|
class="th-filter"
|
|
placeholder="Filter Host / IP"
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"host"})) }
|
|
hx-push-url="true"
|
|
hx-trigger="keyup changed delay:0.4s"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<select
|
|
id="nettype"
|
|
name="nettype"
|
|
class="th-filter"
|
|
autocomplete="off"
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"nettype"})) }
|
|
hx-trigger="change"
|
|
hx-push-url="true"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
>
|
|
<option value="">ANY</option>
|
|
for _, nettype := range nettypes {
|
|
<option value={ fmt.Sprintf("%s", nettype) } selected?={ nettype == q.Nettype }>{ nettype }</option>
|
|
}
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select
|
|
id="protocol"
|
|
name="protocol"
|
|
class="th-filter"
|
|
autocomplete="off"
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"protocol"})) }
|
|
hx-trigger="change"
|
|
hx-push-url="true"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
>
|
|
<option value="">ANY</option>
|
|
for _, protocol := range protocols {
|
|
<option value={ fmt.Sprintf("%s", protocol) } selected?={ protocol == q.Protocol }>{ protocol }</option>
|
|
}
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select
|
|
id="cc"
|
|
name="cc"
|
|
class="th-filter"
|
|
autocomplete="off"
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cc"})) }
|
|
hx-trigger="change"
|
|
hx-push-url="true"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
>
|
|
<option value="any">ANY</option>
|
|
for _, country := range countries {
|
|
if country.CC == "" {
|
|
<option value="UNKNOWN" selected?={ q.CC== "UNKNOWN" }>{ fmt.Sprintf("UNKNOWN (%d)", country.TotalNodes ) }</option>
|
|
} else {
|
|
<option value={ fmt.Sprintf("%s", country.CC) } selected?={ country.CC == q.CC }>{ fmt.Sprintf("%s (%d)", country.Name, country.TotalNodes ) }</option>
|
|
}
|
|
}
|
|
</select>
|
|
</td>
|
|
<td colspan="2">
|
|
<select
|
|
id="status"
|
|
name="status"
|
|
class="th-filter"
|
|
autocomplete="off"
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"status"})) }
|
|
hx-trigger="change"
|
|
hx-push-url="true"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
>
|
|
for _, status := range nodeStatuses {
|
|
<option value={ fmt.Sprintf("%d", status.Code) } selected?={ status.Code == q.Status }>{ status.Text }</option>
|
|
}
|
|
</select>
|
|
</td>
|
|
<td colspan="2">
|
|
<div class="flex justify-center">
|
|
<input
|
|
type="checkbox"
|
|
id="cors"
|
|
name="cors"
|
|
autocomplete="off"
|
|
checked?={ q.CORS == "on" }
|
|
hx-get={ fmt.Sprintf("%s?%s", "/remote-nodes", paging.EncodedQuery(q, []string{"cors"})) }
|
|
hx-trigger="change"
|
|
hx-push-url="true"
|
|
hx-target="#tbl_nodes"
|
|
hx-swap="outerHTML"
|
|
class="shrink-0 mt-0.5 text-orange-400 bg-neutral-800 border-neutral-700 rounded focus:ring-0 checked:bg-orange-400 checked:border-orange-400 focus:ring-offset-orange-500"
|
|
/>
|
|
<label for="cors" class="text-sm ms-3 text-neutral-400">CORS</label>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, row := range data.Items {
|
|
<tr>
|
|
<td>
|
|
@cellHostPort(row.IPAddresses, row.Hostname, row.Port, row.IsTor, row.IPv6Only)
|
|
</td>
|
|
<td>
|
|
@cellNettype(row.Nettype, row.Height)
|
|
</td>
|
|
<td>
|
|
@cellProtocol(row.Protocol, row.CORSCapable)
|
|
</td>
|
|
<td>
|
|
@cellCountry(row.CountryCode, row.CountryName, row.City, row.ASNName, row.ASN)
|
|
</td>
|
|
<td>
|
|
@cellStatuses(row.IsAvailable, monero.ParseNodeStatuses(row.LastCheckStatus))
|
|
</td>
|
|
<td>{ fmt.Sprintf("%d", row.EstimateFee) }</td>
|
|
<td class="text-right">
|
|
@cellUptime(row.Uptime)
|
|
</td>
|
|
<td title={ time.Unix(row.LastChecked, 0).UTC().Format("Jan 2, 2006 15:04 MST") }>{ timeSince(row.LastChecked) }</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700">
|
|
@DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows)
|
|
@DtPagination("/remote-nodes", "#tbl_nodes", q, p)
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ cellHostPort(ips, hostname string, port uint, isTor, ipv6Only bool) {
|
|
if isTor {
|
|
<!-- TODO: Add modal -->
|
|
<button class="max-w-40 truncate text-orange-400">
|
|
👁 { hostname }
|
|
</button>
|
|
<br/>
|
|
.onion:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
|
|
<span class="text-neutral-400">(TOR)</span>
|
|
} else {
|
|
{ ip.FormatHostname(hostname) }:<span class="text-indigo-400">{ fmt.Sprintf("%d", port) }</span>
|
|
<br/>
|
|
<div class="max-w-40 text-ellipsis overflow-x-auto md:overflow-hidden hover:overflow-visible">
|
|
<span class="whitespace-break-spaces text-gray-400">{ strings.ReplaceAll(ips, ",", " ") }</span>
|
|
if ipv6Only {
|
|
<span class="text-rose-400">(IPv6 only)</span>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ cellNettype(nettype string, height uint) {
|
|
switch nettype {
|
|
case "stagenet":
|
|
<span class="font-semibold uppercase text-sky-500">{ nettype }</span>
|
|
case "testnet":
|
|
<span class="font-semibold uppercase text-rose-500">{ nettype }</span>
|
|
default:
|
|
<span class="font-semibold uppercase text-green-500">{ nettype }</span>
|
|
}
|
|
<br/>
|
|
{ fmt.Sprintf("%d", height) }
|
|
}
|
|
|
|
templ cellProtocol(protocol string, cors bool) {
|
|
switch protocol {
|
|
case "http":
|
|
<span class="font-semibold uppercase text-sky-500">{ protocol }</span>
|
|
default:
|
|
<span class="font-semibold uppercase text-green-500">{ protocol }</span>
|
|
}
|
|
if cors {
|
|
<br/>
|
|
(CORS 💪)
|
|
}
|
|
}
|
|
|
|
templ cellCountry(cc, countryName, city, asnName string, asn uint) {
|
|
if cc != "" {
|
|
if city != "" {
|
|
{ city },
|
|
}
|
|
{ countryName }
|
|
<img class="inline-block" src={ fmt.Sprintf("/assets/img/cf/%s.svg", strings.ToLower(cc)) } alt={ fmt.Sprintf("%s Flag", cc) } width="22px"/>
|
|
}
|
|
if asn != 0 {
|
|
<br/>
|
|
<a
|
|
class="external font-semibold underline !text-purple-400"
|
|
href={ templ.URL(fmt.Sprintf("https://www.ditatompel.com/asn/%d", asn)) }
|
|
target="_blank"
|
|
rel="noopener"
|
|
>{ fmt.Sprintf("AS%d", asn) }</a>
|
|
(<span class="font-semibold text-green-500">{ asnName }</span>)
|
|
}
|
|
}
|
|
|
|
templ cellStatuses(isAvailable bool, statuses [5]int) {
|
|
if isAvailable {
|
|
<span class="font-semibold text-green-500">Online</span>
|
|
} else {
|
|
<span class="text-rose-400">Offline</span>
|
|
}
|
|
<br/>
|
|
for _, status := range statuses {
|
|
if status == 1 {
|
|
<span class="text-green-400 mr-1">•</span>
|
|
} else if status == 0 {
|
|
<span class="text-red-400 mr-1">•</span>
|
|
} else {
|
|
<span class="text-neutral-600 mr-1">•</span>
|
|
}
|
|
}
|
|
}
|
|
|
|
templ cellUptime(uptime float64) {
|
|
if uptime >= 98 {
|
|
<span class="text-green-500">{ formatFloat(uptime) }%</span>
|
|
} else if uptime < 98 && uptime >= 80 {
|
|
<span class="text-sky-500">{ formatFloat(uptime) }%</span>
|
|
} else if uptime < 80 && uptime > 75 {
|
|
<span class="text-orange-500">{ formatFloat(uptime) }%</span>
|
|
} else {
|
|
<span class="text-rose-500">{ formatFloat(uptime) }%</span>
|
|
}
|
|
}
|