mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-17 10:19:44 -07:00
server: api + doc
This commit is contained in:
Generated
+3
-1
@@ -504,6 +504,7 @@ dependencies = [
|
||||
"brk_computer",
|
||||
"brk_error",
|
||||
"brk_fetcher",
|
||||
"brk_grouper",
|
||||
"brk_indexer",
|
||||
"brk_interface",
|
||||
"brk_logger",
|
||||
@@ -512,6 +513,7 @@ dependencies = [
|
||||
"brk_server",
|
||||
"brk_store",
|
||||
"brk_structs",
|
||||
"brk_traversable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -710,6 +712,7 @@ dependencies = [
|
||||
name = "brk_interface"
|
||||
version = "0.0.111"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
"brk_computer",
|
||||
"brk_error",
|
||||
@@ -1178,7 +1181,6 @@ dependencies = [
|
||||
"quick_cache",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sonic-rs 0.5.5",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
|
||||
+10
-1
@@ -10,12 +10,15 @@ rust-version.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
default = ["cli"]
|
||||
full = [
|
||||
"binder",
|
||||
"bundler",
|
||||
"cli",
|
||||
"computer",
|
||||
"error",
|
||||
"fetcher",
|
||||
"grouper",
|
||||
"indexer",
|
||||
"interface",
|
||||
"logger",
|
||||
@@ -24,12 +27,15 @@ full = [
|
||||
"server",
|
||||
"store",
|
||||
"structs",
|
||||
"traversable",
|
||||
]
|
||||
binder = ["brk_binder"]
|
||||
bundler = ["brk_bundler"]
|
||||
cli = ["brk_cli"]
|
||||
computer = ["brk_computer"]
|
||||
error = ["brk_error"]
|
||||
fetcher = ["brk_fetcher"]
|
||||
grouper = ["brk_grouper"]
|
||||
indexer = ["brk_indexer"]
|
||||
interface = ["brk_interface"]
|
||||
logger = ["brk_logger"]
|
||||
@@ -38,14 +44,16 @@ parser = ["brk_parser"]
|
||||
server = ["brk_server"]
|
||||
store = ["brk_store"]
|
||||
structs = ["brk_structs"]
|
||||
traversable = ["brk_traversable"]
|
||||
|
||||
[dependencies]
|
||||
brk_binder = { workspace = true, optional = true }
|
||||
brk_bundler = { workspace = true, optional = true }
|
||||
brk_cli = { workspace = true }
|
||||
brk_cli = { workspace = true, optional = true }
|
||||
brk_computer = { workspace = true, optional = true }
|
||||
brk_error = { workspace = true, optional = true }
|
||||
brk_fetcher = { workspace = true, optional = true }
|
||||
brk_grouper = { workspace = true, optional = true }
|
||||
brk_indexer = { workspace = true, optional = true }
|
||||
brk_interface = { workspace = true, optional = true }
|
||||
brk_logger = { workspace = true, optional = true }
|
||||
@@ -54,6 +62,7 @@ brk_parser = { workspace = true, optional = true }
|
||||
brk_server = { workspace = true, optional = true }
|
||||
brk_store = { workspace = true, optional = true }
|
||||
brk_structs = { workspace = true, optional = true }
|
||||
brk_traversable = { workspace = true, optional = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
+17
-8
@@ -8,13 +8,10 @@ pub use brk_binder as binder;
|
||||
#[doc(inline)]
|
||||
pub use brk_bundler as bundler;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
#[doc(inline)]
|
||||
pub use brk_cli as cli;
|
||||
|
||||
#[cfg(feature = "structs")]
|
||||
#[doc(inline)]
|
||||
pub use brk_structs as structs;
|
||||
|
||||
#[cfg(feature = "computer")]
|
||||
#[doc(inline)]
|
||||
pub use brk_computer as computer;
|
||||
@@ -27,10 +24,18 @@ pub use brk_error as error;
|
||||
#[doc(inline)]
|
||||
pub use brk_fetcher as fetcher;
|
||||
|
||||
#[cfg(feature = "grouper")]
|
||||
#[doc(inline)]
|
||||
pub use brk_grouper as grouper;
|
||||
|
||||
#[cfg(feature = "indexer")]
|
||||
#[doc(inline)]
|
||||
pub use brk_indexer as indexer;
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
#[doc(inline)]
|
||||
pub use brk_interface as interface;
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
#[doc(inline)]
|
||||
pub use brk_logger as logger;
|
||||
@@ -43,10 +48,6 @@ pub use brk_mcp as mcp;
|
||||
#[doc(inline)]
|
||||
pub use brk_parser as parser;
|
||||
|
||||
#[cfg(feature = "interface")]
|
||||
#[doc(inline)]
|
||||
pub use brk_interface as interface;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[doc(inline)]
|
||||
pub use brk_server as server;
|
||||
@@ -54,3 +55,11 @@ pub use brk_server as server;
|
||||
#[cfg(feature = "store")]
|
||||
#[doc(inline)]
|
||||
pub use brk_store as store;
|
||||
|
||||
#[cfg(feature = "structs")]
|
||||
#[doc(inline)]
|
||||
pub use brk_structs as structs;
|
||||
|
||||
#[cfg(feature = "traversable")]
|
||||
#[doc(inline)]
|
||||
pub use brk_traversable as traversable;
|
||||
|
||||
@@ -27,6 +27,14 @@ pub enum Error {
|
||||
WrongAddressType,
|
||||
UnindexableDate,
|
||||
QuickCacheError,
|
||||
|
||||
InvalidAddress,
|
||||
InvalidNetwork,
|
||||
InvalidTxid,
|
||||
UnknownAddress,
|
||||
UnknownTxid,
|
||||
UnsupportedType(String),
|
||||
|
||||
Str(&'static str),
|
||||
String(String),
|
||||
}
|
||||
@@ -140,6 +148,16 @@ impl fmt::Display for Error {
|
||||
"Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater"
|
||||
),
|
||||
|
||||
Error::InvalidTxid => write!(f, "The provided TXID appears to be invalid"),
|
||||
Error::InvalidNetwork => write!(f, "Invalid network"),
|
||||
Error::InvalidAddress => write!(f, "The provided address appears to be invalid"),
|
||||
Error::UnknownAddress => write!(
|
||||
f,
|
||||
"Address not found in the blockchain (no transaction history)"
|
||||
),
|
||||
Error::UnknownTxid => write!(f, "Failed to find the TXID in the blockchain"),
|
||||
Error::UnsupportedType(t) => write!(f, "Unsupported type ({t})"),
|
||||
|
||||
Error::Str(s) => write!(f, "{s}"),
|
||||
Error::String(s) => write!(f, "{s}"),
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ rust-version.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { workspace = true }
|
||||
bitcoincore-rpc = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_error = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::{Address, Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_structs::{
|
||||
AddressBytes, AddressBytesHash, AddressInfo, AddressPath, AnyAddressDataIndexEnum, Bitcoin,
|
||||
OutputType,
|
||||
};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
|
||||
use crate::Interface;
|
||||
|
||||
pub fn get_address_info(
|
||||
AddressPath { address }: AddressPath,
|
||||
interface: &Interface,
|
||||
) -> Result<AddressInfo> {
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
let stores = &indexer.stores;
|
||||
|
||||
let script = if let Ok(address) = Address::from_str(&address) {
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err(Error::InvalidNetwork);
|
||||
}
|
||||
let address = address.assume_checked();
|
||||
address.script_pubkey()
|
||||
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
|
||||
ScriptBuf::new_p2pk(&pubkey)
|
||||
} else {
|
||||
return Err(Error::InvalidAddress);
|
||||
};
|
||||
|
||||
let type_ = OutputType::from(&script);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
|
||||
return Err(Error::Str("Failed to convert the address to bytes"));
|
||||
};
|
||||
let hash = AddressBytesHash::from((&bytes, type_));
|
||||
|
||||
let Ok(Some(type_index)) = stores
|
||||
.addressbyteshash_to_typeindex
|
||||
.get(&hash)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownAddress);
|
||||
};
|
||||
|
||||
let stateful = &computer.stateful;
|
||||
let price = computer.price.as_ref().map(|v| {
|
||||
*v.timeindexes_to_price_close
|
||||
.dateindex
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.last()
|
||||
.unwrap()
|
||||
.1
|
||||
.into_owned()
|
||||
});
|
||||
|
||||
let any_address_index = match type_ {
|
||||
OutputType::P2PK33 => stateful
|
||||
.p2pk33addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PK65 => stateful
|
||||
.p2pk65addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PKH => stateful
|
||||
.p2pkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2SH => stateful
|
||||
.p2shaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2TR => stateful
|
||||
.p2traddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WPKH => stateful
|
||||
.p2wpkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WSH => stateful
|
||||
.p2wshaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2A => stateful
|
||||
.p2aaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
t => {
|
||||
return Err(Error::UnsupportedType(t.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let address_data = match any_address_index.to_enum() {
|
||||
AnyAddressDataIndexEnum::Loaded(index) => stateful
|
||||
.loadedaddressindex_to_loadedaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index),
|
||||
AnyAddressDataIndexEnum::Empty(index) => stateful
|
||||
.emptyaddressindex_to_emptyaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index)
|
||||
.into(),
|
||||
};
|
||||
|
||||
let balance = address_data.balance();
|
||||
|
||||
Ok(AddressInfo {
|
||||
address: address.to_string(),
|
||||
r#type: type_,
|
||||
type_index,
|
||||
utxo_count: address_data.utxo_count,
|
||||
total_sent: address_data.sent,
|
||||
total_received: address_data.received,
|
||||
balance,
|
||||
balance_usd: price.map(|p| p * Bitcoin::from(balance)),
|
||||
estimated_total_invested: price.map(|_| address_data.realized_cap),
|
||||
estimated_avg_entry_price: price.map(|_| address_data.realized_price()),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod addresses;
|
||||
mod transactions;
|
||||
|
||||
pub use addresses::*;
|
||||
pub use transactions::*;
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitcoin::{Transaction, consensus::Decodable};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_parser::XORIndex;
|
||||
use brk_structs::{TransactionInfo, Txid, TxidPath, TxidPrefix};
|
||||
use vecdb::VecIterator;
|
||||
|
||||
use crate::Interface;
|
||||
|
||||
pub fn get_transaction_info(
|
||||
TxidPath { txid }: TxidPath,
|
||||
interface: &Interface,
|
||||
) -> Result<TransactionInfo> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err(Error::InvalidTxid);
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let indexer = interface.indexer();
|
||||
let Ok(Some(index)) = indexer
|
||||
.stores
|
||||
.txidprefix_to_txindex
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err(Error::UnknownTxid);
|
||||
};
|
||||
|
||||
let txid = indexer.vecs.txindex_to_txid.iter().unwrap_get_inner(index);
|
||||
|
||||
let parser = interface.parser();
|
||||
let computer = interface.computer();
|
||||
|
||||
let position = computer
|
||||
.blks
|
||||
.txindex_to_position
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
let len = indexer
|
||||
.vecs
|
||||
.txindex_to_total_size
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
|
||||
let blk_index_to_blk_path = parser.blk_index_to_blk_path();
|
||||
|
||||
let Some(blk_path) = blk_index_to_blk_path.get(&position.blk_index()) else {
|
||||
return Err(Error::Str("Failed to get the correct blk file"));
|
||||
};
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
|
||||
let Ok(mut file) = File::open(blk_path) else {
|
||||
return Err(Error::Str("Failed to open blk file"));
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::Str("Failed to seek position in file"));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return Err(Error::Str("Failed to read the transaction (read exact)"));
|
||||
}
|
||||
xori.bytes(&mut buffer, parser.xor_bytes());
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let Ok(_) = Transaction::consensus_decode(&mut reader) else {
|
||||
return Err(Error::Str("Failed decode the transaction"));
|
||||
};
|
||||
|
||||
Ok(TransactionInfo {
|
||||
txid,
|
||||
index,
|
||||
// tx
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,10 @@ use brk_computer::Computer;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_indexer::Indexer;
|
||||
use brk_parser::Parser;
|
||||
use brk_structs::{Height, Index, IndexInfo};
|
||||
use brk_structs::{
|
||||
AddressInfo, AddressPath, Format, Height, Index, IndexInfo, MetricCount, TransactionInfo,
|
||||
TxidPath,
|
||||
};
|
||||
use brk_traversable::TreeNode;
|
||||
use nucleo_matcher::{
|
||||
Config, Matcher,
|
||||
@@ -15,23 +18,22 @@ use nucleo_matcher::{
|
||||
use quick_cache::sync::Cache;
|
||||
use vecdb::{AnyCollectableVec, AnyStoredVec};
|
||||
|
||||
mod count;
|
||||
mod chain;
|
||||
mod deser;
|
||||
mod format;
|
||||
mod metrics;
|
||||
mod output;
|
||||
mod pagination;
|
||||
mod params;
|
||||
mod vecs;
|
||||
|
||||
pub use count::*;
|
||||
pub use format::Format;
|
||||
pub use output::{Output, Value};
|
||||
pub use metrics::{Output, Value};
|
||||
pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam};
|
||||
pub use params::{Params, ParamsDeprec, ParamsOpt};
|
||||
use vecs::Vecs;
|
||||
|
||||
use crate::vecs::{IndexToVec, MetricToVec};
|
||||
use crate::{
|
||||
chain::{get_address_info, get_transaction_info},
|
||||
vecs::{IndexToVec, MetricToVec},
|
||||
};
|
||||
|
||||
pub fn cached_errors() -> &'static Cache<String, String> {
|
||||
static CACHE: OnceLock<Cache<String, String>> = OnceLock::new();
|
||||
@@ -65,6 +67,14 @@ impl<'a> Interface<'a> {
|
||||
Height::from(self.indexer.vecs.height_to_blockhash.stamp())
|
||||
}
|
||||
|
||||
pub fn get_address_info(&self, address: AddressPath) -> Result<AddressInfo> {
|
||||
get_address_info(address, self)
|
||||
}
|
||||
|
||||
pub fn get_transaction_info(&self, txid: TxidPath) -> Result<TransactionInfo> {
|
||||
get_transaction_info(txid, self)
|
||||
}
|
||||
|
||||
pub fn search(&self, params: &Params) -> Result<Vec<(String, &&dyn AnyCollectableVec)>> {
|
||||
let metrics = ¶ms.metrics;
|
||||
let index = params.index;
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::fmt;
|
||||
use derive_deref::Deref;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
mod output;
|
||||
|
||||
pub use output::*;
|
||||
|
||||
#[derive(Debug, Deref, JsonSchema)]
|
||||
pub struct MaybeMetrics(Vec<String>);
|
||||
@@ -33,7 +36,7 @@ impl<'de> Deserialize<'de> for MaybeMetrics {
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
|
||||
if let Some(str) = value.as_str() {
|
||||
if str.len() <= MAX_STRING_SIZE {
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Format;
|
||||
use brk_structs::Format;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Output {
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use brk_structs::Index;
|
||||
use brk_structs::{Format, Index};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
Format,
|
||||
deser::{de_unquote_i64, de_unquote_usize},
|
||||
metrics::MaybeMetrics,
|
||||
};
|
||||
|
||||
@@ -86,8 +86,8 @@ impl<'a> Vecs<'a> {
|
||||
this.catalog.replace(
|
||||
TreeNode::Branch(
|
||||
[
|
||||
("indexer".to_string(), indexer.vecs.to_tree_node()),
|
||||
("computer".to_string(), computer.to_tree_node()),
|
||||
("indexed".to_string(), indexer.vecs.to_tree_node()),
|
||||
("computed".to_string(), computer.to_tree_node()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
|
||||
@@ -30,7 +30,6 @@ log = { workspace = true }
|
||||
quick_cache = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sonic-rs = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-http = { version = "0.6.6", features = ["compression-full", "trace"] }
|
||||
|
||||
@@ -1,239 +1,16 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bitcoin::{Address as BitcoinAddress, Network, PublicKey, ScriptBuf};
|
||||
use brk_structs::{
|
||||
AddressBytes, AddressBytesHash, AnyAddressDataIndexEnum, Bitcoin, Dollars, OutputType, Sats,
|
||||
TypeIndex,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::{AnyIterableVec, VecIterator};
|
||||
use brk_structs::{AddressInfo, AddressPath};
|
||||
|
||||
use crate::extended::TransformResponseExtended;
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Address information
|
||||
struct AddressInfo {
|
||||
/// Bitcoin address string
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
address: String,
|
||||
|
||||
#[schemars(example = OutputType::P2PK65)]
|
||||
r#type: OutputType,
|
||||
|
||||
#[schemars(example = TypeIndex::new(0))]
|
||||
type_index: TypeIndex,
|
||||
|
||||
/// Total satoshis ever sent from this address
|
||||
#[schemars(example = Sats::new(0))]
|
||||
total_sent: Sats,
|
||||
|
||||
/// Total satoshis ever received by this address
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
total_received: Sats,
|
||||
|
||||
/// Number of unspent transaction outputs (UTXOs)
|
||||
#[schemars(example = 10)]
|
||||
utxo_count: u32,
|
||||
|
||||
/// Current spendable balance in satoshis (total_received - total_sent)
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
balance: Sats,
|
||||
|
||||
/// Current balance value in USD at current market price
|
||||
#[schemars(example = Some(Dollars::mint(6_157_891.64)))]
|
||||
balance_usd: Option<Dollars>,
|
||||
|
||||
/// Estimated total USD value at time of deposit for coins currently in this address (not including coins that were later sent out). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(6.2)))]
|
||||
estimated_total_invested: Option<Dollars>,
|
||||
|
||||
/// Estimated average BTC price at time of deposit for coins currently in this address (USD). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(0.12)))]
|
||||
estimated_avg_entry_price: Option<Dollars>,
|
||||
//
|
||||
// Transaction count?
|
||||
// First/last activity timestamps?
|
||||
// Realized/unrealized gains?
|
||||
// Current value (balance × current price)?
|
||||
// "address": address,
|
||||
// "type": output_type,
|
||||
// "index": addri,
|
||||
// "chain_stats": {
|
||||
// "funded_txo_count": null,
|
||||
// "funded_txo_sum": addr_data.received,
|
||||
// "spent_txo_count": null,
|
||||
// "spent_txo_sum": addr_data.sent,
|
||||
// "utxo_count": addr_data.utxos,
|
||||
// "balance": amount,
|
||||
// "balance_usd": price.map_or(Value::new(), |p| {
|
||||
// Value::from(Number::from_f64(*(p * Bitcoin::from(amount))).unwrap())
|
||||
// }),
|
||||
// "realized_value": addr_data.realized_cap,
|
||||
// "tx_count": null,
|
||||
// "avg_cost_basis": addr_data.realized_price()
|
||||
// },
|
||||
// "mempool_stats": null
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct AddressPath {
|
||||
/// Bitcoin address string
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
address: String,
|
||||
}
|
||||
|
||||
async fn get_address_info(
|
||||
Path(AddressPath { address }): Path<AddressPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Json<AddressInfo>, (StatusCode, Json<&'static str>)> {
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let computer = interface.computer();
|
||||
let stores = &indexer.stores;
|
||||
|
||||
let script = if let Ok(address) = BitcoinAddress::from_str(&address) {
|
||||
if !address.is_valid_for_network(Network::Bitcoin) {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided address isn't the Bitcoin Network."),
|
||||
));
|
||||
}
|
||||
let address = address.assume_checked();
|
||||
address.script_pubkey()
|
||||
} else if let Ok(pubkey) = PublicKey::from_str(&address) {
|
||||
ScriptBuf::new_p2pk(&pubkey)
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided address is invalid."),
|
||||
));
|
||||
};
|
||||
|
||||
let type_ = OutputType::from(&script);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, type_)) else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to convert the address to bytes"),
|
||||
));
|
||||
};
|
||||
let hash = AddressBytesHash::from((&bytes, type_));
|
||||
|
||||
let Ok(Some(type_index)) = stores
|
||||
.addressbyteshash_to_typeindex
|
||||
.get(&hash)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json("Address not found in the blockchain (no transaction history)"),
|
||||
));
|
||||
};
|
||||
|
||||
let stateful = &computer.stateful;
|
||||
let price = computer.price.as_ref().map(|v| {
|
||||
*v.timeindexes_to_price_close
|
||||
.dateindex
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.last()
|
||||
.unwrap()
|
||||
.1
|
||||
.into_owned()
|
||||
});
|
||||
|
||||
let any_address_index = match type_ {
|
||||
OutputType::P2PK33 => stateful
|
||||
.p2pk33addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PK65 => stateful
|
||||
.p2pk65addressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2PKH => stateful
|
||||
.p2pkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2SH => stateful
|
||||
.p2shaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2TR => stateful
|
||||
.p2traddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WPKH => stateful
|
||||
.p2wpkhaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2WSH => stateful
|
||||
.p2wshaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
OutputType::P2A => stateful
|
||||
.p2aaddressindex_to_anyaddressindex
|
||||
.iter()
|
||||
.unwrap_get_inner(type_index.into()),
|
||||
_ => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided address uses an unsupported type"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let address_data = match any_address_index.to_enum() {
|
||||
AnyAddressDataIndexEnum::Loaded(index) => stateful
|
||||
.loadedaddressindex_to_loadedaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index),
|
||||
AnyAddressDataIndexEnum::Empty(index) => stateful
|
||||
.emptyaddressindex_to_emptyaddressdata
|
||||
.iter()
|
||||
.unwrap_get_inner(index)
|
||||
.into(),
|
||||
};
|
||||
|
||||
let balance = address_data.balance();
|
||||
|
||||
Ok(Json(AddressInfo {
|
||||
address: address.to_string(),
|
||||
r#type: type_,
|
||||
type_index,
|
||||
utxo_count: address_data.utxo_count,
|
||||
total_sent: address_data.sent,
|
||||
total_received: address_data.received,
|
||||
balance,
|
||||
balance_usd: price.map(|p| p * Bitcoin::from(balance)),
|
||||
estimated_total_invested: price.map(|_| address_data.realized_cap),
|
||||
estimated_avg_entry_price: price.map(|_| address_data.realized_price()),
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_address_info_docs(op: TransformOperation) -> TransformOperation {
|
||||
op.tag("Chain")
|
||||
.summary("Address information")
|
||||
.description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).")
|
||||
.with_ok_response::<AddressInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait AddressesRoutes {
|
||||
fn add_addresses_routes(self) -> Self;
|
||||
}
|
||||
@@ -242,7 +19,24 @@ impl AddressesRoutes for ApiRouter<AppState> {
|
||||
fn add_addresses_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/address/{address}",
|
||||
get_with(get_address_info, get_address_info_docs),
|
||||
get_with(async |Path(address): Path<AddressPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let address_info = app_state.interface.get_address_info(address).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&address_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
}, |op| op
|
||||
.tag("Chain")
|
||||
.summary("Address information")
|
||||
.description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).")
|
||||
.with_ok_response::<AddressInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,16 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use aide::{
|
||||
axum::{ApiRouter, routing::get_with},
|
||||
transform::TransformOperation,
|
||||
};
|
||||
use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bitcoin::{Transaction as BitcoinTransaction, consensus::Decodable};
|
||||
use brk_parser::XORIndex;
|
||||
use brk_structs::{TxIndex, Txid, TxidPrefix};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vecdb::VecIterator;
|
||||
use brk_structs::{TransactionInfo, TxidPath};
|
||||
|
||||
use crate::extended::{ResponseExtended, TransformResponseExtended};
|
||||
use crate::extended::{ResponseExtended, ResultExtended, TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
/// Transaction Information
|
||||
struct TransactionInfo {
|
||||
#[schemars(
|
||||
with = "String",
|
||||
example = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
|
||||
)]
|
||||
txid: Txid,
|
||||
#[schemars(example = TxIndex::new(0))]
|
||||
index: TxIndex,
|
||||
#[serde(flatten)]
|
||||
#[schemars(with = "serde_json::Value")]
|
||||
tx: BitcoinTransaction,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct TxidPath {
|
||||
/// Bitcoin transaction id
|
||||
#[schemars(example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")]
|
||||
txid: String,
|
||||
}
|
||||
|
||||
async fn get_transaction_info(
|
||||
Path(TxidPath { txid }): Path<TxidPath>,
|
||||
state: State<AppState>,
|
||||
) -> Result<Response, (StatusCode, Json<&'static str>)> {
|
||||
let Ok(txid) = bitcoin::Txid::from_str(&txid) else {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json("The provided TXID appears to be invalid."),
|
||||
));
|
||||
};
|
||||
|
||||
let txid = Txid::from(txid);
|
||||
let prefix = TxidPrefix::from(&txid);
|
||||
let interface = state.interface;
|
||||
let indexer = interface.indexer();
|
||||
let Ok(Some(index)) = indexer
|
||||
.stores
|
||||
.txidprefix_to_txindex
|
||||
.get(&prefix)
|
||||
.map(|opt| opt.map(|cow| cow.into_owned()))
|
||||
else {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json("Failed to found the TXID in the blockchain."),
|
||||
));
|
||||
};
|
||||
|
||||
let txid = indexer.vecs.txindex_to_txid.iter().unwrap_get_inner(index);
|
||||
|
||||
let parser = interface.parser();
|
||||
let computer = interface.computer();
|
||||
|
||||
let position = computer
|
||||
.blks
|
||||
.txindex_to_position
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
let len = indexer
|
||||
.vecs
|
||||
.txindex_to_total_size
|
||||
.iter()
|
||||
.unwrap_get_inner(index);
|
||||
|
||||
let blk_index_to_blk_path = parser.blk_index_to_blk_path();
|
||||
|
||||
let Some(blk_path) = blk_index_to_blk_path.get(&position.blk_index()) else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (get blk's path)"),
|
||||
));
|
||||
};
|
||||
|
||||
let mut xori = XORIndex::default();
|
||||
xori.add_assign(position.offset() as usize);
|
||||
|
||||
let Ok(mut file) = File::open(blk_path) else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (open file)"),
|
||||
));
|
||||
};
|
||||
|
||||
if file
|
||||
.seek(SeekFrom::Start(position.offset() as u64))
|
||||
.is_err()
|
||||
{
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (file seek)"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; *len as usize];
|
||||
if file.read_exact(&mut buffer).is_err() {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed to read the transaction (read exact)"),
|
||||
));
|
||||
}
|
||||
xori.bytes(&mut buffer, parser.xor_bytes());
|
||||
|
||||
let mut reader = Cursor::new(buffer);
|
||||
let Ok(tx) = BitcoinTransaction::consensus_decode(&mut reader) else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json("Failed decode the transaction"),
|
||||
));
|
||||
};
|
||||
|
||||
let tx_info = TransactionInfo { txid, index, tx };
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
}
|
||||
|
||||
fn get_transaction_info_docs(op: TransformOperation) -> TransformOperation {
|
||||
op.tag("Chain")
|
||||
.summary("Transaction information")
|
||||
.description(
|
||||
"Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.",
|
||||
)
|
||||
.with_ok_response::<TransactionInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error()
|
||||
}
|
||||
|
||||
pub trait TransactionsRoutes {
|
||||
fn add_transactions_routes(self) -> Self;
|
||||
}
|
||||
@@ -164,7 +19,28 @@ impl TransactionsRoutes for ApiRouter<AppState> {
|
||||
fn add_transactions_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/api/chain/tx/{txid}",
|
||||
get_with(get_transaction_info, get_transaction_info_docs),
|
||||
get_with(
|
||||
async |Path(txid): Path<TxidPath>,
|
||||
State(app_state): State<AppState>|
|
||||
-> Result<Response, (StatusCode, Json<String>)> {
|
||||
let tx_info = app_state.interface.get_transaction_info(txid).to_server_result()?;
|
||||
|
||||
let bytes = sonic_rs::to_vec(&tx_info).unwrap();
|
||||
|
||||
Ok(Response::new_json_from_bytes(bytes))
|
||||
},
|
||||
|op| op
|
||||
.tag("Chain")
|
||||
.summary("Transaction information")
|
||||
.description(
|
||||
"Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.",
|
||||
)
|
||||
.with_ok_response::<TransactionInfo, _>(|res| res)
|
||||
.with_not_modified()
|
||||
.with_bad_request()
|
||||
.with_not_found()
|
||||
.with_server_error(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_interface::{Format, Output, Params};
|
||||
use brk_interface::{Output, Params};
|
||||
use brk_structs::Format;
|
||||
use quick_cache::sync::GuardResult;
|
||||
use vecdb::Stamp;
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_interface::{
|
||||
MetricCount, PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt,
|
||||
};
|
||||
use brk_structs::{Index, IndexInfo};
|
||||
use brk_interface::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt};
|
||||
use brk_structs::{Index, IndexInfo, MetricCount, MetricPath};
|
||||
use brk_traversable::TreeNode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
@@ -27,13 +23,6 @@ pub trait ApiMetricsRoutes {
|
||||
fn add_metrics_routes(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct MetricPath {
|
||||
/// Metric name
|
||||
#[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")]
|
||||
metric: String,
|
||||
}
|
||||
|
||||
const TO_SEPARATOR: &str = "_to_";
|
||||
|
||||
impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
|
||||
@@ -10,8 +10,7 @@ use axum::{
|
||||
response::{Html, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use brk_structs::Health;
|
||||
|
||||
use crate::{
|
||||
VERSION,
|
||||
@@ -31,14 +30,6 @@ pub trait ApiRoutes {
|
||||
fn add_api_routes(self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Server health status
|
||||
struct Health {
|
||||
status: String,
|
||||
service: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
impl ApiRoutes for ApiRouter<AppState> {
|
||||
fn add_api_routes(self) -> Self {
|
||||
self.add_chain_routes()
|
||||
@@ -62,7 +53,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
async || -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "healthy".to_string(),
|
||||
service: "brk-server".to_string(),
|
||||
service: "brk".to_string(),
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
})
|
||||
},
|
||||
@@ -101,5 +92,9 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.route("/api", get(Html::from(include_str!("./scalar.html"))))
|
||||
.route(
|
||||
"/api/{*path}",
|
||||
get(|| async { Redirect::permanent("/api") }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,17 @@ use aide::openapi::{Info, OpenApi, Tag};
|
||||
use crate::VERSION;
|
||||
|
||||
pub fn create_openapi() -> OpenApi {
|
||||
let info = Info {
|
||||
title: "Bitcoin Research Kit".to_string(),
|
||||
description: Some(
|
||||
"API for querying Bitcoin blockchain data including addresses, transactions, and chain statistics. This API provides low-level access to indexed blockchain data with advanced analytics capabilities.\n\n\
|
||||
⚠️ **Early Development**: This API is in very early stages of development. Breaking changes may occur without notice. For a more stable experience, [self-host](/install) or use the [hosting service](/service)."
|
||||
.to_string(),
|
||||
),
|
||||
version: format!("v{VERSION}"),
|
||||
..Info::default()
|
||||
};
|
||||
|
||||
let tags = vec![
|
||||
Tag {
|
||||
name: "Chain".to_string(),
|
||||
@@ -44,16 +55,7 @@ pub fn create_openapi() -> OpenApi {
|
||||
];
|
||||
|
||||
OpenApi {
|
||||
info: Info {
|
||||
title: "Bitcoin Research Kit API".to_string(),
|
||||
description: Some(
|
||||
"API for querying Bitcoin blockchain data including addresses, transactions, and chain statistics. This API provides low-level access to indexed blockchain data with advanced analytics capabilities.\n\n\
|
||||
⚠️ **Early Development**: This API is in very early stages of development. Breaking changes may occur without notice. For a more stable experience, self-host or use the hosting service."
|
||||
.to_string(),
|
||||
),
|
||||
version: format!("v{VERSION}"),
|
||||
..Info::default()
|
||||
},
|
||||
info,
|
||||
tags,
|
||||
..OpenApi::default()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
mod header_map;
|
||||
mod response;
|
||||
mod result;
|
||||
mod transform_operation;
|
||||
|
||||
pub use header_map::*;
|
||||
pub use response::*;
|
||||
pub use result::*;
|
||||
pub use transform_operation::*;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use axum::{Json, http::StatusCode};
|
||||
use brk_error::{Error, Result};
|
||||
|
||||
pub trait ResultExtended<T> {
|
||||
fn to_server_result(self) -> Result<T, (StatusCode, Json<String>)>;
|
||||
}
|
||||
|
||||
impl<T> ResultExtended<T> for Result<T> {
|
||||
fn to_server_result(self) -> Result<T, (StatusCode, Json<String>)> {
|
||||
self.map_err(|e| {
|
||||
(
|
||||
match e {
|
||||
Error::InvalidTxid
|
||||
| Error::InvalidNetwork
|
||||
| Error::InvalidAddress
|
||||
| Error::UnsupportedType(_) => StatusCode::BAD_REQUEST,
|
||||
Error::UnknownAddress | Error::UnknownTxid => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
Json(e.to_string()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,25 +99,26 @@ impl Server {
|
||||
|
||||
let router = ApiRouter::new()
|
||||
.add_api_routes()
|
||||
.add_files_routes(state.path.as_ref())
|
||||
.add_mcp_routes(state.interface, mcp)
|
||||
.add_files_routes(state.path.as_ref())
|
||||
.route(
|
||||
"/discord",
|
||||
get(Redirect::temporary("https://discord.gg/WACpShCB7M")),
|
||||
)
|
||||
.route("/crates", get(Redirect::temporary("https://crates.io/crates/brk")))
|
||||
.route("/crate", get(Redirect::temporary("https://crates.io/crates/brk")))
|
||||
.route(
|
||||
"/status",
|
||||
get(Redirect::temporary("https://status.bitview.space")),
|
||||
)
|
||||
.route("/github", get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk")))
|
||||
.route("/changelog", get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk/blob/main/docs/CHANGELOG.md")))
|
||||
.route(
|
||||
"/cli",
|
||||
get(Redirect::temporary("https://crates.io/crates/brk_cli")),
|
||||
"/install",
|
||||
get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_cli/README.md#brk_cli")),
|
||||
)
|
||||
.route(
|
||||
"/hosting",
|
||||
get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk?tab=readme-ov-file#hosting-as-a-service")),
|
||||
"/service",
|
||||
get(Redirect::temporary("https://github.com/bitcoinresearchkit/brk?tab=readme-ov-file#professional-hosting")),
|
||||
)
|
||||
.route("/nostr", get(Redirect::temporary("https://primal.net/p/npub1jagmm3x39lmwfnrtvxcs9ac7g300y3dusv9lgzhk2e4x5frpxlrqa73v44")))
|
||||
.with_state(state)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{Dollars, OutputType, Sats, TypeIndex};
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Address information
|
||||
pub struct AddressInfo {
|
||||
/// Bitcoin address string
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
pub address: String,
|
||||
|
||||
#[schemars(example = OutputType::P2PK65)]
|
||||
pub r#type: OutputType,
|
||||
|
||||
#[schemars(example = TypeIndex::new(0))]
|
||||
pub type_index: TypeIndex,
|
||||
|
||||
/// Total satoshis ever sent from this address
|
||||
#[schemars(example = Sats::new(0))]
|
||||
pub total_sent: Sats,
|
||||
|
||||
/// Total satoshis ever received by this address
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
pub total_received: Sats,
|
||||
|
||||
/// Number of unspent transaction outputs (UTXOs)
|
||||
#[schemars(example = 10)]
|
||||
pub utxo_count: u32,
|
||||
|
||||
/// Current spendable balance in satoshis (total_received - total_sent)
|
||||
#[schemars(example = Sats::new(5001008380))]
|
||||
pub balance: Sats,
|
||||
|
||||
/// Current balance value in USD at current market price
|
||||
#[schemars(example = Some(Dollars::mint(6_157_891.64)))]
|
||||
pub balance_usd: Option<Dollars>,
|
||||
|
||||
/// Estimated total USD value at time of deposit for coins currently in this address (not including coins that were later sent out). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(6.2)))]
|
||||
pub estimated_total_invested: Option<Dollars>,
|
||||
|
||||
/// Estimated average BTC price at time of deposit for coins currently in this address (USD). Not suitable for tax calculations
|
||||
#[schemars(example = Some(Dollars::mint(0.12)))]
|
||||
pub estimated_avg_entry_price: Option<Dollars>,
|
||||
//
|
||||
// Transaction count?
|
||||
// First/last activity timestamps?
|
||||
// Realized/unrealized gains?
|
||||
// Current value (balance × current price)?
|
||||
// "address": address,
|
||||
// "type": output_type,
|
||||
// "index": addri,
|
||||
// "chain_stats": {
|
||||
// "funded_txo_count": null,
|
||||
// "funded_txo_sum": addr_data.received,
|
||||
// "spent_txo_count": null,
|
||||
// "spent_txo_sum": addr_data.sent,
|
||||
// "utxo_count": addr_data.utxos,
|
||||
// "balance": amount,
|
||||
// "balance_usd": price.map_or(Value::new(), |p| {
|
||||
// Value::from(Number::from_f64(*(p * Bitcoin::from(amount))).unwrap())
|
||||
// }),
|
||||
// "realized_value": addr_data.realized_cap,
|
||||
// "tx_count": null,
|
||||
// "avg_cost_basis": addr_data.realized_price()
|
||||
// },
|
||||
// "mempool_stats": null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct AddressPath {
|
||||
/// Bitcoin address string
|
||||
#[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")]
|
||||
pub address: String,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ impl AnyAddressIndex {
|
||||
impl From<LoadedAddressIndex> for AnyAddressIndex {
|
||||
fn from(value: LoadedAddressIndex) -> Self {
|
||||
if u32::from(value) >= MIN_EMPTY_INDEX {
|
||||
panic!("")
|
||||
panic!("{value} is higher than MIN_EMPTY_INDEX ({MIN_EMPTY_INDEX})")
|
||||
}
|
||||
Self(*value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
/// Server health status
|
||||
pub struct Health {
|
||||
pub status: String,
|
||||
pub service: String,
|
||||
pub timestamp: String,
|
||||
}
|
||||
@@ -6,6 +6,8 @@ use brk_error::{Error, Result};
|
||||
|
||||
mod addressbytes;
|
||||
mod addressbyteshash;
|
||||
mod addressinfo;
|
||||
mod addresspath;
|
||||
mod anyaddressindex;
|
||||
mod bitcoin;
|
||||
mod blkmetadata;
|
||||
@@ -23,13 +25,17 @@ mod emptyaddressdata;
|
||||
mod emptyaddressindex;
|
||||
mod emptyoutputindex;
|
||||
mod feerate;
|
||||
mod format;
|
||||
mod halvingepoch;
|
||||
mod health;
|
||||
mod height;
|
||||
mod index;
|
||||
mod indexinfo;
|
||||
mod inputindex;
|
||||
mod loadedaddressdata;
|
||||
mod loadedaddressindex;
|
||||
mod metriccount;
|
||||
mod metricpath;
|
||||
mod monthindex;
|
||||
mod ohlc;
|
||||
mod opreturnindex;
|
||||
@@ -63,8 +69,10 @@ mod stored_u8;
|
||||
mod timestamp;
|
||||
mod treenode;
|
||||
mod txid;
|
||||
mod txidpath;
|
||||
mod txidprefix;
|
||||
mod txindex;
|
||||
mod txinfo;
|
||||
mod txversion;
|
||||
mod typeindex;
|
||||
mod typeindex_with_outputindex;
|
||||
@@ -78,6 +86,8 @@ mod yearindex;
|
||||
|
||||
pub use addressbytes::*;
|
||||
pub use addressbyteshash::*;
|
||||
pub use addressinfo::*;
|
||||
pub use addresspath::*;
|
||||
pub use anyaddressindex::*;
|
||||
pub use bitcoin::*;
|
||||
pub use blkmetadata::*;
|
||||
@@ -95,13 +105,17 @@ pub use emptyaddressdata::*;
|
||||
pub use emptyaddressindex::*;
|
||||
pub use emptyoutputindex::*;
|
||||
pub use feerate::*;
|
||||
pub use format::*;
|
||||
pub use halvingepoch::*;
|
||||
pub use health::*;
|
||||
pub use height::*;
|
||||
pub use index::*;
|
||||
pub use indexinfo::*;
|
||||
pub use inputindex::*;
|
||||
pub use loadedaddressdata::*;
|
||||
pub use loadedaddressindex::*;
|
||||
pub use metriccount::*;
|
||||
pub use metricpath::*;
|
||||
pub use monthindex::*;
|
||||
pub use ohlc::*;
|
||||
pub use opreturnindex::*;
|
||||
@@ -135,8 +149,10 @@ pub use stored_u64::*;
|
||||
pub use timestamp::*;
|
||||
pub use treenode::*;
|
||||
pub use txid::*;
|
||||
pub use txidpath::*;
|
||||
pub use txidprefix::*;
|
||||
pub use txindex::*;
|
||||
pub use txinfo::*;
|
||||
pub use txversion::*;
|
||||
pub use typeindex::*;
|
||||
pub use typeindex_with_outputindex::*;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct MetricPath {
|
||||
/// Metric name
|
||||
#[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")]
|
||||
pub metric: String,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct TxidPath {
|
||||
/// Bitcoin transaction id
|
||||
#[schemars(example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")]
|
||||
pub txid: String,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{TxIndex, Txid};
|
||||
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
/// Transaction Information
|
||||
pub struct TransactionInfo {
|
||||
#[schemars(
|
||||
with = "String",
|
||||
example = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
|
||||
)]
|
||||
pub txid: Txid,
|
||||
#[schemars(example = TxIndex::new(0))]
|
||||
pub index: TxIndex,
|
||||
// #[serde(flatten)]
|
||||
// #[schemars(with = "serde_json::Value")]
|
||||
// pub tx: Transaction,
|
||||
}
|
||||
+1
-4
@@ -83,11 +83,8 @@
|
||||
- create map of all single words
|
||||
- do some kind of score with that ?
|
||||
- FEAT: discoverability
|
||||
- catalog (tree/groups)
|
||||
- search
|
||||
- BUG: failover to `/api`
|
||||
- ???: no HTML / redirects ?
|
||||
- FEAT: support keyed version when fetching dataset: {date: value} / {date: [value]}
|
||||
- ???: remove redirects ?
|
||||
- FEAT: add support for https (rustls)
|
||||
- _STORE_
|
||||
- FEAT: save height and version in one file
|
||||
|
||||
@@ -4214,29 +4214,28 @@ export function createPartialOptions({ colors, brk }) {
|
||||
{
|
||||
name: "API",
|
||||
url: () => "/api",
|
||||
title: "Link to API documentation",
|
||||
title: "API documentation",
|
||||
},
|
||||
{
|
||||
name: "MCP",
|
||||
url: () =>
|
||||
"https://github.com/bitcoinresearchkit/brk/tree/main/crates/brk_mcp#brk-mcp",
|
||||
title: "Link to MCP documentation",
|
||||
"https://github.com/bitcoinresearchkit/brk/blob/main/crates/brk_mcp/README.md#brk_mcp",
|
||||
title: "Model Context Protocol documentation",
|
||||
},
|
||||
{
|
||||
name: "Crates",
|
||||
url: () => "/crates",
|
||||
title: "Link to BRK on crates.io",
|
||||
name: "Crate",
|
||||
url: () => "/crate",
|
||||
title: "View on crates.io",
|
||||
},
|
||||
{
|
||||
name: "Source",
|
||||
url: () => "/github",
|
||||
title: "Link to BRK's repository",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
url: () =>
|
||||
"https://github.com/bitcoinresearchkit/brk/blob/main/docs/CHANGELOG.md#changelog",
|
||||
title: "BRK's changelog",
|
||||
url: () => "/changelog",
|
||||
title: "Release notes and changelog",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -4246,37 +4245,37 @@ export function createPartialOptions({ colors, brk }) {
|
||||
{
|
||||
name: "Status",
|
||||
url: () => "/status",
|
||||
title: "Link to servers status",
|
||||
title: "Service status and uptime",
|
||||
},
|
||||
{
|
||||
name: "Self",
|
||||
url: () => "/cli",
|
||||
title: "Link to self-hosting documentation",
|
||||
name: "Self-host",
|
||||
url: () => "/install",
|
||||
title: "Install and run yourself",
|
||||
},
|
||||
{
|
||||
name: "As a service",
|
||||
url: () => "/hosting",
|
||||
title: "Link to hosting service",
|
||||
name: "Service",
|
||||
url: () => "/service",
|
||||
title: "Hosted service offering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Social",
|
||||
name: "Community",
|
||||
tree: [
|
||||
{
|
||||
name: "Discord",
|
||||
url: () => "/discord",
|
||||
title: "Join the Discord server",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
url: () => "/github",
|
||||
title: "Link to Github",
|
||||
title: "Source code and issues",
|
||||
},
|
||||
{
|
||||
name: "Nostr",
|
||||
url: () => "/nostr",
|
||||
title: "Link to BRK's nostr account",
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
url: () => "/discord",
|
||||
title: "Link to BRK's discord server",
|
||||
title: "Follow on Nostr",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -386,7 +386,7 @@ export function init({ colors, createChartElement, signals, resources }) {
|
||||
}),
|
||||
},
|
||||
fees: {
|
||||
percentage: signals.createSignal(/** @type {number | null} */ (0.25), {
|
||||
percentage: signals.createSignal(/** @type {number | null} */ (1), {
|
||||
save: {
|
||||
...serdeOptNumber,
|
||||
keyPrefix,
|
||||
@@ -606,8 +606,6 @@ export function init({ colors, createChartElement, signals, resources }) {
|
||||
resultsElement.append(p2);
|
||||
const p3 = window.document.createElement("p");
|
||||
resultsElement.append(p3);
|
||||
const p4 = window.document.createElement("p");
|
||||
resultsElement.append(p4);
|
||||
|
||||
const owner = signals.getOwner();
|
||||
|
||||
|
||||
@@ -4,348 +4,6 @@ import { tableElement } from "../core/elements";
|
||||
import { serdeMetrics, serdeString, serdeUnit } from "../core/serde";
|
||||
import { resetParams } from "../core/url";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Option} args.option
|
||||
* @param {Signals} args.signals
|
||||
* @param {BRK} args.brk
|
||||
* @param {Resources} args.resources
|
||||
*/
|
||||
function createTable({ brk, signals, option, resources }) {
|
||||
const indexToMetrics = createIndexToMetrics(metricToIndexes);
|
||||
|
||||
const serializedIndexes = createSerializedIndexes();
|
||||
/** @type {SerializedIndex} */
|
||||
const defaultSerializedIndex = "height";
|
||||
const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
|
||||
signals.createSignal(
|
||||
/** @type {SerializedIndex} */ (defaultSerializedIndex),
|
||||
{
|
||||
save: {
|
||||
...serdeString,
|
||||
keyPrefix: "table",
|
||||
key: "index",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
const index = signals.createMemo(() =>
|
||||
serializedIndexToIndex(serializedIndex()),
|
||||
);
|
||||
|
||||
const table = window.document.createElement("table");
|
||||
const obj = {
|
||||
element: table,
|
||||
/** @type {VoidFunction | undefined} */
|
||||
addRandomCol: undefined,
|
||||
};
|
||||
|
||||
signals.createEffect(index, (index, prevIndex) => {
|
||||
if (prevIndex !== undefined) {
|
||||
resetParams(option);
|
||||
}
|
||||
|
||||
const possibleMetrics = indexToMetrics[index];
|
||||
|
||||
const columns = signals.createSignal(/** @type {Metric[]} */ ([]), {
|
||||
equals: false,
|
||||
save: {
|
||||
...serdeMetrics,
|
||||
keyPrefix: `table-${serializedIndex()}`,
|
||||
key: `columns`,
|
||||
},
|
||||
});
|
||||
columns.set((l) => l.filter((id) => possibleMetrics.includes(id)));
|
||||
|
||||
signals.createEffect(columns, (columns) => {
|
||||
console.log(columns);
|
||||
});
|
||||
|
||||
table.innerHTML = "";
|
||||
const thead = window.document.createElement("thead");
|
||||
table.append(thead);
|
||||
const trHead = window.document.createElement("tr");
|
||||
thead.append(trHead);
|
||||
const tbody = window.document.createElement("tbody");
|
||||
table.append(tbody);
|
||||
|
||||
const rowElements = signals.createSignal(
|
||||
/** @type {HTMLTableRowElement[]} */ ([]),
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {HTMLSelectElement} args.select
|
||||
* @param {Unit} [args.unit]
|
||||
* @param {(event: MouseEvent) => void} [args.onLeft]
|
||||
* @param {(event: MouseEvent) => void} [args.onRight]
|
||||
* @param {(event: MouseEvent) => void} [args.onRemove]
|
||||
*/
|
||||
function addThCol({ select, onLeft, onRight, onRemove, unit: _unit }) {
|
||||
const th = window.document.createElement("th");
|
||||
th.scope = "col";
|
||||
trHead.append(th);
|
||||
const div = window.document.createElement("div");
|
||||
div.append(select);
|
||||
// const top = window.document.createElement("div");
|
||||
// div.append(top);
|
||||
// top.append(select);
|
||||
// top.append(
|
||||
// createAnchorElement({
|
||||
// href: "",
|
||||
// blank: true,
|
||||
// }),
|
||||
// );
|
||||
const bottom = window.document.createElement("div");
|
||||
const unit = window.document.createElement("span");
|
||||
if (_unit) {
|
||||
unit.innerHTML = _unit;
|
||||
}
|
||||
const moveLeft = createButtonElement({
|
||||
inside: "←",
|
||||
title: "Move column to the left",
|
||||
onClick: onLeft || (() => {}),
|
||||
});
|
||||
const moveRight = createButtonElement({
|
||||
inside: "→",
|
||||
title: "Move column to the right",
|
||||
onClick: onRight || (() => {}),
|
||||
});
|
||||
const remove = createButtonElement({
|
||||
inside: "×",
|
||||
title: "Remove column",
|
||||
onClick: onRemove || (() => {}),
|
||||
});
|
||||
bottom.append(unit);
|
||||
bottom.append(moveLeft);
|
||||
bottom.append(moveRight);
|
||||
bottom.append(remove);
|
||||
div.append(bottom);
|
||||
th.append(div);
|
||||
return {
|
||||
element: th,
|
||||
/**
|
||||
* @param {Unit} _unit
|
||||
*/
|
||||
setUnit(_unit) {
|
||||
unit.innerHTML = _unit;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
addThCol({
|
||||
...createSelect({
|
||||
list: serializedIndexes,
|
||||
signal: serializedIndex,
|
||||
}),
|
||||
unit: "index",
|
||||
});
|
||||
|
||||
let from = 0;
|
||||
let to = 0;
|
||||
|
||||
resources
|
||||
.getOrCreate(index, serializedIndex())
|
||||
.fetch()
|
||||
.then((vec) => {
|
||||
if (!vec) return;
|
||||
from = /** @type {number} */ (vec[0]);
|
||||
to = /** @type {number} */ (vec.at(-1)) + 1;
|
||||
const trs = /** @type {HTMLTableRowElement[]} */ ([]);
|
||||
for (let i = vec.length - 1; i >= 0; i--) {
|
||||
const value = vec[i];
|
||||
const tr = window.document.createElement("tr");
|
||||
trs.push(tr);
|
||||
tbody.append(tr);
|
||||
const th = window.document.createElement("th");
|
||||
th.innerHTML = serializeValue({
|
||||
value,
|
||||
unit: "index",
|
||||
});
|
||||
th.scope = "row";
|
||||
tr.append(th);
|
||||
}
|
||||
rowElements.set(() => trs);
|
||||
});
|
||||
|
||||
const owner = signals.getOwner();
|
||||
|
||||
/**
|
||||
* @param {Metric} metric
|
||||
* @param {number} [_colIndex]
|
||||
*/
|
||||
function addCol(metric, _colIndex = columns().length) {
|
||||
signals.runWithOwner(owner, () => {
|
||||
/** @type {VoidFunction | undefined} */
|
||||
let dispose;
|
||||
signals.createRoot((_dispose) => {
|
||||
dispose = _dispose;
|
||||
|
||||
const metricOption = signals.createSignal({
|
||||
name: metric,
|
||||
value: metric,
|
||||
});
|
||||
const { select } = createSelect({
|
||||
list: possibleMetrics.map((metric) => ({
|
||||
name: metric,
|
||||
value: metric,
|
||||
})),
|
||||
signal: metricOption,
|
||||
});
|
||||
|
||||
signals.createEffect(metricOption, (metricOption) => {
|
||||
select.style.width = `${21 + 7.25 * metricOption.name.length}px`;
|
||||
});
|
||||
|
||||
if (_colIndex === columns().length) {
|
||||
columns.set((l) => {
|
||||
l.push(metric);
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
const colIndex = signals.createSignal(_colIndex);
|
||||
|
||||
/**
|
||||
* @param {boolean} right
|
||||
* @returns {(event: MouseEvent) => void}
|
||||
*/
|
||||
function createMoveColumnFunction(right) {
|
||||
return () => {
|
||||
const oldColIndex = colIndex();
|
||||
const newColIndex = oldColIndex + (right ? 1 : -1);
|
||||
|
||||
const currentTh = /** @type {HTMLTableCellElement} */ (
|
||||
trHead.childNodes[oldColIndex + 1]
|
||||
);
|
||||
const oterTh = /** @type {HTMLTableCellElement} */ (
|
||||
trHead.childNodes[newColIndex + 1]
|
||||
);
|
||||
|
||||
if (right) {
|
||||
oterTh.after(currentTh);
|
||||
} else {
|
||||
oterTh.before(currentTh);
|
||||
}
|
||||
|
||||
columns.set((l) => {
|
||||
[l[oldColIndex], l[newColIndex]] = [
|
||||
l[newColIndex],
|
||||
l[oldColIndex],
|
||||
];
|
||||
return l;
|
||||
});
|
||||
|
||||
const rows = rowElements();
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const element = rows[i].childNodes[oldColIndex + 1];
|
||||
const sibling = rows[i].childNodes[newColIndex + 1];
|
||||
const temp = element.textContent;
|
||||
element.textContent = sibling.textContent;
|
||||
sibling.textContent = temp;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const th = addThCol({
|
||||
select,
|
||||
unit: serdeUnit.deserialize(metric),
|
||||
onLeft: createMoveColumnFunction(false),
|
||||
onRight: createMoveColumnFunction(true),
|
||||
onRemove: () => {
|
||||
const ci = colIndex();
|
||||
trHead.childNodes[ci + 1].remove();
|
||||
columns.set((l) => {
|
||||
l.splice(ci, 1);
|
||||
return l;
|
||||
});
|
||||
const rows = rowElements();
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
rows[i].childNodes[ci + 1].remove();
|
||||
}
|
||||
dispose?.();
|
||||
},
|
||||
});
|
||||
|
||||
signals.createEffect(columns, () => {
|
||||
colIndex.set(Array.from(trHead.children).indexOf(th.element) - 1);
|
||||
});
|
||||
|
||||
console.log(colIndex());
|
||||
|
||||
signals.createEffect(rowElements, (rowElements) => {
|
||||
if (!rowElements.length) return;
|
||||
for (let i = 0; i < rowElements.length; i++) {
|
||||
const td = window.document.createElement("td");
|
||||
rowElements[i].append(td);
|
||||
}
|
||||
|
||||
signals.createEffect(
|
||||
() => metricOption().name,
|
||||
(metric, prevMetric) => {
|
||||
const unit = serdeUnit.deserialize(metric);
|
||||
th.setUnit(unit);
|
||||
|
||||
const vec = resources.getOrCreate(index, metric);
|
||||
|
||||
vec.fetch({ from, to });
|
||||
|
||||
const fetchedKey = resources.genFetchedKey({ from, to });
|
||||
|
||||
columns.set((l) => {
|
||||
const i = l.indexOf(prevMetric ?? metric);
|
||||
if (i === -1) {
|
||||
l.push(metric);
|
||||
} else {
|
||||
l[i] = metric;
|
||||
}
|
||||
return l;
|
||||
});
|
||||
|
||||
signals.createEffect(
|
||||
() => vec.fetched().get(fetchedKey)?.vec(),
|
||||
(vec) => {
|
||||
if (!vec?.length) return;
|
||||
|
||||
const thIndex = colIndex() + 1;
|
||||
|
||||
for (let i = 0; i < rowElements.length; i++) {
|
||||
const iRev = vec.length - 1 - i;
|
||||
const value = vec[iRev];
|
||||
// @ts-ignore
|
||||
rowElements[i].childNodes[thIndex].innerHTML =
|
||||
serializeValue({
|
||||
value,
|
||||
unit,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => metric;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
signals.onCleanup(() => {
|
||||
dispose?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
columns().forEach((metric, colIndex) => addCol(metric, colIndex));
|
||||
|
||||
obj.addRandomCol = function () {
|
||||
addCol(randomFromArray(possibleMetrics));
|
||||
};
|
||||
|
||||
return () => index;
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Signals} args.signals
|
||||
@@ -354,53 +12,397 @@ function createTable({ brk, signals, option, resources }) {
|
||||
* @param {BRK} args.brk
|
||||
*/
|
||||
export function init({ signals, option, resources, brk }) {
|
||||
const parent = tableElement;
|
||||
const { headerElement } = createHeader("Table");
|
||||
parent.append(headerElement);
|
||||
tableElement.innerHTML = "wip, will hopefuly be back soon, sorry !";
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
parent.append(div);
|
||||
// const parent = tableElement;
|
||||
// const { headerElement } = createHeader("Table");
|
||||
// parent.append(headerElement);
|
||||
|
||||
const table = createTable({
|
||||
signals,
|
||||
brk,
|
||||
resources,
|
||||
option,
|
||||
});
|
||||
div.append(table.element);
|
||||
// const div = window.document.createElement("div");
|
||||
// parent.append(div);
|
||||
|
||||
const span = window.document.createElement("span");
|
||||
span.innerHTML = "Add column";
|
||||
div.append(
|
||||
createButtonElement({
|
||||
onClick: () => {
|
||||
table.addRandomCol?.();
|
||||
},
|
||||
inside: span,
|
||||
title: "Click or tap to add a column to the table",
|
||||
}),
|
||||
);
|
||||
// const table = createTable({
|
||||
// signals,
|
||||
// brk,
|
||||
// resources,
|
||||
// option,
|
||||
// });
|
||||
// div.append(table.element);
|
||||
|
||||
// const span = window.document.createElement("span");
|
||||
// span.innerHTML = "Add column";
|
||||
// div.append(
|
||||
// createButtonElement({
|
||||
// onClick: () => {
|
||||
// table.addRandomCol?.();
|
||||
// },
|
||||
// inside: span,
|
||||
// title: "Click or tap to add a column to the table",
|
||||
// }),
|
||||
// );
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param {Object} args
|
||||
// * @param {Option} args.option
|
||||
// * @param {Signals} args.signals
|
||||
// * @param {BRK} args.brk
|
||||
// * @param {Resources} args.resources
|
||||
// */
|
||||
// function createTable({ brk, signals, option, resources }) {
|
||||
// const indexToMetrics = createIndexToMetrics(metricToIndexes);
|
||||
|
||||
// const serializedIndexes = createSerializedIndexes();
|
||||
// /** @type {SerializedIndex} */
|
||||
// const defaultSerializedIndex = "height";
|
||||
// const serializedIndex = /** @type {Signal<SerializedIndex>} */ (
|
||||
// signals.createSignal(
|
||||
// /** @type {SerializedIndex} */ (defaultSerializedIndex),
|
||||
// {
|
||||
// save: {
|
||||
// ...serdeString,
|
||||
// keyPrefix: "table",
|
||||
// key: "index",
|
||||
// },
|
||||
// },
|
||||
// )
|
||||
// );
|
||||
// const index = signals.createMemo(() =>
|
||||
// serializedIndexToIndex(serializedIndex()),
|
||||
// );
|
||||
|
||||
// const table = window.document.createElement("table");
|
||||
// const obj = {
|
||||
// element: table,
|
||||
// /** @type {VoidFunction | undefined} */
|
||||
// addRandomCol: undefined,
|
||||
// };
|
||||
|
||||
// signals.createEffect(index, (index, prevIndex) => {
|
||||
// if (prevIndex !== undefined) {
|
||||
// resetParams(option);
|
||||
// }
|
||||
|
||||
// const possibleMetrics = indexToMetrics[index];
|
||||
|
||||
// const columns = signals.createSignal(/** @type {Metric[]} */ ([]), {
|
||||
// equals: false,
|
||||
// save: {
|
||||
// ...serdeMetrics,
|
||||
// keyPrefix: `table-${serializedIndex()}`,
|
||||
// key: `columns`,
|
||||
// },
|
||||
// });
|
||||
// columns.set((l) => l.filter((id) => possibleMetrics.includes(id)));
|
||||
|
||||
// signals.createEffect(columns, (columns) => {
|
||||
// console.log(columns);
|
||||
// });
|
||||
|
||||
// table.innerHTML = "";
|
||||
// const thead = window.document.createElement("thead");
|
||||
// table.append(thead);
|
||||
// const trHead = window.document.createElement("tr");
|
||||
// thead.append(trHead);
|
||||
// const tbody = window.document.createElement("tbody");
|
||||
// table.append(tbody);
|
||||
|
||||
// const rowElements = signals.createSignal(
|
||||
// /** @type {HTMLTableRowElement[]} */ ([]),
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @param {Object} args
|
||||
// * @param {HTMLSelectElement} args.select
|
||||
// * @param {Unit} [args.unit]
|
||||
// * @param {(event: MouseEvent) => void} [args.onLeft]
|
||||
// * @param {(event: MouseEvent) => void} [args.onRight]
|
||||
// * @param {(event: MouseEvent) => void} [args.onRemove]
|
||||
// */
|
||||
// function addThCol({ select, onLeft, onRight, onRemove, unit: _unit }) {
|
||||
// const th = window.document.createElement("th");
|
||||
// th.scope = "col";
|
||||
// trHead.append(th);
|
||||
// const div = window.document.createElement("div");
|
||||
// div.append(select);
|
||||
// // const top = window.document.createElement("div");
|
||||
// // div.append(top);
|
||||
// // top.append(select);
|
||||
// // top.append(
|
||||
// // createAnchorElement({
|
||||
// // href: "",
|
||||
// // blank: true,
|
||||
// // }),
|
||||
// // );
|
||||
// const bottom = window.document.createElement("div");
|
||||
// const unit = window.document.createElement("span");
|
||||
// if (_unit) {
|
||||
// unit.innerHTML = _unit;
|
||||
// }
|
||||
// const moveLeft = createButtonElement({
|
||||
// inside: "←",
|
||||
// title: "Move column to the left",
|
||||
// onClick: onLeft || (() => {}),
|
||||
// });
|
||||
// const moveRight = createButtonElement({
|
||||
// inside: "→",
|
||||
// title: "Move column to the right",
|
||||
// onClick: onRight || (() => {}),
|
||||
// });
|
||||
// const remove = createButtonElement({
|
||||
// inside: "×",
|
||||
// title: "Remove column",
|
||||
// onClick: onRemove || (() => {}),
|
||||
// });
|
||||
// bottom.append(unit);
|
||||
// bottom.append(moveLeft);
|
||||
// bottom.append(moveRight);
|
||||
// bottom.append(remove);
|
||||
// div.append(bottom);
|
||||
// th.append(div);
|
||||
// return {
|
||||
// element: th,
|
||||
// /**
|
||||
// * @param {Unit} _unit
|
||||
// */
|
||||
// setUnit(_unit) {
|
||||
// unit.innerHTML = _unit;
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
// addThCol({
|
||||
// ...createSelect({
|
||||
// list: serializedIndexes,
|
||||
// signal: serializedIndex,
|
||||
// }),
|
||||
// unit: "index",
|
||||
// });
|
||||
|
||||
// let from = 0;
|
||||
// let to = 0;
|
||||
|
||||
// resources
|
||||
// .getOrCreate(index, serializedIndex())
|
||||
// .fetch()
|
||||
// .then((vec) => {
|
||||
// if (!vec) return;
|
||||
// from = /** @type {number} */ (vec[0]);
|
||||
// to = /** @type {number} */ (vec.at(-1)) + 1;
|
||||
// const trs = /** @type {HTMLTableRowElement[]} */ ([]);
|
||||
// for (let i = vec.length - 1; i >= 0; i--) {
|
||||
// const value = vec[i];
|
||||
// const tr = window.document.createElement("tr");
|
||||
// trs.push(tr);
|
||||
// tbody.append(tr);
|
||||
// const th = window.document.createElement("th");
|
||||
// th.innerHTML = serializeValue({
|
||||
// value,
|
||||
// unit: "index",
|
||||
// });
|
||||
// th.scope = "row";
|
||||
// tr.append(th);
|
||||
// }
|
||||
// rowElements.set(() => trs);
|
||||
// });
|
||||
|
||||
// const owner = signals.getOwner();
|
||||
|
||||
// /**
|
||||
// * @param {Metric} metric
|
||||
// * @param {number} [_colIndex]
|
||||
// */
|
||||
// function addCol(metric, _colIndex = columns().length) {
|
||||
// signals.runWithOwner(owner, () => {
|
||||
// /** @type {VoidFunction | undefined} */
|
||||
// let dispose;
|
||||
// signals.createRoot((_dispose) => {
|
||||
// dispose = _dispose;
|
||||
|
||||
// const metricOption = signals.createSignal({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// });
|
||||
// const { select } = createSelect({
|
||||
// list: possibleMetrics.map((metric) => ({
|
||||
// name: metric,
|
||||
// value: metric,
|
||||
// })),
|
||||
// signal: metricOption,
|
||||
// });
|
||||
|
||||
// signals.createEffect(metricOption, (metricOption) => {
|
||||
// select.style.width = `${21 + 7.25 * metricOption.name.length}px`;
|
||||
// });
|
||||
|
||||
// if (_colIndex === columns().length) {
|
||||
// columns.set((l) => {
|
||||
// l.push(metric);
|
||||
// return l;
|
||||
// });
|
||||
// }
|
||||
|
||||
// const colIndex = signals.createSignal(_colIndex);
|
||||
|
||||
// /**
|
||||
// * @param {boolean} right
|
||||
// * @returns {(event: MouseEvent) => void}
|
||||
// */
|
||||
// function createMoveColumnFunction(right) {
|
||||
// return () => {
|
||||
// const oldColIndex = colIndex();
|
||||
// const newColIndex = oldColIndex + (right ? 1 : -1);
|
||||
|
||||
// const currentTh = /** @type {HTMLTableCellElement} */ (
|
||||
// trHead.childNodes[oldColIndex + 1]
|
||||
// );
|
||||
// const oterTh = /** @type {HTMLTableCellElement} */ (
|
||||
// trHead.childNodes[newColIndex + 1]
|
||||
// );
|
||||
|
||||
// if (right) {
|
||||
// oterTh.after(currentTh);
|
||||
// } else {
|
||||
// oterTh.before(currentTh);
|
||||
// }
|
||||
|
||||
// columns.set((l) => {
|
||||
// [l[oldColIndex], l[newColIndex]] = [
|
||||
// l[newColIndex],
|
||||
// l[oldColIndex],
|
||||
// ];
|
||||
// return l;
|
||||
// });
|
||||
|
||||
// const rows = rowElements();
|
||||
// for (let i = 0; i < rows.length; i++) {
|
||||
// const element = rows[i].childNodes[oldColIndex + 1];
|
||||
// const sibling = rows[i].childNodes[newColIndex + 1];
|
||||
// const temp = element.textContent;
|
||||
// element.textContent = sibling.textContent;
|
||||
// sibling.textContent = temp;
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// const th = addThCol({
|
||||
// select,
|
||||
// unit: serdeUnit.deserialize(metric),
|
||||
// onLeft: createMoveColumnFunction(false),
|
||||
// onRight: createMoveColumnFunction(true),
|
||||
// onRemove: () => {
|
||||
// const ci = colIndex();
|
||||
// trHead.childNodes[ci + 1].remove();
|
||||
// columns.set((l) => {
|
||||
// l.splice(ci, 1);
|
||||
// return l;
|
||||
// });
|
||||
// const rows = rowElements();
|
||||
// for (let i = 0; i < rows.length; i++) {
|
||||
// rows[i].childNodes[ci + 1].remove();
|
||||
// }
|
||||
// dispose?.();
|
||||
// },
|
||||
// });
|
||||
|
||||
// signals.createEffect(columns, () => {
|
||||
// colIndex.set(Array.from(trHead.children).indexOf(th.element) - 1);
|
||||
// });
|
||||
|
||||
// console.log(colIndex());
|
||||
|
||||
// signals.createEffect(rowElements, (rowElements) => {
|
||||
// if (!rowElements.length) return;
|
||||
// for (let i = 0; i < rowElements.length; i++) {
|
||||
// const td = window.document.createElement("td");
|
||||
// rowElements[i].append(td);
|
||||
// }
|
||||
|
||||
// signals.createEffect(
|
||||
// () => metricOption().name,
|
||||
// (metric, prevMetric) => {
|
||||
// const unit = serdeUnit.deserialize(metric);
|
||||
// th.setUnit(unit);
|
||||
|
||||
// const vec = resources.getOrCreate(index, metric);
|
||||
|
||||
// vec.fetch({ from, to });
|
||||
|
||||
// const fetchedKey = resources.genFetchedKey({ from, to });
|
||||
|
||||
// columns.set((l) => {
|
||||
// const i = l.indexOf(prevMetric ?? metric);
|
||||
// if (i === -1) {
|
||||
// l.push(metric);
|
||||
// } else {
|
||||
// l[i] = metric;
|
||||
// }
|
||||
// return l;
|
||||
// });
|
||||
|
||||
// signals.createEffect(
|
||||
// () => vec.fetched().get(fetchedKey)?.vec(),
|
||||
// (vec) => {
|
||||
// if (!vec?.length) return;
|
||||
|
||||
// const thIndex = colIndex() + 1;
|
||||
|
||||
// for (let i = 0; i < rowElements.length; i++) {
|
||||
// const iRev = vec.length - 1 - i;
|
||||
// const value = vec[iRev];
|
||||
// // @ts-ignore
|
||||
// rowElements[i].childNodes[thIndex].innerHTML =
|
||||
// serializeValue({
|
||||
// value,
|
||||
// unit,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
|
||||
// return () => metric;
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
// signals.onCleanup(() => {
|
||||
// dispose?.();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// columns().forEach((metric, colIndex) => addCol(metric, colIndex));
|
||||
|
||||
// obj.addRandomCol = function () {
|
||||
// addCol(randomFromArray(possibleMetrics));
|
||||
// };
|
||||
|
||||
// return () => index;
|
||||
// });
|
||||
|
||||
// return obj;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param {MetricToIndexes} metricToIndexes
|
||||
*/
|
||||
function createIndexToMetrics(metricToIndexes) {
|
||||
const indexToMetrics = Object.entries(metricToIndexes).reduce(
|
||||
(arr, [_id, indexes]) => {
|
||||
const id = /** @type {Metric} */ (_id);
|
||||
indexes.forEach((i) => {
|
||||
arr[i] ??= [];
|
||||
arr[i].push(id);
|
||||
});
|
||||
return arr;
|
||||
},
|
||||
/** @type {Metric[][]} */ (Array.from({ length: 24 })),
|
||||
);
|
||||
indexToMetrics.forEach((arr) => {
|
||||
arr.sort();
|
||||
});
|
||||
return indexToMetrics;
|
||||
// const indexToMetrics = Object.entries(metricToIndexes).reduce(
|
||||
// (arr, [_id, indexes]) => {
|
||||
// const id = /** @type {Metric} */ (_id);
|
||||
// indexes.forEach((i) => {
|
||||
// arr[i] ??= [];
|
||||
// arr[i].push(id);
|
||||
// });
|
||||
// return arr;
|
||||
// },
|
||||
// /** @type {Metric[][]} */ (Array.from({ length: 24 })),
|
||||
// );
|
||||
// indexToMetrics.forEach((arr) => {
|
||||
// arr.sort();
|
||||
// });
|
||||
// return indexToMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,16 +53,16 @@ sw.addEventListener("fetch", (event) => {
|
||||
if (
|
||||
req.method !== "GET" ||
|
||||
url.pathname.startsWith("/api") ||
|
||||
url.pathname === "/mcp" ||
|
||||
url.pathname === "/crates" ||
|
||||
url.pathname === "/github" ||
|
||||
url.pathname === "/status" ||
|
||||
url.pathname === "/cli" ||
|
||||
url.pathname === "/hosting" ||
|
||||
url.pathname === "/nostr" ||
|
||||
url.pathname === "/changelog" ||
|
||||
url.pathname === "/crate" ||
|
||||
url.pathname === "/discord" ||
|
||||
url.pathname === "/nostr" ||
|
||||
url.pathname === "/github" ||
|
||||
url.pathname === "/health" ||
|
||||
url.pathname === "/install" ||
|
||||
url.pathname === "/mcp" ||
|
||||
url.pathname === "/nostr" ||
|
||||
url.pathname === "/service" ||
|
||||
url.pathname === "/status" ||
|
||||
url.pathname === "/version"
|
||||
) {
|
||||
return; // let the browser handle it
|
||||
|
||||
Reference in New Issue
Block a user