feat(testnet): L2 sequencer and archiver example (#2001)

Co-authored-by: Antonio Antonino <antonio@status.im>
Co-authored-by: Petar Radovic <petar.radovic@gmail.com>
This commit is contained in:
gusto 2026-01-12 11:15:22 -05:00 committed by GitHub
parent 74a6e0ce9e
commit 3c249f67d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 6010 additions and 100 deletions

4
.gitignore vendored
View File

@ -28,3 +28,7 @@ zk/**/bin/*
# C headers
nomos-c/libnomos.h
# Demo sequencer related ignores
node_modules
*.database

321
Cargo.lock generated
View File

@ -1147,7 +1147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
"axum-core 0.4.5",
"bytes",
"futures-util",
"http 1.4.0",
@ -1156,7 +1156,7 @@ dependencies = [
"hyper 1.8.1",
"hyper-util",
"itoa",
"matchit",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
@ -1174,6 +1174,36 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core 0.5.6",
"bytes",
"futures-util",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
"hyper-util",
"itoa",
"matchit 0.8.4",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.5"
@ -1195,6 +1225,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "az"
version = "1.2.1"
@ -1263,9 +1311,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.2"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bcder"
@ -1639,7 +1687,7 @@ checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799"
dependencies = [
"clap",
"heck 0.5.0",
"indexmap 2.12.1",
"indexmap 2.13.0",
"log",
"proc-macro2",
"quote",
@ -1647,14 +1695,14 @@ dependencies = [
"serde_json",
"syn 2.0.114",
"tempfile",
"toml 0.9.10+spec-1.1.0",
"toml 0.9.11+spec-1.1.0",
]
[[package]]
name = "cc"
version = "1.2.51"
version = "1.2.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
dependencies = [
"find-msvc-tools",
"jobserver",
@ -1724,7 +1772,7 @@ dependencies = [
name = "cfgsync"
version = "0.1.0"
dependencies = [
"axum",
"axum 0.7.9",
"clap",
"nomos-core 0.1.0",
"nomos-da-network-core 0.1.0",
@ -1748,10 +1796,10 @@ dependencies = [
[[package]]
name = "cfgsync"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"anyhow",
"axum",
"axum 0.7.9",
"clap",
"groth16 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"hex",
@ -2182,6 +2230,7 @@ name = "common-http-client"
version = "0.1.0"
dependencies = [
"broadcast-service 0.1.0",
"chain-service 0.1.0",
"futures",
"nomos-core 0.1.0",
"nomos-da-messages 0.1.0",
@ -2677,6 +2726,7 @@ dependencies = [
"humantime",
"inventory",
"itertools 0.14.0",
"junit-report",
"linked-hash-map",
"pin-project",
"ref-cast",
@ -2718,7 +2768,7 @@ dependencies = [
[[package]]
name = "cucumber_ext"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"cucumber",
"testing-framework-core",
@ -2842,15 +2892,15 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.9.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "data-encoding-macro"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb"
dependencies = [
"data-encoding",
"data-encoding-macro-internal",
@ -2858,9 +2908,9 @@ dependencies = [
[[package]]
name = "data-encoding-macro-internal"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
dependencies = [
"data-encoding",
"syn 2.0.114",
@ -2901,6 +2951,29 @@ dependencies = [
"windows 0.32.0",
]
[[package]]
name = "demo-sequencer"
version = "0.1.0"
dependencies = [
"axum 0.7.9",
"common-http-client 0.1.0",
"hex",
"key-management-system-service 0.1.0",
"nomos-core 0.1.0",
"owo-colors",
"rand 0.8.5",
"redb 2.6.3",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tower-http 0.6.8",
"tracing",
"tracing-subscriber 0.3.22",
]
[[package]]
name = "der"
version = "0.7.10"
@ -2947,6 +3020,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive-getters"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@ -3675,9 +3759,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]]
name = "findshlibs"
@ -3931,9 +4015,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
@ -4142,7 +4226,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.12.1",
"indexmap 2.13.0",
"slab",
"tokio",
"tokio-util",
@ -4161,7 +4245,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.4.0",
"indexmap 2.12.1",
"indexmap 2.13.0",
"slab",
"tokio",
"tokio-util",
@ -4878,9 +4962,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.1"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
@ -4904,7 +4988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88"
dependencies = [
"ahash",
"indexmap 2.12.1",
"indexmap 2.13.0",
"is-terminal",
"itoa",
"log",
@ -5148,6 +5232,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "junit-report"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06c3a3342e6720a82d7d179f380e9841b73a1dd49344e33959fdfe571ce56b55"
dependencies = [
"derive-getters",
"quick-xml 0.31.0",
"strip-ansi-escapes",
"time",
]
[[package]]
name = "jzon"
version = "0.12.5"
@ -5397,9 +5493,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.179"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libfuzzer-sys"
@ -5437,7 +5533,7 @@ dependencies = [
"either",
"futures",
"futures-timer",
"getrandom 0.2.16",
"getrandom 0.2.17",
"libp2p-allow-block-list",
"libp2p-autonat",
"libp2p-connection-limits",
@ -5564,7 +5660,7 @@ dependencies = [
"fnv",
"futures",
"futures-timer",
"getrandom 0.2.16",
"getrandom 0.2.17",
"hashlink",
"hex_fmt",
"libp2p-core",
@ -5973,6 +6069,32 @@ dependencies = [
"value-bag",
]
[[package]]
name = "logos-blockchain-archiver"
version = "0.1.0"
dependencies = [
"async-stream",
"axum 0.8.8",
"broadcast-service 0.1.0",
"clap",
"common-http-client 0.1.0",
"demo-sequencer",
"futures",
"hex",
"nomos-core 0.1.0",
"owo-colors",
"redb 3.1.0",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
"tower-http 0.6.8",
"url",
]
[[package]]
name = "loki-api"
version = "0.1.3"
@ -6050,6 +6172,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matrixmultiply"
version = "0.3.10"
@ -6550,7 +6678,7 @@ name = "nomos-api"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"axum 0.7.9",
"broadcast-service 0.1.0",
"bytes",
"chain-service 0.1.0",
@ -7092,7 +7220,7 @@ dependencies = [
"cached",
"fixed",
"futures",
"indexmap 2.12.1",
"indexmap 2.13.0",
"key-management-system-keys 0.1.0",
"kzgrs 0.1.0",
"kzgrs-backend 0.1.0",
@ -7123,7 +7251,7 @@ dependencies = [
"cached",
"fixed",
"futures",
"indexmap 2.12.1",
"indexmap 2.13.0",
"kzgrs-backend 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"libp2p",
"libp2p-stream",
@ -7330,7 +7458,7 @@ name = "nomos-executor"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"axum 0.7.9",
"broadcast-service 0.1.0",
"clap",
"color-eyre",
@ -7369,7 +7497,7 @@ version = "0.1.0"
source = "git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917#1fce2dc3f482c16361316eb2a1b6ccd1206aa917"
dependencies = [
"async-trait",
"axum",
"axum 0.7.9",
"broadcast-service 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"clap",
"color-eyre",
@ -7406,7 +7534,7 @@ dependencies = [
name = "nomos-http-api-common"
version = "0.1.0"
dependencies = [
"axum",
"axum 0.7.9",
"governor",
"key-management-system-keys 0.1.0",
"nomos-core 0.1.0",
@ -7424,7 +7552,7 @@ name = "nomos-http-api-common"
version = "0.1.0"
source = "git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917#1fce2dc3f482c16361316eb2a1b6ccd1206aa917"
dependencies = [
"axum",
"axum 0.7.9",
"governor",
"key-management-system-keys 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"nomos-core 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
@ -7586,7 +7714,7 @@ name = "nomos-node"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"axum 0.7.9",
"broadcast-service 0.1.0",
"chain-leader 0.1.0",
"chain-network 0.1.0",
@ -7652,7 +7780,7 @@ version = "0.1.0"
source = "git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917#1fce2dc3f482c16361316eb2a1b6ccd1206aa917"
dependencies = [
"async-trait",
"axum",
"axum 0.7.9",
"broadcast-service 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"chain-leader 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
"chain-network 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
@ -8222,7 +8350,7 @@ dependencies = [
"crc32fast",
"flate2",
"hashbrown 0.14.5",
"indexmap 2.12.1",
"indexmap 2.13.0",
"memchr",
"ruzstd",
]
@ -8492,7 +8620,7 @@ dependencies = [
"cbc",
"cipher",
"des",
"getrandom 0.2.16",
"getrandom 0.2.17",
"hmac",
"lazy_static",
"rc2",
@ -8919,7 +9047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64 0.22.1",
"indexmap 2.12.1",
"indexmap 2.13.0",
"quick-xml 0.38.4",
"serde",
"time",
@ -9400,7 +9528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973"
dependencies = [
"anyhow",
"indexmap 2.12.1",
"indexmap 2.13.0",
"log",
"protobuf",
"protobuf-support",
@ -9488,6 +9616,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
@ -9647,7 +9784,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
"getrandom 0.2.17",
]
[[package]]
@ -9823,6 +9960,24 @@ dependencies = [
"yasna",
]
[[package]]
name = "redb"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01"
dependencies = [
"libc",
]
[[package]]
name = "redb"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06"
dependencies = [
"libc",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@ -9847,7 +10002,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.16",
"getrandom 0.2.17",
"libredox",
"thiserror 1.0.69",
]
@ -9858,7 +10013,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"getrandom 0.2.17",
"libredox",
"thiserror 2.0.17",
]
@ -10044,7 +10199,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
@ -10794,7 +10949,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.12.1",
"indexmap 2.13.0",
"schemars 0.9.0",
"schemars 1.2.0",
"serde_core",
@ -10821,7 +10976,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.12.1",
"indexmap 2.13.0",
"itoa",
"ryu",
"serde",
@ -11223,6 +11378,15 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "strip-ansi-escapes"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
dependencies = [
"vte",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -11577,7 +11741,7 @@ dependencies = [
"serde_json",
"serde_with",
"thiserror 2.0.17",
"toml 0.9.10+spec-1.1.0",
"toml 0.9.11+spec-1.1.0",
"url",
"urlpattern",
"uuid",
@ -11662,7 +11826,7 @@ dependencies = [
[[package]]
name = "testing-framework-config"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"blst",
"chain-leader 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
@ -11704,7 +11868,7 @@ dependencies = [
[[package]]
name = "testing-framework-core"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"anyhow",
"async-trait",
@ -11743,12 +11907,12 @@ dependencies = [
[[package]]
name = "testing-framework-env"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
[[package]]
name = "testing-framework-runner-compose"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"anyhow",
"async-trait",
@ -11771,7 +11935,7 @@ dependencies = [
[[package]]
name = "testing-framework-runner-local"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"async-trait",
"testing-framework-core",
@ -11782,7 +11946,7 @@ dependencies = [
[[package]]
name = "testing-framework-workflows"
version = "0.1.0"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#0576f58a19c0629d49124d91d0589bf69ba24910"
source = "git+https://github.com/logos-blockchain/logos-blockchain-testing.git?branch=master#1b336c2c080715b0873bdb5b36bd526dd0e74baf"
dependencies = [
"async-trait",
"chain-service 0.1.0 (git+https://github.com/logos-co/nomos-node.git?rev=1fce2dc3f482c16361316eb2a1b6ccd1206aa917)",
@ -12114,11 +12278,11 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.10+spec-1.1.0"
version = "0.9.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
dependencies = [
"indexmap 2.12.1",
"indexmap 2.13.0",
"serde_core",
"serde_spanned 1.0.4",
"toml_datetime 0.7.5+spec-1.1.0",
@ -12151,7 +12315,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.12.1",
"indexmap 2.13.0",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
@ -12165,7 +12329,7 @@ version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap 2.12.1",
"indexmap 2.13.0",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
@ -12200,7 +12364,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum",
"axum 0.7.9",
"base64 0.22.1",
"bytes",
"h2 0.4.13",
@ -12312,7 +12476,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3790eac6ad3fb8d9d96c2b040ae06e2517aa24b067545d1078b96ae72f7bb9a7"
dependencies = [
"axum",
"axum 0.7.9",
"forwarded-header-value",
"governor",
"http 1.4.0",
@ -12872,7 +13036,7 @@ version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23"
dependencies = [
"indexmap 2.12.1",
"indexmap 2.13.0",
"serde",
"serde_json",
"utoipa-gen",
@ -12896,7 +13060,7 @@ version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e"
dependencies = [
"axum",
"axum 0.7.9",
"mime_guess",
"regex",
"reqwest 0.12.28",
@ -12993,6 +13157,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vte"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
dependencies = [
"memchr",
]
[[package]]
name = "walkdir"
version = "2.5.0"
@ -13955,18 +14128,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.32"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.32"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [
"proc-macro2",
"quote",
@ -14070,7 +14243,7 @@ dependencies = [
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.12.1",
"indexmap 2.13.0",
"num_enum",
"thiserror 1.0.69",
]
@ -14084,7 +14257,7 @@ dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap 2.12.1",
"indexmap 2.13.0",
"memchr",
"zopfli",
]
@ -14140,9 +14313,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
[[package]]
name = "zmij"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec"
[[package]]
name = "zopfli"

View File

@ -62,6 +62,8 @@ members = [
"nomos-tracing",
"nomos-utils",
"testnet/cfgsync",
"testnet/l2-sequencer-archival-demo/archiver",
"testnet/l2-sequencer-archival-demo/sequencer",
"tests",
"utxotree",
"wallet",
@ -81,6 +83,7 @@ resolver = "2"
[workspace.dependencies]
# Internal
archiver = { default-features = false, path = "./testnet/l2-sequencer-archival-demo/archiver" }
broadcast-service = { default-features = false, path = "./nomos-services/chain/broadcast-service" }
bundler = { default-features = false, path = "./nomos-bundler" }
cfgsync = { default-features = false, path = "./testnet/cfgsync" }
@ -94,6 +97,7 @@ circuits-verifier = { default-features = false, path = "./zk/circuit
common-http-client = { default-features = false, path = "./nodes/nomos-node/http-client" }
cryptarchia-engine = { default-features = false, path = "./consensus/cryptarchia-engine" }
cryptarchia-sync = { default-features = false, path = "./consensus/cryptarchia-sync" }
demo-sequencer = { default-features = false, path = "./testnet/l2-sequencer-archival-demo/sequencer" }
executor-http-client = { default-features = false, path = "./nodes/nomos-executor/http-client" }
groth16 = { default-features = false, path = "./zk/groth16" }
key-management-system-keys = { default-features = false, path = "./nomos-kms/keys" }

View File

@ -125,3 +125,40 @@ services:
- "4317:4317" # otlp grpc
depends_on:
- tempo-init
# L2 Sequencer Demo Services
l2-nginx:
container_name: l2_nginx
image: ghcr.io/logos-blockchain/logos-blockchain:testnet
ports:
- "8200:80"
depends_on:
- l2-sequencer
- l2-archiver
entrypoint: ["nginx", "-g", "daemon off;"]
l2-sequencer:
container_name: l2_sequencer
image: ghcr.io/logos-blockchain/logos-blockchain:testnet
environment:
- SEQUENCER_LISTEN_ADDR=0.0.0.0:8080
- SEQUENCER_NODE_ENDPOINT=http://nomos-node-0:18080
- SEQUENCER_DB_PATH=/data/sequencer.db
- SEQUENCER_SIGNING_KEY_PATH=/data/sequencer.key
- SEQUENCER_CHANNEL_ID=6d656d636f696e00000000000000000000000000000000000000000000000001
- SEQUENCER_INITIAL_BALANCE=1000
volumes:
- l2-sequencer-data:/data
entrypoint: ["/usr/bin/demo-sequencer"]
l2-archiver:
container_name: l2_archiver
image: ghcr.io/logos-blockchain/logos-blockchain:testnet
environment:
- TESTNET_ENDPOINT=http://nomos-node-1:18080
- CHANNEL_ID=6d656d636f696e00000000000000000000000000000000000000000000000001
- TOKEN_NAME=MEM
entrypoint: ["/usr/bin/logos-blockchain-archiver"]
volumes:
l2-sequencer-data:

View File

@ -14,11 +14,12 @@ workspace = true
[dependencies]
broadcast-service = { workspace = true }
chain-service = { workspace = true }
futures = { default-features = false, version = "0.3.31" }
nomos-core = { workspace = true }
nomos-da-messages = { workspace = true }
nomos-http-api-common = { workspace = true }
reqwest = { features = ["json", "stream"], workspace = true }
reqwest = { features = ["json", "rustls-tls", "stream"], workspace = true }
serde = { workspace = true }
serde_json = { default-features = false, version = "1.0.140" }
thiserror = "1.0"

View File

@ -1,14 +1,15 @@
use std::{collections::HashSet, fmt::Debug, hash::Hash, sync::Arc};
use broadcast_service::BlockInfo;
use chain_service::CryptarchiaInfo;
use futures::{Stream, StreamExt as _};
use nomos_core::da::blob::Share;
use nomos_core::{block::Block, da::blob::Share, header::HeaderId, mantle::SignedMantleTx};
use nomos_da_messages::http::da::{
DASharesCommitmentsRequest, DaSamplingRequest, GetSharesRequest,
};
use nomos_http_api_common::paths::{
CRYPTARCHIA_LIB_STREAM, DA_GET_LIGHT_SHARE, DA_GET_SHARES, DA_GET_STORAGE_SHARES_COMMITMENTS,
MEMPOOL_ADD_TX,
CRYPTARCHIA_INFO, CRYPTARCHIA_LIB_STREAM, DA_GET_LIGHT_SHARE, DA_GET_SHARES,
DA_GET_STORAGE_SHARES_COMMITMENTS, MEMPOOL_ADD_TX, STORAGE_BLOCK,
};
use reqwest::{Client, ClientBuilder, RequestBuilder, StatusCode, Url};
use serde::{Serialize, de::DeserializeOwned};
@ -138,6 +139,20 @@ impl CommonHttpClient {
}
}
pub async fn get_block_by_id<HeaderId>(
&self,
base_url: Url,
header_id: HeaderId,
) -> Result<Option<Block<SignedMantleTx>>, Error>
where
HeaderId: Serialize + Send + Sync,
{
let request_url = base_url
.join(STORAGE_BLOCK.trim_start_matches('/'))
.map_err(Error::Url)?;
self.post(request_url, &header_id).await
}
/// Get the commitments for a Blob
pub async fn get_storage_commitments<S>(
&self,
@ -226,4 +241,24 @@ impl CommonHttpClient {
.map_err(Error::Url)?;
self.post(request_url, &transaction).await
}
/// Get consensus info (tip, height, etc.)
pub async fn consensus_info(&self, base_url: Url) -> Result<CryptarchiaInfo, Error> {
let request_url = base_url
.join(CRYPTARCHIA_INFO.trim_start_matches('/'))
.map_err(Error::Url)?;
self.get::<(), CryptarchiaInfo>(request_url, None).await
}
/// Get a block by its header ID
pub async fn get_block(
&self,
base_url: Url,
header_id: HeaderId,
) -> Result<Option<Block<SignedMantleTx>>, Error> {
let request_url = base_url
.join(STORAGE_BLOCK.trim_start_matches('/'))
.map_err(Error::Url)?;
self.post(request_url, &header_id).await
}
}

View File

@ -153,6 +153,12 @@ impl TryFrom<&[u8]> for HeaderId {
}
}
impl AsRef<[u8]> for HeaderId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<[u8; 32]> for ContentId {
fn from(id: [u8; 32]) -> Self {
Self(id)

BIN
restore.dat Normal file

Binary file not shown.

View File

@ -7,57 +7,63 @@ ARG VERSION=v0.3.1
# ===========================
# BUILD IMAGE
# ===========================
FROM rust:1.92.0-slim-bookworm AS builder
ARG VERSION
LABEL maintainer="augustinas@status.im" \
source="https://github.com/logos-co/nomos-node" \
description="Nomos testnet build image"
WORKDIR /nomos
COPY . .
# Install dependencies needed for building RocksDB.
RUN apt-get update && apt-get install -yq \
git gcc g++ clang libssl-dev pkg-config ca-certificates curl
git gcc g++ clang libssl-dev pkg-config ca-certificates curl unzip
RUN chmod +x scripts/setup-nomos-circuits.sh && \
scripts/setup-nomos-circuits.sh "$VERSION" "/opt/circuits"
# Install Bun for L2 demo frontend.
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:$PATH"
# Build Frontend Webapp
WORKDIR /nomos/testnet/l2-sequencer-archival-demo/webapp
RUN bun install --frozen-lockfile
RUN VITE_SEQUENCER_URL=/api/sequencer VITE_ARCHIVER_URL=/api/archiver bun run build
# Build Rust Binaries & Circuits
WORKDIR /nomos
ENV NOMOS_CIRCUITS=/opt/circuits
RUN scripts/setup-nomos-circuits.sh "$VERSION" "$NOMOS_CIRCUITS"
RUN cargo build --locked --release --all-features
# ===========================
# NODE IMAGE
# ===========================
FROM debian:bookworm-slim
ARG VERSION
LABEL maintainer="augustinas@status.im" \
source="https://github.com/logos-co/nomos-node" \
description="Nomos node image"
description="Logos Blockchain node and other binaries image"
RUN apt-get update && apt-get install -yq \
libstdc++6 \
libssl3 \
ca-certificates \
libstdc++6 libssl3 ca-certificates nginx \
&& rm -rf /var/lib/apt/lists/*
# Copy Circuits
COPY --from=builder /opt/circuits /opt/circuits
COPY --from=builder /nomos/target/release/nomos-node /usr/bin/nomos-node
COPY --from=builder /nomos/target/release/nomos-executor /usr/bin/nomos-executor
COPY --from=builder /nomos/target/release/nomos-cli /usr/bin/nomos-cli
COPY --from=builder /nomos/target/release/cfgsync-server /usr/bin/cfgsync-server
COPY --from=builder /nomos/target/release/cfgsync-client /usr/bin/cfgsync-client
ENV NOMOS_CIRCUITS=/opt/circuits
EXPOSE 3000 8080 9000 60000
# Copy Binaries
COPY --from=builder /nomos/target/release/nomos-node /usr/bin/
COPY --from=builder /nomos/target/release/nomos-executor /usr/bin/
COPY --from=builder /nomos/target/release/nomos-cli /usr/bin/
COPY --from=builder /nomos/target/release/cfgsync-server /usr/bin/
COPY --from=builder /nomos/target/release/cfgsync-client /usr/bin/
COPY --from=builder /nomos/target/release/demo-sequencer /usr/bin/
COPY --from=builder /nomos/target/release/logos-blockchain-archiver /usr/bin/
# Copy Frontend & Nginx config
COPY --from=builder /nomos/testnet/l2-sequencer-archival-demo/webapp/dist /var/www/l2-demo
COPY --from=builder /nomos/testnet/l2-sequencer-archival-demo/nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80 3000 8080 8090 9000 18080 60000
ENTRYPOINT ["/usr/bin/nomos-node"]

View File

@ -0,0 +1,15 @@
# Sequencer node endpoint (for submitting transactions)
SEQUENCER_NODE_ENDPOINT=https://testnet.nomos.tech/node/1/
SEQUENCER_NODE_USERNAME=
SEQUENCER_NODE_PASSWORD=
# Archiver node endpoint (for reading blocks - can be different node)
ARCHIVER_NODE_ENDPOINT=https://testnet.nomos.tech/node/2/
ARCHIVER_NODE_USERNAME=
ARCHIVER_NODE_PASSWORD=
# Channel ID (64 hex chars) - must be same for both
CHANNEL_ID=6d656d636f696e00000000000000000000000000000000000000000000000011
# Token name for display
TOKEN_NAME=MEM

View File

@ -0,0 +1,9 @@
# L2 Demo - Local Development Environment
# Usage: ./run-local.sh --env-file ~/Eng/offsite-sequencer-env/.env-local
SEQUENCER_NODE_ENDPOINT=https://testnet.nomos.tech/node/2/
ARCHIVER_NODE_ENDPOINT=https://testnet.nomos.tech/node/2/
TOKEN_NAME=MEM
TESTNET_USERNAME=
TESTNET_PASSWORD=

View File

@ -0,0 +1,102 @@
# Logos L2 Sequencer & Archival Demo
This directory contains a reference implementation of a l2 solution using the Logos Blockchain as a Settlement layer. It consists of three primary components working in tandem to provide fast L2 transactions with L1 security.
## System Architecture
The demo follows a classic rollup-style architecture where the Sequencer handles execution, and the Archiver handles state derivation from L1 data.
1. **L2 Sequencer**: The entry point for users. It accepts transactions, maintains a local mempool, batches them into L2 blocks, and "inscribes" them to a specific **Channel ID** on the Logos L1.
2. **Logos L1**: Acts as the immutable ledger. It doesn't know the "rules" of the L2; it simply stores the L2 data in a verifiable sequence.
3. **Archiver**: Watches the Logos L1 stream. It pulls L2 data from the designated channel, validates the transactions against L2 state rules (e.g., balance checks), and provides a verified API for the frontend.
4. **Frontend**: A simple dashboard to visualize transfers, account balances, and real-time block production.
---
## Project Structure
Each component is a standalone service that can be run independently or via Docker.
| Component | Directory | Responsibility |
| --- | --- | --- |
| **Sequencer** | `sequencer/` | Transaction ingestion, batching, and L1 inscription. |
| **Archiver** | `archiver/` | L1 monitoring, L2 block validation, and data serving. |
| **Frontend** | `webapp/` | UI for monitoring L2 state and sending transactions. |
---
## Getting Started (Local Run)
### Prerequisites
* **Docker & Docker Compose**
* **Logos Testnet Credentials**: If connecting to the public testnet, you need basic auth credentials (Username/Password). Contact the team via Discord to obtain these.
### 1. Configuration
Copy the example environment file and fill in your credentials.
```bash
cp testnet/l2-sequencer-archival-demo/.env.example testnet/l2-sequencer-archival-demo/.env
```
### 2. Launch with Docker Compose
The simplest way to run the entire stack is using our prebuilt images.
```bash
# Navigate to the demo directory
cd testnet/l2-sequencer-archival-demo
# Start all services
docker compose up
```
Once running, the web application will be available at `http://localhost:8200`.
---
## Manual Development Setup
For developers on macOS (including ARM/M1/M2) or Linux who wish to run components outside of Docker, we provide a unified helper script: `run-local.sh`.
### Prerequisites
* **Rust**: For building the Sequencer and Archiver binaries.
* **Bun**: For running the frontend development server.
* **OpenSSL**: For generating unique Channel IDs.
### Using the Local Runner
The script automates building binaries, managing data directories, and linking environment variables between services.
```bash
# Usage
./run-local.sh <service> --env-file <path-to-env> [--clean]
# 1. Run the entire stack (Sequencer + Archiver + Frontend)
./run-local.sh all --env-file .env-local
# 2. Run only a specific component
./run-local.sh sequencer --env-file .env-local
# 3. Start fresh (deletes local databases/keys)
./run-local.sh all --env-file .env-local --clean
```
### Environment Setup
You will need a running **Nomos Node** (L1). If you are running one locally, ensure the `TESTNET_ENDPOINT` in your `.env` points to your local node.
---
## Component READMEs
For detailed configuration flags, API documentation, and internal logic for each component, please refer to their individual documentation:
* **[Sequencer In-Depth](./sequencer/README.md)** - Transaction batching and L1 submission logic.
* **[Archiver In-Depth](./archiver/README.md)** - Validation engine and the SSE block stream.
* **[Webapp Setup](./webapp/README.md)** - UI development and customization instructions.

View File

@ -0,0 +1,35 @@
[package]
categories = { workspace = true }
description = { workspace = true }
edition = { workspace = true }
keywords = { workspace = true }
license = { workspace = true }
name = "logos-blockchain-archiver"
readme = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
[lints]
workspace = true
[dependencies]
async-stream = { default-features = false, version = "0.3.6" }
axum = { default-features = false, version = "0.8.7", features = ["http2", "json", "tokio"] }
broadcast-service = { workspace = true }
clap = { default-features = false, features = ["derive", "env", "std"], version = "4.5.13" }
common-http-client = { workspace = true }
demo-sequencer = { workspace = true }
futures = { default-features = false, version = "0.3" }
hex = { default-features = false, features = ["alloc"], version = "0.4" }
nomos-core = { workspace = true }
owo-colors = { default-features = false, version = "4.2.3" }
redb = { default-features = false, version = "3.1.0" }
reqwest = { features = ["json", "rustls-tls"], workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-stream = { default-features = false, version = "0.1" }
tokio-util = { default-features = false, version = "0.7" }
tower-http = { version = "0.6.8", features = ["cors"] }
url = { default-features = false, version = "2.5.4" }

View File

@ -0,0 +1,203 @@
# Archiver Demo
A real-time block archiver that subscribes to a Logos Blockchain node's Last Immutable Block (LIB) stream, extracts L2 sequencer inscriptions from a specified channel, validates transactions, and exposes them via HTTP endpoints.
## What It Does
1. **Connects to a Logos Blockchain node** via HTTP to subscribe to the LIB stream
2. **Filters inscriptions** by channel ID to extract L2 sequencer block data
3. **Validates blocks** — a block is invalid if:
- Its parent block (except genesis block 0) was previously marked as invalid, or
- It contains a transaction where the sender has insufficient balance
4. **Persists valid blocks** in a redb database and tracks invalid block IDs
5. **Re-validates blocks** — previously invalid blocks are automatically marked as valid when they appear again with valid ancestry
6. **Broadcasts blocks** to connected clients via an SSE endpoint at `/block_stream`
7. **Serves historical blocks** via a REST endpoint at `/blocks`
8. **Pretty prints** transaction details to the console with colored output
## Building
```bash
cargo build --release -p logos-blockchain-archiver
```
## Running
### Command Line Arguments
| Flag | Env Variable | Description | Default |
|------|--------------|-------------|---------|
| `-e` | `TESTNET_ENDPOINT` | Logos Blockchain node HTTP endpoint URL | Required |
| `-u` | `TESTNET_USERNAME` | Basic auth username | Optional |
| `-p` | `TESTNET_PASSWORD` | Basic auth password | Optional |
| `-c` | `CHANNEL_ID` | Channel ID (64 hex chars / 32 bytes) | Required |
| `-t` | `TOKEN_NAME` | Token name to display in output | Required |
| `-b` | `INITIAL_BALANCE` | Initial balance for new accounts | `1000` |
| `-n` | `PORT_NUMBER` | HTTP server port | `8090` |
### Using CLI Flags
```bash
./target/release/logos-blockchain-archiver \
-e http://localhost:8080 \
-c 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
-t DEMO
```
With optional authentication:
```bash
./target/release/logos-blockchain-archiver \
-e http://localhost:8080 \
-u admin \
-p secret \
-c 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
-t DEMO \
-b 1000 \
-n 8090
```
### Using Environment Variables
```bash
export TESTNET_ENDPOINT=http://localhost:8080
export CHANNEL_ID=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
export TOKEN_NAME=DEMO
# Optional
export TESTNET_USERNAME=admin
export TESTNET_PASSWORD=secret
export INITIAL_BALANCE=1000
export PORT_NUMBER=8090
./target/release/logos-blockchain-archiver
```
### Using a `.env` File
Create a `.env` file:
```env
TESTNET_ENDPOINT=http://localhost:8080
CHANNEL_ID=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
TOKEN_NAME=DEMO
# Optional
TESTNET_USERNAME=admin
TESTNET_PASSWORD=secret
INITIAL_BALANCE=1000
PORT_NUMBER=8090
```
Then run with a tool like `dotenv`:
```bash
dotenv ./target/release/logos-blockchain-archiver
```
## HTTP API
The archiver starts an HTTP server on the configured port (default `8090`). CORS is enabled for all origins.
### GET `/block_stream`
Server-Sent Events stream of validated L2 blocks in real-time.
**Example:**
```bash
curl -N http://localhost:8090/block_stream
```
**Response format:**
```
data: {"data":{"block_id":1,"parent_block_id":0,"transactions":[{"id":"...","from":"alice","to":"bob","amount":100,"confirmed":false,"index":0}]},"l1_block_id":"..."}
data: {"data":{"block_id":2,"parent_block_id":1,"transactions":[{"id":"...","from":"bob","to":"charlie","amount":50,"confirmed":false,"index":0}]},"l1_block_id":"..."}
```
Each `data:` line contains a JSON-serialized validated block object with the L1 block ID where it was inscribed.
### GET `/blocks`
Returns all stored validated blocks as a JSON array.
**Example:**
```bash
curl http://localhost:8090/blocks
```
**Response format:**
```json
[
{
"data": {
"block_id": 1,
"parent_block_id": 0,
"transactions": [
{
"id": "tx-uuid",
"from": "alice",
"to": "bob",
"amount": 100,
"confirmed": false,
"index": 0
}
]
},
"l1_block_id": "0123456789abcdef..."
}
]
```
## Data Storage
The archiver uses [redb](https://github.com/cberner/redb) for persistent storage:
- **`blocks.database`** — Stores validated L2 blocks
- **`accounts.database`** — Tracks account balances for transaction validation
## Console Output
When running, the archiver displays:
- A startup banner with connection details
- Real-time L1 block notifications with height and header ID
- L2 block details with transaction information
- Colored output showing sender → receiver transfers
Example:
```
_ _ _ ____
/ \ _ __ ___| |__ (_)_ _____ _ __ | _ \ ___ _ __ ___ ___
/ _ \ | '__/ __| '_ \| \ \ / / _ \ '__|| | | |/ _ \ '_ ` _ \ / _ \
/ ___ \| | | (__| | | | |\ V / __/ | | |_| | __/ | | | | | (_) |
/_/ \_\_| \___|_| |_|_| \_/ \___|_| |____/ \___|_| |_| |_|\___/
══════════════════════════════════════════════════════════════════════
📡 Nomos Node: http://localhost:8080
📺 Channel ID: 0123456789abcdef...
🌐 HTTP Server: http://0.0.0.0:8090/blocks
══════════════════════════════════════════════════════════════════════
⏳ Waiting for blocks...
🔗 Block at height 42 (abc123...)
│ 📦 Block #1
│ 💳 2 transaction(s)
│ ↳ alice → bob (100 DEMO)
│ ↳ bob → charlie (50 DEMO)
```
## Graceful Shutdown
Press `Ctrl+C` to initiate a graceful shutdown. The archiver will:
1. Stop accepting new SSE connections
2. Complete any in-flight block processing
3. Close all connections cleanly

View File

@ -0,0 +1,225 @@
use async_stream::stream;
use broadcast_service::BlockInfo;
use common_http_client::CommonHttpClient;
use demo_sequencer::{BlockData, db::AccountDb};
use futures::{Stream, StreamExt as _};
use nomos_core::{
header::HeaderId,
mantle::{
Op, SignedMantleTx, Transaction as _, TxHash,
ops::channel::{ChannelId, inscribe::InscriptionOp},
},
};
use owo_colors::OwoColorize as _;
use serde::{Deserialize, Serialize};
use tokio::select;
use tokio_util::sync::CancellationToken;
use url::Url;
use crate::db::BlockStore;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct L2BlockInfo {
pub data: BlockData,
pub l1_block_id: HeaderId,
pub l1_transaction_id: TxHash,
}
pub struct BlockStream;
impl BlockStream {
pub fn create(
cancellation_token: CancellationToken,
http_client: CommonHttpClient,
endpoint_url: &Url,
channel_id: &ChannelId,
token_name: &str,
) -> impl Stream<Item = L2BlockInfo> {
#[expect(tail_expr_drop_order, reason = "Generated internally by stream macro.")]
let block_stream = stream! {
let mut lib_stream = Box::pin(http_client
.get_lib_stream(endpoint_url.clone())
.await.unwrap());
loop {
select! {
// Always poll cancellation token first.
biased;
() = cancellation_token.cancelled() => {
break;
}
block_info = lib_stream.next() => {
let Some(BlockInfo { header_id, height }) = block_info else {
println!(
" {} Stream ended unexpectedly",
"⚠️".yellow()
);
break;
};
println!(" {} Block at height {} ({})","🔗".blue(),
height.bright_white().bold(),
&hex::encode(header_id.as_ref()
).dimmed());
let block = http_client.get_block_by_id(endpoint_url.clone(), header_id).await.unwrap().unwrap();
for (l2_block, l1_transaction_id) in extract_l2_blocks(block.transactions().cloned(), channel_id, token_name) {
yield L2BlockInfo {
data: l2_block,
l1_block_id: block.header().id(),
l1_transaction_id,
};
}
}
}
}
};
block_stream
}
}
fn extract_l2_blocks(
block_txs: impl Iterator<Item = SignedMantleTx>,
decoded_channel_id: &ChannelId,
token_name: &str,
) -> Vec<(BlockData, TxHash)> {
let block_channel_ops: Vec<(BlockData, TxHash)> = block_txs
.flat_map(|tx| {
let tx_hash = tx.mantle_tx.hash();
tx.mantle_tx
.ops
.iter()
.filter_map(|op| match op {
Op::ChannelInscribe(InscriptionOp {
channel_id,
inscription,
..
}) if channel_id == decoded_channel_id => {
let Ok(block_data) = serde_json::from_slice::<BlockData>(inscription)
else {
println!(
" {} Failed to decode L2 block in tx {}",
"⚠️".yellow(),
hex::encode(tx_hash.as_signing_bytes()).dimmed()
);
return None;
};
Some((block_data, tx_hash))
}
_ => None,
})
.collect::<Vec<_>>()
})
.collect();
if block_channel_ops.is_empty() {
println!(" {} No inscriptions in this block", "".dimmed());
} else {
for (block_data, _) in &block_channel_ops {
println!("{}", "".bright_green());
println!(
"│ {} Block #{}",
"📦".green(),
block_data.block_id.bright_green().bold()
);
println!(
"│ 💳 {} transaction(s)",
block_data.transactions.len().yellow().bold()
);
for tx_item in &block_data.transactions {
println!(
"│ {} {} → {} ({} {})",
"".dimmed(),
tx_item.from.bright_cyan(),
tx_item.to.bright_magenta(),
tx_item.amount.yellow(),
token_name
);
}
println!("{}", "".bright_green());
}
}
block_channel_ops
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ValidatedBlockData(BlockData);
impl AsRef<BlockData> for ValidatedBlockData {
fn as_ref(&self) -> &BlockData {
&self.0
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ValidatedL2Info(L2BlockInfo);
impl ValidatedL2Info {
pub fn new(
validated_block_data: ValidatedBlockData,
l1_block_id: HeaderId,
l1_transaction_id: TxHash,
) -> Self {
Self(L2BlockInfo {
data: validated_block_data.0,
l1_block_id,
l1_transaction_id,
})
}
}
impl AsRef<L2BlockInfo> for ValidatedL2Info {
fn as_ref(&self) -> &L2BlockInfo {
&self.0
}
}
pub async fn validate_block(
block: BlockData,
accounts_db: &AccountDb,
blocks_db: &BlockStore,
) -> Result<ValidatedBlockData, BlockData> {
// We consider block `0` to be the genesis block and always valid, hence its
// children won't be checked against the DB.
if block.parent_block_id > 0
&& !blocks_db
.is_block_valid(block.parent_block_id)
.await
.unwrap()
{
println!(
" {} Block {} rejected: parent block {} is invalid",
"".red(),
block.block_id.bright_red().bold(),
block.parent_block_id.yellow()
);
return Err(block);
}
let are_txs_valid = accounts_db
.try_apply_transfers(
block
.transactions
.iter()
.map(|tx| (tx.from.as_str(), tx.to.as_str(), tx.amount)),
)
.await
.is_ok();
if !are_txs_valid {
println!(
" {} Block {} rejected: contains invalid transactions",
"".red(),
block.block_id.bright_red().bold()
);
return Err(block);
}
Ok(ValidatedBlockData(block))
}

View File

@ -0,0 +1,47 @@
use core::convert::Infallible;
use clap::Parser;
use nomos_core::mantle::ops::channel::ChannelId;
use url::Url;
#[derive(Parser, Debug)]
pub struct CliArgs {
#[clap(short = 'e', env = "TESTNET_ENDPOINT")]
pub nomos_node_http_endpoint: Url,
#[clap(short = 'u', env = "TESTNET_USERNAME")]
pub username: Option<String>,
#[clap(short = 'p', env = "TESTNET_PASSWORD")]
pub password: Option<String>,
#[clap(short = 'c', env = "CHANNEL_ID", value_parser = parse_channel_id)]
pub channel_id: ChannelId,
#[clap(short = 't', env = "TOKEN_NAME")]
pub token_name: String,
#[clap(short = 'b', env = "INITIAL_BALANCE", default_value = "1000")]
pub initial_balance: u64,
#[clap(short = 'n', env = "PORT_NUMBER", default_value = "8090")]
pub port_number: u16,
#[clap(
long,
env = "ARCHIVER_BLOCKS_DB_PATH",
default_value = "blocks.database"
)]
pub blocks_db_path: String,
#[clap(
long,
env = "ARCHIVER_ACCOUNTS_DB_PATH",
default_value = "accounts.database"
)]
pub accounts_db_path: String,
}
#[expect(
clippy::unnecessary_wraps,
reason = "Clap requires a Result type for custom parsers"
)]
fn parse_channel_id(encoded_channel_id: &str) -> Result<ChannelId, Infallible> {
Ok(
<[u8; 32]>::try_from(hex::decode(encoded_channel_id).unwrap())
.unwrap()
.into(),
)
}

View File

@ -0,0 +1,11 @@
use owo_colors::OwoColorize as _;
use tokio::signal::ctrl_c;
use tokio_util::sync::CancellationToken;
pub fn listen_for_sigint(cancellation_token: CancellationToken) {
tokio::spawn(async move {
ctrl_c().await.unwrap();
println!("\n {} Graceful shutdown initiated...", "🛑".red());
cancellation_token.cancel();
});
}

View File

@ -0,0 +1,106 @@
use std::sync::Arc;
use nomos_core::codec::{DeserializeOp as _, SerializeOp as _};
use redb::{
CommitError, Database, DatabaseError, ReadableDatabase as _, ReadableTable as _, StorageError,
TableDefinition, TableError, TransactionError,
};
use thiserror::Error;
use tokio::sync::RwLock;
use crate::block::ValidatedL2Info;
const BLOCKS_TABLE: TableDefinition<u64, &[u8]> = TableDefinition::new("blocks");
const INVALID_BLOCKS_TABLE: TableDefinition<u64, ()> = TableDefinition::new("invalid_blocks");
#[derive(Debug, Error)]
pub enum DbError {
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Transaction error: {0}")]
Transaction(#[from] Box<TransactionError>),
#[error("Table error: {0}")]
Table(#[from] TableError),
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Commit error: {0}")]
Commit(#[from] CommitError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl From<TransactionError> for DbError {
fn from(err: TransactionError) -> Self {
Self::Transaction(Box::new(err))
}
}
/// Persistent storage for blocks using redb
#[derive(Clone)]
pub struct BlockStore {
db: Arc<RwLock<Database>>,
}
impl BlockStore {
pub fn new(path: &str) -> Result<Self, DbError> {
let db = Database::create(path)?;
let write_txn = db.begin_write()?;
drop(write_txn.open_table(BLOCKS_TABLE)?);
drop(write_txn.open_table(INVALID_BLOCKS_TABLE)?);
write_txn.commit()?;
Ok(Self {
db: Arc::new(RwLock::new(db)),
})
}
pub async fn add_block(&self, block: ValidatedL2Info) -> Result<(), DbError> {
let serialized = block.to_bytes().unwrap();
let write_txn = self.db.write().await.begin_write()?;
write_txn
.open_table(BLOCKS_TABLE)?
.insert(block.as_ref().data.block_id, &*serialized)?;
write_txn.commit()?;
Ok(())
}
pub async fn mark_block_as_invalid(&self, block_id: u64) -> Result<(), DbError> {
let write_txn = self.db.write().await.begin_write()?;
write_txn
.open_table(INVALID_BLOCKS_TABLE)?
.insert(block_id, &())?;
write_txn.commit()?;
Ok(())
}
pub async fn unmark_block_as_invalid(&self, block_id: u64) -> Result<bool, DbError> {
let write_txn = self.db.write().await.begin_write()?;
let is_old_value_removed = write_txn
.open_table(INVALID_BLOCKS_TABLE)?
.remove(&block_id)?
.is_some();
write_txn.commit()?;
Ok(is_old_value_removed)
}
pub async fn is_block_valid(&self, block_id: u64) -> Result<bool, DbError> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(INVALID_BLOCKS_TABLE)?;
Ok(table.get(&block_id)?.is_none())
}
pub async fn get_all_blocks(&self) -> Result<Vec<ValidatedL2Info>, DbError> {
let read_txn = self.db.read().await.begin_read()?;
let deserialized_blocks: Vec<ValidatedL2Info> = read_txn
.open_table(BLOCKS_TABLE)?
.iter()?
.filter_map(Result::ok)
.map(|(_, value)| value)
.map(|value| ValidatedL2Info::from_bytes(value.value()).unwrap())
.collect();
Ok(deserialized_blocks)
}
}

View File

@ -0,0 +1,100 @@
use core::{convert::Infallible, net::SocketAddr};
use axum::{
Json, Router,
extract::State,
response::{Sse, sse::Event},
routing::get,
serve,
};
use futures::{Stream, StreamExt as _};
use reqwest::{Method, header};
use tokio::{net::TcpListener, sync::broadcast::Receiver};
use tokio_stream::wrappers::BroadcastStream;
use tokio_util::sync::CancellationToken;
use tower_http::cors::{Any, CorsLayer};
use crate::{block::ValidatedL2Info, db::BlockStore};
pub struct Server {
block_receiver_channel: Receiver<ValidatedL2Info>,
cancellation_token: CancellationToken,
blocks_db: BlockStore,
}
impl Server {
pub const fn new(
block_receiver_channel: Receiver<ValidatedL2Info>,
cancellation_token: CancellationToken,
blocks_db: BlockStore,
) -> Self {
Self {
block_receiver_channel,
cancellation_token,
blocks_db,
}
}
pub fn start(self, address: SocketAddr) {
let (router, cancellation_token) = self.into_router_and_cancellation_token();
tokio::spawn(async move {
serve(TcpListener::bind(address).await.unwrap(), router)
.with_graceful_shutdown(async move {
cancellation_token.cancelled().await;
})
.await
.unwrap();
});
}
fn into_router_and_cancellation_token(self) -> (Router, CancellationToken) {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
(
Router::new()
.route("/block_stream", get(handle_block_stream))
.route("/blocks", get(handle_get_blocks))
.with_state(AppState {
block_receiver_channel: self.block_receiver_channel,
blocks_db: self.blocks_db,
})
.layer(cors),
self.cancellation_token,
)
}
}
struct AppState {
block_receiver_channel: Receiver<ValidatedL2Info>,
blocks_db: BlockStore,
}
impl Clone for AppState {
fn clone(&self) -> Self {
Self {
block_receiver_channel: self.block_receiver_channel.resubscribe(),
blocks_db: self.blocks_db.clone(),
}
}
}
async fn handle_block_stream(
State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let stream = BroadcastStream::new(state.block_receiver_channel)
.map(|block_data_result| block_data_result.unwrap())
.map(|block_data| serde_json::to_string(&block_data).unwrap())
.map(|json_serialized_block_data| Ok(Event::default().data(json_serialized_block_data)));
Sse::new(stream)
}
async fn handle_get_blocks(
State(state): State<AppState>,
) -> Result<Json<Vec<ValidatedL2Info>>, Infallible> {
let blocks = state.blocks_db.get_all_blocks().await.unwrap();
Ok(Json(blocks))
}

View File

@ -0,0 +1,110 @@
#![expect(clippy::non_ascii_literal, reason = "Demo, so emojis are fine.")]
use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use clap::Parser as _;
use common_http_client::{BasicAuthCredentials, CommonHttpClient};
use demo_sequencer::db::AccountDb;
use futures::StreamExt as _;
use owo_colors::OwoColorize as _;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use crate::{
block::{BlockStream, ValidatedL2Info, validate_block},
cli::CliArgs,
ctrl_c::listen_for_sigint,
db::BlockStore,
http::Server,
output::print_startup_banner,
};
mod block;
mod cli;
mod ctrl_c;
mod db;
mod http;
mod output;
#[tokio::main]
async fn main() {
let CliArgs {
nomos_node_http_endpoint,
username,
password,
channel_id,
token_name,
initial_balance,
port_number,
blocks_db_path,
accounts_db_path,
} = CliArgs::parse();
let listen_address = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port_number));
print_startup_banner(&nomos_node_http_endpoint, &channel_id, &listen_address);
// Setup
let (rollup_block_sender, _) = broadcast::channel::<ValidatedL2Info>(100);
let cancellation_token = CancellationToken::new();
let client = CommonHttpClient::new(username.map(|u| BasicAuthCredentials::new(u, password)));
let blocks_db = BlockStore::new(&blocks_db_path).unwrap();
let accounts_db = AccountDb::new(&accounts_db_path, initial_balance).unwrap();
// Start sigint handler
listen_for_sigint(cancellation_token.clone());
// Start HTTP server
Server::new(
rollup_block_sender.subscribe(),
cancellation_token.clone(),
blocks_db.clone(),
)
.start(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 8090).into());
// Start LIB subscriber
let mut block_stream = Box::pin(BlockStream::create(
cancellation_token,
client,
&nomos_node_http_endpoint,
&channel_id,
token_name.as_str(),
));
while let Some(block) = block_stream.next().await {
match validate_block(block.data, &accounts_db, &blocks_db).await {
Ok(validated_l2_block) => {
let validated_l2_info = ValidatedL2Info::new(
validated_l2_block.clone(),
block.l1_block_id,
block.l1_transaction_id,
);
blocks_db
.add_block(validated_l2_info.clone())
.await
.unwrap();
let block_id = validated_l2_block.as_ref().block_id;
if blocks_db.unmark_block_as_invalid(block_id).await.unwrap() {
println!(
" {} Previously invalid block {block_id} now marked as valid",
"".green(),
);
}
rollup_block_sender.send(validated_l2_info).unwrap();
}
Err(invalid_l2_block) => {
blocks_db
.mark_block_as_invalid(invalid_l2_block.block_id)
.await
.unwrap();
}
}
}
}

View File

@ -0,0 +1,35 @@
use core::net::SocketAddr;
use nomos_core::mantle::ops::channel::ChannelId;
use owo_colors::OwoColorize as _;
use url::Url;
const BANNER: &str = r"
_ _ _ ____
/ \ _ __ ___| |__ (_)_ _____ _ __ | _ \ ___ _ __ ___ ___
/ _ \ | '__/ __| '_ \| \ \ / / _ \ '__|| | | |/ _ \ '_ ` _ \ / _ \
/ ___ \| | | (__| | | | |\ V / __/ | | |_| | __/ | | | | | (_) |
/_/ \_\_| \___|_| |_|_| \_/ \___|_| |____/ \___|_| |_| |_|\___/
";
pub fn print_startup_banner(endpoint: &Url, channel_id: &ChannelId, listen_addr: &SocketAddr) {
println!("{}", BANNER.cyan().bold());
println!("{}", "".repeat(70).dimmed());
println!(
" {} {}",
"📡 Nomos Node:".bright_blue().bold(),
endpoint.white()
);
println!(
" {} {}",
"📺 Channel ID:".bright_blue().bold(),
hex::encode(channel_id.as_ref()).white()
);
println!(
" {} {}",
"🌐 HTTP Server:".bright_blue().bold(),
format!("http://{listen_addr}/blocks").green()
);
println!("{}", "".repeat(70).dimmed());
println!(" {} Waiting for blocks...\n", "".yellow());
}

View File

@ -0,0 +1,47 @@
# NOTE: This docker-compose only works on x86_64 (AMD64) architecture.
# The ZK prover will panic when running under ARM emulation.
# For ARM machines (e.g., Apple Silicon), run the binaries directly instead.
services:
l2-nginx:
container_name: l2_nginx
image: l2-demo-local
ports:
- "8200:80"
depends_on:
- l2-sequencer
- l2-archiver
entrypoint: ["nginx", "-g", "daemon off;"]
l2-sequencer:
container_name: l2_sequencer
image: l2-demo-local
environment:
- SEQUENCER_LISTEN_ADDR=0.0.0.0:8080
- SEQUENCER_NODE_ENDPOINT=${SEQUENCER_NODE_ENDPOINT}
- SEQUENCER_DB_PATH=/data/sequencer.db
- SEQUENCER_SIGNING_KEY_PATH=/data/sequencer.key
- SEQUENCER_CHANNEL_ID=${CHANNEL_ID}
- SEQUENCER_INITIAL_BALANCE=1000
- SEQUENCER_NODE_AUTH_USERNAME=${SEQUENCER_NODE_USERNAME}
- SEQUENCER_NODE_AUTH_PASSWORD=${SEQUENCER_NODE_PASSWORD}
volumes:
- l2-sequencer-data:/data
entrypoint: ["/usr/bin/demo-sequencer"]
l2-archiver:
container_name: l2_archiver
image: l2-demo-local
environment:
- TESTNET_ENDPOINT=${ARCHIVER_NODE_ENDPOINT}
- TESTNET_USERNAME=${ARCHIVER_NODE_USERNAME}
- TESTNET_PASSWORD=${ARCHIVER_NODE_PASSWORD}
- CHANNEL_ID=${CHANNEL_ID}
- TOKEN_NAME=${TOKEN_NAME:-MEM}
volumes:
- l2-archiver-data:/data
entrypoint: ["/usr/bin/logos-blockchain-archiver"]
volumes:
l2-sequencer-data:
l2-archiver-data:

View File

@ -0,0 +1,47 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name _;
# Frontend static files
location / {
root /var/www/l2-demo;
index index.html;
try_files $uri $uri/ /index.html;
}
# Sequencer API
location /api/sequencer/ {
proxy_pass http://l2-sequencer:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Archiver API
location /api/archiver/ {
proxy_pass http://l2-archiver:8090/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE specific
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
}
}
}

View File

@ -0,0 +1,278 @@
#!/bin/bash
# L2 Demo - Local Development Runner
# Runs sequencer, archiver, and/or frontend without Docker (works on ARM Mac)
#
# Usage:
# ./run-local.sh <service> --env-file /path/to/.env-local [--clean]
#
# Services:
# sequencer - Run only the sequencer
# archiver - Run only the archiver
# frontend - Run only the frontend
# all - Run all services (default)
#
# Examples:
# ./run-local.sh all --env-file ~/Eng/offsite-sequencer-env/.env-local
# ./run-local.sh sequencer --env-file ~/Eng/offsite-sequencer-env/.env-local
# ./run-local.sh archiver --env-file ~/Eng/offsite-sequencer-env/.env-local
# ./run-local.sh frontend --env-file ~/Eng/offsite-sequencer-env/.env-local
# ./run-local.sh all --env-file ~/Eng/offsite-sequencer-env/.env-local --clean
#
# Required env vars:
# SEQUENCER_NODE_ENDPOINT - Nomos node HTTP endpoint for sequencer
# ARCHIVER_NODE_ENDPOINT - Nomos node HTTP endpoint for archiver
# TOKEN_NAME - Token name (e.g., "MEM")
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
DATA_DIR="$SCRIPT_DIR/data"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Parse service argument (first positional arg)
SERVICE="all"
if [[ $# -gt 0 && ! "$1" =~ ^-- ]]; then
SERVICE="$1"
shift
fi
# Validate service
case $SERVICE in
sequencer|archiver|frontend|all)
;;
*)
echo -e "${RED}Unknown service: $SERVICE${NC}"
echo "Valid services: sequencer, archiver, frontend, all"
exit 1
;;
esac
# Parse remaining arguments
ENV_FILE=""
CLEAN_START=false
while [[ $# -gt 0 ]]; do
case $1 in
--env-file)
ENV_FILE="$2"
shift 2
;;
--clean)
CLEAN_START=true
shift
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
done
# Load env file if provided
if [ -n "$ENV_FILE" ]; then
if [ -f "$ENV_FILE" ]; then
echo -e "${BLUE}Loading environment from: $ENV_FILE${NC}"
set -a
source "$ENV_FILE"
set +a
else
echo -e "${RED}Error: env file not found: $ENV_FILE${NC}"
exit 1
fi
fi
# Validate required env vars
missing_vars=()
[ -z "$SEQUENCER_NODE_ENDPOINT" ] && missing_vars+=("SEQUENCER_NODE_ENDPOINT")
[ -z "$ARCHIVER_NODE_ENDPOINT" ] && missing_vars+=("ARCHIVER_NODE_ENDPOINT")
[ -z "$TOKEN_NAME" ] && missing_vars+=("TOKEN_NAME")
if [ ${#missing_vars[@]} -ne 0 ]; then
echo -e "${RED}Error: Missing required environment variables:${NC}"
for var in "${missing_vars[@]}"; do
echo " - $var"
done
echo ""
echo "See .env-local.example for the required format."
exit 1
fi
# Clean data directory if requested
if [ "$CLEAN_START" = true ]; then
echo -e "${YELLOW}Cleaning data directory...${NC}"
rm -rf "$DATA_DIR"
fi
# Create data directory (needed for channel ID file)
mkdir -p "$DATA_DIR"
# Handle CHANNEL_ID - check env, then data file, then generate new
CHANNEL_ID_FILE="$DATA_DIR/channel_id"
if [ -n "$CHANNEL_ID" ]; then
# Use env var and save it
echo "$CHANNEL_ID" > "$CHANNEL_ID_FILE"
echo -e "${BLUE}Using CHANNEL_ID from environment${NC}"
elif [ -f "$CHANNEL_ID_FILE" ]; then
# Read from saved file
CHANNEL_ID=$(cat "$CHANNEL_ID_FILE")
echo -e "${BLUE}Using saved CHANNEL_ID from $CHANNEL_ID_FILE${NC}"
else
# Generate new random one
CHANNEL_ID=$(openssl rand -hex 32)
echo "$CHANNEL_ID" > "$CHANNEL_ID_FILE"
echo -e "${YELLOW}Generated new CHANNEL_ID: ${CHANNEL_ID}${NC}"
fi
# Set both channel ID vars to the same value
export CHANNEL_ID
export SEQUENCER_CHANNEL_ID="$CHANNEL_ID"
# Map shared credentials to what each binary expects
export SEQUENCER_NODE_AUTH_USERNAME="$TESTNET_USERNAME"
export SEQUENCER_NODE_AUTH_PASSWORD="$TESTNET_PASSWORD"
export TESTNET_ENDPOINT="$ARCHIVER_NODE_ENDPOINT"
# Set defaults for sequencer
export SEQUENCER_DB_PATH="${SEQUENCER_DB_PATH:-$DATA_DIR/sequencer.db}"
export SEQUENCER_SIGNING_KEY_PATH="${SEQUENCER_SIGNING_KEY_PATH:-$DATA_DIR/sequencer.key}"
# Set defaults for archiver
export ARCHIVER_BLOCKS_DB_PATH="${ARCHIVER_BLOCKS_DB_PATH:-$DATA_DIR/blocks.database}"
export ARCHIVER_ACCOUNTS_DB_PATH="${ARCHIVER_ACCOUNTS_DB_PATH:-$DATA_DIR/accounts.database}"
# Get local IP for sharing
LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
# Set VITE URLs using local IP so they work over network
export VITE_SEQUENCER_URL="${VITE_SEQUENCER_URL:-http://$LOCAL_IP:8080}"
export VITE_ARCHIVER_URL="${VITE_ARCHIVER_URL:-http://$LOCAL_IP:8090}"
export VITE_EXPLORER_URL="${VITE_EXPLORER_URL:-http://$LOCAL_IP:8000}"
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN} L2 Demo - $SERVICE${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
echo -e "${BLUE}Configuration:${NC}"
echo " Sequencer endpoint: $SEQUENCER_NODE_ENDPOINT"
echo " Archiver endpoint: $ARCHIVER_NODE_ENDPOINT"
echo " Channel ID: $CHANNEL_ID"
echo " Token: $TOKEN_NAME"
echo " Data directory: $DATA_DIR"
echo ""
# Check if binaries exist, if not build them
SEQUENCER_BIN="$REPO_ROOT/target/release/demo-sequencer"
ARCHIVER_BIN="$REPO_ROOT/target/release/logos-blockchain-archiver"
if [[ "$SERVICE" == "sequencer" || "$SERVICE" == "all" ]] && [ ! -f "$SEQUENCER_BIN" ]; then
echo -e "${YELLOW}Building sequencer...${NC}"
cd "$REPO_ROOT"
cargo build --release -p demo-sequencer
fi
if [[ "$SERVICE" == "archiver" || "$SERVICE" == "all" ]] && [ ! -f "$ARCHIVER_BIN" ]; then
echo -e "${YELLOW}Building archiver...${NC}"
cd "$REPO_ROOT"
cargo build --release -p logos-blockchain-archiver
fi
# Run the selected service(s)
case $SERVICE in
sequencer)
echo -e "${GREEN}Starting sequencer...${NC}"
cd "$REPO_ROOT"
exec "$SEQUENCER_BIN"
;;
archiver)
echo -e "${GREEN}Starting archiver...${NC}"
cd "$REPO_ROOT"
exec "$ARCHIVER_BIN"
;;
frontend)
cd "$SCRIPT_DIR/webapp"
if ! command -v bun &> /dev/null; then
echo -e "${RED}Error: bun is not installed. Install it with: curl -fsSL https://bun.sh/install | bash${NC}"
exit 1
fi
if [ ! -d "node_modules" ]; then
echo -e "${YELLOW}Installing frontend dependencies...${NC}"
bun install
fi
echo -e "${GREEN}Starting frontend...${NC}"
echo ""
echo -e "${BLUE}Access points:${NC}"
echo " Frontend: http://localhost:5173"
echo " Frontend: http://$LOCAL_IP:5173 (share this with others on same network)"
echo " Sequencer: $VITE_SEQUENCER_URL"
echo " Archiver: $VITE_ARCHIVER_URL"
echo " Explorer: $VITE_EXPLORER_URL"
echo ""
exec bun run dev --host
;;
all)
# Trap to kill background processes on exit
cleanup() {
echo ""
echo -e "${YELLOW}Shutting down...${NC}"
kill $SEQUENCER_PID 2>/dev/null || true
kill $ARCHIVER_PID 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM
# Start sequencer
echo -e "${GREEN}Starting sequencer...${NC}"
cd "$REPO_ROOT"
"$SEQUENCER_BIN" &
SEQUENCER_PID=$!
sleep 2
# Start archiver
echo -e "${GREEN}Starting archiver...${NC}"
"$ARCHIVER_BIN" &
ARCHIVER_PID=$!
sleep 2
# Start frontend dev server
echo -e "${GREEN}Starting frontend...${NC}"
cd "$SCRIPT_DIR/webapp"
if ! command -v bun &> /dev/null; then
echo -e "${RED}Error: bun is not installed. Install it with: curl -fsSL https://bun.sh/install | bash${NC}"
cleanup
fi
if [ ! -d "node_modules" ]; then
echo -e "${YELLOW}Installing frontend dependencies...${NC}"
bun install
fi
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN} All services running!${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
echo -e "${BLUE}Access points:${NC}"
echo " Frontend: http://localhost:5173"
echo " Frontend: http://$LOCAL_IP:5173 (share this with others on same network)"
echo " Sequencer: $VITE_SEQUENCER_URL"
echo " Archiver: $VITE_ARCHIVER_URL"
echo " Explorer: $VITE_EXPLORER_URL"
echo ""
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
echo ""
# Run frontend in foreground
bun run dev --host
;;
esac

View File

@ -0,0 +1,21 @@
# Address the sequencer HTTP server listens on
export SEQUENCER_LISTEN_ADDR="0.0.0.0:8080"
# Nomos node HTTP endpoint to submit transactions
export SEQUENCER_NODE_ENDPOINT="https://testnet.nomos.tech/node/1/"
# Path to store the redb database
export SEQUENCER_DB_PATH="./sequencer.db"
# Path to store/load the signing key (persists across restarts)
export SEQUENCER_SIGNING_KEY_PATH="./sequencer.key"
# Channel ID for inscriptions (64-character hex string = 32 bytes)
export SEQUENCER_CHANNEL_ID="6d656d636f696e00000000000000000000000000000000000000000000000000"
# Initial balance for new accounts (default: 1000)
export SEQUENCER_INITIAL_BALANCE="1000"
# Basic auth credentials for node endpoint (optional)
export SEQUENCER_NODE_AUTH_USERNAME=
export SEQUENCER_NODE_AUTH_PASSWORD=

View File

@ -0,0 +1,32 @@
[package]
categories = { workspace = true }
description = "Simple sequencer for demo - receives HTTP transactions and submits to Nomos node"
edition = { workspace = true }
keywords = { workspace = true }
license = { workspace = true }
name = "demo-sequencer"
readme = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
[lints]
workspace = true
[dependencies]
axum = { default-features = false, features = ["http1", "http2", "json", "tokio"], version = "0.7.5" }
common-http-client = { workspace = true }
hex = "0.4"
key-management-system-service = { workspace = true }
nomos-core = { workspace = true }
owo-colors = { default-features = false, version = "4.2.3" }
rand = { workspace = true }
redb = "2.2"
reqwest = { features = ["json", "rustls-tls"], workspace = true }
serde = { features = ["derive"], workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { default-features = false, features = ["macros", "net", "rt-multi-thread", "signal", "sync"], version = "1" }
tokio-util = { version = "0.7" }
tower-http = { version = "0.6.8", features = ["cors"] }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -0,0 +1,179 @@
# MemChain Sequencer Demo
A demo L2 sequencer that processes token transfers and inscribes block data onto the Logos Blockchain.
It maintains account balances, batches transactions into blocks, and submits them as channel inscriptions.
## What It Does
1. **Accepts transfer requests** via a REST API
2. **Maintains account balances** in a persistent database (redb)
3. **Batches transactions** into blocks periodically
4. **Inscribes blocks** onto the Logos Blockchain via channel inscriptions
5. **Tracks confirmation status** by monitoring inscribed blocks on-chain
## Building
```bash
cargo build --release -p demo-sequencer
```
## Running
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `SEQUENCER_LISTEN_ADDR` | HTTP server listen address | `0.0.0.0:8080` |
| `SEQUENCER_NODE_ENDPOINT` | Nomos node HTTP endpoint | `http://localhost:18080` |
| `SEQUENCER_DB_PATH` | Path to redb database file | `sequencer.redb` |
| `SEQUENCER_SIGNING_KEY_PATH` | Path to signing key file (created if missing) | `sequencer.key` |
| `SEQUENCER_CHANNEL_ID` | Channel ID for inscriptions (64 hex chars) | **Required** |
| `SEQUENCER_INITIAL_BALANCE` | Initial token balance for new accounts | `1000` |
| `SEQUENCER_NODE_AUTH_USERNAME` | Basic auth username for Nomos node (optional) | - |
| `SEQUENCER_NODE_AUTH_PASSWORD` | Basic auth password for Nomos node (optional) | - |
### Example
```bash
export SEQUENCER_LISTEN_ADDR=0.0.0.0:3000
export SEQUENCER_NODE_ENDPOINT=http://localhost:18080
export SEQUENCER_DB_PATH=./data/sequencer.redb
export SEQUENCER_SIGNING_KEY_PATH=./data/sequencer.key
export SEQUENCER_INITIAL_BALANCE=1000
export SEQUENCER_NODE_AUTH_USERNAME=admin
export SEQUENCER_NODE_AUTH_PASSWORD=secret
./target/release/demo-sequencer
```
### Using a `.env` File
Create a `.env` file:
```env
SEQUENCER_LISTEN_ADDR=0.0.0.0:3000
SEQUENCER_NODE_ENDPOINT=http://localhost:18080
SEQUENCER_DB_PATH=./data/sequencer.redb
SEQUENCER_SIGNING_KEY_PATH=./data/sequencer.key
SEQUENCER_INITIAL_BALANCE=1000
SEQUENCER_NODE_AUTH_USERNAME=admin
SEQUENCER_NODE_AUTH_PASSWORD=secret
```
Then run with a tool like `dotenv`:
```bash
dotenv ./target/release/demo-sequencer
```
## HTTP API
### POST `/transfer`
Submit a token transfer between accounts.
**Request:**
```bash
curl -X POST http://localhost:8080/transfer \
-H "Content-Type: application/json" \
-d '{"from": "alice", "to": "bob", "amount": 100}'
```
**Response:**
```json
{
"from_balance": 900,
"to_balance": 1100,
"tx_hash": "a1b2c3d4..."
}
```
### GET `/accounts/:account`
Get account balance and optionally transaction history.
**Request:**
```bash
# Balance only
curl http://localhost:8080/accounts/alice
# With transaction history
curl http://localhost:8080/accounts/alice?tx=true
```
**Response:**
```json
{
"account": "alice",
"balance": 900,
"confirmed_balance": 900,
"transactions": [
{
"id": "abc123...",
"from": "alice",
"to": "bob",
"amount": 100
}
]
}
```
### GET `/accounts`
List all accounts and their balances.
**Request:**
```bash
curl http://localhost:8080/accounts
```
**Response:**
```json
{
"accounts": [
{ "account": "alice", "balance": 900 },
{ "account": "bob", "balance": 1100 }
]
}
```
### GET `/health`
Health check endpoint.
**Request:**
```bash
curl http://localhost:8080/health
```
**Response:**
```
OK
```
## Logging
The sequencer uses `tracing` for structured logging. Control log level via the `RUST_LOG` environment variable:
```bash
# Debug logging
RUST_LOG=debug ./target/release/demo-sequencer
# Only show warnings and errors
RUST_LOG=warn ./target/release/demo-sequencer
```
## Data Persistence
- **Database:** Account balances and transaction history are stored in a [redb](https://github.com/cberner/redb) database file
- **Signing Key:** An Ed25519 signing key is generated on first run and stored at the configured path
Both files are created automatically if they don't exist.

View File

@ -0,0 +1,202 @@
use std::sync::Arc;
use axum::{
Json,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
};
use demo_sequencer::{Transaction, TransferRequest, db::DbError};
use reqwest::{Method, header};
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use tracing::{debug, error};
use crate::sequencer::{Sequencer, SequencerError};
pub type AppState = Arc<Sequencer>;
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: String,
}
fn friendly_error(err: &SequencerError) -> String {
match err {
SequencerError::Db(db_err) => match db_err.as_ref() {
DbError::InsufficientBalance {
account,
balance,
required,
} => {
format!("Insufficient balance: {account} has {balance} tokens but needs {required}")
}
DbError::SelfTransfer { account } => {
format!("Cannot transfer to yourself ({account})")
}
_ => "Internal database error".to_owned(),
},
SequencerError::Timeout => "Transaction timed out waiting for confirmation".to_owned(),
SequencerError::Serialization(_) => "Invalid transaction data".to_owned(),
_ => "Internal server error".to_owned(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BalanceResponse {
pub account: String,
pub balance: u64,
pub confirmed_balance: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub transactions: Option<Vec<Transaction>>,
}
#[derive(Debug, Deserialize)]
pub struct AccountQuery {
#[serde(default)]
pub tx: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AccountEntry {
pub account: String,
pub balance: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AccountsResponse {
pub accounts: Vec<AccountEntry>,
}
/// POST /transfer
/// Request body: { "from": "alice", "to": "bob", "amount": 100 }
async fn transfer(
State(sequencer): State<AppState>,
Json(request): Json<TransferRequest>,
) -> impl IntoResponse {
debug!(
"API /transfer {} -> {} ({})",
request.from, request.to, request.amount
);
match sequencer.process_transfer(request).await {
Ok(response) => (StatusCode::OK, Json(response)).into_response(),
Err(e) => {
error!("Transfer failed: {e}");
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: friendly_error(&e),
}),
)
.into_response()
}
}
}
async fn fetch_account_data(
sequencer: &Sequencer,
account: &str,
include_tx: bool,
) -> Result<(u64, u64, Option<Vec<Transaction>>), String> {
let balance = sequencer
.get_balance(account)
.await
.map_err(|e| e.to_string())?;
let confirmed_balance = sequencer
.get_confirmed_balance(account)
.await
.map_err(|e| e.to_string())?;
let transactions = if include_tx {
Some(
sequencer
.get_account_transactions(account)
.await
.map_err(|e| e.to_string())?,
)
} else {
None
};
Ok((balance, confirmed_balance, transactions))
}
/// GET /accounts/{account}?tx=true
async fn get_balance(
State(sequencer): State<AppState>,
axum::extract::Path(account): axum::extract::Path<String>,
axum::extract::Query(query): axum::extract::Query<AccountQuery>,
) -> impl IntoResponse {
debug!("API /accounts/{}", account);
match fetch_account_data(&sequencer, &account, query.tx).await {
Ok((balance, confirmed_balance, transactions)) => (
StatusCode::OK,
Json(BalanceResponse {
account,
balance,
confirmed_balance,
transactions,
}),
)
.into_response(),
Err(e) => {
error!("Get account failed: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { error: e }),
)
.into_response()
}
}
}
/// GET /accounts
/// Returns all accounts and their balances
async fn list_accounts(State(sequencer): State<AppState>) -> impl IntoResponse {
debug!("API /accounts");
match sequencer.list_accounts().await {
Ok(accounts) => {
let accounts = accounts
.into_iter()
.map(|(account, balance)| AccountEntry { account, balance })
.collect();
(StatusCode::OK, Json(AccountsResponse { accounts })).into_response()
}
Err(e) => {
error!("List accounts failed: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: e.to_string(),
}),
)
.into_response()
}
}
}
/// GET /health
/// Health check endpoint
async fn health() -> impl IntoResponse {
(StatusCode::OK, "OK")
}
pub fn create_router(sequencer: Arc<Sequencer>) -> axum::Router {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
axum::Router::new()
.route("/transfer", post(transfer))
.route("/accounts/:account", get(get_balance))
.route("/accounts", get(list_accounts))
.route("/health", get(health))
.with_state(sequencer)
.layer(cors)
}

View File

@ -0,0 +1,69 @@
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// HTTP server listen address (e.g., "0.0.0.0:8080")
pub listen_addr: SocketAddr,
/// Nomos node HTTP endpoint to submit transactions to (e.g., "<http://localhost:18080>")
pub node_endpoint: String,
/// Path to the redb database file
pub db_path: String,
/// Path to the signing key file (will be created if it doesn't exist)
pub signing_key_path: String,
/// Channel ID for inscriptions (hex string, will be padded/truncated to 32
/// bytes)
pub channel_id: String,
/// Initial balance for new accounts
#[serde(default = "default_initial_balance")]
pub initial_balance: u64,
/// Basic auth username for node endpoint (optional)
pub node_auth_username: Option<String>,
/// Basic auth password for node endpoint (optional)
pub node_auth_password: Option<String>,
}
const fn default_initial_balance() -> u64 {
1000
}
impl Default for Config {
fn default() -> Self {
Self {
listen_addr: "0.0.0.0:8080".parse().expect("valid address"),
node_endpoint: "http://localhost:18080".to_owned(),
db_path: "sequencer.redb".to_owned(),
signing_key_path: "sequencer.key".to_owned(),
channel_id: String::new(),
initial_balance: default_initial_balance(),
node_auth_username: None,
node_auth_password: None,
}
}
}
impl Config {
pub fn from_env() -> Self {
Self {
listen_addr: std::env::var("SEQUENCER_LISTEN_ADDR")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| "0.0.0.0:8080".parse().unwrap()),
node_endpoint: std::env::var("SEQUENCER_NODE_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:18080".to_owned()),
db_path: std::env::var("SEQUENCER_DB_PATH")
.unwrap_or_else(|_| "sequencer.redb".to_owned()),
signing_key_path: std::env::var("SEQUENCER_SIGNING_KEY_PATH")
.unwrap_or_else(|_| "sequencer.key".to_owned()),
channel_id: std::env::var("SEQUENCER_CHANNEL_ID")
.expect("SEQUENCER_CHANNEL_ID env var is required"),
initial_balance: std::env::var("SEQUENCER_INITIAL_BALANCE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(default_initial_balance),
node_auth_username: std::env::var("SEQUENCER_NODE_AUTH_USERNAME").ok(),
node_auth_password: std::env::var("SEQUENCER_NODE_AUTH_PASSWORD").ok(),
}
}
}

View File

@ -0,0 +1,11 @@
use owo_colors::OwoColorize as _;
use tokio::signal::ctrl_c;
use tokio_util::sync::CancellationToken;
pub fn listen_for_sigint(cancellation_token: CancellationToken) {
tokio::spawn(async move {
ctrl_c().await.unwrap();
println!("\n {} Graceful shutdown initiated...", "\u{1f6d1}".red());
cancellation_token.cancel();
});
}

View File

@ -0,0 +1,327 @@
use core::iter::once;
use std::sync::Arc;
use redb::{Database, ReadableTable as _, ReadableTableMetadata as _, TableDefinition};
use thiserror::Error;
use tokio::sync::RwLock;
const ACCOUNTS_TABLE: TableDefinition<&str, u64> = TableDefinition::new("accounts");
const STATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("state");
const COUNTER_TABLE: TableDefinition<&str, u64> = TableDefinition::new("counters");
// Queue table: key is tx_id, value is JSON-serialized PendingTransfer
const QUEUE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("queue");
// Transactions table: key is tx_id, value is JSON-serialized Transaction
const TRANSACTIONS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("transactions");
const LAST_MSG_ID_KEY: &str = "last_msg_id";
const BLOCK_ID_KEY: &str = "block_id";
const TX_INDEX_KEY: &str = "tx_index";
#[derive(Debug, Error)]
pub enum DbError {
#[error("Database error: {0}")]
Database(#[from] redb::DatabaseError),
#[error("Transaction error: {0}")]
Transaction(#[from] Box<redb::TransactionError>),
#[error("Table error: {0}")]
Table(#[from] redb::TableError),
#[error("Storage error: {0}")]
Storage(#[from] redb::StorageError),
#[error("Commit error: {0}")]
Commit(#[from] redb::CommitError),
#[error("Insufficient balance: account {account} has {balance}, needs {required}")]
InsufficientBalance {
account: String,
balance: u64,
required: u64,
},
#[error("Cannot transfer to self: {account}")]
SelfTransfer { account: String },
}
impl From<redb::TransactionError> for DbError {
fn from(err: redb::TransactionError) -> Self {
Self::Transaction(Box::new(err))
}
}
pub type Result<T> = std::result::Result<T, DbError>;
#[derive(Clone)]
pub struct AccountDb {
db: Arc<RwLock<Database>>,
initial_balance: u64,
}
impl AccountDb {
pub fn new(path: &str, initial_balance: u64) -> Result<Self> {
let db = Database::create(path)?;
// Create the tables if they don't exist
let write_txn = db.begin_write()?;
{
drop(write_txn.open_table(ACCOUNTS_TABLE)?);
drop(write_txn.open_table(STATE_TABLE)?);
drop(write_txn.open_table(COUNTER_TABLE)?);
drop(write_txn.open_table(QUEUE_TABLE)?);
drop(write_txn.open_table(TRANSACTIONS_TABLE)?);
};
write_txn.commit()?;
Ok(Self {
db: Arc::new(RwLock::new(db)),
initial_balance,
})
}
/// Get the balance of an account, creating it with initial balance if it
/// doesn't exist.
pub async fn get_or_create_balance(&self, account: &str) -> Result<u64> {
let write_txn = self.db.write().await.begin_write()?;
let balance = {
let mut table = write_txn.open_table(ACCOUNTS_TABLE)?;
if let Some(existing) = table.get(account)? {
existing.value()
} else {
// New account - initialize with initial balance
table.insert(account, self.initial_balance)?;
self.initial_balance
}
};
write_txn.commit()?;
Ok(balance)
}
/// Transfer amount from one account to another.
/// Creates accounts with initial balance if they don't exist.
/// Returns error if sender has insufficient balance or if from == to.
pub async fn transfer(&self, from: &str, to: &str, amount: u64) -> Result<(u64, u64)> {
self.try_apply_transfers(once((from, to, amount))).await?;
let read_txn = self
.db
.read()
.await
.begin_read()?
.open_table(ACCOUNTS_TABLE)?;
Ok((
read_txn.get(from)?.unwrap().value(),
read_txn.get(to)?.unwrap().value(),
))
}
pub async fn try_apply_transfers(
&self,
transfers: impl Iterator<Item = (&str, &str, u64)>,
) -> Result<()> {
let write_txn = self.db.write().await.begin_write()?;
{
let mut table = write_txn.open_table(ACCOUNTS_TABLE)?;
for (from, to, amount) in transfers {
if from == to {
return Err(DbError::SelfTransfer {
account: from.to_owned(),
});
}
// Get or create 'from' account balance
let from_balance = if let Some(existing) = table.get(&from)? {
existing.value()
} else {
table.insert(&from, self.initial_balance)?;
self.initial_balance
};
// Check if sender has enough balance
if from_balance < amount {
return Err(DbError::InsufficientBalance {
account: from.to_owned(),
balance: from_balance,
required: amount,
});
}
// Get or create 'to' account balance
let to_balance = if let Some(existing) = table.get(&to)? {
existing.value()
} else {
table.insert(&to, self.initial_balance)?;
self.initial_balance
};
// Perform the transfer
let new_from_balance = from_balance - amount;
let new_to_balance = to_balance + amount;
table.insert(&from, new_from_balance)?;
table.insert(&to, new_to_balance)?;
}
}
write_txn.commit()?;
Ok(())
}
/// List all accounts and their balances
pub async fn list_accounts(&self) -> Result<Vec<(String, u64)>> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(ACCOUNTS_TABLE)?;
let mut accounts = Vec::new();
for entry in table.iter()? {
let (key, value) = entry?;
accounts.push((key.value().to_owned(), value.value()));
}
Ok(accounts)
}
/// Get the last message ID, returns None if not set (use root)
pub async fn get_last_msg_id(&self) -> Result<Option<[u8; 32]>> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(STATE_TABLE)?;
if let Some(value) = table.get(LAST_MSG_ID_KEY)? {
let bytes = value.value();
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(bytes);
return Ok(Some(arr));
}
}
Ok(None)
}
/// Set the last message ID
pub async fn set_last_msg_id(&self, msg_id: &[u8; 32]) -> Result<()> {
let write_txn = self.db.write().await.begin_write()?;
{
let mut table = write_txn.open_table(STATE_TABLE)?;
table.insert(LAST_MSG_ID_KEY, msg_id.as_slice())?;
};
write_txn.commit()?;
Ok(())
}
/// Get the next block ID and increment the counter
/// Returns (`new_block_id`, `parent_block_id`) where parent is 0 for
/// genesis
pub async fn next_block_id(&self) -> Result<(u64, u64)> {
let write_txn = self.db.write().await.begin_write()?;
let (block_id, parent_id) = {
let mut table = write_txn.open_table(COUNTER_TABLE)?;
let current = table.get(BLOCK_ID_KEY)?.map_or(0, |v| v.value());
let next = current + 1;
table.insert(BLOCK_ID_KEY, next)?;
(next, current)
};
write_txn.commit()?;
Ok((block_id, parent_id))
}
/// Get the next transaction index and increment the counter
pub async fn next_tx_index(&self) -> Result<u64> {
let write_txn = self.db.write().await.begin_write()?;
let tx_index = {
let mut table = write_txn.open_table(COUNTER_TABLE)?;
let current = table.get(TX_INDEX_KEY)?.map_or(0, |v| v.value());
let next = current + 1;
table.insert(TX_INDEX_KEY, next)?;
next
};
write_txn.commit()?;
Ok(tx_index)
}
/// Add a pending transfer to the queue
pub async fn queue_push(&self, tx_id: &str, data: &[u8]) -> Result<()> {
let write_txn = self.db.write().await.begin_write()?;
{
let mut table = write_txn.open_table(QUEUE_TABLE)?;
table.insert(tx_id, data)?;
};
write_txn.commit()?;
Ok(())
}
/// Get all pending transfers from the queue and clear it
pub async fn queue_drain(&self) -> Result<Vec<(String, Vec<u8>)>> {
let write_txn = self.db.write().await.begin_write()?;
let items = {
let mut table = write_txn.open_table(QUEUE_TABLE)?;
let mut items = Vec::new();
for entry in table.iter()? {
let (key, value) = entry?;
items.push((key.value().to_owned(), value.value().to_vec()));
}
// Clear the queue
for (key, _) in &items {
table.remove(key.as_str())?;
}
items
};
write_txn.commit()?;
Ok(items)
}
/// Check if queue is empty
pub async fn queue_is_empty(&self) -> Result<bool> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(QUEUE_TABLE)?;
Ok(table.is_empty()?)
}
/// Get queue length
pub async fn queue_len(&self) -> Result<u64> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(QUEUE_TABLE)?;
Ok(table.len()?)
}
/// Save a transaction to the transactions table
pub async fn save_transaction(&self, tx_id: &str, data: &[u8]) -> Result<()> {
let write_txn = self.db.write().await.begin_write()?;
{
let mut table = write_txn.open_table(TRANSACTIONS_TABLE)?;
table.insert(tx_id, data)?;
};
write_txn.commit()?;
Ok(())
}
pub async fn delete_transaction(&self, tx_id: &str) -> Result<()> {
let write_txn = self.db.write().await.begin_write()?;
{
let mut table = write_txn.open_table(TRANSACTIONS_TABLE)?;
table.remove(tx_id)?;
};
write_txn.commit()?;
Ok(())
}
/// Get all raw transaction data from the database
pub async fn get_all_transactions_raw(&self) -> Result<Vec<Vec<u8>>> {
let read_txn = self.db.read().await.begin_read()?;
let table = read_txn.open_table(TRANSACTIONS_TABLE)?;
let mut transactions = Vec::new();
for entry in table.iter()? {
let (_key, value) = entry?;
transactions.push(value.value().to_vec());
}
Ok(transactions)
}
#[must_use]
pub const fn initial_balance(&self) -> u64 {
self.initial_balance
}
}

View File

@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
pub mod db;
/// Request to transfer funds between accounts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferRequest {
pub from: String,
pub to: String,
pub amount: u64,
}
/// Transaction with unique ID for on-chain inscription
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub id: String,
pub from: String,
pub to: String,
pub amount: u64,
#[serde(default)]
pub confirmed: bool,
#[serde(default)]
pub index: u64,
}
/// Response after successful transfer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferResponse {
pub from_balance: u64,
pub to_balance: u64,
pub tx_hash: String,
}
/// Block data format inscribed on-chain
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockData {
pub block_id: u64,
/// Parent block ID (0 for genesis)
#[serde(default)]
pub parent_block_id: u64,
pub transactions: Vec<Transaction>,
}

View File

@ -0,0 +1,114 @@
mod api;
mod config;
mod ctrl_c;
mod sequencer;
use std::sync::Arc;
use demo_sequencer::db::AccountDb;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _};
use crate::{api::create_router, config::Config, ctrl_c::listen_for_sigint, sequencer::Sequencer};
fn print_banner() {
const BLUE: &str = "\x1b[38;5;39m";
const RESET: &str = "\x1b[0m";
println!(
r"
{BLUE} __ __ _____ _ _
| \/ | ___ _ __ ___ / ____| |__ __ _(_)_ __
| |\/| |/ _ \ '_ ` _ \| | | '_ \ / _` | | '_ \
| | | | __/ | | | | | |____| | | | (_| | | | | |
|_| |_|\___|_| |_| |_|\_____|_| |_|\__,_|_|_| |_|
____
/ ___| ___ __ _ _ _ ___ _ __ ___ ___ _ __
\___ \ / _ \/ _` | | | |/ _ \ '_ \ / __/ _ \ '__|
___) | __/ (_| | |_| | __/ | | | (_| __/ |
|____/ \___|\__, |\__,_|\___|_| |_|\___\___|_|
|_|{RESET}
"
);
}
#[tokio::main]
async fn main() {
// Initialize tracing
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(tracing_subscriber::fmt::layer())
.init();
print_banner();
info!("MemChainSequencer starting up...");
// Load configuration
let config = Config::from_env();
info!("Configuration");
info!(" HTTP API: {}", config.listen_addr);
info!(" Nomos Node: {}", config.node_endpoint);
info!(" Database: {}", config.db_path);
info!(" Channel ID: {}", config.channel_id);
info!(" Initial funds: {} tokens", config.initial_balance);
// Initialize database
let db = match AccountDb::new(&config.db_path, config.initial_balance) {
Ok(db) => db,
Err(e) => {
error!("Database initialization failed: {e}");
std::process::exit(1);
}
};
info!("Database ready");
// Initialize sequencer
let sequencer = match Sequencer::new(
db,
&config.node_endpoint,
&config.signing_key_path,
&config.channel_id,
config.node_auth_username,
config.node_auth_password,
) {
Ok(s) => Arc::new(s),
Err(e) => {
error!("Sequencer initialization failed: {e}");
std::process::exit(1);
}
};
info!("Sequencer ready");
// Setup cancellation token for graceful shutdown
let cancellation_token = CancellationToken::new();
listen_for_sigint(cancellation_token.clone());
// Spawn background processing loop
let sequencer_clone = Arc::clone(&sequencer);
tokio::spawn(async move {
sequencer_clone.run_processing_loop().await;
});
info!("Background processor started");
// Create HTTP router
let app = create_router(sequencer);
// Start HTTP server
info!("MemChainSequencer listening on {}", config.listen_addr);
let listener = match tokio::net::TcpListener::bind(config.listen_addr).await {
Ok(l) => l,
Err(e) => {
error!("Failed to bind: {e}");
std::process::exit(1);
}
};
let shutdown_signal = cancellation_token.cancelled_owned();
if let Err(e) = axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal)
.await
{
error!("Server error: {e}");
std::process::exit(1);
}
}

View File

@ -0,0 +1,625 @@
use std::{collections::HashSet, fs, io, path::Path, time::Duration};
use common_http_client::CommonHttpClient;
use demo_sequencer::{
BlockData, Transaction, TransferRequest, TransferResponse,
db::{AccountDb, DbError},
};
use key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
use nomos_core::{
header::HeaderId,
mantle::{
MantleTx, SignedMantleTx, Transaction as _,
ledger::Tx as LedgerTx,
ops::{
Op, OpProof,
channel::{ChannelId, Ed25519PublicKey, MsgId, inscribe::InscriptionOp},
},
tx::TxHash,
},
};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::time::sleep;
use tracing::{debug, info, warn};
#[derive(Debug, Error)]
pub enum SequencerError {
#[error("Database error: {0}")]
Db(#[from] Box<DbError>),
#[error("HTTP client error: {0}")]
Http(#[from] common_http_client::Error),
#[error("URL parse error: {0}")]
Url(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Invalid key file: expected {expected} bytes, got {actual}")]
InvalidKeyFile { expected: usize, actual: usize },
#[error("{0}")]
InvalidChannelId(String),
#[error("Transaction not included after timeout")]
Timeout,
#[error("Serialization error: {0}")]
Serialization(String),
}
impl From<DbError> for SequencerError {
fn from(err: DbError) -> Self {
Self::Db(Box::new(err))
}
}
pub type Result<T> = std::result::Result<T, SequencerError>;
/// Pending transfer stored in the DB queue
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingTransfer {
pub tx_id: String,
#[serde(default)]
pub tx_index: u64,
pub request: TransferRequest,
pub from_balance: u64,
pub to_balance: u64,
}
/// The sequencer that handles transactions
pub struct Sequencer {
db: AccountDb,
http_client: CommonHttpClient,
node_url: Url,
signing_key: Ed25519Key,
channel_id: ChannelId,
}
const MAX_DEPTH_PER_POLL: usize = 50;
fn empty_ledger_signature(tx_hash: &TxHash) -> key_management_system_service::keys::ZkSignature {
key_management_system_service::keys::ZkKey::multi_sign(&[], tx_hash.as_ref())
.expect("multi-sign with empty key set works")
}
/// Load signing key from file or generate a new one if it doesn't exist
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
if path.exists() {
debug!("Loading existing signing key from {:?}", path);
let key_bytes = fs::read(path)?;
if key_bytes.len() != ED25519_SECRET_KEY_SIZE {
return Err(SequencerError::InvalidKeyFile {
expected: ED25519_SECRET_KEY_SIZE,
actual: key_bytes.len(),
});
}
let key_array: [u8; ED25519_SECRET_KEY_SIZE] =
key_bytes.try_into().expect("length already checked");
Ok(Ed25519Key::from_bytes(&key_array))
} else {
debug!("Generating new signing key and saving to {:?}", path);
let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);
fs::write(path, key_bytes)?;
Ok(Ed25519Key::from_bytes(&key_bytes))
}
}
impl Sequencer {
pub fn new(
db: AccountDb,
node_endpoint: &str,
signing_key_path: &str,
channel_id_str: &str,
node_auth_username: Option<String>,
node_auth_password: Option<String>,
) -> Result<Self> {
let node_url = Url::parse(node_endpoint).map_err(|e| SequencerError::Url(e.to_string()))?;
let basic_auth = node_auth_username.map(|username| {
common_http_client::BasicAuthCredentials::new(username, node_auth_password)
});
let http_client = CommonHttpClient::new(basic_auth);
// Load or generate the signing key
let signing_key = load_or_create_signing_key(Path::new(signing_key_path))?;
// Decode channel ID from 64-character hex string (32 bytes)
let decoded = hex::decode(channel_id_str).map_err(|_| {
SequencerError::InvalidChannelId(format!(
"SEQUENCER_CHANNEL_ID must be a valid hex string, got: '{channel_id_str}'"
))
})?;
let channel_bytes: [u8; 32] = decoded.try_into().map_err(|v: Vec<u8>| {
SequencerError::InvalidChannelId(format!(
"SEQUENCER_CHANNEL_ID must be exactly 64 hex characters (32 bytes), got {} characters ({} bytes)",
v.len() * 2,
v.len()
))
})?;
let channel_id = ChannelId::from(channel_bytes);
info!("Channel ID: {}", hex::encode(channel_id.as_ref()));
Ok(Self {
db,
http_client,
node_url,
signing_key,
channel_id,
})
}
/// Get the last message ID from the database, or root if not set
async fn get_last_msg_id(&self) -> Result<MsgId> {
(self.db.get_last_msg_id().await?)
.map_or_else(|| Ok(MsgId::root()), |bytes| Ok(MsgId::from(bytes)))
}
/// Save the last message ID to the database
async fn set_last_msg_id(&self, msg_id: MsgId) -> Result<()> {
let bytes: [u8; 32] = msg_id.into();
self.db.set_last_msg_id(&bytes).await?;
Ok(())
}
/// Create and sign a transaction for inscribing data
fn create_inscribe_tx(&self, data: Vec<u8>, parent: MsgId) -> SignedMantleTx {
let verifying_key_bytes = self.signing_key.public_key().to_bytes();
let verifying_key =
Ed25519PublicKey::from_bytes(&verifying_key_bytes).expect("valid ed25519 public key");
let inscribe_op = InscriptionOp {
channel_id: self.channel_id,
inscription: data,
parent,
signer: verifying_key,
};
let ledger_tx = LedgerTx::new(vec![], vec![]);
let inscribe_tx = MantleTx {
ops: vec![Op::ChannelInscribe(inscribe_op)],
ledger_tx,
storage_gas_price: 0,
execution_gas_price: 0,
};
let tx_hash = inscribe_tx.hash();
let signature_bytes = self
.signing_key
.sign_payload(tx_hash.as_signing_bytes().as_ref())
.to_bytes();
let signature =
key_management_system_service::keys::Ed25519Signature::from_bytes(&signature_bytes);
SignedMantleTx {
ops_proofs: vec![OpProof::Ed25519Sig(signature)],
ledger_tx_proof: empty_ledger_signature(&tx_hash),
mantle_tx: inscribe_tx,
}
}
/// Post a transaction to the node and wait for inclusion
async fn post_and_wait(&self, tx: &SignedMantleTx) -> Result<()> {
// Post the transaction
self.http_client
.post_transaction(self.node_url.clone(), tx.clone())
.await?;
debug!("Transaction posted, waiting for inclusion...");
// Wait for the transaction to be included
self.wait_for_inclusion(tx).await?;
Ok(())
}
fn block_contains_inscription(
block: &nomos_core::block::Block<SignedMantleTx>,
expected: &InscriptionOp,
block_id: HeaderId,
) -> bool {
for tx in block.transactions() {
for op in &tx.mantle_tx.ops {
if let Op::ChannelInscribe(inscribe) = op {
tracing::debug!(
"Found inscription: channel={}, parent={}",
hex::encode(inscribe.channel_id.as_ref()),
hex::encode(<[u8; 32]>::from(inscribe.parent))
);
if inscribe.inscription == expected.inscription
&& inscribe.channel_id == expected.channel_id
&& inscribe.parent == expected.parent
{
debug!("Transaction included in block {}", block_id);
return true;
}
}
}
}
false
}
/// Walk back from tip checking blocks for the expected inscription
async fn check_blocks_for_inscription(
&self,
expected: &InscriptionOp,
checked_blocks: &mut HashSet<HeaderId>,
tip: HeaderId,
) -> Result<bool> {
let mut current_id = Some(tip);
let mut depth = 0;
while let Some(block_id) = current_id {
if checked_blocks.contains(&block_id) || depth >= MAX_DEPTH_PER_POLL {
break;
}
let Some(block) = self
.http_client
.get_block(self.node_url.clone(), block_id)
.await?
else {
break;
};
checked_blocks.insert(block_id);
depth += 1;
tracing::debug!(
"Checking block {} (depth {}): {} transactions",
block_id,
depth,
block.transactions().len()
);
if Self::block_contains_inscription(&block, expected, block_id) {
return Ok(true);
}
current_id = Some(block.header().parent());
}
Ok(false)
}
fn get_expected_inscription(tx: &SignedMantleTx) -> &InscriptionOp {
let expected_op = tx
.mantle_tx
.ops
.first()
.expect("transaction should have at least one op");
let Op::ChannelInscribe(expected_inscription) = expected_op else {
panic!("Expected ChannelInscribe op")
};
expected_inscription
}
async fn poll_for_inclusion(
&self,
expected: &InscriptionOp,
checked_blocks: &mut HashSet<HeaderId>,
) -> Result<bool> {
let info = self
.http_client
.consensus_info(self.node_url.clone())
.await?;
tracing::debug!(
"Polling: tip={}, height={}, checked_blocks={}",
info.tip,
info.height,
checked_blocks.len()
);
self.check_blocks_for_inscription(expected, checked_blocks, info.tip)
.await
}
/// Wait for a transaction to be included in a block.
async fn wait_for_inclusion(&self, tx: &SignedMantleTx) -> Result<()> {
let expected_inscription = Self::get_expected_inscription(tx);
let timeout_duration = Duration::from_mins(5);
let poll_interval = Duration::from_millis(500);
let start = std::time::Instant::now();
let mut checked_blocks: HashSet<HeaderId> = HashSet::new();
tracing::debug!(
"Waiting for inscription: channel={}, parent={}",
hex::encode(expected_inscription.channel_id.as_ref()),
hex::encode(<[u8; 32]>::from(expected_inscription.parent))
);
while start.elapsed() < timeout_duration {
if self
.poll_for_inclusion(expected_inscription, &mut checked_blocks)
.await?
{
return Ok(());
}
sleep(poll_interval).await;
}
warn!(
"Timeout waiting for chain inclusion after {:?}",
timeout_duration
);
Err(SequencerError::Timeout)
}
/// Process a transfer request - validates, updates DB, adds to queue,
/// returns immediately
pub async fn process_transfer(&self, request: TransferRequest) -> Result<TransferResponse> {
info!(
"TRANSFER {} -> {} ({} tokens)",
request.from, request.to, request.amount
);
// Validate and update balances in the database first
let (from_balance, to_balance) = self
.db
.transfer(&request.from, &request.to, request.amount)
.await?;
// Generate transaction ID
let tx_id = {
let mut id_bytes = [0u8; 16];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut id_bytes);
hex::encode(id_bytes)
};
// Save transaction immediately with confirmed=false
let tx_index = self.db.next_tx_index().await?;
let tx = Transaction {
id: tx_id.clone(),
from: request.from.clone(),
to: request.to.clone(),
amount: request.amount,
confirmed: false,
index: tx_index,
};
let tx_data =
serde_json::to_vec(&tx).map_err(|e| SequencerError::Serialization(e.to_string()))?;
self.db.save_transaction(&tx_id, &tx_data).await?;
// Create pending transfer and serialize for DB queue
let pending = PendingTransfer {
tx_id: tx_id.clone(),
tx_index,
request,
from_balance,
to_balance,
};
let data = serde_json::to_vec(&pending)
.map_err(|e| SequencerError::Serialization(e.to_string()))?;
// Add to DB queue
self.db.queue_push(&tx_id, &data).await?;
let queue_len = self.db.queue_len().await?;
debug!("Queued tx {} (queue size: {})", tx_id, queue_len);
// Return success immediately - actual on-chain posting happens in background
Ok(TransferResponse {
from_balance,
to_balance,
tx_hash: tx_id,
})
}
/// Background processing loop - call this in a spawned task
pub async fn run_processing_loop(&self) {
let poll_interval = Duration::from_millis(100);
loop {
// Check if there are pending transfers
let is_empty = match self.db.queue_is_empty().await {
Ok(empty) => empty,
Err(e) => {
tracing::error!("Failed to check queue: {}", e);
sleep(poll_interval).await;
continue;
}
};
if is_empty {
sleep(poll_interval).await;
continue;
}
// Drain and process all pending transfers
if let Err(e) = self.process_pending_batch().await {
tracing::error!("Batch processing failed: {}", e);
}
}
}
fn deserialize_pending_transfers(items: &[(String, Vec<u8>)]) -> Vec<PendingTransfer> {
let mut pending = Vec::new();
for (tx_id, data) in items {
match serde_json::from_slice::<PendingTransfer>(data) {
Ok(p) => pending.push(p),
Err(e) => {
tracing::error!("Failed to deserialize pending transfer {}: {}", tx_id, e);
}
}
}
pending
}
async fn revert_transfers(&self, pending: &[PendingTransfer]) {
for p in pending {
if let Err(revert_err) = self
.db
.transfer(&p.request.to, &p.request.from, p.request.amount)
.await
{
tracing::error!(
"Failed to revert transfer {} -> {}: {}",
p.request.from,
p.request.to,
revert_err
);
} else {
warn!(
"REVERTED {} -> {} ({} tokens)",
p.request.from, p.request.to, p.request.amount
);
}
}
}
async fn confirm_transactions(&self, pending: &[PendingTransfer]) -> Result<()> {
for p in pending {
let tx = Transaction {
id: p.tx_id.clone(),
from: p.request.from.clone(),
to: p.request.to.clone(),
amount: p.request.amount,
confirmed: true,
index: p.tx_index,
};
let tx_data = serde_json::to_vec(&tx)
.map_err(|e| SequencerError::Serialization(e.to_string()))?;
self.db.save_transaction(&tx.id, &tx_data).await?;
}
Ok(())
}
async fn create_and_post_block(
&self,
pending: &[PendingTransfer],
) -> Result<(BlockData, MsgId)> {
let (block_id, parent_block_id) = self.db.next_block_id().await?;
let transactions: Vec<Transaction> = pending
.iter()
.map(|p| Transaction {
id: p.tx_id.clone(),
from: p.request.from.clone(),
to: p.request.to.clone(),
amount: p.request.amount,
confirmed: false,
index: p.tx_index,
})
.collect();
let block_data = BlockData {
block_id,
parent_block_id,
transactions,
};
let inscription_data = serde_json::to_vec(&block_data)
.map_err(|e| SequencerError::Serialization(e.to_string()))?;
info!(
"BLOCK #{} (parent: #{}) posting to chain ({} tx)",
block_id,
parent_block_id,
pending.len()
);
let parent = self.get_last_msg_id().await?;
let tx = self.create_inscribe_tx(inscription_data, parent);
let new_msg_id = match tx.mantle_tx.ops.first() {
Some(Op::ChannelInscribe(inscribe)) => inscribe.id(),
_ => panic!("Expected ChannelInscribe op"),
};
self.post_and_wait(&tx).await?;
Ok((block_data, new_msg_id))
}
/// Process all pending transfers as a single block
async fn process_pending_batch(&self) -> Result<()> {
let items = self.db.queue_drain().await?;
if items.is_empty() {
return Ok(());
}
let pending = Self::deserialize_pending_transfers(&items);
if pending.is_empty() {
return Ok(());
}
let count = pending.len();
debug!("Processing batch of {} transfers", count);
match self.create_and_post_block(&pending).await {
Ok((block_data, new_msg_id)) => {
self.set_last_msg_id(new_msg_id).await?;
self.confirm_transactions(&pending).await?;
info!(
"BLOCK #{} confirmed on chain ({} tx)",
block_data.block_id, count
);
Ok(())
}
Err(e) => {
self.revert_transfers(&pending).await;
self.delete_transactions(&pending).await;
Err(e)
}
}
}
async fn delete_transactions(&self, pending: &[PendingTransfer]) {
for p in pending {
if let Err(e) = self.db.delete_transaction(&p.tx_id).await {
warn!("Failed to delete transaction {}: {}", p.tx_id, e);
}
}
}
/// Get the balance of an account
pub async fn get_balance(&self, account: &str) -> Result<u64> {
Ok(self.db.get_or_create_balance(account).await?)
}
/// List all accounts
pub async fn list_accounts(&self) -> Result<Vec<(String, u64)>> {
Ok(self.db.list_accounts().await?)
}
/// Get all transactions for an account (as sender or receiver), sorted by
/// index
pub async fn get_account_transactions(&self, account: &str) -> Result<Vec<Transaction>> {
let all_txs = self.db.get_all_transactions_raw().await?;
let mut transactions: Vec<Transaction> = all_txs
.iter()
.filter_map(|data| serde_json::from_slice::<Transaction>(data).ok())
.filter(|tx| tx.from == account || tx.to == account)
.collect();
transactions.sort_by_key(|tx| tx.index);
transactions.reverse();
Ok(transactions)
}
/// Get confirmed balance based only on confirmed transactions
pub async fn get_confirmed_balance(&self, account: &str) -> Result<u64> {
let initial_balance = self.db.initial_balance();
let all_txs = self.db.get_all_transactions_raw().await?;
let mut balance = initial_balance;
for data in all_txs {
if let Ok(tx) = serde_json::from_slice::<Transaction>(&data)
&& tx.confirmed
{
if tx.from == account {
balance = balance.saturating_sub(tx.amount);
}
if tx.to == account {
balance = balance.saturating_add(tx.amount);
}
}
}
Ok(balance)
}
}

View File

@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:8080

View File

@ -0,0 +1 @@
VITE_API_BASE_URL=https://demo.testnet.nomos.tech

View File

@ -0,0 +1,21 @@
# Simple L2 frontend
A website built with Vuejs, Headless UI and Tailwind.
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

View File

@ -0,0 +1,768 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "inkaguco",
"dependencies": {
"@headlessui/vue": "^1.7.16",
"@tailwindcss/vite": "^4.1.18",
"heroicons": "^2.1.1",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue-router": "^4.2.5",
},
"devDependencies": {
"@types/bun": "latest",
"@vitejs/plugin-vue": "^4.5.2",
"autoprefixer": "latest",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-vue": "^9.19.2",
"tailwindcss": "latest",
"vite": "^5.0.10",
},
"peerDependencies": {
"typescript": "^5.0.0",
},
},
},
"packages": {
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
"@headlessui/vue": ["@headlessui/vue@1.7.23", "", { "dependencies": { "@tanstack/vue-virtual": "^3.0.0-beta.60" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.13", "", {}, "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA=="],
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.13", "", { "dependencies": { "@tanstack/virtual-core": "3.13.13" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@4.6.2", "", { "peerDependencies": { "vite": "^4.0.0 || ^5.0.0", "vue": "^3.2.25" } }, "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.25", "", { "dependencies": { "@vue/compiler-core": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.25", "@vue/compiler-dom": "3.5.25", "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/reactivity": ["@vue/reactivity@3.5.25", "", { "dependencies": { "@vue/shared": "3.5.25" } }, "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/runtime-core": "3.5.25", "@vue/shared": "3.5.25", "csstype": "^3.1.3" } }, "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.25", "", { "dependencies": { "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "vue": "3.5.25" } }, "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ=="],
"@vue/shared": ["@vue/shared@3.5.25", "", {}, "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
"eslint-config-airbnb-base": ["eslint-config-airbnb-base@15.0.0", "", { "dependencies": { "confusing-browser-globals": "^1.0.10", "object.assign": "^4.1.2", "object.entries": "^1.1.5", "semver": "^6.3.0" }, "peerDependencies": { "eslint": "^7.32.0 || ^8.2.0", "eslint-plugin-import": "^2.25.2" } }, "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
"eslint-plugin-vue": ["eslint-plugin-vue@9.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "globals": "^13.24.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw=="],
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"heroicons": ["heroicons@2.2.0", "", {}, "sha512-yOwvztmNiBWqR946t+JdgZmyzEmnRMC2nxvHFC90bF1SUttwB6yJKYeme1JeEcBfobdOs827nCyiWBS2z/brog=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
"object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"pinia": ["pinia@2.3.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vue": ["vue@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", "@vue/runtime-dom": "3.5.25", "@vue/server-renderer": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g=="],
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
"vue-eslint-parser": ["vue-eslint-parser@9.4.3", "", { "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", "eslint-visitor-keys": "^3.3.0", "espree": "^9.3.1", "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg=="],
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"eslint-plugin-vue/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"vue-eslint-parser/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LOGOS COOKIE CHAIN L2 BLAZING FAST</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,37 @@
{
"name": "inkaguco",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --mode dev",
"build": "vite build",
"build:dev": "vite build --mode dev",
"build:testnet": "vite build --mode testnet",
"preview": "vite preview",
"lint": "eslint --fix src/"
},
"dependencies": {
"@headlessui/vue": "^1.7.16",
"@tailwindcss/vite": "^4.1.18",
"heroicons": "^2.1.1",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@types/bun": "latest",
"@vitejs/plugin-vue": "^4.5.2",
"autoprefixer": "latest",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-vue": "^9.19.2",
"tailwindcss": "latest",
"vite": "^5.0.10"
},
"module": "index.ts",
"peerDependencies": {
"typescript": "^5.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,527 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="4.4406605mm"
height="3.803216mm"
viewBox="0 0 4.4406605 3.803216"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<filter
style="color-interpolation-filters:sRGB"
id="filter357"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix356" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix357" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter359"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix358" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix359" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter361"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix360" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix361" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter363"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix362" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix363" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter365"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix364" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix365" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter367"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix366" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix367" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter369"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix368" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix369" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter371"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix370" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix371" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
id="filter373"
x="0"
y="0"
width="1"
height="1">
<feColorMatrix
type="hueRotate"
values="180"
result="color1"
id="feColorMatrix372" />
<feColorMatrix
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.21 -0.72 -0.07 2 0 "
result="color2"
id="feColorMatrix373" />
</filter>
</defs>
<g
id="layer1"
transform="translate(-75.095468,-77.508589)">
<g
id="g27"
transform="rotate(42.189961,59.167567,102.48634)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.16"
id="rect419"
width="8.4005213"
height="6.6807294"
x="-3.8882036"
y="95.595848"
transform="rotate(-42.189961)" />
<rect
style="fill:#000000;fill-opacity:1;stroke-width:0.16"
id="rect418"
width="8.4005213"
height="6.6807294"
x="-2.5945358"
y="89.80809"
transform="rotate(-42.189961)" />
<rect
style="fill:#000080;fill-opacity:1;stroke-width:0.16"
id="rect19"
width="2.8371327"
height="0.52539498"
x="46.383415"
y="73.725441" />
<rect
style="fill:#ff8080;fill-opacity:1;stroke-width:0.16"
id="rect20"
width="0.46535105"
height="1.2634716"
x="46.383415"
y="73.988136" />
<rect
style="fill:#ff2a2a;fill-opacity:1;stroke-width:0.16"
id="rect21"
width="0.46535105"
height="1.2634716"
x="48.019646"
y="73.988136" />
<rect
style="fill:#00ff00;fill-opacity:1;stroke-width:0.16"
id="rect22"
width="0.46535105"
height="3.228164"
x="47.118965"
y="71.769485" />
<rect
style="fill:#ffaaaa;fill-opacity:1;stroke-width:0.16"
id="rect23"
width="0.46535105"
height="3.228164"
x="48.755196"
y="71.769485" />
<rect
style="fill:#ff6600;fill-opacity:1;stroke-width:0.160001"
id="rect24"
width="0.46535105"
height="1.6362293"
x="71.769485"
y="-48.755196"
transform="rotate(90)" />
<rect
style="fill:#ffff00;fill-opacity:1;stroke-width:0.160001"
id="rect25"
width="0.46535105"
height="1.6362293"
x="73.49276"
y="-48.904259"
transform="rotate(90)" />
<rect
style="fill:#008080;fill-opacity:1;stroke-width:0.161158"
id="rect26"
width="0.21415083"
height="1.0134755"
x="82.0989"
y="-19.439381"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#010101;fill-opacity:1;stroke-width:0.161158"
id="rect27"
width="0.21415083"
height="1.0134755"
x="82.46711"
y="-20.779257"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.16"
id="rect28"
width="2.8371327"
height="0.52539498"
x="51.404686"
y="69.5159" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.16"
id="rect29"
width="0.46535105"
height="1.2634716"
x="51.404686"
y="69.778595" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.16"
id="rect30"
width="0.46535105"
height="1.2634716"
x="53.040916"
y="69.778595" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.16"
id="rect31"
width="0.46535105"
height="3.228164"
x="52.140236"
y="67.559944" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.16"
id="rect32"
width="0.46535105"
height="3.228164"
x="53.776466"
y="67.559944" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.160001"
id="rect33"
width="0.46535105"
height="1.6362293"
x="67.559944"
y="-53.776466"
transform="rotate(90)" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.160001"
id="rect34"
width="0.46535105"
height="1.6362293"
x="69.283218"
y="-53.925529"
transform="rotate(90)" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.161158"
id="rect35"
width="0.21415083"
height="1.0134755"
x="78.824844"
y="-25.693598"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#241c1c;fill-opacity:1;stroke-width:0.161158"
id="rect36"
width="0.21415083"
height="1.0134755"
x="79.193054"
y="-27.033474"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#000080;fill-opacity:1;stroke-width:0.16;filter:url(#filter373)"
id="rect102"
width="2.8371327"
height="0.52539498"
x="50.074295"
y="77.797348" />
<rect
style="fill:#ff8080;fill-opacity:1;stroke-width:0.16;filter:url(#filter371)"
id="rect103"
width="0.46535105"
height="1.2634716"
x="50.074295"
y="78.060043" />
<rect
style="fill:#ff2a2a;fill-opacity:1;stroke-width:0.16;filter:url(#filter369)"
id="rect104"
width="0.46535105"
height="1.2634716"
x="51.710526"
y="78.060043" />
<rect
style="fill:#00ff00;fill-opacity:1;stroke-width:0.16;filter:url(#filter367)"
id="rect105"
width="0.46535105"
height="3.228164"
x="50.809845"
y="75.841393" />
<rect
style="fill:#ffaaaa;fill-opacity:1;stroke-width:0.16;filter:url(#filter365)"
id="rect106"
width="0.46535105"
height="3.228164"
x="52.446075"
y="75.841393" />
<rect
style="fill:#ff6600;fill-opacity:1;stroke-width:0.160001;filter:url(#filter363)"
id="rect107"
width="0.46535105"
height="1.6362293"
x="75.841393"
y="-52.446075"
transform="rotate(90)" />
<rect
style="fill:#ffff00;fill-opacity:1;stroke-width:0.160001;filter:url(#filter361)"
id="rect108"
width="0.46535105"
height="1.6362293"
x="77.564667"
y="-52.595139"
transform="rotate(90)" />
<rect
style="fill:#008080;fill-opacity:1;stroke-width:0.161158;filter:url(#filter359)"
id="rect109"
width="0.21415083"
height="1.0134755"
x="86.841415"
y="-21.529877"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#010101;fill-opacity:1;stroke-width:0.161158;filter:url(#filter357)"
id="rect110"
width="0.21415083"
height="1.0134755"
x="87.209625"
y="-22.869753"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#c5ee00;fill-opacity:1;stroke-width:0.16"
id="rect400"
width="2.8371327"
height="0.52539498"
x="55.595005"
y="73.302124" />
<rect
style="fill:#80a3ff;fill-opacity:1;stroke-width:0.16"
id="rect401"
width="0.46535105"
height="1.2634716"
x="55.595005"
y="73.564819" />
<rect
style="fill:#6aedff;fill-opacity:1;stroke-width:0.16"
id="rect402"
width="0.46535105"
height="1.2634716"
x="57.231236"
y="73.564819" />
<rect
style="fill:#ff00e7;fill-opacity:1;stroke-width:0.16"
id="rect403"
width="0.46535105"
height="3.228164"
x="56.330555"
y="71.346169" />
<rect
style="fill:#00cf7c;fill-opacity:1;stroke-width:0.16"
id="rect404"
width="0.46535105"
height="3.228164"
x="57.966785"
y="71.346169" />
<rect
style="fill:#00afff;fill-opacity:1;stroke-width:0.160001"
id="rect405"
width="0.46535105"
height="1.6362293"
x="71.346169"
y="-57.966785"
transform="rotate(90)" />
<rect
style="fill:#002fff;fill-opacity:1;stroke-width:0.160001"
id="rect406"
width="0.46535105"
height="1.6362293"
x="73.069443"
y="-58.115849"
transform="rotate(90)" />
<rect
style="fill:#ff8c00;fill-opacity:1;stroke-width:0.161158"
id="rect407"
width="0.21415083"
height="1.0134755"
x="83.374405"
y="-28.359638"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.161158"
id="rect408"
width="0.21415083"
height="1.0134755"
x="83.742615"
y="-29.699514"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#f7ff66;fill-opacity:1;stroke-width:0.16"
id="rect409"
width="2.8371327"
height="0.52539498"
x="61.030411"
y="68.464607" />
<rect
style="fill:#80a3ff;fill-opacity:1;stroke-width:0.16"
id="rect410"
width="0.46535105"
height="1.2634716"
x="61.030411"
y="68.727303" />
<rect
style="fill:#6aedff;fill-opacity:1;stroke-width:0.16"
id="rect411"
width="0.46535105"
height="1.2634716"
x="62.666641"
y="68.727303" />
<rect
style="fill:#ff00e7;fill-opacity:1;stroke-width:0.16"
id="rect412"
width="0.46535105"
height="3.228164"
x="61.765961"
y="66.508652" />
<rect
style="fill:#aaffdd;fill-opacity:1;stroke-width:0.16"
id="rect413"
width="0.46535105"
height="3.228164"
x="63.402191"
y="66.508652" />
<rect
style="fill:#00afff;fill-opacity:1;stroke-width:0.160001"
id="rect414"
width="0.46535105"
height="1.6362293"
x="66.508652"
y="-63.402191"
transform="rotate(90)" />
<rect
style="fill:#002fff;fill-opacity:1;stroke-width:0.160001"
id="rect415"
width="0.46535105"
height="1.6362293"
x="68.231926"
y="-63.551254"
transform="rotate(90)" />
<rect
style="fill:#ff8c00;fill-opacity:1;stroke-width:0.161158"
id="rect416"
width="0.21415083"
height="1.0134755"
x="79.550194"
y="-35.227894"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.161158"
id="rect417"
width="0.21415083"
height="1.0134755"
x="79.918404"
y="-36.567772"
transform="matrix(0.3447906,0.93867963,-0.98335818,0.18167745,0,0)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,18 @@
<template>
<NavBar class="fixed top-0 pl-11 pr-11 left-0 right-0 z-10 h-16 mx-auto navbar" />
<RouterView />
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { RouterView } from 'vue-router'
import NavBar from './components/navbar/NavbarCompact.vue'
</script>
<style scoped>
.navbar {
max-width: var(--app-max-w);
margin: 1.5rem auto;
}
</style>

View File

@ -0,0 +1,104 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #000000;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-border-neutral: var(--vt-c-white-mute);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
--app-max-w: 1280px;
}
.dark {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-border-neutral: var(--vt-c-black-mute);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
/* Only when not using tailwind dark=class strategy.
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*/
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
outline: none;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 20px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="194" height="194" version="1.1">
<circle fill="#000000" cx="97" cy="97" r="97" />
<path fill="#ffffff" d="m 94,9.2 a 88,88 0 0 0 -55,21.8 l 27,0 28,-14.4 0,-7.4 z m 6,0 0,7.4 28,14.4 27,0 a 88,88 0 0 0 -55,-21.8 z m -67.2,27.8 a 88,88 0 0 0 -20,34.2 l 16,27.6 23,-3.6 21,-36.2 -8.4,-22 -31.6,0 z m 96.8,0 -8.4,22 21,36.2 23,3.6 15.8,-27.4 a 88,88 0 0 0 -19.8,-34.4 l -31.6,0 z m -50,26 -20.2,35.2 17.8,30.8 39.6,0 17.8,-30.8 -20.2,-35.2 -34.8,0 z m -68.8,16.6 a 88,88 0 0 0 -1.8,17.4 88,88 0 0 0 10.4,41.4 l 7.4,-4.4 -1.4,-29 -14.6,-25.4 z m 172.4,0.2 -14.6,25.2 -1.4,29 7.4,4.4 a 88,88 0 0 0 10.4,-41.4 88,88 0 0 0 -1.8,-17.2 z m -106,57.2 -15.4,19 L 77.2,182.6 a 88,88 0 0 0 19.8,2.4 88,88 0 0 0 19.8,-2.4 l 15.4,-26.6 -15.4,-19 -39.6,0 z m -47.8,2.6 -7,4 A 88,88 0 0 0 68.8,180.4 l -14,-24.6 -25.4,-16.2 z m 135.2,0 -25.4,16.2 -14,24.4 a 88,88 0 0 0 46.4,-36.6 l -7,-4 z"/>
</svg>

After

Width:  |  Height:  |  Size: 972 B

View File

@ -0,0 +1,53 @@
@import './base.css';
#app {
max-width: var(--app-max-w);
width: 100%;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
.weird-img {
width: 100%;
max-width: 500px;
height: auto;
transform-origin: bottom center;
animation: wiggle 7s ease-in-out infinite;
z-index: -10;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
padding: 0 2rem;
}
}
@keyframes wiggle {
0%, 100% {
transform: rotate(-3deg);
}
50% {
transform: rotate(3deg);
}
}

View File

@ -0,0 +1,6 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.98 10.503 10.503 0 0 1-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 0 1 .818.162Z" clip-rule="evenodd" />
</svg>
</template>

View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.59 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6 12a.75.75 0 0 1-.75.75H3a.75.75 0 0 1 0-1.5h2.25A.75.75 0 0 1 6 12ZM6.697 7.757a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 0 0-1.061 1.06l1.59 1.591Z" />
</svg>
</template>

View File

@ -0,0 +1,4 @@
import IconMoon from './IconMoon.vue';
import IconSun from './IconSun.vue';
export { IconMoon, IconSun };

View File

@ -0,0 +1,4 @@
export * from './icons';
export * from './navbar';
export * from './ui';
export * from './wallet';

View File

@ -0,0 +1,52 @@
<template>
<div class="fixed top-4 left-0 right-0 z-50 flex justify-center px-4">
<nav class="flex items-center justify-between w-full px-4 sm:px-6 py-3 rounded-full bg-white dark:bg-black border border-gray-100 dark:border-gray-800 transition-all duration-300">
<div class="flex items-center gap-8">
<SpinningLogo class="h-7 w-7 sm:h-8 sm:w-8 flex-shrink-0 mr-8 sm:mr-16" />
<div class="flex items-center gap-8 sm:gap-16 text-[11px] sm:text-sm font-bold uppercase tracking-wider">
<RouterLink
to="/"
class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors whitespace-nowrap"
active-class="text-gray-900 dark:text-white"
>
Wallet
</RouterLink>
<div class="h-4 w-[1px] bg-gray-200 dark:bg-gray-700"></div>
<RouterLink
to="/archive"
class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors whitespace-nowrap"
active-class="text-gray-900 dark:text-white"
>
Archive
</RouterLink>
</div>
</div>
<div class="flex items-center space-x-3 sm:space-x-5">
<a
href="https://github.com/logos-blockchain/logos-blockchain"
class="hover:opacity-70 transition-opacity flex-shrink-0"
target="_blank"
rel="noopener noreferrer"
>
<ThemeImg><img :src="githubSrc" class="h-4 w-4 sm:h-5 sm:w-5"/></ThemeImg>
</a>
<ThemeSwitch class="flex-shrink-0" />
</div>
</nav>
</div>
</template>
<script setup>
import { RouterLink } from 'vue-router';
import {
ThemeImg,
SpinningLogo,
ThemeSwitch
} from '@/components';
import githubSrc from '@/assets/github-mark.svg';
</script>

View File

@ -0,0 +1,35 @@
<template>
<RouterLink
to="/"
active-class="noop-link"
exact-active-class="noop-link"
class="noop-link">
<ThemeImg>
<img :src="logoSrc" :style="{ height: '20px', transform: `rotate(${logoRotation}deg)` }">
</ThemeImg>
</RouterLink>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ThemeImg } from '@/components/ui';
import logoSrc from '@/assets/logo.svg';
const logoRotation = ref(0);
let lastScrollTop = 0;
const handleScroll = () => {
const st = window.pageYOffset || document.documentElement.scrollTop;
const scrollDelta = st - lastScrollTop;
lastScrollTop = st <= 0 ? 0 : st;
logoRotation.value += scrollDelta * 0.9;
};
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
</script>

View File

@ -0,0 +1,50 @@
<template>
<Switch :modelValue="enabled" @update:modelValue="toggleTheme">
<template v-slot:icon-enabled>
<IconMoon class="h-4 w-4 text-gray-400"/>
</template>
<template v-slot:icon-disabled>
<IconSun class="h-4 w-4"/>
</template>
</Switch>
</template>
<script setup>
import { ref, watchEffect, onMounted, onUnmounted } from 'vue';
import {
Switch,
IconMoon,
IconSun,
} from '@/components';
const enabled = ref(false);
const updateTheme = (isDarkMode) => {
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
};
const toggleTheme = () => {
enabled.value = !enabled.value;
updateTheme(enabled.value);
};
onMounted(() => {
enabled.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
updateTheme(enabled.value)
});
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)');
systemPrefersDark.addEventListener('change', (e) => {
enabled.value = e.matches;
updateTheme(e.matches);
});
onUnmounted(() => {
systemPrefersDark.removeEventListener('change', handleSystemThemeChange);
});
</script>

View File

@ -0,0 +1,7 @@
import NavbarCompact from './NavbarCompact.vue';
import SpinningLogo from './SpinningLogo.vue';
import ThemeSwitch from './ThemeSwitch.vue';
export {
NavbarCompact, SpinningLogo, ThemeSwitch,
};

View File

@ -0,0 +1,48 @@
<template>
<Switch
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
:class="{
'bg-transparent': true, // Always transparent background
// Optional: Add a subtle background color if the switch is ON/Enabled
// 'bg-gray-200': modelValue,
}"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2
border-gray-500 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-700/75"
>
<span class="sr-only">Toggle Theme</span>
<span
aria-hidden="true"
:class="modelValue ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out"
>
<div class="h-full w-full flex items-center justify-center text-gray-800">
<template v-if="modelValue">
<slot name="icon-enabled"></slot>
</template>
<template v-else>
<slot name="icon-disabled"></slot>
</template>
</div>
</span>
</Switch>
</template>
<script setup>
// ... (script remains the same)
import { Switch } from '@headlessui/vue';
defineProps({
modelValue: {
type: Boolean,
required: true,
},
});
defineEmits(['update:modelValue']);
</script>

View File

@ -0,0 +1,23 @@
<template>
<div class="w-auto" :class="{ 'invert': invertColor }">
<slot></slot>
</div>
</template>
<script setup>
const props = defineProps({
invertColor: {
type: Boolean,
default: true,
},
});
</script>
<style scoped>
.invert {
filter: none;
}
.dark .invert {
filter: invert(100%);
}
</style>

View File

@ -0,0 +1,6 @@
import ThemeImg from './ThemeImg.vue';
import Switch from './Switch.vue';
export {
ThemeImg, Switch,
};

View File

@ -0,0 +1,76 @@
<template>
<div class="block-list space-y-12">
<div
v-for="block in blocks"
:key="block.data.block_id"
class="block-container pb-20"
>
<div class="flex items-center justify-between mb-4 px-2">
<div class="flex items-center space-x-6">
<div class="bg-black dark:bg-white text-white dark:text-black px-4 py-1.5 rounded-xl font-mono font-black text-sm shadow-sm mr-5">
BLOCK #{{ block.data.block_id }}
</div>
<div class="flex flex-col pl-5">
<span class="text-[10px] text-gray-400 uppercase font-bold tracking-widest">L1 block ID</span>
<span
class="text-xs font-mono text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 truncate max-w-[150px] sm:max-w-xs cursor-pointer underline decoration-dotted"
@click="emit('openBlock', block.l1_block_id)"
:title="`View block ${block.l1_block_id} in explorer`"
>
{{ block.l1_block_id }}
</span>
</div>
<div class="flex flex-col pl-5">
<span class="text-[10px] text-gray-400 uppercase font-bold tracking-widest">L1 tx ID</span>
<span
class="text-xs font-mono text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 truncate max-w-[150px] sm:max-w-xs cursor-pointer underline decoration-dotted"
@click="emit('openTransaction', block.l1_transaction_id)"
:title="`View transaction ${block.l1_transaction_id} in explorer`"
>
{{ block.l1_transaction_id }}
</span>
</div>
</div>
<div class="hidden sm:block text-right">
<span class="text-[10px] text-gray-400 uppercase font-bold tracking-widest block">Parent</span>
<span class="text-xs font-mono text-gray-400">#{{ block.data.parent_block_id }}</span>
</div>
</div>
<TransactionList
:transactions="block.data.transactions"
:currentAccount="currentAccount"
@selectTransaction="(tx) => emit('openTransaction', tx.l1_transaction_id)"
class="mt-0"
/>
</div>
<!-- Empty state when no blocks -->
<div
v-if="!blocks || blocks.length === 0"
class="w-full p-1 border border-gray-100 dark:border-gray-400 rounded-2xl"
>
<div class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
No recent transactions found.
</div>
</div>
</div>
</template>
<script setup>
import { TransactionList } from '@/components';
defineProps({
blocks: {
type: Array,
required: true
},
currentAccount: {
type: String,
default: 'ARCHIVE_VIEW'
}
});
const emit = defineEmits(['openBlock', 'openTransaction']);
</script>

View File

@ -0,0 +1,140 @@
<template>
<div class="send-money-card flex flex-col p-6 sm:p-8 lg:p-10
rounded-2xl transition-all duration-300 w-full max-w-none border border-gray-100 dark:border-gray-400">
<div class="flex items-center justify-between mb-6 border-b pb-4 border-gray-200 dark:border-gray-700">
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
Send MEM
</h2>
<div class="flex items-center space-x-1 bg-gray-50 dark:bg-gray-800/50 p-1 rounded-xl">
<button
@click="prevRecipient"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Previous Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
@click="randomRecipient"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Random Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button
@click="nextRecipient"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Next Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
<form @submit.prevent="handleTransfer" class="space-y-6">
<div class="flex flex-col">
<label class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-1">
To Character
</label>
<p class="text-base sm:text-lg font-mono text-gray-700 dark:text-white h-8 flex items-center">
{{ transferData.to || 'Select a recipient...' }}
</p>
</div>
<div class="flex flex-col">
<label class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-2">
Amount (MEM)
</label>
<input
v-model.number="transferData.amount"
type="number"
step="0.01"
placeholder="0.00"
class="w-full p-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-transparent text-gray-900 dark:text-white focus:ring-2 focus:ring-gray-400 outline-none font-bold text-lg transition-all"
required
/>
</div>
<div class="pt-4">
<button
type="submit"
class="w-full py-4 rounded-xl bg-gray-900 dark:bg-white text-white dark:text-gray-900 font-bold text-lg hover:opacity-90 active:scale-[0.98] transition-all shadow-lg"
>
Confirm Transfer
</button>
</div>
</form>
</div>
</template>
<script setup>
import { reactive, computed, onMounted } from 'vue';
const props = defineProps({
fromAddress: {
type: String,
default: 'Tsubasa'
},
characters: {
type: Array,
default: () => ['Tsubasa', 'Hyuga', 'Misaki', 'Wakabayashi', 'Wakashimazu', 'Misugi', 'Matsuyama', 'Ishizaki']
}
});
const emit = defineEmits(['transfer']);
const transferData = reactive({
to: '',
amount: null
});
const availableRecipients = computed(() => {
return props.characters.filter(name => name !== props.fromAddress);
});
const nextRecipient = () => {
const list = availableRecipients.value;
const currentIndex = list.indexOf(transferData.to);
const nextIndex = (currentIndex + 1) % list.length;
transferData.to = list[nextIndex];
};
const prevRecipient = () => {
const list = availableRecipients.value;
const currentIndex = list.indexOf(transferData.to);
const prevIndex = (currentIndex - 1 + list.length) % list.length;
transferData.to = list[prevIndex];
};
const randomRecipient = () => {
const list = availableRecipients.value;
let newRecipient;
do {
newRecipient = list[Math.floor(Math.random() * list.length)];
} while (newRecipient === transferData.to && list.length > 1);
transferData.to = newRecipient;
};
onMounted(() => {
if (availableRecipients.value.length > 0) {
transferData.to = availableRecipients.value[0];
}
});
const handleTransfer = () => {
if (!transferData.to || !transferData.amount) return;
emit('transfer', { to: transferData.to, amount: transferData.amount });
transferData.amount = null;
};
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="transaction-list-container w-full mt-6 p-1 border border-gray-100 dark:border-gray-400 rounded-2xl">
<div>
<div class="inline-block min-w-full align-middle">
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="sticky top-0 z-10 ">
<tr>
<th scope="col" class="px-5 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-5 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Transaction Hash (ID)
</th>
<th scope="col" class="px-5 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
From / To
</th>
<th scope="col" class="px-5 py-4 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Amount
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr
v-for="tx in transactions"
:key="tx.id"
class="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition duration-150"
@click="emit('selectTransaction', tx)"
>
<td class="px-5 py-5 whitespace-nowrap text-sm">
<span
:class="[
'px-2 py-1 rounded-full text-xs font-medium',
tx.confirmed
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400 animate-pulse'
]"
>
{{ tx.confirmed ? 'Confirmed' : 'Pending' }}
</span>
</td>
<td class="px-5 py-5 text-sm text-gray-700 dark:text-gray-300 font-mono text-xs break-all max-w-[200px]">
{{ tx.id }}
</td>
<td class="px-5 py-5 text-sm text-gray-700 dark:text-gray-300">
<div class="flex flex-col">
<span class="font-mono text-xs">{{ tx.from }}</span>
<div class="text-xs text-gray-500 flex items-center">
<span class="mr-1"></span> {{ tx.to }}
</div>
</div>
</td>
<td class="px-5 py-5 whitespace-nowrap text-sm text-right font-semibold">
<span :class="tx.from === currentAccount ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'">
{{ tx.from === currentAccount ? '-' : '+' }}{{ tx.amount.toFixed(2) }} MEM
</span>
</td>
</tr>
<tr v-if="!transactions || transactions.length === 0">
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
No recent transactions found.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
transactions: {
type: Array,
required: true,
},
// We need the current account name to decide if the amount is +/-
currentAccount: {
type: String,
default: 'Alisa'
}
});
const emit = defineEmits(['selectTransaction']);
</script>

View File

@ -0,0 +1,81 @@
<template>
<div class="wallet-card flex flex-col p-6 sm:p-8 lg:p-10
rounded-2xl transition-all duration-300 w-full max-w-none border border-gray-100 dark:border-gray-400">
<div class="flex items-center justify-between mb-6 border-b pb-4 border-gray-200 dark:border-gray-700">
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
Wallet Overview
</h2>
<div class="flex items-center space-x-1 bg-gray-50 dark:bg-gray-800/50 p-1 rounded-xl">
<button
@click="emit('prev')"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Previous Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
@click="emit('regenerate')"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Random Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button
@click="emit('next')"
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm transition-all group"
title="Next Character"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
<div class="space-y-6">
<div class="flex flex-col">
<span class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-1">
Character Name
</span>
<p class="text-base sm:text-lg font-mono break-all text-gray-700 dark:text-white">
{{ walletAddress }}
</p>
</div>
<div class="flex flex-col pt-4">
<span class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-2">
Current Balance
</span>
<p class="text-4xl sm:text-5xl font-extrabold text-gray-900 dark:text-white">
{{ walletAmount }}
<span class="text-xl font-medium text-gray-500 dark:text-gray-400 ps-2">
MEM
</span>
</p>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
walletAddress: {
type: String,
required: true,
},
walletAmount: {
type: [String, Number],
required: true,
},
});
const emit = defineEmits(['regenerate', 'prev', 'next']);
</script>

View File

@ -0,0 +1,8 @@
import ArchivedBlockList from './ArchivedBlockList.vue';
import TransactionCreator from './TransactionCreator.vue';
import TransactionList from './TransactionList.vue';
import WalletInfo from './WalletInfo.vue';
export {
ArchivedBlockList, TransactionCreator, TransactionList, WalletInfo
};

View File

@ -0,0 +1,3 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

View File

@ -0,0 +1,15 @@
import './assets/main.css';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './index.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');

View File

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router';
import { ArchiveView, WalletView } from '@/views';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'wallet',
component: WalletView,
},
{
path: '/archive',
name: 'archive',
component: ArchiveView,
},
],
scrollBehavior(to, from, savedPosition) {
return { top: 0 }
},
});
export default router;

View File

@ -0,0 +1,98 @@
import { defineStore } from 'pinia';
const BASE_URL = import.meta.env.VITE_ARCHIVER_URL || 'http://localhost:8090';
const EXPLORER_URL = import.meta.env.VITE_EXPLORER_URL || 'http://localhost:8000';
const STREAM_URL = `${BASE_URL}/block_stream`;
const CACHE_URL = `${BASE_URL}/blocks`;
export const useArchiveStore = defineStore('archive', {
state: () => ({
blocks: [],
loading: false,
eventSource: null,
// Status can be: 'disconnected', 'waiting', 'connected', or 'error'
connectionStatus: 'disconnected',
}),
actions: {
processBlocks(blocks) {
return blocks.map(block => ({
...block,
data: {
...block.data,
transactions: (block.data?.transactions || []).map(tx => ({
...tx,
confirmed: true // Overwriting the pending status from the raw data
}))
}
}));
},
async fetchCachedBlocks() {
this.loading = true;
try {
const res = await fetch(CACHE_URL);
if (!res.ok) throw new Error('Failed to fetch cached blocks');
const data = await res.json();
const processed = this.processBlocks(data);
// Reverse so newest blocks (highest ID) are at the top
this.blocks = [...processed].reverse().slice(0, 50);
} catch (err) {
console.error("Error fetching cached blocks:", err);
} finally {
this.loading = false;
}
},
startStream() {
if (this.eventSource) return;
this.connectionStatus = 'connecting';
this.eventSource = new EventSource(STREAM_URL);
this.eventSource.onopen = () => {
this.connectionStatus = 'connected';
console.log("Archive Stream Connected");
};
this.eventSource.onmessage = (event) => {
try {
const rawData = JSON.parse(event.data);
const rawBlocks = Array.isArray(rawData) ? rawData : [rawData];
const processed = this.processBlocks(rawBlocks);
// Prepend new blocks to the beginning of the state
this.blocks = [...processed.reverse(), ...this.blocks].slice(0, 50);
} catch (err) {
console.error("Error parsing stream data:", err);
}
};
this.eventSource.onerror = () => {
this.connectionStatus = 'error';
this.stopStream();
setTimeout(() => this.startStream(), 5000);
};
},
stopStream() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
this.connectionStatus = 'disconnected';
}
},
openBlockInExplorer(blockId) {
if (!blockId) return;
window.open(`${EXPLORER_URL}/blocks/${blockId}`, '_blank');
},
openTransactionInExplorer(txId) {
if (!txId) return;
window.open(`${EXPLORER_URL}/transactions/${txId}`, '_blank');
},
}
});

View File

@ -0,0 +1,124 @@
import { defineStore } from 'pinia';
const BASE_URL = import.meta.env.VITE_SEQUENCER_URL || 'http://localhost:8080';
const TSUBASA_NAMES = [
'Tsubasa', 'Hyuga', 'Misaki', 'Wakabayashi',
'Wakashimazu', 'Misugi', 'Matsuyama', 'Ishizaki'
];
export const useWalletStore = defineStore('wallet', {
state: () => ({
accountName: '',
balance: 0,
confirmedBalance: 0,
transactions: [],
pollingInterval: null,
}),
actions: {
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
},
setCookie(name, value, days = 7) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = `${name}=${value}; ${expires}; path=/`;
},
async fetchAccountData() {
if (!this.accountName) return;
try {
const res = await fetch(`${BASE_URL}/accounts/${this.accountName}?tx=true`);
if (!res.ok) throw new Error('Account not found');
const data = await res.json();
this.balance = data.balance;
this.confirmedBalance = data.confirmed_balance;
this.transactions = data.transactions || [];
} catch (err) {
console.error('Polling error:', err);
}
},
async sendTransaction(payload) {
try {
const res = await fetch(`${BASE_URL}/transfer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: this.accountName,
to: payload.to,
amount: payload.amount
}),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || 'Transfer failed');
}
await this.fetchAccountData();
return await res.json();
} catch (err) {
alert(err.message);
throw err;
}
},
initializeWallet() {
const savedName = this.getCookie('sequencer_wallet_name');
if (savedName) {
this.accountName = savedName;
} else {
const randomName = TSUBASA_NAMES[Math.floor(Math.random() * TSUBASA_NAMES.length)];
this.accountName = randomName;
this.setCookie('sequencer_wallet_name', randomName);
}
},
regenerateWallet() {
document.cookie = "sequencer_wallet_name=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
this.stopPolling();
this.initializeWallet();
this.startPolling();
},
nextWallet() {
const currentIndex = TSUBASA_NAMES.indexOf(this.accountName);
const nextIndex = (currentIndex + 1) % TSUBASA_NAMES.length;
this.updateWallet(TSUBASA_NAMES[nextIndex]);
},
prevWallet() {
const currentIndex = TSUBASA_NAMES.indexOf(this.accountName);
const prevIndex = (currentIndex - 1 + TSUBASA_NAMES.length) % TSUBASA_NAMES.length;
this.updateWallet(TSUBASA_NAMES[prevIndex]);
},
updateWallet(newName) {
this.stopPolling();
this.accountName = newName;
this.setCookie('sequencer_wallet_name', newName);
this.startPolling();
},
startPolling() {
this.initializeWallet();
this.stopPolling();
this.fetchAccountData();
this.pollingInterval = setInterval(() => this.fetchAccountData(), 2000);
},
stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
}
}
});

View File

@ -0,0 +1,107 @@
<template>
<div class="archive flex flex-col min-h-screen pt-24 p-4 sm:p-6 lg:p-8">
<header class="w-full grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-12 items-center mb-12 pt-20 pb-10">
<div class="w-full">
<div class="flex flex-col p-6 sm:p-8 lg:p-10 rounded-2xl border border-gray-100 dark:border-gray-400 bg-white dark:bg-black transition-all duration-300 ">
<div class="flex items-center justify-between mb-6 border-b pb-4 border-gray-200 dark:border-gray-700">
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
Archive Explorer
</h2>
</div>
<div class="space-y-6">
<div class="flex flex-col">
<span class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-1">
Data Source
</span>
<p class="text-base sm:text-lg font-mono text-gray-700 dark:text-white">
Testnet Archiver
</p>
</div>
<div class="flex flex-col pt-4">
<span class="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-3">
Feed Status
</span>
<div class="flex items-center space-x-3 bg-gray-50 dark:bg-gray-800/50 px-4 py-2 rounded-xl border border-gray-100 dark:border-gray-800 w-fit">
<span class="relative flex h-2 w-2">
<span
:class="statusClasses.ping"
class="absolute inline-flex h-full w-full rounded-full opacity-75"
></span>
<span
:class="statusClasses.dot"
class="relative inline-flex rounded-full h-2 w-2"
></span>
</span>
<p class="text-gray-500 dark:text-gray-400 font-mono uppercase tracking-[0.1em] text-[10px] font-bold">
{{ statusText }}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center md:justify-start">
<img
src="@/assets/graphics/tsubasa-archive.png"
alt="Logo"
class="w-[280px] sm:w-[380px] object-contain "
/>
</div>
</header>
<main class="w-full pb-24">
<ArchivedBlockList
:blocks="archiveStore.blocks"
@openBlock="archiveStore.openBlockInExplorer"
@openTransaction="archiveStore.openTransactionInExplorer"
/>
</main>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, computed } from 'vue';
import { useArchiveStore } from '@/stores/archiveView';
import { ArchivedBlockList } from '@/components';
const archiveStore = useArchiveStore();
const statusClasses = computed(() => {
const isConnected = archiveStore.connectionStatus === 'connected';
const isError = archiveStore.connectionStatus === 'error';
return {
dot: isConnected ? 'bg-green-500' : (isError ? 'bg-red-500' : 'bg-yellow-500'),
ping: isConnected ? 'animate-ping bg-green-400' : (isError ? 'bg-red-400' : 'bg-yellow-400')
};
});
const statusText = computed(() => {
switch (archiveStore.connectionStatus) {
case 'connected':
return 'Live • Stream Active';
case 'waiting':
return 'Waiting...';
case 'error':
return 'Offline • Reconnecting';
default:
return 'Stream Disconnected';
}
});
onMounted(async () => {
await archiveStore.fetchCachedBlocks();
archiveStore.startStream()
})
onUnmounted(() => {
archiveStore.stopStream()
})
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="archive flex flex-col min-h-screen pt-24 p-4 sm:p-6 lg:p-8">
<div class="flex justify-center m-20 pb-20 sm:pt-20">
<img
src="@/assets/graphics/tsubasa-lg.png"
alt="Logo"
class="w-[300px]"
/>
</div>
<header class="w-full grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8 items-start mb-8">
<div class="w-full relative group">
<WalletInfo
:walletAddress="walletStore.accountName"
:walletAmount="walletStore.balance"
@regenerate="walletStore.regenerateWallet"
@next="walletStore.nextWallet"
@prev="walletStore.prevWallet"
class="max-w-none w-full"
/>
</div>
<div class="w-full relative group">
<TransactionCreator @transfer="handleTransfer" class="max-w-none w-full" />
</div>
</header>
<main class="w-full pb-12 pt-12">
<TransactionList
:currentAccount="walletStore.accountName"
:transactions="walletStore.transactions"
class="max-w-none w-full"
/>
</main>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useWalletStore } from '@/stores/walletView.js';
import { TransactionCreator, TransactionList, WalletInfo } from '@/components';
const walletStore = useWalletStore();
const handleTransfer = async (data) => {
await walletStore.sendTransaction(data);
};
onMounted(() => {
walletStore.startPolling();
});
onUnmounted(() => {
walletStore.stopPolling();
});
</script>

View File

@ -0,0 +1,4 @@
import ArchiveView from './ArchiveView.vue';
import WalletView from './WalletView.vue';
export { ArchiveView, WalletView };

View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
darkMode: ['class'],
}

View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})