global: snapshot

This commit is contained in:
k
2024-09-13 22:59:20 +02:00
parent deffaef2b5
commit bf2034b80c
18 changed files with 5154 additions and 645 deletions

135
README.md
View File

@@ -1,75 +1,95 @@
# SATONOMICS
<p align="center">
<a href="https://kibo.money" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-light.svg">
<img alt="kibō" src="https://raw.githubusercontent.com/satonomics-org/satonomics/main/assets/logo-full-light.svg" width="300" height="auto" style="max-width: 100%;">
</picture>
</a>
</p>
![Image of the Satonomics Web App](./assets/latest.jpg)
<p align="center">
A better, FOSS, Bitcoin-only, self-hostable Glassnode.
</p>
## Description
kibō (hope) is a better, FOSS, Bitcoin-only, self-hostable Glassnode.
While [mempool.space](https://mempool.space) gives a very micro view of the network where you can follow the journey of any address, this tool is the exact opposite and very complimentary by giving you a much more global/macro view of the flow and various dynamics of the network via thousands of charts.
To promote even more transparency and trust in the network, this project is committed to making on-chain data accessible and verifiable by all, no matter your intentions or financial situation. That is why, the whole project is completely free, from code to services, including a real-time API with thousands and thousands of routes which can be used at will.
**Having anyone be able to easily do a health-check of the network is incredibly important and should be wanted by every single bitcoiner.**
## Warning
This project is in a very early stage. The web app will have bugs, the API might break and the data can definitely to be false or slightly false.
## Donations
The project is a lot of work and being worked on full-time. It doesn't have any ads and solely relies on donations. If you find this project useful, any sat would really help make it even better and would be really appreciated.
This project was started as an answer to the outrageous pricing from Glassnode (and their third tier starting at $833.33/month !).
You can donate on the project's [Geyser Fund](https://geyser.fund/project/satonomics/).
But it is a lot of work and has been worked on _**full-time since November of 2023**_ and has also been operational since then without any ads.
_**At the time of writing (2024-09-12), this project has made around 2,200,000 sats, which is around $1300 or $120/month. It's unsustainable.**_
So if you find this project useful, [please send some sats](https://geyser.fund/project/satonomics/), it would be really appreciated.
[Geyser Fund](https://geyser.fund/project/satonomics/)
## Warning
This project is in a very early stage. Until more people look at the code and check the various computations, the datasets might be in the worst case completely false.
## Instances
Web App:
- [app.satonomics.xyz](https://app.satonomics.xyz)
API:
- [api.satonomics.xyz](https://api.satonomics.xyz)
- [api-bkp.satonomics.xyz](https://api-bkp.satonomics.xyz)
- [kibo.money](https://kibo.money)
- [backup.kibo.money](https://backup.kibo.money)
## Structure
- `parser`: The backbone of the project, it does most of the work by parsing and then computing datasets from the timechain.
- `server`: A small server which automatically creates routes to access through an API all created datasets.
- `app`: A web app which displays the generated datasets in various charts and dashboards.
- `website`: A web app which displays the generated datasets in various charts and dashboards.
- `server`: A small server which will serve the
## How to run
## Setup
### Requirements
- `rustup`
- 1 TB of free space (will use 60-80% of that)
- A running instance of bitcoin-core with txindex=1 and rpc credentials
### Parser
### Docker
Coming soon
### Manual
#### Hardware
#### 1. Rust
```bash
./run.sh --datadir=$HOME/Developer/bitcoin
# https://www.rust-lang.org/tools/install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# https://github.com/watchexec/cargo-watch?tab=readme-ov-file#install
cargo install cargo-watch --locked
```
### Server
#### 2. Parser
```bash
# The first run needs several information about your bitcoin-core config
./run.sh --datadir=$HOME/Developer/bitcoin --rpcuser=satoshi --rpcpassword=nakamoto
# Next time you can just do: ./run.sh
# As everything is saved in
```
#### Server
```bash
# Install rustup
# Update ./run.sh if needed
./run.sh
```
Then the easiest to let others access your server is with `cloudflared` which will also cache requests.
## Limitations
- Needs to stop the node to parse the files (at least for now)
- Needs a **LOT** a disk space for databases (~700 GB for data from 2009 to mid 2024)
## Goals / Philosophy
Adjectives that describe what this project is or strives to be, in no particular order:
## Philosophy
- **Best**: Replace Glassnode as the go to
- **Diverse**: Have as many charts/datasets as possible and something for everyone
@@ -83,46 +103,25 @@ Adjectives that describe what this project is or strives to be, in no particular
- **Versatile**: You can view the data in charts, you can download the data, you can fetch the data via an API
- **Accessible**: Free Website and API with all the datasets for everyone
## Milestones
## Logo
Big features that are planned, in no particular order:
The dove (borrowed from [svgrepo](https://www.svgrepo.com/svg/351969/dove) for now) represents _**hope**_ (kibō in japanese).
The orange background represents Bitcoin and when in a circle, it also represents the sun, which means that while it's our hope for a better future, we still have to be careful with our collective goals and actions, to not end up like Icarus.
## Roadmap
- **Homepage**: A landing page to explains the project and what it does
- **More Datasets/Charts**: If a dataset can be computed, it should exist and have its related charts
- **Dashboards**: For a quick and real-time view of the latest data of all the datasets
- **NOSTR integration**: First to save preferences, later to add some social functionnality
- **Datasets by block timestamp**: In addition to having datasets by block date and block height
- **Nostr integration**: First to save preferences, later to add some social functionnality
- **Datasets by block timestamp**: In addition to having datasets by date and height
- **Descriptions**: Add text to describe all charts and what they mean
- **Start9 Add-on**: By making the whole suite much easier to self-host, it's quite rough right now
- **Start9 support**: By making the whole suite much easier to self-host, it's quite rough right now
- **API Documentation**: Highly needed to explain what's what
_Maybe_:
## Iterations
- A Desktop app
- A mobile app
## Brand
- **Name**: Willing to change if someone thinks of something better !
- **Logo**: Most likely a placeholder
## Collaboration
- Repositories:
- [Github](https://github.com/satonomics-org/satonomics)
- [Codeberg](https://codeberg.org/satonomics/satonomics)
- Issues:
- [Github](https://github.com/satonomics-org/satonomics/issues)
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/issues)
- Proposals:
- [Github](https://github.com/satonomics-org/satonomics/pulls)
- [NOSTR](https://gitworkshop.dev/r/naddr1qq99xct5dahx7mtfvdesz9thwden5te0wp6hyurvv4ex2mrp0yhxxmmdqgsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03srqsqqqaueek2h03/proposals)
## Proof of Work
Aka: Previous iterations
The initial idea was totally different yet morphed over time into what it is today: a fully FOSS self-hostable on-chain data generator.
A list of all the previous versions and ideas:
- https://github.com/drgarlic/satonomics
- https://github.com/drgarlic/satonomics-parser

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env bash
# if [ "$(uname)" == "Darwin" ]; then
# if [ mdutil -s / | grep "disabled" ]; then
# sudo mdutil -a -i on
# fi
# fi
bitcoin-cli -datadir=/Users/k/Developer/bitcoin stop

2
server/Cargo.lock generated
View File

@@ -2482,6 +2482,7 @@ version = "0.4.0"
dependencies = [
"axum",
"bincode",
"chrono",
"color-eyre",
"derive_deref",
"itertools",
@@ -2492,7 +2493,6 @@ dependencies = [
"serde_json",
"swc",
"swc_common",
"swc_ecma_minifier",
"tokio",
"tower-http",
]

View File

@@ -5,17 +5,17 @@ edition = "2021"
[dependencies]
axum = "0.7.5"
color-eyre = "0.6.3"
itertools = "0.13.0"
regex = "1.10.6"
bincode = { git = "https://github.com/bincode-org/bincode.git" }
chrono = "0.4.38"
color-eyre = "0.6.3"
derive_deref = "1.1.1"
itertools = "0.13.0"
parser = { path = "../parser" }
regex = "1.10.6"
reqwest = { version = "0.12.7", features = ["json"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.128" }
tokio = { version = "1.40.0", features = ["full"] }
tower-http = { version = "0.5.2", features = ["compression-full"] }
parser = { path = "../parser" }
derive_deref = "1.1.1"
swc = "0.286.0"
swc_common = "0.37.5"
swc_ecma_minifier = "0.205.2"
tokio = { version = "1.40.0", features = ["full"] }
tower-http = { version = "0.5.2", features = ["compression-full"] }

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env bash
if cargo watch --help &> /dev/null; then
TRIGGER="./in/datasets_len.txt"

View File

@@ -6,7 +6,7 @@ use axum::{
response::{IntoResponse, Response},
};
use color_eyre::{eyre::eyre, owo_colors::OwoColorize};
use reqwest::{header::HOST, StatusCode};
use reqwest::StatusCode;
use serde::Deserialize;
use parser::{log, Date, DateMap, Height, HeightMap, MapChunkId, HEIGHT_MAP_CHUNK_SIZE, OHLC};
@@ -14,10 +14,11 @@ use parser::{log, Date, DateMap, Height, HeightMap, MapChunkId, HEIGHT_MAP_CHUNK
use crate::{
api::structs::{Chunk, Kind, Route},
header_map::HeaderMapUtils,
response::typed_value_to_response,
AppState,
};
use super::response::typed_value_to_response;
#[derive(Deserialize)]
pub struct Params {
chunk: Option<usize>,
@@ -168,13 +169,8 @@ where
let offsetted_to_url = |offseted| {
datasets.get(&ChunkId::from_usize(offseted)).map(|_| {
let host = headers[HOST].to_str().unwrap();
let scheme = if host.contains("0.0.0.0") || host.contains("localhost") {
"http"
} else {
"https"
};
let scheme = headers.get_scheme();
let host = headers.get_host();
format!("{scheme}://{host}{}?chunk={offseted}", route.url_path)
})
};

View File

@@ -1,7 +1,9 @@
use axum::{extract::State, http::HeaderMap, response::Response};
use reqwest::header::HOST;
use crate::{response::generic_to_reponse, AppState};
use crate::AppState;
use super::response::generic_to_reponse;
pub async fn fallback(headers: HeaderMap, State(app_state): State<AppState>) -> Response {
generic_to_reponse(

View File

@@ -1,5 +1,6 @@
mod dataset;
mod fallback;
mod response;
pub use dataset::*;
pub use fallback::*;

View File

@@ -101,7 +101,7 @@ impl Routes {
.map(|route| format!("\"{}\"", route.url_path))
.join(" | ");
format!("// This file is auto generated by the server\n// Manual changes are forbidden\n\ntype {}Path = {};\n", name, paths)
format!("export type {}Path = {};\n", name, paths)
};
let date_type = map_to_type("Date", &self.date);
@@ -112,7 +112,7 @@ impl Routes {
fs::write(
format!("{WEBSITE_TYPES_PATH}/paths.d.ts"),
format!("{date_type}\n{height_type}\n{last_type}"),
format!("// This file is auto generated by the server\n// Manual changes are forbidden\n\n{date_type}\n{height_type}\n{last_type}"),
)
.unwrap();
}

View File

@@ -1,14 +1,23 @@
use std::path::Path;
use axum::http::{header, HeaderMap};
use chrono::{DateTime, Utc};
use parser::log;
use reqwest::header::HOST;
const STALE_IF_ERROR: u64 = 31_536_000; // 1 Year
const STALE_IF_ERROR: u64 = 30_000_000; // 1 Year ish
pub trait HeaderMapUtils {
fn get_scheme(&self) -> &str;
fn get_host(&self) -> &str;
fn check_if_host_is_any_local(&self) -> bool;
fn check_if_host_is_0000(&self) -> bool;
fn check_if_host_is_localhost(&self) -> bool;
fn insert_cors(&mut self);
fn insert_cache_control(&mut self, max_age: u64, stale_while_revalidate: u64);
fn insert_last_modified(&mut self, date: DateTime<Utc>);
fn insert_content_type(&mut self, path: &Path);
fn insert_content_type_image_icon(&mut self);
@@ -24,6 +33,30 @@ pub trait HeaderMapUtils {
}
impl HeaderMapUtils for HeaderMap {
fn get_scheme(&self) -> &str {
if self.check_if_host_is_any_local() {
"http"
} else {
"https"
}
}
fn get_host(&self) -> &str {
self[HOST].to_str().unwrap()
}
fn check_if_host_is_any_local(&self) -> bool {
self.check_if_host_is_localhost() || self.check_if_host_is_0000()
}
fn check_if_host_is_0000(&self) -> bool {
self.get_host().contains("0.0.0.0")
}
fn check_if_host_is_localhost(&self) -> bool {
self.get_host().contains("localhost")
}
fn insert_cors(&mut self) {
self.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap());
self.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "*".parse().unwrap());
@@ -39,6 +72,12 @@ impl HeaderMapUtils for HeaderMap {
);
}
fn insert_last_modified(&mut self, date: DateTime<Utc>) {
let formatted = date.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
self.insert(header::LAST_MODIFIED, formatted.parse().unwrap());
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
fn insert_content_type(&mut self, path: &Path) {
match path.extension().unwrap().to_str().unwrap() {
@@ -60,24 +99,15 @@ impl HeaderMapUtils for HeaderMap {
}
fn insert_content_type_image_icon(&mut self) {
self.insert(
header::CONTENT_TYPE,
"image/x-icon".parse().unwrap(),
);
self.insert(header::CONTENT_TYPE, "image/x-icon".parse().unwrap());
}
fn insert_content_type_image_jpeg(&mut self) {
self.insert(
header::CONTENT_TYPE,
"image/jpeg".parse().unwrap(),
);
self.insert(header::CONTENT_TYPE, "image/jpeg".parse().unwrap());
}
fn insert_content_type_image_png(&mut self) {
self.insert(
header::CONTENT_TYPE,
"image/png".parse().unwrap(),
);
self.insert(header::CONTENT_TYPE, "image/png".parse().unwrap());
}
fn insert_content_type_application_javascript(&mut self) {
@@ -92,7 +122,10 @@ impl HeaderMapUtils for HeaderMap {
}
fn insert_content_type_application_manifest_json(&mut self) {
self.insert(header::CONTENT_TYPE, "application/manifest+json".parse().unwrap());
self.insert(
header::CONTENT_TYPE,
"application/manifest+json".parse().unwrap(),
);
}
fn insert_content_type_text_css(&mut self) {

View File

@@ -10,7 +10,6 @@ use website::WebsiteRoutes;
mod api;
mod header_map;
mod response;
mod website;
#[derive(Clone, Debug, Default, Serialize)]

View File

@@ -9,6 +9,7 @@ use axum::{
http::HeaderMap,
response::{IntoResponse, Response},
};
use chrono::{DateTime, Utc};
use parser::log;
use reqwest::StatusCode;
@@ -38,17 +39,22 @@ pub async fn file_handler(headers: HeaderMap, path: extract::Path<String>) -> Re
}
}
path_to_response(&path)
path_to_response(headers, &path)
}
pub async fn index_handler(headers: HeaderMap) -> Response {
path_to_response(&str_to_path("index.html"))
path_to_response(headers, &str_to_path("index.html"))
}
fn path_to_response(path: &Path) -> Response {
fn path_to_response(headers: HeaderMap, path: &Path) -> Response {
let mut response;
if path.extension().unwrap() == "js" {
let time = path.metadata().unwrap().modified().unwrap();
let date: DateTime<Utc> = time.into();
let is_localhost = headers.check_if_host_is_localhost();
if !is_localhost && path.extension().unwrap() == "js" {
let content = minify_js(path);
response = Response::new(content.into());
@@ -67,6 +73,12 @@ fn path_to_response(path: &Path) -> Response {
headers.insert_cors();
headers.insert_content_type(path);
if !is_localhost {
headers.insert_cache_control(10, 50);
}
headers.insert_last_modified(date);
response
}

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env bash
pwa-asset-generator "../assets/logo-dove-orange.svg" "./assets" \
--index "./assets/index.html" \
--manifest "./manifest.webmanifest" \

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -11,22 +11,14 @@
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta
name="theme-color"
content="#fffaf6"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="#110f0e"
media="(prefers-color-scheme: dark)"
/>
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="mobile-web-app-capable" content="yes" />
<!-- Styles -->
<style>
/* Tailwind base */
*,
::after,
::before,
@@ -271,7 +263,6 @@
--line-height-xl: 1.75rem; /* 28px */
--font-weight-base: 450;
--font-weight-medium: 575;
--font-weight-bold: 700;
--transform-scale-active: scaleX(0.95) scaleY(0.9);
@@ -365,11 +356,14 @@
@media (min-width: 768px) {
flex-direction: row;
html[data-display="standalone"] & {
border-top: 1px;
}
}
}
button {
font-weight: var(--font-weight-medium);
cursor: pointer;
background-color: var(--background-color);
@@ -444,7 +438,6 @@
h4 {
font-size: var(--font-size-base);
line-height: var(--line-height-base);
font-weight: var(--font-weight-medium);
}
header {
@@ -517,7 +510,6 @@
flex-direction: column;
user-select: none;
-webkit-user-select: none;
font-weight: var(--font-weight-medium);
color: var(--off-color);
position: relative;
@@ -539,9 +531,7 @@
}
> span.emoji {
line-height: 0.9;
filter: grayscale(100%) brightness(60%) contrast(150%);
font-size: 1.0625rem;
}
> svg.favorite {
@@ -582,7 +572,7 @@
color: var(--off-color);
font-size: var(--font-size-2xs);
line-height: var(--line-height-2xs);
overflow: visible;
overflow: visible !important;
}
> *:not(input[type="radio"]):not(svg) {
@@ -645,10 +635,14 @@
order: 999;
overflow-y: auto;
html[data-display="standalone"] & {
padding-bottom: 1.5rem /* 24px */;
}
@media (min-width: 768px) {
border-width: 0px 1px 0px 0px;
order: 0;
padding: 1rem 0.75rem 1.375rem 0.75rem;
padding: 1rem 0.75rem 1.375rem 0.75rem !important;
}
&,
@@ -767,68 +761,101 @@
.tree {
user-select: none;
-webkit-user-select: none;
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
summary {
list-style: none;
display: flex;
align-items: center;
cursor: pointer;
ul {
overflow: hidden;
&::marker,
&::-webkit-details-marker {
display: none;
}
&:hover,
&[data-highlight] {
* {
color: var(--orange) !important;
}
}
/* &:hover * {
color: var(--orange) !important;
} */
> span.marker {
color: var(--border-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
z-index: 10;
margin-left: -5px;
margin-bottom: 0.0625rem;
}
> small {
margin-top: 0.125rem;
margin-right: 2.5px;
}
}
li {
display: block;
position: relative;
padding-left: 12px;
border-left: 1px;
&:not(:has(~ li:not([hidden]))) {
border-color: transparent !important;
}
&::before {
content: "";
li {
display: block;
position: absolute;
top: -12px;
left: -1px;
width: 9px;
height: 1.75rem;
border-color: var(--border-color);
border-width: 0 0 1px 1px;
border-radius: 0px 0px 0px 4px;
position: relative;
padding-left: 12px;
border-left: 1px;
&:has(input:checked) {
&::before {
border-color: var(--orange) !important;
}
> details > summary > span.marker {
color: var(--orange) !important;
}
}
&:not(:has(~ li:not([hidden]))) {
border-color: transparent !important;
}
&::before {
content: "";
display: block;
position: absolute;
top: -12px;
left: -1px;
width: 9px;
height: 1.75rem;
border-color: var(--border-color);
border-width: 0 0 1px 1px;
border-radius: 0px 0px 0px 4px;
}
&:has(input:checked) {
> details > summary::after {
border-color: var(--orange) !important;
}
}
&:has(~ li input:checked) {
border-color: var(--orange) !important;
&::before {
z-index: -10;
}
}
> details > summary {
list-style: none;
display: flex;
align-items: center;
cursor: pointer;
position: relative;
&::marker,
&::-webkit-details-marker {
display: none;
}
&:hover {
color: var(--orange);
}
details[open] > & {
&::after {
content: "";
display: block;
position: absolute;
bottom: 0;
height: 12px;
border-color: var(--border-color);
border-width: 0 0 0px 1px;
}
}
> span.marker {
color: var(--border-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
z-index: 10;
margin-left: -5px;
margin-bottom: 0.0625rem;
}
> small {
margin-top: 0.125rem;
margin-right: 2.5px;
}
}
}
}
}
@@ -876,7 +903,6 @@
}
.sats {
font-weight: 500;
color: var(--orange);
}
}
@@ -916,8 +942,8 @@
display: flex;
align-items: center;
gap: 1.5rem;
margin: 0 -1.5rem;
padding: 0 1.5rem;
margin: -0.75rem -1.5rem;
padding: 0.75rem 1.5rem;
overflow-x: auto;
> div {
@@ -976,12 +1002,6 @@
min-height: 0;
z-index: 20;
> .shadow-bottom {
bottom: 1.75rem;
width: 80px;
left: auto;
}
> .chart-wrapper {
height: 100%;
position: relative;
@@ -1103,8 +1123,14 @@
right: 0;
z-index: 10;
pointer-events: none;
#chart-list > & {
bottom: 1.75rem;
width: 80px;
left: auto;
}
}
.shadow-left:not(:has(~ #selected-frame[hidden])) {
.shadow-left {
position: absolute;
background-image: linear-gradient(
to left,
@@ -1118,7 +1144,7 @@
z-index: 30;
pointer-events: none;
}
.shadow-right:not(:has(~ #selected-frame[hidden])) {
.shadow-right {
position: absolute;
background-image: linear-gradient(
to right,
@@ -1131,6 +1157,10 @@
bottom: 0;
z-index: 10;
pointer-events: none;
&:not(:has(~ #selected-frame:not([hidden]))) {
display: none;
}
}
@media (min-width: 768px) {
@@ -1144,42 +1174,38 @@
<script>
// @ts-check
// Keep in sync with js files
/** @typedef {'system' | 'dark' | 'light'} SettingsTheme */
/**
* @import { SettingsTheme } from "./types/self"
*/
const settingsThemeLocalStorageKey = "settings-theme";
let theme = /** @type {SettingsTheme} */ (
localStorage.getItem(settingsThemeLocalStorageKey) || "system"
const theme = /** @type {SettingsTheme} */ (
localStorage.getItem(settingsThemeLocalStorageKey)
);
if (theme !== "dark" && theme !== "light" && theme !== "system") {
theme = "system";
}
const preferredColorSchemeMatchMedia = window.matchMedia(
"(prefers-color-scheme: dark)",
"(prefers-color-scheme: dark)"
);
function updateDataTheme() {
localStorage.setItem(settingsThemeLocalStorageKey, theme);
if (
theme === "dark" ||
((!theme || theme === "system") &&
preferredColorSchemeMatchMedia.matches)
) {
window.document.documentElement.dataset.theme = "dark";
} else {
window.document.documentElement.dataset.theme = undefined;
}
if (
theme === "dark" ||
(theme !== "light" && preferredColorSchemeMatchMedia.matches)
) {
window.document.documentElement.dataset.theme = "dark";
} else {
delete window.document.documentElement.dataset.theme;
}
updateDataTheme();
const backgroundColor = getComputedStyle(
window.document.documentElement
).getPropertyValue("--background-color");
const meta = window.document.createElement("meta");
meta.name = "theme-color";
meta.content = backgroundColor;
window.document.getElementsByTagName("head")[0].appendChild(meta);
preferredColorSchemeMatchMedia.addEventListener("change", () => {
if (theme === "system") {
updateDataTheme();
}
});
if ("standalone" in window.navigator && !!window.navigator.standalone) {
window.document.documentElement.dataset.display = "standalone";
}
</script>
<!-- Icons -->
@@ -1583,7 +1609,7 @@
for="folders-filter-all"
title="Chart"
>
<span class="absolute" id="folders-filter-all-count">0</span>
<span class="absolute" id="folders-filter-all-count"></span>
<input
type="radio"
name="folders-filter"
@@ -1597,9 +1623,10 @@
for="folders-filter-favorites"
title="Chart"
>
<span class="absolute" id="folders-filter-favorites-count"
>0</span
>
<span
class="absolute"
id="folders-filter-favorites-count"
></span>
<input
type="radio"
name="folders-filter"
@@ -1613,7 +1640,7 @@
for="folders-filter-new"
title="Chart"
>
<span class="absolute" id="folders-filter-new-count">0</span>
<span class="absolute" id="folders-filter-new-count"></span>
<input
type="radio"
name="folders-filter"
@@ -1682,7 +1709,7 @@
<hr />
<h4>General</h4>
<fieldset>
<div class="field">
<div class="field" id="settings-theme-field">
<legend>Theme</legend>
<hr />
<div>
@@ -1695,7 +1722,6 @@
name="settings-theme"
id="settings-theme-system-input"
value="system"
onchange="theme = 'system'; updateDataTheme();"
/>
System
</label>
@@ -1708,7 +1734,6 @@
name="settings-theme"
id="settings-theme-dark-input"
value="dark"
onchange="theme = 'dark'; updateDataTheme();"
/>
Dark
</label>
@@ -1721,33 +1746,10 @@
name="settings-theme"
id="settings-theme-light-input"
value="light"
onchange="theme = 'light'; updateDataTheme();"
/>
Light
</label>
</div>
<script>
switch (/** @type {SettingsTheme} */ (theme)) {
case "light": {
window.document
.getElementById("settings-theme-light-input")
?.click();
break;
}
case "dark": {
window.document
.getElementById("settings-theme-dark-input")
?.click();
break;
}
case "system": {
window.document
.getElementById("settings-theme-system-input")
?.click();
break;
}
}
</script>
</div>
<small>Options for the application's color scheme</small>
</fieldset>
@@ -1765,194 +1767,7 @@
<ol id="leaderboard"></ol>
<small>And everybody else !</small>
<script>
const leaderboard = window.document.getElementById("leaderboard");
if (!leaderboard) throw "Leaderboard should exist by now";
const donations = [
{
name: "_Checkɱate",
// url: "https://xcancel.com/_Checkmatey_",
url: "https://primal.net/p/npub1qh5sal68c8swet6ut0w5evjmj6vnw29x3k967h7atn45unzjyeyq6ceh9r",
amount: 500_000,
},
{
name: "avvi |",
url: "https://primal.net/p/npub1md2q6fexrtmd5hx9gw2p5640vg662sjlpxyz3tdmu4j4g8hhkm6scn6hx3",
amount: 5_000,
},
{
name: "mutatrum",
url: "https://primal.net/p/npub1hklphk7fkfdgmzwclkhshcdqmnvr0wkfdy04j7yjjqa9lhvxuflsa23u2k",
amount: 5_000,
},
{
name: "Gunnar",
url: "https://primal.net/p/npub1rx9wg2d5lhah45xst3580sajcld44m0ll9u5dqhu2t74p6xwufaqwghtd4",
amount: 1_000,
},
{
name: "Blokchain Boog",
url: "https://xcancel.com/BlokchainB",
amount: 1_500 + 1590,
},
{
name: "Josh",
url: "https://primal.net/p/npub1pc57ls4rad5kvsp733suhzl2d4u9y7h4upt952a2pucnalc59teq33dmza",
amount: 1_000,
},
{
name: "Alp",
url: "https://primal.net/p/npub175nul9cvufswwsnpy99lvyhg7ad9nkccxhkhusznxfkr7e0zxthql9g6w0",
amount: 1_000,
},
{
name: "Ulysses",
url: "https://primal.net/p/npub1n7n3dssm90hfsfjtamwh2grpzwjlvd2yffae9pqgg99583lxdypsnn9gtv",
amount: 1_000,
},
{
name: "btcschellingpt",
url: "https://primal.net/p/npub1nvfgglea9zlcs58tcqlc6j26rt50ngkgdk7699wfq4txrx37aqcsz4e7zd",
amount: 1_000 + 1_000,
},
{
name: "Coinatra",
url: "https://primal.net/p/npub1eut9kcejweegwp9waq3a4g03pvprdzkzvjjvl8fvj2a2wlx030eswzfna8",
amount: 1_000,
},
{
name: "Printer Go Brrrr",
url: "https://primal.net/p/npub1l5pxvjzhw77h86tu0sml2gxg8jpwxch7fsj6d05n7vuqpq75v34syk4q0n",
amount: 1_000,
},
{
name: "b81776c32d7b",
url: "https://primal.net/p/npub1hqthdsed0wpg57sqsc5mtyqxxgrh3s7493ja5h49v23v2nhhds4qk4w0kz",
amount: 17_509,
},
{
name: "DerGigi",
url: "https://primal.net/p/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc",
amount: 6001,
},
{
name: "Adarnit",
url: "https://primal.net/p/npub17armdveqy42uhuuuwjc5m2dgjkz7t7epgvwpuccqw8jusm8m0g4sn86n3s",
amount: 17_726,
},
{
name: "Auburn Citadel",
url: "https://primal.net/p/npub1730y5k2s9u82w9snx3hl37r8gpsrmqetc2y3xyx9h65yfpf28rtq0y635y",
amount: 17_471,
},
{
name: "anon",
amount: 210_000,
},
{
name: "Daniel ∞/21M",
url: "https://twitter.com/DanielAngelovBG",
amount: 21_000,
},
{
name: "Ivo",
url: "https://primal.net/p/npub1mnwjn40hr042rsmzu64rsnwsw07uegg4tjkv620c94p6e797wkvq3qeujc",
amount: 5_000,
},
{
name: "lassdas",
url: "https://primal.net/p/npub1gmhctt2hmjqz8ay2x8h5f8fl3h4fpfcezwqneal3usu3u65qca4s8094ea",
amount: 210_000,
},
{
name: "anon",
amount: 21_000,
},
{
name: "xplbzx",
url: "https://primal.net/p/npub1e0f808a350rxrhppu4zylzljt3arfpvrrpqdg6ft78xy6u49kq5slf0g92",
amount: 12_110,
},
{
name: "SoundMoney=Prosperity4ALL",
url: "https://xcancel.com/SoundmoneyP",
amount: 420_000,
},
{
name: "Johan",
url: "https://primal.net/p/npub1a4sd4cprrucfkvkfq9zs99ur4xe7lxw3uhhgvuzx6nqxhnpa2yyqlsa26u",
amount: 500_000,
},
{
name: "highperfocused",
url: "https://primal.net/p/npub1fq8vrf63vsrqjrwqgtwlvauqauc0yme6se8g8dqhcpf6tfs3equqntmzut",
amount: 4620,
},
{
name: "ClearMined",
url: "https://primal.net/p/npub1dj8zwktp3eyktfhs5mjlw8v0v2838xlquxr7ddsanayhcw98fcks8ddrq9",
amount: 300_000,
},
];
donations.sort((a, b) =>
b.amount !== a.amount
? b.amount - a.amount
: a.name.localeCompare(b.name),
);
donations.slice(0, 21).forEach(({ name, url, amount }) => {
const li = window.document.createElement("li");
leaderboard.append(li);
const a = window.document.createElement("a");
a.href = url || "";
a.target = "_blank";
a.rel = "noopener noreferrer";
a.innerHTML = name;
li.append(a);
li.append(" — ");
const small = window.document.createElement("small");
small.classList.add("sats");
small.innerHTML = `${amount.toLocaleString("en-us")} sats`;
li.append(small);
});
</script>
<script>
const standalone =
"standalone" in window.navigator && !!window.navigator.standalone;
const userAgent = navigator.userAgent.toLowerCase();
const isChrome = userAgent.includes("chrome");
const safari = userAgent.includes("safari");
const safariOnly = safari && !isChrome;
const macOS = userAgent.includes("mac os");
const iphone = userAgent.includes("iphone");
const ipad = userAgent.includes("ipad");
if (!standalone && safariOnly && (macOS || ipad || iphone)) {
const frame = window.document.getElementById("settings-frame");
if (!frame) throw "Settings frame should exist by now";
const hr = window.document.createElement("hr");
frame.append(hr);
const heading = window.document.createElement("h4");
heading.innerHTML = "Install";
frame.append(heading);
const p = window.document.createElement("p");
frame.append(p);
if (macOS) {
p.innerHTML = `This app can be installed by clicking on the <strong>File</strong> tab on the menu bar and then on <strong>Add to dock</strong>.`;
} else {
p.innerHTML = `This app can be installed by tapping on the <strong>Share</strong> button tab of Safari and then on <strong>Add to Home Screen</strong>.`;
}
}
</script>
<p id="settings-install-instructions" hidden></p>
<hr class="md:hidden" />
@@ -1965,43 +1780,10 @@
"
class="md:hidden"
></p>
<script>
const anchorApi = /** @type {HTMLAnchorElement | undefined} */ (
window.document.getElementById("anchor-api")?.cloneNode(true)
);
const anchorGit = /** @type {HTMLAnchorElement | undefined} */ (
window.document.getElementById("anchor-git")?.cloneNode(true)
);
const anchorNostr = /** @type {HTMLAnchorElement | undefined} */ (
window.document.getElementById("anchor-nostr")?.cloneNode(true)
);
const anchorGeyser = /** @type {HTMLAnchorElement | undefined} */ (
window.document.getElementById("anchor-geyser")?.cloneNode(true)
);
if (!anchorApi || !anchorGit || !anchorNostr || !anchorGeyser)
throw "Anchors should exist by now";
anchorApi.id = "";
anchorGit.id = "";
anchorNostr.id = "";
anchorGeyser.id = "";
const nav = window.document.getElementById("settings-nav");
if (!nav) throw "Should exist by now";
nav.append(anchorApi);
nav.append(anchorGit);
nav.append(anchorNostr);
nav.append(anchorGeyser);
</script>
<hr />
<p style="color: var(--off-color); padding: 1rem; text-align: center">
<p style="color: var(--off-color); text-align: center">
<span>Charts are displayed via </span>
<a
href="https://www.tradingview.com"
@@ -2067,7 +1849,7 @@
style="
text-transform: uppercase;
display: block;
font-weight: 400;
font-weight: var(--font-weight-base);
font-size: var(--font-size-2xs);
line-height: var(--line-height-2xs);
"

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,9 @@ import {
ISeriesApi,
} from "../libraries/lightweight-charts/types";
import { DatePath, HeightPath } from "./paths";
import { Owner } from "../libraries/solid-signals/types/owner";
type SettingsTheme = "system" | "dark" | "light";
type Signal<T> = Accessor<T> & { set: Setter<T> };
@@ -52,6 +55,7 @@ type AnySpecificSeriesBlueprint =
type SpecificSeriesBlueprintWithChart<A extends AnySpecificSeriesBlueprint> = {
chart: IChartApi;
owner: Owner | null;
} & Omit<A, "type">;
type SeriesBlueprint = {
@@ -129,7 +133,7 @@ interface OHLC {
interface ResourceDataset<
S extends Scale,
Type extends OHLC | number = number,
Type extends OHLC | number = number
> {
scale: S;
url: string;
@@ -147,7 +151,7 @@ interface FetchedResult<
SingleValueData | ValuedCandlestickData
> = DatasetValue<
Type extends number ? SingleValueData : ValuedCandlestickData
>,
>
> {
at: Date | null;
json: Signal<FetchedJSON<S, Type> | null>;
@@ -177,7 +181,7 @@ interface FetchedChunk {
type FetchedDataset<
S extends Scale,
Type extends number | OHLC,
Type extends number | OHLC
> = S extends "date" ? FetchedDateDataset<Type> : FetchedHeightDataset<Type>;
interface Versioned {