feat: Add Announcement viewer

Every transaction can have zero or more announcements, which are essentially
messages that must be included across all future mergers and updates.
Announcements are typically used for transmitting information related to
receiving UTXOs, encrypted. However, a new use case is that of *transparent
transaction info*, which is an announcement containing the UTXOs and the
commitment randomnesses needed to reproduce the transaction's inputs and
outputs. With such a transparent transaction info announcement, third parties
can transparently audit transactions.

This commit adds a page for viewing announcements, and if the announcement can
be parsed as a transparent transaction info type announcement then it is
rendered as such, complete with native currency amounts and linkable UTXOs
(where possible).

The path for accessing announcements is any of
 - `/announcement/digest/<hex-string>/<index>`
 - `/announcement/tip/<index>`
 - `/announcement/genesis/<index>`
 - `/announcement/height/<height>/<index>`
 - `/announcement/height_or_digest/<height-or-diges>/index/<index>`.
The last bullet point exists to support the quick lookup functionality, which is
also new. A `AnnouncementSelector` has display and from-string methods relating
these path formats to the relevant object. A suite of (prop)tests verifies
parsing.

Also, this commit enables mocking, which is useful when the neptune-core node
the explorer is connected to is outdated, unsynced, or for whatever reason does
not serve the desired data. The RPC client call is intercepted, and if it fails
and mocking is enabled, an imagined (pseudorandom) resource of the requested
type is returned. To enable mocking, compile with the "mock" feature flag and
set the "MOCK" environment variable.
This commit is contained in:
Alan Szepieniec 2025-08-12 18:21:27 +02:00
parent 8fa76ec8a3
commit 86698687a8
15 changed files with 1205 additions and 477 deletions

435
Cargo.lock generated
View File

@ -168,6 +168,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arraystring" name = "arraystring"
version = "0.3.0" version = "0.3.0"
@ -178,6 +184,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@ -361,12 +373,40 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "blake3"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -408,21 +448,6 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.31" version = "1.2.31"
@ -494,15 +519,6 @@ dependencies = [
"strsim", "strsim",
] ]
[[package]]
name = "clap_complete"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.41" version = "4.5.41"
@ -551,20 +567,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "const_format" name = "const_format"
version = "0.2.34" version = "0.2.34"
@ -585,6 +587,12 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -644,47 +652,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"mio 1.0.4",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -749,6 +716,18 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive-ex"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba95f299f6b9cd47f68a847eca2ae9060a2713af532dc35c342065544845407"
dependencies = [
"proc-macro2",
"quote",
"structmeta",
"syn 2.0.104",
]
[[package]] [[package]]
name = "derive-where" name = "derive-where"
version = "1.5.0" version = "1.5.0"
@ -935,12 +914,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@ -1146,11 +1119,6 @@ name = "hashbrown"
version = "0.15.4" version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]] [[package]]
name = "heck" name = "heck"
@ -1447,12 +1415,6 @@ dependencies = [
"rayon", "rayon",
] ]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.4" version = "0.1.4"
@ -1462,19 +1424,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "instability"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "interpolator" name = "interpolator"
version = "0.5.0" version = "0.5.0"
@ -1507,15 +1456,6 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@ -1618,12 +1558,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.9.4" version = "0.9.4"
@ -1652,15 +1586,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.4",
]
[[package]] [[package]]
name = "manyhow" name = "manyhow"
version = "0.11.4" version = "0.11.4"
@ -1765,18 +1690,6 @@ dependencies = [
"adler2", "adler2",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.4" version = "1.0.4"
@ -1784,7 +1697,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@ -1825,8 +1737,6 @@ dependencies = [
[[package]] [[package]]
name = "neptune-cash" name = "neptune-cash"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0420017f9e5e1c2e9d255a9bd4da9a78989fe0d8f9be550560859d2735d0756"
dependencies = [ dependencies = [
"aead", "aead",
"aes-gcm", "aes-gcm",
@ -1840,8 +1750,6 @@ dependencies = [
"bytesize", "bytesize",
"chrono", "chrono",
"clap", "clap",
"clap_complete",
"crossterm 0.27.0",
"directories", "directories",
"field_count", "field_count",
"futures", "futures",
@ -1856,7 +1764,6 @@ dependencies = [
"priority-queue", "priority-queue",
"rand 0.9.2", "rand 0.9.2",
"rand_distr", "rand_distr",
"ratatui",
"rayon", "rayon",
"readonly", "readonly",
"regex", "regex",
@ -1867,8 +1774,8 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"sha3", "sha3",
"strum 0.27.2", "strum",
"strum_macros 0.27.2", "strum_macros",
"sysinfo", "sysinfo",
"systemstat", "systemstat",
"tarpc", "tarpc",
@ -1882,7 +1789,6 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tracing-test", "tracing-test",
"unicode-width 0.1.14",
"zeroize", "zeroize",
] ]
@ -1891,8 +1797,10 @@ name = "neptune-explorer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arbitrary",
"arc-swap", "arc-swap",
"axum", "axum",
"blake3",
"boilerplate", "boilerplate",
"chrono", "chrono",
"clap", "clap",
@ -1901,10 +1809,14 @@ dependencies = [
"indexmap 2.10.0", "indexmap 2.10.0",
"lettre", "lettre",
"neptune-cash", "neptune-cash",
"proptest",
"proptest-arbitrary-interop",
"rand 0.9.2",
"readonly", "readonly",
"serde", "serde",
"serde_json", "serde_json",
"tarpc", "tarpc",
"test-strategy",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tower-http", "tower-http",
@ -2210,12 +2122,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pbkdf2" name = "pbkdf2"
version = "0.11.0" version = "0.11.0"
@ -2403,6 +2309,36 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proptest"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f"
dependencies = [
"bit-set",
"bit-vec",
"bitflags",
"lazy_static",
"num-traits",
"rand 0.9.2",
"rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax 0.8.5",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "proptest-arbitrary-interop"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1981e49bd2432249da8b0e11e5557099a8e74690d6b94e721f7dc0bb7f3555f"
dependencies = [
"arbitrary",
"proptest",
]
[[package]] [[package]]
name = "psm" name = "psm"
version = "0.1.26" version = "0.1.26"
@ -2412,6 +2348,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@ -2525,24 +2467,12 @@ dependencies = [
] ]
[[package]] [[package]]
name = "ratatui" name = "rand_xorshift"
version = "0.29.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [ dependencies = [
"bitflags", "rand_core 0.9.3",
"cassowary",
"compact_str",
"crossterm 0.28.1",
"indoc",
"instability",
"itertools 0.13.0",
"lru",
"paste",
"strum 0.26.3",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
] ]
[[package]] [[package]]
@ -2668,19 +2598,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.8" version = "1.0.8"
@ -2690,7 +2607,7 @@ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.9.4", "linux-raw-sys",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@ -2700,6 +2617,18 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -2858,28 +2787,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio 0.8.11",
"mio 1.0.4",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.6"
@ -2949,12 +2856,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "strum" name = "structmeta"
version = "0.26.3" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
dependencies = [ dependencies = [
"strum_macros 0.26.4", "proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.104",
]
[[package]]
name = "structmeta-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
] ]
[[package]] [[package]]
@ -2963,20 +2884,7 @@ version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [ dependencies = [
"strum_macros 0.27.2", "strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
] ]
[[package]] [[package]]
@ -3116,7 +3024,7 @@ dependencies = [
"rand 0.9.2", "rand 0.9.2",
"serde", "serde",
"serde_json", "serde_json",
"strum 0.27.2", "strum",
"tasm-object-derive", "tasm-object-derive",
"triton-vm", "triton-vm",
] ]
@ -3141,10 +3049,23 @@ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.0.8", "rustix",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "test-strategy"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b12f9683de37f9980e485167ee624bfaa0b6b04da661e98e25ef9c2669bc1b"
dependencies = [
"derive-ex",
"proc-macro2",
"quote",
"structmeta",
"syn 2.0.104",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -3293,7 +3214,7 @@ dependencies = [
"bytes", "bytes",
"io-uring", "io-uring",
"libc", "libc",
"mio 1.0.4", "mio",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@ -3514,7 +3435,7 @@ checksum = "973a0422b170667a558ae36e21643ba827e76d2c663607087f0797888f4c59ad"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"itertools 0.14.0", "itertools 0.14.0",
"strum 0.27.2", "strum",
"triton-constraint-circuit", "triton-constraint-circuit",
"triton-isa", "triton-isa",
"twenty-first", "twenty-first",
@ -3530,7 +3451,7 @@ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strum 0.27.2", "strum",
"syn 2.0.104", "syn 2.0.104",
"triton-air", "triton-air",
"triton-constraint-circuit", "triton-constraint-circuit",
@ -3567,7 +3488,7 @@ dependencies = [
"nom-language", "nom-language",
"num-traits", "num-traits",
"serde", "serde",
"strum 0.27.2", "strum",
"thiserror 2.0.12", "thiserror 2.0.12",
"twenty-first", "twenty-first",
] ]
@ -3592,7 +3513,7 @@ dependencies = [
"rand 0.9.2", "rand 0.9.2",
"rayon", "rayon",
"serde", "serde",
"strum 0.27.2", "strum",
"syn 2.0.104", "syn 2.0.104",
"thiserror 2.0.12", "thiserror 2.0.12",
"triton-air", "triton-air",
@ -3600,7 +3521,7 @@ dependencies = [
"triton-constraint-circuit", "triton-constraint-circuit",
"triton-isa", "triton-isa",
"twenty-first", "twenty-first",
"unicode-width 0.2.0", "unicode-width",
] ]
[[package]] [[package]]
@ -3636,6 +3557,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.8.1" version = "2.8.1"
@ -3657,29 +3584,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools 0.13.0",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.0"
@ -3743,6 +3647,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.1+wasi-snapshot-preview1" version = "0.11.1+wasi-snapshot-preview1"

View File

@ -38,3 +38,17 @@ derive_more = { version = "1.0.0", features = ["display"] }
# not a direct dep. workaround for weird "could not resolve" cargo error # not a direct dep. workaround for weird "could not resolve" cargo error
indexmap = "2.7.0" indexmap = "2.7.0"
blake3 = {version = "1.8.2", optional = true}
rand = {version = "0.9.2", optional = true}
#[dev-dependencies]
test-strategy = "0.4.3"
proptest = "1.7.0"
arbitrary = "1.4.1"
proptest-arbitrary-interop = "0.1.0"
[patch.crates-io]
neptune-cash = {path = "../neptune-core/neptune-core/" }
[features]
mock = ["dep:blake3", "dep:rand"]

View File

@ -41,13 +41,18 @@ Notes:
* The block-explorer automatically uses the same network (mainnet, testnet, etc) as the neptune-core instance it is connected to, and the network is displayed in the web interface. * The block-explorer automatically uses the same network (mainnet, testnet, etc) as the neptune-core instance it is connected to, and the network is displayed in the web interface.
* If neptune-core RPC server is running on a non-standard port, you can provide it with the `--neptune-rpc-port` flag. * If neptune-core RPC server is running on a non-standard port, you can provide it with the `--neptune-rpc-port` flag.
* neptune-explorer listens for http requests on port 3000 by default. This can be changed with the `--listen-port` flag. * neptune-explorer listens for http requests on port 3000 by default. This can be changed with the `--listen-port` flag.
* Site name can be specified with the `--site-name` flag. * Site name must be specified with the `--site-name` flag.
## Connecting via Browser ## Connecting via Browser
Just navigate to http://localhost:3000/ Just navigate to http://localhost:3000/
## Mocking
When connected to an out-of-date or unsynced neptune-core node, it might be a good idea to turn on mocking so that whenever a resource is unavailable, a random one is generated and returned. To do this, compile with the feature flag "mock" and make sure that the "MOCK" environment variable is set.
In one command: `MOCK=1 cargo run --features "mock" -- --site-name testname`
## SSL/TLS, Nginx, etc. ## SSL/TLS, Nginx, etc.

View File

@ -1,7 +1,7 @@
use crate::model::app_state::AppStateInner; use crate::model::app_state::AppStateInner;
use html_escaper::Escape; use html_escaper::Escape;
#[derive(boilerplate::Boilerplate)] #[derive(Debug, Clone, boilerplate::Boilerplate)]
#[boilerplate(filename = "web/html/components/header.html")] #[boilerplate(filename = "web/html/components/header.html")]
pub struct HeaderHtml<'a> { pub struct HeaderHtml<'a> {
pub state: &'a AppStateInner, pub state: &'a AppStateInner,

View File

@ -0,0 +1,87 @@
use crate::html::component::header::HeaderHtml;
use crate::html::page::not_found::not_found_html_response;
use crate::http_util::rpc_method_err;
use crate::model::announcement_selector::AnnouncementSelector;
use crate::model::announcement_type::AnnouncementType;
use crate::model::app_state::AppState;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
use axum::response::Html;
use axum::response::Response;
use html_escaper::Escape;
use html_escaper::Trusted;
use neptune_cash::api::export::BlockHeight;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use neptune_cash::prelude::triton_vm::prelude::BFieldCodec;
use neptune_cash::prelude::twenty_first::tip5::Tip5;
use std::sync::Arc;
use tarpc::context;
#[axum::debug_handler]
pub async fn announcement_page(
maybe_path: Result<Path<AnnouncementSelector>, PathRejection>,
State(state_rw): State<Arc<AppState>>,
) -> Result<Html<String>, Response> {
#[derive(Debug, Clone, boilerplate::Boilerplate)]
#[boilerplate(filename = "web/html/page/announcement.html")]
pub struct AnnouncementHtmlPage<'a> {
header: HeaderHtml<'a>,
index: usize,
num_announcements: usize,
block_hash: Digest,
block_height: BlockHeight,
announcement_type: AnnouncementType,
}
let state = &state_rw.load();
let Path(AnnouncementSelector {
block_selector,
index,
}) = maybe_path.map_err(|e| not_found_html_response(state, Some(e.to_string())))?;
let block_info = state
.rpc_client
.block_info(context::current(), state.token(), block_selector)
.await
.map_err(|e| not_found_html_response(state, Some(e.to_string())))?
.map_err(rpc_method_err)?
.ok_or(not_found_html_response(
state,
Some("The requested block does not exist".to_string()),
))?;
let block_hash = block_info.digest;
let block_height = block_info.height;
let announcements = state
.rpc_client
.announcements_in_block(context::current(), state.token(), block_selector)
.await
.map_err(|e| not_found_html_response(state, Some(e.to_string())))?
.map_err(rpc_method_err)?
.expect(
"block guaranteed to exist because we got here; getting its announcements should work",
);
let num_announcements = announcements.len();
let announcement = announcements
.get(index)
.ok_or(not_found_html_response(
state,
Some("The requested announcement does not exist".to_string()),
))?
.clone();
let announcement_type = AnnouncementType::parse(announcement);
let header = HeaderHtml { state };
let utxo_page = AnnouncementHtmlPage {
index,
header,
block_hash,
block_height,
num_announcements,
announcement_type,
};
Ok(Html(utxo_page.to_string()))
}

View File

@ -1,3 +1,4 @@
pub mod announcement;
pub mod block; pub mod block;
pub mod not_found; pub mod not_found;
pub mod redirect_qs_to_path; pub mod redirect_qs_to_path;

View File

@ -3,6 +3,7 @@ use axum::routing::get;
use axum::routing::post; use axum::routing::post;
use axum::routing::Router; use axum::routing::Router;
use neptune_explorer::alert_email; use neptune_explorer::alert_email;
use neptune_explorer::html::page::announcement::announcement_page;
use neptune_explorer::html::page::block::block_page; use neptune_explorer::html::page::block::block_page;
use neptune_explorer::html::page::not_found::not_found_html_fallback; use neptune_explorer::html::page::not_found::not_found_html_fallback;
use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path; use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path;
@ -62,6 +63,7 @@ pub fn setup_routes(app_state: AppState) -> Router {
.route("/", get(root)) .route("/", get(root))
.route("/block/*selector", get(block_page)) .route("/block/*selector", get(block_page))
.route("/utxo/:value", get(utxo_page)) .route("/utxo/:value", get(utxo_page))
.route("/announcement/*selector", get(announcement_page))
// -- Rewrite query-strings to path -- // -- Rewrite query-strings to path --
.route("/rqs", get(redirect_query_string_to_path)) .route("/rqs", get(redirect_query_string_to_path))
// -- Static files -- // -- Static files --

View File

@ -0,0 +1,336 @@
use neptune_cash::api::export::BlockHeight;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use std::fmt::Display;
use std::str::FromStr;
/// newtype for `BlockSelector` that provides ability to parse `height_or_digest/value`.
///
/// This is useful for HTML form(s) that allow user to enter either height or
/// digest into the same text input field.
///
/// In particular it is necessary to support javascript-free website with such
/// an html form.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnouncementSelector {
pub block_selector: BlockSelector,
pub index: usize,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum AnnouncementSelectorParseError {
#[error("too many or too few parts in announcement path")]
NumberOfParts,
#[error("error parsing index for announcement in tip: {0}")]
TipIndex(std::num::ParseIntError),
#[error("error parsing index for announcement in genesis block: {0}")]
GenesisIndex(std::num::ParseIntError),
#[error("error parsing block height: {0}")]
BlockHeight(std::num::ParseIntError),
#[error("error parsing index for announcement in block {0}: {1}")]
HeightIndex(BlockHeight, std::num::ParseIntError),
#[error("error parsing digest: {0}")]
BlockDigest(neptune_cash::prelude::twenty_first::error::TryFromHexDigestError),
#[error("error parsing index for announcement in block {0}: {1}")]
DigestIndex(Digest, std::num::ParseIntError),
#[error("error parsing block-height-or-digest: {0} / {1}")]
HeightNorDigest(
std::num::ParseIntError,
neptune_cash::prelude::twenty_first::error::TryFromHexDigestError,
),
#[error("error parsing index for block-height-or-digest {0}: {1}")]
HeightOrDigestIndex(BlockSelector, std::num::ParseIntError),
#[error("invalid keyword {0} or {1}")]
InvalidKeyword(String, String),
#[error("invalid prefix: {0}")]
InvalidPrefix(String),
}
impl FromStr for AnnouncementSelector {
type Err = AnnouncementSelectorParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = input.split('/').collect();
let (block_selector, index) = match parts.as_slice() {
["tip", index] => {
let index = index.parse::<u64>().map_err(Self::Err::TipIndex)?;
(BlockSelector::Tip, index)
}
["genesis", index] => index
.parse::<u64>()
.map(|i| (BlockSelector::Genesis, i))
.map_err(Self::Err::GenesisIndex)?,
["height", number, index] => {
let height_as_u64 = number.parse::<u64>().map_err(Self::Err::BlockHeight)?;
let block_height = BlockHeight::from(height_as_u64);
(
BlockSelector::Height(block_height),
index
.parse()
.map_err(|e| Self::Err::HeightIndex(block_height, e))?,
)
}
["digest", hash, index] => {
let digest = Digest::try_from_hex(hash).map_err(Self::Err::BlockDigest)?;
let index = index
.parse::<u64>()
.map_err(|e| Self::Err::DigestIndex(digest, e))?;
(BlockSelector::Digest(digest), index)
}
["height_or_digest", hod, "index", index] => {
let parsed_height = hod.parse::<u64>();
let parsed_digest = Digest::try_from_hex(hod);
let block_selector = match (parsed_height, parsed_digest) {
(Ok(_), Ok(digest)) => {
// unreachable? Not in theory ...
BlockSelector::Digest(digest)
}
(Ok(h), Err(_)) => BlockSelector::Height(BlockHeight::from(h)),
(Err(_), Ok(digest)) => BlockSelector::Digest(digest),
(Err(pie), Err(hde)) => {
return Err(Self::Err::HeightNorDigest(pie, hde));
}
};
let index = index
.parse::<u64>()
.map_err(|e| Self::Err::HeightOrDigestIndex(block_selector, e))?;
(block_selector, index)
}
[prefix, _, keyword, _] => {
return Err(Self::Err::InvalidKeyword(
prefix.to_string(),
keyword.to_string(),
))
}
[prefix, _, _] | [prefix, _] => {
return Err(Self::Err::InvalidPrefix(prefix.to_string()))
}
&[] | &[_] | &[_, _, _, _, _, ..] => return Err(Self::Err::NumberOfParts),
};
Ok(AnnouncementSelector {
block_selector,
index: index as usize,
})
}
}
impl Display for AnnouncementSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.block_selector {
BlockSelector::Digest(digest) => write!(f, "digest/{digest:x}/{}", self.index),
BlockSelector::Height(block_height) => {
write!(f, "height/{}/{}", block_height, self.index)
}
BlockSelector::Genesis => write!(f, "genesis/{}", self.index),
BlockSelector::Tip => write!(f, "tip/{}", self.index),
}
}
}
// note: axum uses serde Deserialize for Path elements.
impl<'de> Deserialize<'de> for AnnouncementSelector {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(D::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arbitrary::{Arbitrary, Unstructured};
use proptest::string::string_regex;
use proptest::{prop_assert, prop_assert_eq};
use proptest_arbitrary_interop::arb;
use std::str::FromStr;
use test_strategy::proptest;
impl<'a> Arbitrary<'a> for AnnouncementSelector {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
// Pick one of the variants randomly
let variant = u.int_in_range(0..=3)?;
let selector = match variant {
0 => {
// Digest selector
let digest = Digest::arbitrary(u)?;
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Digest(digest),
index,
}
}
1 => {
// Height selector
let height_u64 = u64::arbitrary(u)?;
let bh = BlockHeight::from(height_u64);
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Height(bh),
index,
}
}
2 => {
// Genesis selector
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Genesis,
index,
}
}
3 => {
// Tip selector
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Tip,
index,
}
}
_ => unreachable!(),
};
Ok(selector)
}
fn size_hint(_depth: usize) -> (usize, Option<usize>) {
// Not very precise, but enough for fuzzing/proptesting
(1, None)
}
}
#[proptest]
fn display_roundtrip(#[strategy(arb())] announcement_selector: AnnouncementSelector) {
let as_string = announcement_selector.to_string();
let parsed = AnnouncementSelector::from_str(&as_string).unwrap();
prop_assert_eq!(announcement_selector, parsed.clone());
let as_string_again = parsed.to_string();
prop_assert_eq!(as_string, as_string_again);
}
#[proptest]
fn parse_height_or_digest_digest(
#[strategy(arb())] digest: Digest,
#[strategy(0usize..20)] index: usize,
) {
let str = format!("height_or_digest/{digest:x}/index/{index}");
AnnouncementSelector::from_str(&str).unwrap(); // no crash
}
#[proptest]
fn parse_height_or_digest_height(
#[strategy(arb())] block_height: u64,
#[strategy(0usize..20)] index: usize,
) {
let str = format!("height_or_digest/{block_height}/index/{index}");
AnnouncementSelector::from_str(&str).unwrap(); // no crash
}
#[proptest]
fn parse_invalid_number_of_parts(s: String) {
// Strings with fewer than 2 parts OR more than 3 parts should fail
let parts: Vec<&str> = s.split('/').collect();
if !(2..=4).contains(&parts.len()) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::NumberOfParts)
));
}
}
#[proptest]
fn parse_invalid_tip_index(#[strategy(string_regex("tip/[a-z]+").unwrap())] s: String) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::TipIndex(_))
));
}
#[proptest]
fn parse_invalid_genesis_index(#[strategy(string_regex("genesis/[a-z]+").unwrap())] s: String) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::GenesisIndex(_))
));
}
#[proptest]
fn parse_invalid_height_number(
#[strategy(string_regex("height/[a-z]+/0").unwrap())] s: String,
) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::BlockHeight(_))
));
}
#[proptest]
fn parse_invalid_height_index(
#[strategy(string_regex("height/42/[a-z]+").unwrap())] s: String,
) {
let res = AnnouncementSelector::from_str(&s);
if let Err(AnnouncementSelectorParseError::HeightIndex(_, _)) = res {
// OK
} else {
panic!("Expected HeightIndex error, got {res:?}");
}
}
#[proptest]
fn parse_invalid_digest_hex(
#[strategy(string_regex("digest/z[0-9a-f]{79}/0").unwrap())] s: String,
) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::BlockDigest(_))
));
}
#[proptest]
fn parse_invalid_digest_length_too_short(
#[strategy(string_regex("digest/[0-9a-f]{0,79}/0").unwrap())] s: String,
) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::BlockDigest(_))
));
}
#[proptest]
fn parse_invalid_digest_length_too_long(
#[strategy(string_regex("digest/[0-9a-f]{81,200}/0").unwrap())] s: String,
) {
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::BlockDigest(_))
));
}
#[proptest]
fn parse_invalid_digest_index(#[strategy(arb())] digest: Digest) {
let s = format!("digest/{digest:x}/notanumber");
let res = AnnouncementSelector::from_str(&s);
prop_assert!(matches!(
res,
Err(AnnouncementSelectorParseError::DigestIndex(_, _))
));
}
}

View File

@ -0,0 +1,29 @@
use neptune_cash::api::export::Announcement;
use neptune_cash::api::export::TransparentTransactionInfo;
use neptune_cash::prelude::triton_vm::prelude::BFieldElement;
#[derive(Debug, Clone)]
pub enum AnnouncementType {
Unknown(Vec<BFieldElement>),
TransparentTxInfo(TransparentTransactionInfo),
}
impl AnnouncementType {
pub fn parse(announcement: Announcement) -> Self {
if let Ok(transparent_transaction_info) =
TransparentTransactionInfo::try_from_announcement(&announcement)
{
Self::TransparentTxInfo(transparent_transaction_info)
} else {
Self::Unknown(announcement.message)
}
}
pub fn name(&self) -> String {
match self {
AnnouncementType::Unknown(_) => "unknown",
AnnouncementType::TransparentTxInfo(_) => "transparent transaction info",
}
.to_string()
}
}

View File

@ -9,6 +9,7 @@ use neptune_cash::prelude::twenty_first::tip5::Digest;
use neptune_cash::rpc_auth; use neptune_cash::rpc_auth;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct AppStateInner { pub struct AppStateInner {
pub network: Network, pub network: Network,
pub config: Config, pub config: Config,

View File

@ -1,3 +1,5 @@
pub mod announcement_selector;
pub mod announcement_type;
pub mod app_state; pub mod app_state;
pub mod block_selector_extended; pub mod block_selector_extended;
pub mod config; pub mod config;

View File

@ -6,12 +6,17 @@ use chrono::DateTime;
use chrono::TimeDelta; use chrono::TimeDelta;
use chrono::Utc; use chrono::Utc;
use clap::Parser; use clap::Parser;
use neptune_cash::api::export::Announcement;
use neptune_cash::config_models::data_directory::DataDirectory; use neptune_cash::config_models::data_directory::DataDirectory;
use neptune_cash::config_models::network::Network; use neptune_cash::config_models::network::Network;
use neptune_cash::models::blockchain::block::block_height::BlockHeight; use neptune_cash::models::blockchain::block::block_height::BlockHeight;
use neptune_cash::models::blockchain::block::block_info::BlockInfo;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use neptune_cash::rpc_auth; use neptune_cash::rpc_auth;
use neptune_cash::rpc_server::error::RpcError; use neptune_cash::rpc_server::error::RpcError;
use neptune_cash::rpc_server::RPCClient; use neptune_cash::rpc_server::RPCClient;
use neptune_cash::rpc_server::RpcResult;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::net::SocketAddr; use std::net::SocketAddr;
use tarpc::client; use tarpc::client;
@ -19,6 +24,10 @@ use tarpc::context;
use tarpc::tokio_serde::formats::Json as RpcJson; use tarpc::tokio_serde::formats::Json as RpcJson;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
#[cfg(feature = "mock")]
const MOCK_KEY: &str = "MOCK";
#[derive(Debug, Clone)]
pub struct AuthenticatedClient { pub struct AuthenticatedClient {
pub client: RPCClient, pub client: RPCClient,
pub token: rpc_auth::Token, pub token: rpc_auth::Token,
@ -33,6 +42,122 @@ impl std::ops::Deref for AuthenticatedClient {
} }
} }
impl AuthenticatedClient {
/// Intercept and relay call to [`RPCClient::block_info`]
pub async fn block_info(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
block_selector: BlockSelector,
) -> ::core::result::Result<RpcResult<Option<BlockInfo>>, ::tarpc::client::RpcError> {
let rpc_result = self.client.block_info(ctx, token, block_selector).await;
// if the RPC call was successful, return that
if let Ok(Ok(Some(_))) = rpc_result {
return rpc_result;
}
// if MOCK environment variable is set and feature is enabled,
// imagine some mock block info
#[cfg(feature = "mock")]
if std::env::var(MOCK_KEY).is_ok() {
use blake3::Hasher;
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
tracing::warn!("RPC query failed and MOCK flag set, so returning an imagined block");
let mut hasher = Hasher::new();
hasher.update(&block_selector.to_string().bytes().collect::<Vec<_>>());
let mut rng = StdRng::from_seed(*hasher.finalize().as_bytes());
let mut block_info: BlockInfo = rng.random();
match block_selector {
BlockSelector::Digest(digest) => {
block_info.digest = digest;
}
BlockSelector::Height(height) => {
block_info.height = height;
}
_ => {}
};
return Ok(Ok(Some(block_info)));
}
// otherwise, return the original error
rpc_result
}
/// Intercept and relay call to [`RPCClient::utxo_digest`]
pub async fn utxo_digest(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
leaf_index: u64,
) -> ::core::result::Result<RpcResult<Option<Digest>>, ::tarpc::client::RpcError> {
self.client.utxo_digest(ctx, token, leaf_index).await
}
/// Intercept and relay call to [`RPCClient::announcements_in_block`]
pub async fn announcements_in_block(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
block_selector: BlockSelector,
) -> Result<Result<Option<Vec<Announcement>>, RpcError>, ::tarpc::client::RpcError> {
let rpc_result = self
.client
.announcements_in_block(ctx, token, block_selector)
.await;
// if the RPC call was successful, return that
if let Ok(Ok(Some(_))) = rpc_result {
return rpc_result;
}
// if MOCK environment variable is set and feature is enabled,
// imagine some mock block info
#[cfg(feature = "mock")]
if std::env::var(MOCK_KEY).is_ok() {
use blake3::Hasher;
use neptune_cash::api::export::TransparentTransactionInfo;
use neptune_cash::prelude::triton_vm::prelude::BFieldElement;
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
tracing::warn!("RPC query failed and MOCK flag set, so returning an imagined block");
let mut hasher = Hasher::new();
hasher.update(&block_selector.to_string().bytes().collect::<Vec<_>>());
let mut rng = StdRng::from_seed(*hasher.finalize().as_bytes());
// make sure the number of announcements matches with the block
let block_info = self
.block_info(ctx, token, block_selector)
.await
.unwrap()
.unwrap()
.unwrap();
let num_announcements = block_info.num_announcements;
let mut announcements = vec![];
for _ in 0..num_announcements {
let announcement = if rng.random_bool(0.5_f64) {
let message = (0..rng.random_range(0..256))
.map(|_| rng.random::<BFieldElement>())
.collect::<Vec<_>>();
Announcement::new(message)
} else {
rng.random::<TransparentTransactionInfo>().to_announcement()
};
announcements.push(announcement);
}
return Ok(Ok(Some(announcements)));
}
// otherwise, return the original error
rpc_result
}
}
/// generates RPCClient, for querying neptune-core RPC server. /// generates RPCClient, for querying neptune-core RPC server.
pub async fn gen_authenticated_rpc_client() -> Result<AuthenticatedClient, anyhow::Error> { pub async fn gen_authenticated_rpc_client() -> Result<AuthenticatedClient, anyhow::Error> {
let client = gen_rpc_client().await?; let client = gen_rpc_client().await?;

View File

@ -0,0 +1,150 @@
<html>
<head>
<title>{{self.header.state.config.site_name}}: Announcement {{self.block_height}}/{{self.index}}</title>
{{html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"),
"/templates/web/html/components/head.html")))}}
</head>
<body>
{{Trusted(self.header.to_string())}}
<main class="container">
<article>
<h2>Announcement
</h2>
<h3>
Metadata
</h3>
<table class="striped">
<tr>
<td>Block Height</td>
<td><a href='/block/digest/{{self.block_hash.to_hex()}}'>{{self.block_height}}</a></td>
</tr>
<tr>
<td>Block Hash</td>
<td class="mono"><a
href='/block/digest/{{self.block_hash.to_hex()}}'>{{self.block_hash.to_hex()}}</a></td>
</tr>
<tr>
<td>Index</td>
<td>{{self.index}}/{{self.num_announcements}}</td>
</tr>
<tr>
<td>Type</td>
<td>{{self.announcement_type.name()}}</td>
</tr>
</table>
<h3>Payload</h3>
{% match &self.announcement_type { AnnouncementType::TransparentTxInfo(tx_info) => { %}
<details open>
<summary>Transparent Transaction Info</summary>
<table class="striped">
<tr>
<th colspan=2 style="font-weight: bold;">inputs</th>
</tr>
{% for input in tx_info.inputs.iter() { %}
<tr>
<td>
<details>
<summary>
<a
href='/utxo/{{input.aocl_leaf_index}}'>{{input.addition_record().canonical_commitment.to_hex()}}</a>
</summary>
<table>
<tr>
<td>UTXO digest:</td>
<td class="mono">{{Tip5::hash(&input.utxo).to_hex()}}</td>
</tr>
<tr>
<td>sender randomness:</td>
<td class="mono">{{input.sender_randomness.to_hex()}}</td>
</tr>
<tr>
<td>receiver preimage:</td>
<td class="mono">{{input.receiver_preimage.to_hex()}}</td>
</tr>
</table>
</details>
</td>
<td style="text-align: right" class="mono">
{{input.utxo.get_native_currency_amount().display_n_decimals(5)}}
NPT
</td>
</tr>
{% } %}
</table>
<table class="striped">
<tr>
<th colspan="2" style="font-weight: bold;">outputs</th>
</tr>
{% for output in tx_info.outputs.iter() { %}
<tr>
<td>
<details>
<summary>
{{output.addition_record().canonical_commitment.to_hex()}}
</summary>
<table>
<tr>
<td>UTXO digest:</td>
<td class="mono">{{Tip5::hash(&output.utxo).to_hex()}}</td>
</tr>
<tr>
<td>sender randomness:</td>
<td class="mono">{{output.sender_randomness.to_hex()}}</td>
</tr>
<tr>
<td>receiver digest:</td>
<td class="mono">{{output.receiver_digest.to_hex()}}</td>
</tr>
</table>
</details>
</td>
<td style="text-align: right" class="mono">
{{output.utxo.get_native_currency_amount().display_n_decimals(5)}}
NPT
</td>
</tr>
{% } %}
</table>
</details>
{% }, AnnouncementType::Unknown(payload) => { %}
<details>
<summary>Unknown Type</summary>
{% for chunk in payload.encode().chunks(4) { %}
<p class="mono">
{% for d in chunk { %}
{{ format!("{:016x}", d.value()) }}
{% } %}
</p>
{% } %}
</details>
{% }, } %}
</article>
<article>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a>
{% if self.index == 0 { %}
| Previous Announcement
{% } else { %}
| <a href='/announcement/digest/{{self.block_hash.to_hex()}}/{{self.index - 1}}'>Previous
Announcement</a>
{% } %}
{% if self.index+1 >= self.num_announcements { %}
| Next Announcement
{% } else { %}
| <a href='/announcement/digest/{{self.block_hash.to_hex()}}/{{self.index + 1}}'>Next Announcement</a>
{% } %}
</p>
</article>
</main>
</body>
</html>

View File

@ -3,140 +3,164 @@
<head> <head>
<title>{{self.header.state.config.site_name}}: Block Height {{self.block_info.height}}</title> <title>{{self.header.state.config.site_name}}: Block Height {{self.block_info.height}}</title>
{{html_escaper::Trusted(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/templates/web/html/components/head.html")))}} {{html_escaper::Trusted(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/templates/web/html/components/head.html")))}}
</head> </head>
<body> <body>
{{Trusted(self.header.to_string())}} {{Trusted(self.header.to_string())}}
<main class="container"> <main class="container">
<article> <article>
<h2>Block height: {{self.block_info.height}}</h2> <h2>Block height: {{self.block_info.height}}</h2>
<!-- special_block_notice --> <!-- special_block_notice -->
%% if self.block_info.is_genesis { %% if self.block_info.is_genesis {
<p>This is the genesis block</p> <p>This is the genesis block</p>
%% } %% }
%% if self.block_info.is_tip { %% if self.block_info.is_tip {
<p>This is the latest block (tip)</p> <p>This is the latest block (tip)</p>
%% } %% }
<table class="striped"> <table class="striped">
<tr> <tr>
<td>Digest</td> <td>Digest</td>
<td class="mono">{{self.block_info.digest.to_hex()}}</td> <td class="mono">{{self.block_info.digest.to_hex()}}</td>
</tr> </tr>
<tr> <tr>
<td>Created</td> <td>Created</td>
<td>{{self.block_info.timestamp.standard_format()}}</td> <td>{{self.block_info.timestamp.standard_format()}}</td>
</tr> </tr>
<tr> <tr>
<td>Size <td>Size
<span class="tooltip"> <span class="tooltip">
<span class="tooltiptext">Unit: number of BFieldElements. One BFieldElement consists of 8 bytes.</span> <span class="tooltiptext">Unit: number of BFieldElements. One BFieldElement consists of 8
</span> bytes.</span>
</td> </span>
<td>{{self.block_info.size}}</td> </td>
</tr> <td>{{self.block_info.size}}</td>
<tr> </tr>
<td>Inputs</td> <tr>
<td>{{self.block_info.num_inputs}}<br /></td> <td>Inputs</td>
</tr> <td>{{self.block_info.num_inputs}}<br /></td>
<tr> </tr>
<td>Outputs</td> <tr>
<td>{{self.block_info.num_outputs}}</td> <td>Outputs</td>
</tr> <td>{{self.block_info.num_outputs}}</td>
<tr> </tr>
<td>Difficulty</td> <tr>
<td>{{self.block_info.difficulty}}</td> <td>Announcements
</tr> <span class="tooltip">
<tr> <span class="tooltiptext">
<td>Cumulative Proof-Of-Work Data broadcast as part of the transaction.
<span class="tooltip"> </span>
<span class="tooltiptext">estimated total # of hashes performed by miners from genesis block to this block.</span> </span>
</span> </td>
</td> <td>
<td>{{self.block_info.cumulative_proof_of_work}}</td> {% if self.block_info.num_announcements > 0 { %}
</tr> <a
<tr> href='/announcement/digest/{{self.block_info.digest.to_hex()}}/0'>{{self.block_info.num_announcements}}</a>
<td>Coinbase {% } else { %}
<span class="tooltip"> 0
<span class="tooltiptext">Total block reward amount paid to the miner(s) that found this block.</span> {% } %}
</span> </td>
</td> </tr>
<td>{{self.block_info.coinbase_amount}}</td> <tr>
</tr> <td>Difficulty</td>
%% if self.block_info.coinbase_amount != self.block_info.expected_coinbase_amount() { <td>{{self.block_info.difficulty}}</td>
<tr> </tr>
<td>Expected Coinbase <tr>
<span class="tooltip"> <td>Cumulative Proof-Of-Work
<span class="tooltiptext">Expected (maximum) block reward amount paid to the miner(s) that find a block at this block-height.</span> <span class="tooltip">
</span> <span class="tooltiptext">estimated total # of hashes performed by miners from genesis block
</td> to this block.</span>
<td>{{self.block_info.expected_coinbase_amount()}}</td> </span>
</tr> </td>
%% } <td>{{self.block_info.cumulative_proof_of_work}}</td>
<tr> </tr>
<td>Fee</td> <tr>
<td>{{self.block_info.fee}}</td> <td>Coinbase
</tr> <span class="tooltip">
<tr> <span class="tooltiptext">Total block reward amount paid to the miner(s) that found this
<td>Canonical block.</span>
<span class="tooltip"> </span>
<span class="tooltiptext"> </td>
The canonical blockchain is the chain with the most accumulated proof-of-work and is considered the <td>{{self.block_info.coinbase_amount}}</td>
official record of transaction history. </tr>
</span> %% if self.block_info.coinbase_amount != self.block_info.expected_coinbase_amount() {
</span> <tr>
</td> <td>Expected Coinbase
<td> <span class="tooltip">
%% if self.block_info.is_canonical { <span class="tooltiptext">Expected (maximum) block reward amount paid to the miner(s) that
Yes. This block is in the canonical blockchain. find a block at this block-height.</span>
%% } else { </span>
No. This block is not in the canonical blockchain. </td>
<td>{{self.block_info.expected_coinbase_amount()}}</td>
</tr>
%% } %% }
</td> <tr>
</tr> <td>Fee</td>
<tr> <td>{{self.block_info.fee}}</td>
<td>Sibling Blocks </tr>
<span class="tooltip"> <tr>
<span class="tooltiptext"> <td>Canonical
Blocks that exist at the same height as this block. Only one sibling can be in the canonical blockchain. <span class="tooltip">
</span> <span class="tooltiptext">
</span> The canonical blockchain is the chain with the most accumulated proof-of-work and is
</td> considered the
<td class="mono"> official record of transaction history.
%% for sibling_digest in self.block_info.sibling_blocks.iter().map(|d| d.to_hex()) { </span>
<a href='/block/digest/{{sibling_digest}}'>{{sibling_digest}}</a><br/> </span>
%% } </td>
</td> <td>
</tr> %% if self.block_info.is_canonical {
Yes. This block is in the canonical blockchain.
%% } else {
No. This block is not in the canonical blockchain.
%% }
</td>
</tr>
<tr>
<td>Sibling Blocks
<span class="tooltip">
<span class="tooltiptext">
Blocks that exist at the same height as this block. Only one sibling can be in the
canonical blockchain.
</span>
</span>
</td>
<td class="mono">
%% for sibling_digest in self.block_info.sibling_blocks.iter().map(|d| d.to_hex()) {
<a href='/block/digest/{{sibling_digest}}'>{{sibling_digest}}</a><br />
%% }
</td>
</tr>
</table> </table>
</article> </article>
<article> <article>
<p> <p>
<a href="/">Home</a> <a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a> | <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a> | <a href='/block/tip'>Tip</a>
%% if self.block_info.is_genesis { %% if self.block_info.is_genesis {
| Previous Block | Previous Block
%% } else { %% } else {
| <a href='/block/height/{{self.block_info.height.previous().unwrap()}}'>Previous Block</a> | <a href='/block/height/{{self.block_info.height.previous().unwrap()}}'>Previous Block</a>
%% } %% }
%% if self.block_info.is_tip { %% if self.block_info.is_tip {
| Next Block | Next Block
%% } else { %% } else {
| <a href='/block/height/{{self.block_info.height.next()}}'>Next Block</a> | <a href='/block/height/{{self.block_info.height.next()}}'>Next Block</a>
%% } %% }
</p> </p>
</article> </article>
</main> </main>
</body> </body>
</html> </html>

View File

@ -1,113 +1,152 @@
<html> <html>
<head> <head>
<title>{{self.state.config.site_name}}: (network: {{self.state.network}})</title> <title>{{self.state.config.site_name}}: (network: {{self.state.network}})</title>
{{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html"))) }} {{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"),
"/templates/web/html/components/head.html"))) }}
</head> </head>
<body> <body>
<header class="container"> <header class="container">
<h1> <h1>
<img src="/image/neptune-logo-circle-small.png" align="right"/> <img src="/image/neptune-logo-circle-small.png" align="right" />
{{self.state.config.site_name}} (network: {{self.state.network}}) {{self.state.config.site_name}} (network: {{self.state.network}})
</h1> </h1>
The blockchain tip is at height: {{self.tip_height}} The blockchain tip is at height: {{self.tip_height}}
</header> </header>
<main class="container"> <main class="container">
<article> <article>
<details open> <details open>
<summary> <summary>
Block Lookup Block Lookup
</summary> </summary>
<form action="/rqs" method="get"> <form action="/rqs" method="get">
<input type="hidden" name="block" value="" /> <input type="hidden" name="block" value="" />
<input type="hidden" name="_ig" value="l"/> <input type="hidden" name="_ig" value="l" />
<span class="tooltip"> <span class="tooltip">
<span class="tooltiptext"> <span class="tooltiptext">
Provide a numeric block height or hexadecimal digest identifier to lookup any block in the Neptune blockchain. Provide a numeric block height or hexadecimal digest identifier to lookup any block in the
</span> Neptune blockchain.
</span> </span>
</span>
Block height or digest: Block height or digest:
<input type="text" size="80" name="height_or_digest" class="mono"/> <input type="text" size="80" name="height_or_digest" class="mono" />
<input type="submit" name="l" value="Lookup Block"/> <input type="submit" name="l" value="Lookup Block" />
</form> </form>
Quick Lookup: Quick Lookup:
<a href="/block/genesis">Genesis Block</a> | <a href="/block/genesis">Genesis Block</a> |
<a href="/block/tip">Tip</a><br/> <a href="/block/tip">Tip</a><br />
</details> </details>
</article> </article>
<article> <article>
<details open> <details open>
<summary>UTXO Lookup</summary> <summary>UTXO Lookup</summary>
<form action="/rqs" method="get"> <form action="/rqs" method="get">
<input type="hidden" name="_ig" value="l" /> <input type="hidden" name="_ig" value="l" />
<span class="tooltip"> <span class="tooltip">
<span class="tooltiptext"> <span class="tooltiptext">
An Unspent Transaction Output (UTXO) index can be found in the output of <i>neptune-cli wallet-status</i>. Look for the field: <b>aocl_leaf_index</b> An Unspent Transaction Output (UTXO) index can be found in the output of <i>neptune-cli
</span> wallet-status</i>. Look for the field: <b>aocl_leaf_index</b>
</span> </span>
UTXO index: </span>
<input type="text" size="10" name="utxo" /> UTXO index:
<input type="submit" name="l" value="Lookup Utxo" /> <input type="text" size="10" name="utxo" />
</form> <input type="submit" name="l" value="Lookup Utxo" />
</details> </form>
</article> </details>
</article>
<article> <article>
<details> <details open>
<summary>REST RPCs</summary> <summary>Announcement Lookup</summary>
<section> <form action="/rqs" method="get">
RPC endpoints are available for automating block explorer queries: <input type="hidden" name="announcement" value="" />
</section> <input type="hidden" name="_ig" value="l" />
<span class="tooltip">
<span class="tooltiptext">
A numeric block height or hexadecimal digest to identify the block in which the announcement
lives.
</span>
</span>
Block height or digest:
<input type="text" size="80" name="height_or_digest" class="mono" />
<details> <span class="tooltip">
<summary>/block_info</summary> <span class="tooltiptext">
<div class="indent"> The index of the announcement within the block (as a block can have many announcements).
<h4>Examples</h4> </span>
</span>
Announcement index:
<input type="text" size="10" name="index" />
<input type="submit" name="l" value="Lookup Announcement" />
</form>
</details>
</article>
<ul> <article>
<li><a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a></li> <details>
<li><a href="/rpc/block_info/tip">/rpc/block_info/tip</a></li> <summary>REST RPCs</summary>
<li><a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a></li> <section>
<li><a href="/rpc/block_info/digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.state.genesis_digest.to_hex()}}</a></li> RPC endpoints are available for automating block explorer queries:
<li><a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a></li> </section>
</ul>
</div>
</details>
<details> <details>
<summary>/block_digest</summary> <summary>/block_info</summary>
<div class="indent"> <div class="indent">
<h4>Examples</h4> <h4>Examples</h4>
<ul> <ul>
<li><a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a></li> <li><a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a></li>
<li><a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a></li> <li><a href="/rpc/block_info/tip">/rpc/block_info/tip</a></li>
<li><a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a></li> <li><a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a></li>
<li><a href="/rpc/block_digest/digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.state.genesis_digest.to_hex()}}</a></li> <li><a
<li><a href="/rpc/block_digest/height_or_digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_digest/height_or_digest/{{self.state.genesis_digest.to_hex()}}</a></li> href="/rpc/block_info/digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.state.genesis_digest.to_hex()}}</a>
</ul> </li>
</div> <li><a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a></li>
</details> </ul>
</div>
</details>
<details> <details>
<summary>/utxo_digest</summary> <summary>/block_digest</summary>
<div class="indent"> <div class="indent">
<h4>Examples</h4> <h4>Examples</h4>
<ul> <ul>
<li><a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br/></li> <li><a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a></li>
</ul> <li><a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a></li>
</div> <li><a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a></li>
</details> <li><a
href="/rpc/block_digest/digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.state.genesis_digest.to_hex()}}</a>
</li>
<li><a
href="/rpc/block_digest/height_or_digest/{{self.state.genesis_digest.to_hex()}}">/rpc/block_digest/height_or_digest/{{self.state.genesis_digest.to_hex()}}</a>
</li>
</ul>
</div>
</details>
</details> <details>
</article> <summary>/utxo_digest</summary>
<div class="indent">
<h4>Examples</h4>
</main> <ul>
<li><a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br /></li>
</ul>
</div>
</details>
</details>
</article>
</main>
</body> </body>
</html> </html>