Compare commits
24 Commits
56696050ec
...
v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
7999902cc0
|
|||
|
d6b888e403
|
|||
|
36219c49a0
|
|||
|
ca530bf987
|
|||
|
1c76755ea6
|
|||
|
c371a4f478
|
|||
|
59250289a7
|
|||
|
0f0f0a08c3
|
|||
|
6e6c634e98
|
|||
|
efcd91e6b4
|
|||
| 22b60e55c7 | |||
| 7fe5fd3139 | |||
| b46f85f436 | |||
|
cd24eda111
|
|||
|
8ce3f6a445
|
|||
|
190cac929e
|
|||
|
5e1d0d7c54
|
|||
|
9081e08785
|
|||
|
06db6f1d48
|
|||
|
ba8f828e89
|
|||
|
9abfad29b9
|
|||
|
bc9ec1a48c
|
|||
|
c461232095
|
|||
|
fbe61a5862
|
58
.dockerignore
Normal file
58
.dockerignore
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitsecret
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Environment files (secrets)
|
||||||
|
*.env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Private keys
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
!public_key.pem
|
||||||
|
!data/public_key.pem
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Scripts (local dev only)
|
||||||
|
bal-server.sh
|
||||||
|
bal-pusher.sh
|
||||||
|
download_bal_db.sh
|
||||||
|
sendtx.sh
|
||||||
|
lib.sh
|
||||||
|
contrib/
|
||||||
|
update
|
||||||
|
update_codebase.txt
|
||||||
|
|
||||||
|
# Service files
|
||||||
|
*.service
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
Cargo.lock
|
||||||
|
generate_random_ascii.sh
|
||||||
|
test/
|
||||||
|
invalid_txs/
|
||||||
|
valid_txs/
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,9 +8,6 @@
|
|||||||
.env.production
|
.env.production
|
||||||
.env.secret
|
.env.secret
|
||||||
|
|
||||||
# Shell scripts that load env vars (contain secrets, local only)
|
|
||||||
bal-pusher.sh
|
|
||||||
bal-server.sh
|
|
||||||
|
|
||||||
# Private keys - NEVER commit to git
|
# Private keys - NEVER commit to git
|
||||||
# Only public_key.pem should be tracked (if needed)
|
# Only public_key.pem should be tracked (if needed)
|
||||||
@@ -39,3 +36,4 @@ Cargo.lock
|
|||||||
!lib/
|
!lib/
|
||||||
!contrib/
|
!contrib/
|
||||||
!src/
|
!src/
|
||||||
|
make_release.sh
|
||||||
|
|||||||
275
Cargo.lock
generated
275
Cargo.lock
generated
@@ -312,7 +312,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bal_server"
|
name = "bal_server"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-governor",
|
"actix-governor",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
@@ -325,12 +325,11 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"confy",
|
"ed25519-dalek",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hex",
|
"hex",
|
||||||
"hex-conservative 0.1.1",
|
"hex-conservative 0.1.1",
|
||||||
"log",
|
"log",
|
||||||
"openssl",
|
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -364,6 +363,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bech32"
|
name = "bech32"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -592,16 +597,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "confy"
|
name = "const-oid"
|
||||||
version = "0.6.1"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
dependencies = [
|
|
||||||
"directories",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
@@ -747,6 +746,33 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"curve25519-dalek-derive",
|
||||||
|
"digest 0.10.7",
|
||||||
|
"fiat-crypto",
|
||||||
|
"rustc_version",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek-derive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@@ -760,6 +786,17 @@ dependencies = [
|
|||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid 0.9.6",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -809,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.1",
|
"block-buffer 0.12.1",
|
||||||
"const-oid",
|
"const-oid 0.10.2",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -824,27 +861,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "directories"
|
|
||||||
version = "5.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"option-ext",
|
|
||||||
"redox_users",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -856,6 +872,30 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -916,6 +956,12 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -1578,16 +1624,6 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libredox"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.9.0",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1759,15 +1795,6 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-src"
|
|
||||||
version = "300.5.4+3.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.110"
|
version = "0.9.110"
|
||||||
@@ -1776,17 +1803,10 @@ checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"openssl-src",
|
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "option-ext"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1810,6 +1830,15 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -1828,6 +1857,16 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@@ -1994,17 +2033,6 @@ dependencies = [
|
|||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.14",
|
|
||||||
"libredox",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.4"
|
version = "1.10.4"
|
||||||
@@ -2326,6 +2354,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
@@ -2373,6 +2410,16 @@ dependencies = [
|
|||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlite"
|
name = "sqlite"
|
||||||
version = "0.34.0"
|
version = "0.34.0"
|
||||||
@@ -2499,26 +2546,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.59"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.59"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.44"
|
version = "0.3.44"
|
||||||
@@ -3032,15 +3059,6 @@ dependencies = [
|
|||||||
"windows-link 0.1.1",
|
"windows-link 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -3068,21 +3086,6 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.48.5",
|
|
||||||
"windows_aarch64_msvc 0.48.5",
|
|
||||||
"windows_i686_gnu 0.48.5",
|
|
||||||
"windows_i686_msvc 0.48.5",
|
|
||||||
"windows_x86_64_gnu 0.48.5",
|
|
||||||
"windows_x86_64_gnullvm 0.48.5",
|
|
||||||
"windows_x86_64_msvc 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3116,12 +3119,6 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.53.1",
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3134,12 +3131,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3152,12 +3143,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3182,12 +3167,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3200,12 +3179,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3218,12 +3191,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
@@ -3236,12 +3203,6 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.5"
|
version = "0.52.5"
|
||||||
|
|||||||
46
Cargo.toml
46
Cargo.toml
@@ -1,42 +1,58 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bal_server"
|
name = "bal_server"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[features]
|
||||||
|
default = ["server", "pusher"]
|
||||||
|
server = ["dep:actix-web", "dep:actix-governor", "dep:actix-rt", "dep:chrono", "dep:hex-conservative"]
|
||||||
|
pusher = ["dep:zmq", "dep:reqwest", "dep:byteorder", "dep:base64", "dep:ed25519-dalek"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { version = "0.22.1" }
|
|
||||||
bs58 = { version = "0.4.0" }
|
bs58 = { version = "0.4.0" }
|
||||||
bytes = { version = "1.2" }
|
bytes = { version = "1.2" }
|
||||||
bitcoin = { version = "0.32.5" }
|
bitcoin = { version = "0.32.5" }
|
||||||
bitcoincore-rpc = { version = "0.19.0" }
|
bitcoincore-rpc = { version = "0.19.0" }
|
||||||
bitcoincore-rpc-json = { version = "0.19.0" }
|
bitcoincore-rpc-json = { version = "0.19.0" }
|
||||||
byteorder = { version = "1.5.0" }
|
|
||||||
confy = { version = "0.6.1" }
|
|
||||||
chrono = { version = "0.4.40" }
|
|
||||||
env_logger = { version = "0.11.5" }
|
env_logger = { version = "0.11.5" }
|
||||||
hex = { version = "0.4.3" }
|
hex = { version = "0.4.3" }
|
||||||
hex-conservative = { version = "0.1.1" }
|
|
||||||
actix-web = { version = "4.9.0" }
|
|
||||||
actix-governor = { version = "0.6.0" }
|
|
||||||
log = { version = "0.4.21" }
|
log = { version = "0.4.21" }
|
||||||
openssl = { version = "0.10.74", features = ["vendored"] }
|
|
||||||
sha2 = { version = "0.10.8" }
|
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.116" }
|
serde_json = { version = "1.0.116" }
|
||||||
sqlite = { version = "0.34.0" }
|
sqlite = { version = "0.34.0" }
|
||||||
regex = { version = "1.10.4" }
|
regex = { version = "1.10.4" }
|
||||||
reqwest = { version = "0.12.24", features = ["json","socks"] }
|
|
||||||
actix-rt = { version = "2.10.0" }
|
|
||||||
tokio = { version = "1", features = ["rt", "net","macros","rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt", "net","macros","rt-multi-thread"] }
|
||||||
url = { version = "2" }
|
url = { version = "2" }
|
||||||
zmq = { version = "0.10.0" }
|
|
||||||
|
# server-only
|
||||||
|
actix-web = { version = "4.9.0", optional = true }
|
||||||
|
actix-governor = { version = "0.6.0", optional = true }
|
||||||
|
actix-rt = { version = "2.10.0", optional = true }
|
||||||
|
chrono = { version = "0.4.40", optional = true }
|
||||||
|
hex-conservative = { version = "0.1.1", optional = true }
|
||||||
|
|
||||||
|
# pusher-only
|
||||||
|
zmq = { version = "0.10.0", optional = true }
|
||||||
|
reqwest = { version = "0.12.24", features = ["json","socks"], optional = true }
|
||||||
|
byteorder = { version = "1.5.0", optional = true }
|
||||||
|
base64 = { version = "0.22.1", optional = true }
|
||||||
|
ed25519-dalek = { version = "2", features = ["pem", "pkcs8"], optional = true }
|
||||||
|
sha2 = { version = "0.10.8" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bal-server"
|
name = "bal-server"
|
||||||
path = "src/bin/bal-server-actix.rs"
|
path = "src/bin/bal-server.rs"
|
||||||
|
required-features = ["server"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bal-pusher"
|
name = "bal-pusher"
|
||||||
path = "src/bin/bal-pusher.rs"
|
path = "src/bin/bal-pusher.rs"
|
||||||
|
required-features = ["pusher"]
|
||||||
|
|||||||
92
Dockerfile
Normal file
92
Dockerfile
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Multi-stage Dockerfile for bal-server + bal-pusher
|
||||||
|
# Security: non-root user, minimal runtime, tini as PID 1
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 1: Builder
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM rust:1.95-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libzmq5-dev \
|
||||||
|
cmake \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Cache dependencies: copy Cargo.toml first, create dummy src to build deps
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
RUN mkdir -p src/bin && \
|
||||||
|
echo 'fn main() {}' > src/bin/bal-server.rs && \
|
||||||
|
echo 'fn main() {}' > src/bin/bal-pusher.rs && \
|
||||||
|
echo '' > src/lib.rs && \
|
||||||
|
echo '' > src/db.rs && \
|
||||||
|
echo '' > src/xpub.rs && \
|
||||||
|
echo '' > src/validation.rs && \
|
||||||
|
cargo build --release --bin bal-server --bin bal-pusher 2>/dev/null || true && \
|
||||||
|
rm -rf src target/release/.fingerprint target/release/deps/*bal_server*
|
||||||
|
|
||||||
|
# Copy real source and build
|
||||||
|
COPY src/ src/
|
||||||
|
RUN cargo build --release --bin bal-server --bin bal-pusher && \
|
||||||
|
strip target/release/bal-server target/release/bal-pusher
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 2: Runtime
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
# Install runtime dependencies + tini for PID 1
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libsodium23 \
|
||||||
|
libzmq5 \
|
||||||
|
libsqlite3-0 \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
|
# Copy binaries from builder
|
||||||
|
COPY --from=builder /build/target/release/bal-server /usr/local/bin/bal-server
|
||||||
|
COPY --from=builder /build/target/release/bal-pusher /usr/local/bin/bal-pusher
|
||||||
|
|
||||||
|
# Create dedicated non-root user
|
||||||
|
RUN groupadd -g 1000 bal && \
|
||||||
|
useradd -u 1000 -g bal -s /usr/sbin/nologin -M bal && \
|
||||||
|
mkdir -p /var/bal /var/bal/.bitcoin && \
|
||||||
|
chown -R bal:bal /var/bal && \
|
||||||
|
chmod 700 /var/bal
|
||||||
|
|
||||||
|
# Copy entrypoint
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Use tini as PID 1 for proper signal handling
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|
||||||
|
# Data directory (mount as volume)
|
||||||
|
VOLUME ["/var/bal"]
|
||||||
|
|
||||||
|
# bal-server port (bind to 127.0.0.1 via env, expose for reverse proxy)
|
||||||
|
EXPOSE 9137
|
||||||
|
|
||||||
|
# Default environment (override at runtime)
|
||||||
|
ENV RUST_LOG=info \
|
||||||
|
BAL_SERVER_BIND_ADDRESS=127.0.0.1 \
|
||||||
|
BAL_SERVER_BIND_PORT=9137 \
|
||||||
|
BAL_SERVER_DB_FILE=/var/bal/bal.db \
|
||||||
|
BAL_PUSHER_DB_FILE=/var/bal/bal.db \
|
||||||
|
BAL_SERVER_URL=http://127.0.0.1:9137 \
|
||||||
|
BAL_SERVER_PUB_KEY_PATH=/var/bal/public_key.pem \
|
||||||
|
SSL_KEY_PATH=/var/bal/private_key.pem
|
||||||
|
|
||||||
|
# Health check: verify bal-server is responding
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -sf http://127.0.0.1:9137/ || exit 1
|
||||||
202
README.md
202
README.md
@@ -1,85 +1,159 @@
|
|||||||
# bal-server
|
# bal-server
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git clone ....
|
git clone https://bitcoin-after.life/gitea/bitcoinafterlife/bal-server.git
|
||||||
$ cd bal-server
|
cd bal-server
|
||||||
$ openssl genpkey -algorithm ED25519 -out private_key.pem
|
openssl genpkey -algorithm ED25519 -out private_key.pem
|
||||||
$ openssl pkey -in private_key.pem -pubout -out public_key.pem
|
openssl pkey -in private_key.pem -pubout -out public_key.pem
|
||||||
$ cargo build --release
|
cargo build --release
|
||||||
$ sudo cp target/release/bal-server /usr/local/bin
|
sudo cp target/release/bal-server target/release/bal-pusher /usr/local/bin
|
||||||
$ bal-server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Docker
|
||||||
|
|
||||||
The `bal-server` application can be configured using environment variables. The following variables are available:
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bal-server .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name bal-server \
|
||||||
|
--network host \
|
||||||
|
--tmpfs /tmp:rw,noexec,nosuid \
|
||||||
|
-v /path/to/data:/var/bal:rw \
|
||||||
|
-v /path/to/.bitcoin/regtest/.cookie:/var/bal/.bitcoin/regtest/.cookie:ro \
|
||||||
|
-e BAL_SERVER_REGTEST_ADDRESS="your_xpub_or_address" \
|
||||||
|
-e BAL_SERVER_REGTEST_FIXED_FEE=50000 \
|
||||||
|
-e BAL_SERVER_INFO="BAL server" \
|
||||||
|
-e BAL_PUSHER_NETWORK=regtest \
|
||||||
|
-e BAL_PUSHER_REGTEST_ZMQ_HASHBLOCK=tcp://127.0.0.1:28332 \
|
||||||
|
-e BAL_PUSHER_REGTEST_COOKIE_FILE=/var/bal/.bitcoin/regtest/.cookie \
|
||||||
|
bal-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker environment variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `BAL_SERVER_CONFIG_FILE` | Path to the configuration file. If the file does not exist, a new one will be created. | `$HOME/.config/bal-server/default-config.toml` |
|
| `BAL_PUSHER_NETWORK` | Network to run pusher on (`bitcoin`, `testnet`, `testnet4`, `signet`, `regtest`). | `bitcoin` |
|
||||||
| `BAL_SERVER_DB_FILE` | Path to the SQLite3 database file. If the file does not exist, a new one will be created. | `bal.db` |
|
| `BAL_PUSHER_REGTEST_ZMQ_HASHBLOCK` | ZMQ endpoint for regtest blocks. | `tcp://127.0.0.1:21332` |
|
||||||
| `BAL_SERVER_BIND_ADDRESS` | Public address for listening to requests. | `127.0.0.1` |
|
| `BAL_PUSHER_REGTEST_COOKIE_FILE` | Absolute path to Bitcoin Core cookie file inside the container. | - |
|
||||||
| `BAL_SERVER_BIND_PORT` | Default port for listening to requests. | `9137` |
|
|
||||||
| `BAL_SERVER_PUB_KEY_PATH` | WillExecutor Ed25519 public key | `public_key.pem` |
|
|
||||||
| `BAL_SERVER_REGTEST_ADDRESS` | Bitcoin address for the regtest environment. | - |
|
|
||||||
| `BAL_SERVER_REGTEST_FIXED_FEE` | Fixed fee for the regtest environment. | 50000 |
|
|
||||||
| `BAL_SERVER_SIGNET_ADDRESS` | Bitcoin address for the signet environment. | - |
|
|
||||||
| `BAL_SERVER_SIGNET_FIXED_FEE` | Fixed fee for the signet environment. | 50000 |
|
|
||||||
| `BAL_SERVER_TESTNET_ADDRESS` | Bitcoin address for the testnet environment. | - |
|
|
||||||
| `BAL_SERVER_TESTNET_FIXED_FEE` | Fixed fee for the testnet environment. | 50000 |
|
|
||||||
| `BAL_SERVER_BITCOIN_ADDRESS` | Bitcoin address for the mainnet environment. | - |
|
|
||||||
| `BAL_SERVER_BITCOIN_FIXED_FEE` | Fixed fee for the mainnet environment. | 50000 |
|
|
||||||
|
|
||||||
|
> **Note:** The container runs as a non-root `bal` user (uid 1000) with `tini` as PID 1.
|
||||||
|
> The `/var/bal` volume stores the database. Mount Bitcoin Core's cookie file as read-only.
|
||||||
|
> When using `--network host`, ensure only `127.0.0.1` is used for internal services.
|
||||||
|
|
||||||
|
## Configuration (bal-server)
|
||||||
|
|
||||||
|
The `bal-server` application can be configured using environment variables.
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BAL_SERVER_DB_FILE` | Path to the SQLite3 database file. | `bal.db` |
|
||||||
|
| `BAL_SERVER_BIND_ADDRESS` | Address to listen on. **Never bind to `0.0.0.0` in production without a reverse proxy.** | `127.0.0.1` |
|
||||||
|
| `BAL_SERVER_BIND_PORT` | Port to listen on. | `9137` |
|
||||||
|
| `BAL_SERVER_INFO` | Server info string returned by the `/` endpoint. | - |
|
||||||
|
| `BAL_SERVER_PUB_KEY_PATH` | Ed25519 public key for signature verification. | `public_key.pem` |
|
||||||
|
| `BAL_SERVER_URL` | Public URL of this server (used for stats reporting). | - |
|
||||||
|
| `SSL_KEY_PATH` | Ed25519 private key for signing stats reports. | `private_key.pem` |
|
||||||
|
| `RUST_LOG` | Log level (`error`, `warn`, `info`, `debug`, `trace`). | `info` |
|
||||||
|
|
||||||
|
### Per-network addresses and fees
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BAL_SERVER_BITCOIN_ADDRESS` | xpub or address for mainnet. | - |
|
||||||
|
| `BAL_SERVER_BITCOIN_FIXED_FEE` | Fixed fee (satoshis) for mainnet. | `50000` |
|
||||||
|
| `BAL_SERVER_REGTEST_ADDRESS` | xpub or address for regtest. | - |
|
||||||
|
| `BAL_SERVER_REGTEST_FIXED_FEE` | Fixed fee (satoshis) for regtest. | `50000` |
|
||||||
|
| `BAL_SERVER_SIGNET_ADDRESS` | xpub or address for signet. | - |
|
||||||
|
| `BAL_SERVER_SIGNET_FIXED_FEE` | Fixed fee (satoshis) for signet. | `50000` |
|
||||||
|
| `BAL_SERVER_TESTNET_ADDRESS` | xpub or address for testnet. | - |
|
||||||
|
| `BAL_SERVER_TESTNET_FIXED_FEE` | Fixed fee (satoshis) for testnet. | `50000` |
|
||||||
|
| `BAL_SERVER_TESTNET4_ADDRESS` | xpub or address for testnet4. | - |
|
||||||
|
| `BAL_SERVER_TESTNET4_FIXED_FEE` | Fixed fee (satoshis) for testnet4. | `50000` |
|
||||||
|
|
||||||
|
### DoS protection (Actix Web)
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BAL_SERVER_ACTIX_MAX_BODY_SIZE` | Maximum request body size in bytes. | `1048576` (1 MB) |
|
||||||
|
| `BAL_SERVER_ACTIX_TIMEOUT_SECS` | Request timeout in seconds. | `5` |
|
||||||
|
| `BAL_SERVER_ACTIX_PUSHTXS_PER_SEC` | Rate limit: push txs requests per second. | `1` |
|
||||||
|
| `BAL_SERVER_ACTIX_PUSHTXS_BURST` | Rate limit: push txs burst size. | `3` |
|
||||||
|
| `BAL_SERVER_ACTIX_SEARCHTX_PER_SEC` | Rate limit: search tx requests per second. | `5` |
|
||||||
|
| `BAL_SERVER_ACTIX_SEARCHTX_BURST` | Rate limit: search tx burst size. | `10` |
|
||||||
|
| `BAL_SERVER_ACTIX_INFO_PER_SEC` | Rate limit: info requests per second. | `20` |
|
||||||
|
| `BAL_SERVER_ACTIX_INFO_BURST` | Rate limit: info burst size. | `30` |
|
||||||
|
| `BAL_SERVER_ACTIX_DEFAULT_PER_SEC` | Rate limit: default requests per second. | `50` |
|
||||||
|
| `BAL_SERVER_ACTIX_DEFAULT_BURST` | Rate limit: default burst size. | `100` |
|
||||||
|
| `BAL_SERVER_ACTIX_WORKERS` | Number of Actix worker threads. | `4` |
|
||||||
|
| `BAL_SERVER_ACTIX_MAX_CONNECTIONS` | Maximum concurrent connections. | `100` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# bal-pusher
|
# bal-pusher
|
||||||
|
|
||||||
`bal-pusher` is a tool that retrieves Bitcoin transactions from a database and pushes them to the Bitcoin network when their **locktime** exceeds the **median time past** (MTP). It listens for Bitcoin block updates via ZMQ.
|
`bal-pusher` monitors Bitcoin blocks via ZMQ and pushes time-locked transactions from the database to the Bitcoin network when their **locktime** exceeds the **median time past** (MTP).
|
||||||
|
|
||||||
## Installation
|
## Prerequisites
|
||||||
|
|
||||||
To use `bal-pusher`, you need to compile and install Bitcoin with ZMQ (ZeroMQ) support enabled. Then, configure your Bitcoin node and `bal-pusher` to push the transactions.
|
- **Bitcoin Core** with ZMQ support enabled. Add to `bitcoin.conf`:
|
||||||
|
```
|
||||||
|
zmqpubhashblock=tcp://127.0.0.1:28332
|
||||||
|
```
|
||||||
|
- **Rust and Cargo**: [Rust Installation](https://www.rust-lang.org/tools/install)
|
||||||
|
- **Libraries**: `libssl-dev`, `libsodium-dev`, `libzmq5-dev`, `libsqlite3-dev`
|
||||||
|
|
||||||
### Prerequisites
|
## Running
|
||||||
|
|
||||||
1. **Bitcoin with ZMQ Support**:
|
|
||||||
Ensure that Bitcoin is compiled with ZMQ support. Add the following line to your `bitcoin.conf` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
zmqpubhashblock=tcp://127.0.0.1:28332
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install Rust and Cargo**:
|
|
||||||
If you haven't already installed Rust and Cargo, you can follow the official instructions to do so: [Rust Installation](https://www.rust-lang.org/tools/install).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`bal-pusher` can be configured using environment variables. If no configuration file is provided, a default configuration file will be created.
|
|
||||||
|
|
||||||
### Available Configuration Variables
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|---------------------------------------|------------------------------------------|----------------------------------------------|
|
|
||||||
| `BAL_PUSHER_CONFIG_FILE` | Path to the configuration file. If the file does not exist, it will be created. | `$HOME/.config/bal-pusher/default-config.toml` |
|
|
||||||
| `BAL_PUSHER_DB_FILE` | Path to the SQLite3 database file. If the file does not exist, it will be created. | `bal.db` |
|
|
||||||
| `BAL_PUSHER_ZMQ_LISTENER` | ZMQ listener for Bitcoin updates. | `tcp://127.0.0.1:28332` |
|
|
||||||
| `BAL_PUSHER_BITCOIN_HOST` | Bitcoin server host for RPC connections. | `http://127.0.0.1` |
|
|
||||||
| `BAL_PUSHER_BITCOIN_PORT` | Bitcoin RPC server port. | `8332` |
|
|
||||||
| `BAL_PUSHER_BITCOIN_COOKIE_FILE` | Path to Bitcoin RPC cookie file. | `$HOME/.bitcoin/.cookie` |
|
|
||||||
| `BAL_PUSHER_BITCOIN_RPC_USER` | Bitcoin RPC username. | - |
|
|
||||||
| `BAL_PUSHER_BITCOIN_RPC_PASSWORD` | Bitcoin RPC password. | - |
|
|
||||||
| `BAL_PUSHER_SEND_STATS` | Contact welist to provide times | false |
|
|
||||||
| `WELIST_SERVER_URL` | welist server url to provide times | https://welist.bitcoin-afer.life |
|
|
||||||
| `BAL_SERVER_URL` | WillExecutor server url | - |
|
|
||||||
| `SSL_KEY_PATH` | Ed25519 private key pem file | `private_key.pem` |
|
|
||||||
|
|
||||||
|
|
||||||
## Running `bal-pusher`
|
|
||||||
|
|
||||||
Once the application is installed and configured, you can start `bal-pusher` by running the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ bal-pusher [bitcoin|testnet|regtest|]
|
bal-pusher [bitcoin|testnet|testnet4|signet|regtest]
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the service, which will listen for Bitcoin blocks via ZMQ and push transactions from the database when their locktime exceeds the median time past.
|
If no network is specified, defaults to `bitcoin`.
|
||||||
|
|
||||||
|
## Configuration (bal-pusher)
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BAL_PUSHER_DB_FILE` | Path to the SQLite3 database file. | `bal.db` |
|
||||||
|
| `BAL_PUSHER_SEND_STATS` | Send stats to welist server. | `false` |
|
||||||
|
| `BAL_SERVER_URL` | URL of bal-server (for stats reporting). | - |
|
||||||
|
| `SSL_KEY_PATH` | Ed25519 private key for signing stats reports. | `private_key.pem` |
|
||||||
|
| `WELIST_SERVER_URL` | Welist server URL. | `https://welist.bitcoin-after.life` |
|
||||||
|
|
||||||
|
### Per-network configuration
|
||||||
|
|
||||||
|
Each network (`bitcoin`, `regtest`, `testnet`, `testnet4`, `signet`) supports the following variables.
|
||||||
|
Replace `{NETWORK}` with the uppercase network name (e.g., `REGTEST`, `BITCOIN`).
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_ZMQ_HASHBLOCK` | ZMQ endpoint for block notifications. | `tcp://127.0.0.1:28332` (mainnet) |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_COOKIE_FILE` | Absolute path to Bitcoin Core cookie file. | `$HOME/.bitcoin/{dir}/.cookie` |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_RPC_USER` | Bitcoin Core RPC username (alternative to cookie auth). | - |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_RPC_PASSWORD` | Bitcoin Core RPC password. | - |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_HOST` | Bitcoin Core RPC host. | `http://127.0.0.1` |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_PORT` | Bitcoin Core RPC port. | `8332` (mainnet) |
|
||||||
|
| `BAL_PUSHER_{NETWORK}_DIR_PATH` | Bitcoin Core data directory subfolder. | `` (mainnet) |
|
||||||
|
|
||||||
|
Default ZMQ ports per network:
|
||||||
|
|
||||||
|
| Network | ZMQ Port | RPC Port |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `bitcoin` | 28332 | 8332 |
|
||||||
|
| `regtest` | 21332 | 18443 |
|
||||||
|
| `testnet` | 23332 | 18332 |
|
||||||
|
| `testnet4` | 22332 | 48332 |
|
||||||
|
| `signet` | 24332 | 38332 |
|
||||||
|
|||||||
15
bal-pusher.sh
Normal file
15
bal-pusher.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export RUST_LOG=trace
|
||||||
|
|
||||||
|
export BAL_PUSHER_DB_FILE="$(pwd)/bal.db"
|
||||||
|
#export BAL_PUSHER_BITCOIN_COOKIE_FILE=/~/.bitcoin/.cookie
|
||||||
|
#export BAL_PUSHER_REGTEST_COOKIE_FILE=/~/.bitcoin/regtest/.cookie
|
||||||
|
#export BAL_PUSHER_TESTNET_COOKIE_FILE=/~/.bitcoin/testnet3/.cookie
|
||||||
|
#export BAL_PUSHER_SIGNET_COOKIE_FILE=/~/.bitcoin/signet/.cookie
|
||||||
|
|
||||||
|
export BAL_PUSHER_REGTEST_ZMQ_HASHBLOCK=tcp://127.0.0.1:21332
|
||||||
|
export BAL_PUSHER_SEND_STATS=true
|
||||||
|
export WELIST_SERVER_URL=http://localhost:8086
|
||||||
|
export WELIST_SKIP_URL_VALIDATION=true
|
||||||
|
export BAL_SERVER_URL="http://127.0.0.1:9133"
|
||||||
|
export SSL_KEY_PATH="$(pwd)/private_key.pem"
|
||||||
|
cargo run --bin=bal-pusher regtest --features=pusher
|
||||||
25
docker/entrypoint.sh
Normal file
25
docker/entrypoint.sh
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p /var/bal/.bitcoin
|
||||||
|
chown bal:bal /var/bal /var/bal/.bitcoin 2>/dev/null || true
|
||||||
|
|
||||||
|
PUSHER_NETWORK="${BAL_PUSHER_NETWORK:-bitcoin}"
|
||||||
|
|
||||||
|
echo "[entrypoint] Starting bal-server on ${BAL_SERVER_BIND_ADDRESS:-127.0.0.1}:${BAL_SERVER_BIND_PORT:-9137}"
|
||||||
|
echo "[entrypoint] Starting bal-pusher (network: ${PUSHER_NETWORK})"
|
||||||
|
|
||||||
|
su -s /bin/sh bal -c '/usr/local/bin/bal-server' &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
su -s /bin/sh bal -c "/usr/local/bin/bal-pusher ${PUSHER_NETWORK}" &
|
||||||
|
PUSHER_PID=$!
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "[entrypoint] Shutting down..."
|
||||||
|
kill $SERVER_PID $PUSHER_PID 2>/dev/null
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
trap cleanup TERM INT
|
||||||
|
|
||||||
|
wait
|
||||||
@@ -45,6 +45,7 @@ WELIST_URL=https://welist.example.com/api/stats
|
|||||||
- `BAL_SSL_KEY_PATH`: The path to the Ed25519 private key (`private_key.pem`) used to sign the statistics payload before sending it to the `welist` server. This is a critical secret.
|
- `BAL_SSL_KEY_PATH`: The path to the Ed25519 private key (`private_key.pem`) used to sign the statistics payload before sending it to the `welist` server. This is a critical secret.
|
||||||
- `SEND_STATS`: A boolean flag to enable the reporting of statistics to the remote `welist` server.
|
- `SEND_STATS`: A boolean flag to enable the reporting of statistics to the remote `welist` server.
|
||||||
- `WELIST_URL`: The URL to which the statistics are sent. If `SEND_STATS` is `true`, this URL must be reachable. If the server is unreachable, the pusher will log an error but might not crash (see `08_security_audit.md` for DoS analysis).
|
- `WELIST_URL`: The URL to which the statistics are sent. If `SEND_STATS` is `true`, this URL must be reachable. If the server is unreachable, the pusher will log an error but might not crash (see `08_security_audit.md` for DoS analysis).
|
||||||
|
- `BAL_PUSHER_PREFER_IPV6`: Optional boolean flag (default `false`). When set to `true`, the pusher resolves the `welist` host itself and pins the HTTP connection to its first IPv6 (AAAA) address, still using the hostname for the `Host` header and TLS SNI. This works around networks where the IPv4 route to the `welist` host is broken while IPv6 works — the default connector may otherwise pick the unreachable family and the request would stall. Leave unset unless you hit this specific connectivity problem.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -133,6 +134,14 @@ This script builds a release binary, creates a Git tag, and uploads the release
|
|||||||
```bash
|
```bash
|
||||||
# WARNING: This script contains a hardcoded secret token. Do not use it as-is for production.
|
# WARNING: This script contains a hardcoded secret token. Do not use it as-is for production.
|
||||||
```
|
```
|
||||||
|
- **Release Assets:** It generates a `.tar.gz` archive with the binaries, a `.sha256` checksum file, and both a `.sig` GPG detached binary signature and a `.asc` ASCII-armored version.
|
||||||
|
- **Signature:** The release tarball is signed with the GPG key `Svātantrya <svatantrya@bitcoin-after.life>`. The script verifies that `gpg`, `sha256sum`, and `jq` are installed before proceeding.
|
||||||
|
- **Verification:** The release body includes instructions for verifying the checksum and signature (binary or ASCII-armored):
|
||||||
|
```bash
|
||||||
|
sha256sum -c <release>.tar.gz.sha256
|
||||||
|
gpg --verify <release>.tar.gz.sig <release>.tar.gz
|
||||||
|
gpg --verify <release>.tar.gz.asc <release>.tar.gz
|
||||||
|
```
|
||||||
- **Security:** It also builds and uploads the binaries. The binaries should be built and signed on a separate, clean build machine, not on the production server.
|
- **Security:** It also builds and uploads the binaries. The binaries should be built and signed on a separate, clean build machine, not on the production server.
|
||||||
|
|
||||||
### `download_bal_db.sh` (Database Pull Script)
|
### `download_bal_db.sh` (Database Pull Script)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Regression tests: `tests/panic_regression_tests.rs` (2 tests).
|
|||||||
- Rate limiting: `actix-governor` middleware with token-bucket — configurable via `BAL_SERVER_ACTIX_PUSHTXS_PER_SEC`/`BURST` (default 1 req/s per IP with burst 5)
|
- Rate limiting: `actix-governor` middleware with token-bucket — configurable via `BAL_SERVER_ACTIX_PUSHTXS_PER_SEC`/`BURST` (default 1 req/s per IP with burst 5)
|
||||||
- Connection limits: `workers(4)` and `max_connections(100)` — configurable via `BAL_SERVER_ACTIX_WORKERS`/`MAX_CONNECTIONS`
|
- Connection limits: `workers(4)` and `max_connections(100)` — configurable via `BAL_SERVER_ACTIX_WORKERS`/`MAX_CONNECTIONS`
|
||||||
- Body timeout: configurable via `BAL_SERVER_ACTIX_TIMEOUT_SECS` (default 30s)
|
- Body timeout: configurable via `BAL_SERVER_ACTIX_TIMEOUT_SECS` (default 30s)
|
||||||
**Migration:** Server replaced `hyper` custom server with `actix-web` (see `src/bin/bal-server-actix.rs`). All handlers migrated with `Arc<Mutex<Connection>>` shared DB. Old `bal-server.rs` (Hyper) removed. `bal-pusher` enhanced with ZMQ timeout (`ZMQ_RCVTIMEO` 5000ms) and RPC retry logic. **Priority:** High. (Mitigated)
|
**Migration:** Server replaced `hyper` custom server with `actix-web` (see `src/bin/bal-server.rs`). All handlers migrated with `Arc<Mutex<Connection>>` shared DB. Old `bal-server.rs` (Hyper) removed. `bal-pusher` enhanced with ZMQ timeout (`ZMQ_RCVTIMEO` 5000ms) and RPC retry logic. **Priority:** High. (Mitigated)
|
||||||
|
|
||||||
### 5. SSRF / Network Abuse via `reqwest` (MEDIUM)
|
### 5. SSRF / Network Abuse via `reqwest` (MEDIUM)
|
||||||
**Location:** `src/bin/bal-pusher.rs`.
|
**Location:** `src/bin/bal-pusher.rs`.
|
||||||
@@ -121,7 +121,7 @@ Regression tests: `tests/panic_regression_tests.rs` (2 tests).
|
|||||||
- Rejects symlinks and non-regular files (directories, devices, etc.).
|
- Rejects symlinks and non-regular files (directories, devices, etc.).
|
||||||
- If validation fails, the function returns `Err(String)` instead of panicking, preventing crashes or accidental access to system files.
|
- If validation fails, the function returns `Err(String)` instead of panicking, preventing crashes or accidental access to system files.
|
||||||
- ✅ **WAL mode:** `db::open_db` automatically executes `PRAGMA journal_mode = WAL;` and `PRAGMA synchronous = NORMAL;` on every connection. This is a best practice for safe concurrent access when `bal-server` and `bal-pusher` share the same database file.
|
- ✅ **WAL mode:** `db::open_db` automatically executes `PRAGMA journal_mode = WAL;` and `PRAGMA synchronous = NORMAL;` on every connection. This is a best practice for safe concurrent access when `bal-server` and `bal-pusher` share the same database file.
|
||||||
- ✅ **Replaced `unwrap`:** In `src/bin/bal-server-actix.rs` and `src/bin/bal-pusher.rs`, `sqlite::open(...).unwrap()` was replaced with `db::open_db(...)` with safe error handling (return `Err` in the server, `std::process::exit(1)` in the pusher with a log error).
|
- ✅ **Replaced `unwrap`:** In `src/bin/bal-server.rs` and `src/bin/bal-pusher.rs`, `sqlite::open(...).unwrap()` was replaced with `db::open_db(...)` with safe error handling (return `Err` in the server, `std::process::exit(1)` in the pusher with a log error).
|
||||||
- **Remaining (ops):** Ensure the database file is owned by the `bal` user and not writable by any other user (`chmod 600`). The database file should not reside on a shared or network drive.
|
- **Remaining (ops):** Ensure the database file is owned by the `bal` user and not writable by any other user (`chmod 600`). The database file should not reside on a shared or network drive.
|
||||||
**Regression tests:** `tests/db_path_validation.rs` (5 tests covering traversal, forbidden absolute paths, symlink, WAL pragma, and valid relative paths). All passing.
|
**Regression tests:** `tests/db_path_validation.rs` (5 tests covering traversal, forbidden absolute paths, symlink, WAL pragma, and valid relative paths). All passing.
|
||||||
**Status:** Fixed. **Priority:** Medium.
|
**Status:** Fixed. **Priority:** Medium.
|
||||||
|
|||||||
154
make_release.sh
154
make_release.sh
@@ -1,154 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#author: <your-name>
|
|
||||||
|
|
||||||
source lib.sh
|
|
||||||
usage() {
|
|
||||||
echo_w "./make_release <version> <message>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -n "$1" ]; then release=$1; else usage; exit; fi
|
|
||||||
|
|
||||||
if [ -n "$2" ]; then message=$2; else
|
|
||||||
# Create temporary file using mktemp
|
|
||||||
TEMPFILE=$(mktemp)
|
|
||||||
vi $TEMPFILE
|
|
||||||
message=$(cat $TEMPFILE)
|
|
||||||
rm $TEMPFILE
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo_i $message
|
|
||||||
|
|
||||||
# Load secrets from .env file (not committed to git)
|
|
||||||
if [ -f .env ]; then
|
|
||||||
export $(grep -v '^#' .env | xargs)
|
|
||||||
fi
|
|
||||||
|
|
||||||
TOKEN="${GITEA_API_TOKEN}"
|
|
||||||
if [ -z "$TOKEN" ]; then
|
|
||||||
echo_e "Error: GITEA_API_TOKEN is not set in .env file."
|
|
||||||
echo_e "Please create a .env file with: GITEA_API_TOKEN=your_token_here"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
OWNER="bitcoinafterlife"
|
|
||||||
basename=$(basename $(pwd))
|
|
||||||
REPO=$basename
|
|
||||||
TAG="v$release"
|
|
||||||
binpath="target/release/$basename"
|
|
||||||
release_name="$basename-$release"
|
|
||||||
dest="releases/$release"
|
|
||||||
arch=$(uname -m)
|
|
||||||
platform="linux-gnu"
|
|
||||||
destbin="$dest/$arch"
|
|
||||||
destsrc="$dest/src"
|
|
||||||
assetname="$release_name""_$arch""_$platform"
|
|
||||||
|
|
||||||
asset_tar_gz="$assetname.tar.gz"
|
|
||||||
ASSET_PATH="$destbin/$assetname.tar.gz"
|
|
||||||
|
|
||||||
giteahost="https://bitcoin-after.life/gitea"
|
|
||||||
url_releases="$giteahost/api/v1/repos/$OWNER/$REPO/releases"
|
|
||||||
|
|
||||||
echo_i() {
|
|
||||||
echo -e "\033[1m==> $1\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo_e() {
|
|
||||||
echo -e "\033[31;1m$1\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo_s() {
|
|
||||||
echo -e "\033[32;1m$1\033[0m"
|
|
||||||
}
|
|
||||||
echo_w() {
|
|
||||||
echo -e "\033[33;1m$1\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
prepare_release(){
|
|
||||||
mkdir -p "$destbin/$assetname"
|
|
||||||
if ! cargo build --release; then
|
|
||||||
echo_w "error building release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ls -l $binpath
|
|
||||||
cp target/release/bal-server \
|
|
||||||
target/release/bal-pusher \
|
|
||||||
README.md \
|
|
||||||
"$destbin/$assetname"
|
|
||||||
(
|
|
||||||
cd "$destbin"
|
|
||||||
echo_w $ASSET_PATH
|
|
||||||
echo "ls $(pwd)"
|
|
||||||
ls
|
|
||||||
echo "ls $(pwd)/$assetname"
|
|
||||||
ls "$(pwd)/$assetname"
|
|
||||||
ls $assetname
|
|
||||||
tar -czf "$asset_tar_gz" "$assetname"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
push_tag() {
|
|
||||||
git commit -am"release: $release_name"
|
|
||||||
git push
|
|
||||||
#git tag -a "$TAG" -m"release: $release_name"
|
|
||||||
#git push origin --tags
|
|
||||||
}
|
|
||||||
|
|
||||||
# Configurazioni
|
|
||||||
post_release() {
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo_e "no data to release"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "data: $1"
|
|
||||||
fi
|
|
||||||
echo "token:$TOKEN"
|
|
||||||
echo url_releases: $url_releases
|
|
||||||
RELEASE="$(curl -s -X POST \
|
|
||||||
-H "accept: application/json" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$1" \
|
|
||||||
$url_releases
|
|
||||||
)"
|
|
||||||
echo $RELEASE
|
|
||||||
}
|
|
||||||
|
|
||||||
add_asset_release() {
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo_e "error add_asset_release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo $ASSET_PATH
|
|
||||||
ls -l $ASSET_PATH
|
|
||||||
pwd
|
|
||||||
curl -X POST \
|
|
||||||
-H "accept: application/json" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: multipart/form-data" \
|
|
||||||
-F "attachment=@$ASSET_PATH" \
|
|
||||||
"$url_releases/$1/assets"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Estrae l'ID della release
|
|
||||||
release_data=$(cat <<EOF
|
|
||||||
{
|
|
||||||
"tag_name":"$TAG",
|
|
||||||
"name":"$release_name",
|
|
||||||
"body":"Release: $release_name enjoy\n$message"
|
|
||||||
}
|
|
||||||
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
prepare_release
|
|
||||||
echo_s "prepare release done"
|
|
||||||
push_tag
|
|
||||||
echo_s "push tag done"
|
|
||||||
echo "$release_data"
|
|
||||||
post_release "$release_data"
|
|
||||||
echo_s "prepare release done"
|
|
||||||
echo $RELEASE
|
|
||||||
id_release=
|
|
||||||
add_asset_release $(echo $RELEASE | jq .id)
|
|
||||||
echo_s "done"
|
|
||||||
@@ -5,8 +5,7 @@ use bitcoin::Network;
|
|||||||
use bitcoincore_rpc::{Auth, Client, Error, RpcApi, bitcoin};
|
use bitcoincore_rpc::{Auth, Client, Error, RpcApi, bitcoin};
|
||||||
use bitcoincore_rpc_json::GetBlockchainInfoResult;
|
use bitcoincore_rpc_json::GetBlockchainInfoResult;
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use ed25519_dalek::{Signer as _, SigningKey, pkcs8::DecodePrivateKey};
|
||||||
use hex;
|
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -15,24 +14,19 @@ use sqlite::{Connection, Value};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
use std::io::Cursor;
|
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::{thread, time::Duration};
|
use std::{thread, time::Duration};
|
||||||
use zmq::{Context, DEALER, DONTWAIT, Socket};
|
use zmq::{Context, Socket};
|
||||||
|
|
||||||
use bal_server::db::open_db;
|
use bal_server::db::open_db;
|
||||||
use bal_server::validation::is_valid_welist_url;
|
use bal_server::validation::is_valid_welist_url;
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
use openssl::hash::MessageDigest;
|
|
||||||
use openssl::pkey::PKey;
|
|
||||||
use openssl::sign::Signer;
|
|
||||||
use openssl::sign::Verifier;
|
|
||||||
use reqwest::Client as rClient;
|
use reqwest::Client as rClient;
|
||||||
use std::fs;
|
use std::net::SocketAddr;
|
||||||
use std::time::Instant;
|
use url::Url;
|
||||||
|
|
||||||
const LOCKTIME_THRESHOLD: i64 = 5000000;
|
const LOCKTIME_THRESHOLD: i64 = 5000000;
|
||||||
const VERSION: &str = "0.0.2";
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct MyConfig {
|
struct MyConfig {
|
||||||
db_file: String,
|
db_file: String,
|
||||||
@@ -90,7 +84,7 @@ fn get_network_params(cfg: &MyConfig, network: Network) -> &NetworkParams {
|
|||||||
fn get_network_params_default(network: Network) -> NetworkParams {
|
fn get_network_params_default(network: Network) -> NetworkParams {
|
||||||
match network {
|
match network {
|
||||||
Network::Testnet => NetworkParams {
|
Network::Testnet => NetworkParams {
|
||||||
host: "http://i27.0.0.1".to_string(),
|
host: "http://127.0.0.1".to_string(),
|
||||||
port: 18332,
|
port: 18332,
|
||||||
dir_path: "testnet3/".to_string(),
|
dir_path: "testnet3/".to_string(),
|
||||||
db_field: "testnet".to_string(),
|
db_field: "testnet".to_string(),
|
||||||
@@ -100,7 +94,7 @@ fn get_network_params_default(network: Network) -> NetworkParams {
|
|||||||
zmq_listener: "tcp://127.0.0.1:23332".to_string(),
|
zmq_listener: "tcp://127.0.0.1:23332".to_string(),
|
||||||
},
|
},
|
||||||
Network::Testnet4 => NetworkParams {
|
Network::Testnet4 => NetworkParams {
|
||||||
host: "http://i27.0.0.1".to_string(),
|
host: "http://127.0.0.1".to_string(),
|
||||||
port: 48332,
|
port: 48332,
|
||||||
dir_path: "testnet4/".to_string(),
|
dir_path: "testnet4/".to_string(),
|
||||||
db_field: "testnet4".to_string(),
|
db_field: "testnet4".to_string(),
|
||||||
@@ -143,7 +137,7 @@ fn get_network_params_default(network: Network) -> NetworkParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdError>> {
|
fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdError>> {
|
||||||
if network.cookie_file != "" {
|
if !network.cookie_file.is_empty() {
|
||||||
Ok(network.cookie_file.clone())
|
Ok(network.cookie_file.clone())
|
||||||
} else {
|
} else {
|
||||||
match env::var_os("HOME") {
|
match env::var_os("HOME") {
|
||||||
@@ -161,12 +155,12 @@ fn get_cookie_filename(network: &NetworkParams) -> Result<String, Box<dyn StdErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client_from_username(
|
fn get_client_from_username(
|
||||||
url: &String,
|
url: &str,
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
if network.rpc_user != "" {
|
if !network.rpc_user.is_empty() {
|
||||||
match Client::new(
|
match Client::new(
|
||||||
&url[..],
|
url,
|
||||||
Auth::UserPass(network.rpc_user.to_string(), network.rpc_pass.to_string()),
|
Auth::UserPass(network.rpc_user.to_string(), network.rpc_pass.to_string()),
|
||||||
) {
|
) {
|
||||||
Ok(client) => match client.get_blockchain_info() {
|
Ok(client) => match client.get_blockchain_info() {
|
||||||
@@ -180,57 +174,37 @@ fn get_client_from_username(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client_from_cookie(
|
fn get_client_from_cookie(
|
||||||
url: &String,
|
url: &str,
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
match get_cookie_filename(network) {
|
match get_cookie_filename(network) {
|
||||||
Ok(cookie) => match Client::new(&url[..], Auth::CookieFile(cookie.into())) {
|
Ok(cookie) => match Client::new(url, Auth::CookieFile(cookie.into())) {
|
||||||
Ok(client) => match client.get_blockchain_info() {
|
Ok(client) => match client.get_blockchain_info() {
|
||||||
Ok(bcinfo) => Ok((client, bcinfo)),
|
Ok(bcinfo) => Ok((client, bcinfo)),
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
},
|
},
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_client(
|
fn get_client(
|
||||||
network: &NetworkParams,
|
network: &NetworkParams,
|
||||||
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
) -> Result<(Client, GetBlockchainInfoResult), Box<dyn StdError>> {
|
||||||
let url = format!("{}:{}/", network.host, &network.port);
|
let url = format!("{}:{}/", network.host, network.port);
|
||||||
debug!("trying to connect to bitcoin daemon:{url}");
|
debug!("trying to connect to bitcoin daemon:{url}");
|
||||||
match get_client_from_username(&url, network) {
|
match get_client_from_username(&url, network) {
|
||||||
Ok(client) => Ok(client),
|
Ok(client) => Ok(client),
|
||||||
Err(_) => match get_client_from_cookie(&url, &network) {
|
Err(_) => match get_client_from_cookie(&url, network) {
|
||||||
Ok(client) => Ok(client),
|
Ok(client) => Ok(client),
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(), Error> {
|
async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(), Error> {
|
||||||
/*let url = args.next().expect("Usage: <rpc_url> <username> <password>");
|
|
||||||
let user = args.next().expect("no user given");
|
|
||||||
let pass = args.next().expect("no pass given");
|
|
||||||
*/
|
|
||||||
//let network = Network::Regtest
|
|
||||||
match get_client(network_params) {
|
match get_client(network_params) {
|
||||||
Ok((rpc, bcinfo)) => {
|
Ok((rpc, bcinfo)) => {
|
||||||
info!("connected");
|
info!("connected");
|
||||||
//let best_block_hash = rpc.get_best_block_hash()?;
|
|
||||||
//info!("best block hash: {}", best_block_hash);
|
|
||||||
//let bestblockcount = rpc.get_block_count()?;
|
|
||||||
//info!("best block height: {}", bestblockcount);
|
|
||||||
//let best_block_hash_by_height = rpc.get_block_hash(bestblockcount)?;
|
|
||||||
//info!("best block hash by height: {}", best_block_hash_by_height);
|
|
||||||
//assert_eq!(best_block_hash_by_height, best_block_hash);
|
|
||||||
//let from_block= std::cmp::max(0, bestblockcount - 11);
|
|
||||||
//let mut time_sum:u64=0;
|
|
||||||
//for i in from_block..bestblockcount{
|
|
||||||
// let hash = rpc.get_block_hash(i).unwrap();
|
|
||||||
// let block: bitcoin::Block = rpc.get_by_id(&hash).unwrap();
|
|
||||||
// time_sum += <u32 as Into<u64>>::into(block.header.time);
|
|
||||||
//}
|
|
||||||
//let average_time = time_sum/11;
|
|
||||||
info!("median time: {}", bcinfo.median_time);
|
info!("median time: {}", bcinfo.median_time);
|
||||||
//info!("height time: {}",bcinfo.median_time);
|
//info!("height time: {}",bcinfo.median_time);
|
||||||
info!("blocks: {}", bcinfo.blocks);
|
info!("blocks: {}", bcinfo.blocks);
|
||||||
@@ -247,7 +221,13 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|||||||
info!("db open {}", &cfg.db_file);
|
info!("db open {}", &cfg.db_file);
|
||||||
|
|
||||||
let sqlquery = "SELECT * FROM tbl_tx WHERE network = :network AND status = :status AND ( locktime < :bestblock_height OR locktime > :locktime_threshold AND locktime < :bestblock_time);";
|
let sqlquery = "SELECT * FROM tbl_tx WHERE network = :network AND status = :status AND ( locktime < :bestblock_height OR locktime > :locktime_threshold AND locktime < :bestblock_time);";
|
||||||
let query_tx = db.prepare(sqlquery).unwrap().into_iter();
|
let query_tx = match db.prepare(sqlquery) {
|
||||||
|
Ok(q) => q.into_iter(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("tbl_tx not ready yet (tables may not exist): {}", e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
trace!("query_tx: {}", sqlquery);
|
trace!("query_tx: {}", sqlquery);
|
||||||
trace!(":locktime_threshold: {}", LOCKTIME_THRESHOLD);
|
trace!(":locktime_threshold: {}", LOCKTIME_THRESHOLD);
|
||||||
trace!(":bestblock_time: {}", average_time);
|
trace!(":bestblock_time: {}", average_time);
|
||||||
@@ -257,45 +237,38 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|||||||
//let query_tx = db.prepare("SELECT * FROM tbl_tx where status = :status").unwrap().into_iter();
|
//let query_tx = db.prepare("SELECT * FROM tbl_tx where status = :status").unwrap().into_iter();
|
||||||
let mut pushed_txs: Vec<String> = Vec::new();
|
let mut pushed_txs: Vec<String> = Vec::new();
|
||||||
let mut invalid_txs: std::collections::HashMap<String, String> = HashMap::new();
|
let mut invalid_txs: std::collections::HashMap<String, String> = HashMap::new();
|
||||||
for row in query_tx
|
for row_result in match query_tx.bind::<&[(_, Value)]>(
|
||||||
.bind::<&[(_, Value)]>(
|
&[
|
||||||
&[
|
(":locktime_threshold", LOCKTIME_THRESHOLD.into()),
|
||||||
(":locktime_threshold", (LOCKTIME_THRESHOLD as i64).into()),
|
(":bestblock_time", (average_time as i64).into()),
|
||||||
(":bestblock_time", (average_time as i64).into()),
|
(":bestblock_height", (bcinfo.blocks as i64).into()),
|
||||||
(":bestblock_height", (bcinfo.blocks as i64).into()),
|
(":network", network_params.db_field.clone().into()),
|
||||||
(":network", network_params.db_field.clone().into()),
|
(":status", 0.into()),
|
||||||
(":status", 0.into()),
|
][..],
|
||||||
][..],
|
) {
|
||||||
)
|
Ok(bound) => bound,
|
||||||
.unwrap()
|
Err(e) => {
|
||||||
.map(|row| row.unwrap())
|
error!("Failed to bind query parameters: {}", e);
|
||||||
{
|
return Ok(());
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
let row = match row_result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to read row: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let tx = row.read::<&str, _>("tx");
|
let tx = row.read::<&str, _>("tx");
|
||||||
let txid = row.read::<&str, _>("txid");
|
let txid = row.read::<&str, _>("txid");
|
||||||
let locktime = row.read::<i64, _>("locktime");
|
let locktime = row.read::<i64, _>("locktime");
|
||||||
info!("to be pushed: {}: {}", txid, locktime);
|
info!("to be pushed: {}: {}", txid, locktime);
|
||||||
match rpc.send_raw_transaction(tx) {
|
match rpc.send_raw_transaction(tx) {
|
||||||
Ok(o) => {
|
Ok(o) => {
|
||||||
/*let mut file = OpenOptions::new()
|
|
||||||
.append(true) // Set the append option
|
|
||||||
.create(true) // Create the file if it doesn't exist
|
|
||||||
.open("valid_txs")?;
|
|
||||||
let data = format!("{}\t:\t{}\t:\t{}\n",txid,average_time,locktime);
|
|
||||||
file.write_all(data.as_bytes())?;
|
|
||||||
drop(file);
|
|
||||||
*/
|
|
||||||
info!("tx: {} pusshata PUSHED\n{}", txid, o);
|
info!("tx: {} pusshata PUSHED\n{}", txid, o);
|
||||||
pushed_txs.push(txid.to_string());
|
pushed_txs.push(txid.to_string());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
/*let mut file = OpenOptions::new()
|
|
||||||
.append(true) // Set the append option
|
|
||||||
.create(true) // Create the file if it doesn't exist
|
|
||||||
.open("/home/bal/invalid_txs")?;
|
|
||||||
let data = format!("{}:\t{}\t:\t{}\t:\t{}\n",txid,err,average_time,locktime);
|
|
||||||
file.write_all(data.as_bytes())?;
|
|
||||||
drop(file);
|
|
||||||
*/
|
|
||||||
warn!("Error: {}\n{}", err, txid);
|
warn!("Error: {}\n{}", err, txid);
|
||||||
//store err in invalid_txs
|
//store err in invalid_txs
|
||||||
invalid_txs.insert(txid.to_string(), err.to_string());
|
invalid_txs.insert(txid.to_string(), err.to_string());
|
||||||
@@ -305,19 +278,44 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
|
|||||||
|
|
||||||
for txid in &pushed_txs {
|
for txid in &pushed_txs {
|
||||||
let sql = "UPDATE tbl_tx SET status = 1 WHERE txid = ?";
|
let sql = "UPDATE tbl_tx SET status = 1 WHERE txid = ?";
|
||||||
let mut stmt = db.prepare(sql).unwrap();
|
match db.prepare(sql) {
|
||||||
stmt.bind((1, Value::String(txid.clone()))).unwrap();
|
Ok(mut stmt) => {
|
||||||
let _ = stmt.next();
|
if let Err(e) = stmt.bind((1, Value::String(txid.clone()))) {
|
||||||
|
error!("Failed to bind txid for status update: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = stmt.next();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to prepare status update: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (txid, txerr) in &invalid_txs {
|
for (txid, txerr) in &invalid_txs {
|
||||||
let sql = "UPDATE tbl_tx SET status = 2, push_err = ? WHERE txid = ?";
|
let sql = "UPDATE tbl_tx SET status = 2, push_err = ? WHERE txid = ?";
|
||||||
let mut stmt = db.prepare(sql).unwrap();
|
match db.prepare(sql) {
|
||||||
stmt.bind((1, Value::String(txerr.clone()))).unwrap();
|
Ok(mut stmt) => {
|
||||||
stmt.bind((2, Value::String(txid.clone()))).unwrap();
|
if let Err(e) = stmt.bind((1, Value::String(txerr.clone()))) {
|
||||||
let _ = stmt.next();
|
error!("Failed to bind txerr for error update: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Err(e) = stmt.bind((2, Value::String(txid.clone()))) {
|
||||||
|
error!("Failed to bind txid for error update: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = stmt.next();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to prepare error update: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = send_stats_report(cfg, bcinfo).await {
|
||||||
|
error!("send_stats_report failed: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = calculate_stats(&db, network_params.db_field.clone()).await {
|
||||||
|
warn!("calculate_stats failed: {e}");
|
||||||
}
|
}
|
||||||
let _ = send_stats_report(cfg, bcinfo).await;
|
|
||||||
let _ = calculate_stats(&db, network_params.db_field.clone()).await;
|
|
||||||
}
|
}
|
||||||
Err(erx) => {
|
Err(erx) => {
|
||||||
error!("impossible to get client: {}, retrying on next block", erx);
|
error!("impossible to get client: {}, retrying on next block", erx);
|
||||||
@@ -375,31 +373,6 @@ ON CONFLICT(chain) DO UPDATE SET
|
|||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
let sql = format!("CREATE TABLE tbl_stats AS
|
|
||||||
SELECT
|
|
||||||
CURRENT_TIMESTAMP AS report_date,
|
|
||||||
'{chain}' as chain,
|
|
||||||
(SELECT COUNT(*) FROM tbl_tx WHERE network ='{chain}') AS totals,
|
|
||||||
(SELECT COUNT(*) FROM tbl_tx WHERE status = 0 AND network ='{chain}') AS waiting,
|
|
||||||
(SELECT COUNT(*) FROM tbl_tx WHERE status = 1 AND network ='{chain}') AS sent,
|
|
||||||
(SELECT COUNT(*) FROM tbl_tx WHERE status = 2 AND network ='{chain}') AS failed,
|
|
||||||
(SELECT SUM(our_fees) FROM tbl_tx WHERE status = 0 AND network ='{chain}') AS waiting_profit,
|
|
||||||
(SELECT SUM(our_fees) OR 0 FROM tbl_tx WHERE status = 1 AND network ='{chain}') AS sent_profit,
|
|
||||||
(SELECT SUM(our_fees) FROM tbl_tx WHERE status = 2 AND network ='{chain}') AS missed_profit,
|
|
||||||
(SELECT COUNT(*) FROM tbl_inp JOIN tbl_tx ON(tbl_inp.txid = tbl_tx.txid) WHERE tbl_tx.status=0 AND tbl_tx.network ='{chain}') AS unique_inputs;
|
|
||||||
");
|
|
||||||
let sql = "UPDATE tbl_stats set
|
|
||||||
totals = (SELECT COUNT(*) FROM tbl_tx WHERE network ='{chain}'),
|
|
||||||
waiting = (SELECT COUNT(*) FROM tbl_tx WHERE status = 0 AND network ='{chain}'),
|
|
||||||
sent = (SELECT COUNT(*) FROM tbl_tx WHERE status = 1 AND network ='{chain}'),
|
|
||||||
failed = (SELECT COUNT(*) FROM tbl_tx WHERE status = 1 AND network ='{chain}'),
|
|
||||||
waiting_profit = (SELECT SUM(our_fees) FROM tbl_tx WHERE status = 0 AND network ='{chain}'),
|
|
||||||
sent_profit = (SELECT SUM(our_fees) FROM tbl_tx WHERE status = 0 AND network ='{chain}'),
|
|
||||||
missed_profit = (SELECT SUM(our_fees) FROM tbl_tx WHERE status = 0 AND network ='{chain}')
|
|
||||||
unique_inputs = (SELECT COUNT(*) FROM tbl_inp JOIN tbl_tx ON(tbl_inp.txid = tbl_tx.txid) WHERE tbl_tx.status=0 AND tbl_tx.network ='{chain}')
|
|
||||||
WHERE chain = '{chain}'
|
|
||||||
*/
|
|
||||||
if let Err(err) = db.execute(&sql) {
|
if let Err(err) = db.execute(&sql) {
|
||||||
error!("error inserting creating stats table {err}");
|
error!("error inserting creating stats table {err}");
|
||||||
} else {
|
} else {
|
||||||
@@ -407,6 +380,84 @@ ON CONFLICT(chain) DO UPDATE SET
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
/// Parse the `(host, port)` pair from a base URL like `https://host[:port]`.
|
||||||
|
///
|
||||||
|
/// Falls back to the scheme's well-known default port (443 for `https`,
|
||||||
|
/// 80 for plain `http`), or to 443 when the scheme is unknown.
|
||||||
|
fn parse_host_port(base_url: &str) -> Option<(String, u16)> {
|
||||||
|
let url = Url::parse(base_url).ok()?;
|
||||||
|
let host = url
|
||||||
|
.host_str()?
|
||||||
|
.trim_start_matches('[')
|
||||||
|
.trim_end_matches(']')
|
||||||
|
.to_string();
|
||||||
|
let port = url.port_or_known_default().unwrap_or(443);
|
||||||
|
Some((host, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `host:port` and return the first IPv6 (AAAA) address, if any.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the host has no IPv6 address.
|
||||||
|
async fn resolve_first_ipv6(host: &str, port: u16) -> Option<SocketAddr> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
let host = host.to_string();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
format!("{}:{}", host, port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut addrs| addrs.find(|a| a.is_ipv6()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the HTTP client used for welist reports.
|
||||||
|
///
|
||||||
|
/// When `BAL_PUSHER_PREFER_IPV6` is truthy, the welist host is resolved and
|
||||||
|
/// the client is pinned to its first IPv6 address (the original hostname is
|
||||||
|
/// still used for the `Host` header and TLS SNI). This works around networks
|
||||||
|
/// where the IPv4 route to the welist host is broken while IPv6 works: the
|
||||||
|
/// default connector may otherwise pick the broken family and the request
|
||||||
|
/// stalls. When the variable is unset (the default), behavior is unchanged.
|
||||||
|
async fn welist_http_client(welist_url: &str) -> rClient {
|
||||||
|
let prefer_ipv6 = env::var("BAL_PUSHER_PREFER_IPV6")
|
||||||
|
.unwrap_or("false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !prefer_ipv6 {
|
||||||
|
return new_welist_client();
|
||||||
|
}
|
||||||
|
let (host, port) = match parse_host_port(welist_url) {
|
||||||
|
Some(hp) => hp,
|
||||||
|
None => {
|
||||||
|
warn!("BAL_PUSHER_PREFER_IPV6: cannot parse '{welist_url}', using default resolver");
|
||||||
|
return new_welist_client();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match resolve_first_ipv6(&host, port).await {
|
||||||
|
Some(addr) => {
|
||||||
|
debug!("BAL_PUSHER_PREFER_IPV6: pinning {host} to {addr}");
|
||||||
|
rClient::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.resolve(&host, addr)
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| new_welist_client())
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("BAL_PUSHER_PREFER_IPV6: no IPv6 address for {host}, using default resolver");
|
||||||
|
new_welist_client()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_welist_client() -> rClient {
|
||||||
|
rClient::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| rClient::new())
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_stats_report(
|
async fn send_stats_report(
|
||||||
cfg: &MyConfig,
|
cfg: &MyConfig,
|
||||||
bcinfo: GetBlockchainInfoResult,
|
bcinfo: GetBlockchainInfoResult,
|
||||||
@@ -415,14 +466,18 @@ async fn send_stats_report(
|
|||||||
debug!("sending report to welist");
|
debug!("sending report to welist");
|
||||||
let welist_url = env::var("WELIST_SERVER_URL")
|
let welist_url = env::var("WELIST_SERVER_URL")
|
||||||
.unwrap_or("https://welist.bitcoin-after.life".to_string());
|
.unwrap_or("https://welist.bitcoin-after.life".to_string());
|
||||||
if !is_valid_welist_url(&welist_url) {
|
let skip_validation = env::var("WELIST_SKIP_URL_VALIDATION")
|
||||||
|
.unwrap_or("false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !skip_validation && !is_valid_welist_url(&welist_url) {
|
||||||
warn!(
|
warn!(
|
||||||
"Invalid or unsafe WELIST_SERVER_URL: {}. Skipping stats report.",
|
"Invalid or unsafe WELIST_SERVER_URL: {}. Skipping stats report.",
|
||||||
welist_url
|
welist_url
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let client = rClient::new();
|
let client = welist_http_client(&welist_url).await;
|
||||||
let url = format!("{}/ping", welist_url);
|
let url = format!("{}/ping", welist_url);
|
||||||
debug!("welist url: {}", url);
|
debug!("welist url: {}", url);
|
||||||
let chain = bcinfo.chain.to_string().to_lowercase();
|
let chain = bcinfo.chain.to_string().to_lowercase();
|
||||||
@@ -431,7 +486,7 @@ async fn send_stats_report(
|
|||||||
cfg.url, chain, bcinfo.blocks, bcinfo.median_time, bcinfo.best_block_hash
|
cfg.url, chain, bcinfo.blocks, bcinfo.median_time, bcinfo.best_block_hash
|
||||||
);
|
);
|
||||||
trace!("message to be sent: {}", message);
|
trace!("message to be sent: {}", message);
|
||||||
let sign = sign_message(cfg.ssl_key_path.as_str(), &message.as_str());
|
let sign = sign_message(cfg.ssl_key_path.as_str(), message.as_str());
|
||||||
let response = client
|
let response = client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("User-Agent", format!("bal-pusher/{}", VERSION))
|
.header("User-Agent", format!("bal-pusher/{}", VERSION))
|
||||||
@@ -446,32 +501,23 @@ async fn send_stats_report(
|
|||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if !response.status().is_success() {
|
let status = response.status();
|
||||||
warn!(
|
let body = response.text().await?;
|
||||||
"Non-success response: {} {}",
|
info!(
|
||||||
response.status(),
|
"Report to welist({}) status={} body={}",
|
||||||
response.status().canonical_reason().unwrap_or("")
|
welist_url, status, body
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let body = &(response.text().await?);
|
|
||||||
info!("Report to welist({})\tSent: {}", welist_url, body);
|
|
||||||
} else {
|
} else {
|
||||||
debug!("Not sending stats");
|
debug!("Not sending stats");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn sign_message(private_key_path: &str, message: &str) -> String {
|
fn sign_message(private_key_path: &str, message: &str) -> String {
|
||||||
let key_data = fs::read(private_key_path).unwrap();
|
let signing_key =
|
||||||
|
SigningKey::read_pkcs8_pem_file(private_key_path).expect("failed to parse private key PEM");
|
||||||
|
let signature = signing_key.sign(message.as_bytes());
|
||||||
|
|
||||||
let private_key = PKey::private_key_from_pem(&key_data).unwrap();
|
general_purpose::STANDARD.encode(signature.to_bytes())
|
||||||
let mut signer = Signer::new_without_digest(&private_key).unwrap();
|
|
||||||
|
|
||||||
let signature = signer.sign_oneshot_to_vec(message.as_bytes()).unwrap();
|
|
||||||
|
|
||||||
let signature_b64 = general_purpose::STANDARD.encode(&signature);
|
|
||||||
|
|
||||||
signature_b64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_env(cfg: &mut MyConfig) {
|
fn parse_env(cfg: &mut MyConfig) {
|
||||||
@@ -490,144 +536,47 @@ fn parse_env_netconfig(cfg_lock: &mut MyConfig, chain: &str) -> NetworkParams {
|
|||||||
"testnet4" => &mut cfg_lock.testnet4,
|
"testnet4" => &mut cfg_lock.testnet4,
|
||||||
&_ => &mut cfg_lock.mainnet,
|
&_ => &mut cfg_lock.mainnet,
|
||||||
};
|
};
|
||||||
match env::var(format!("BAL_PUSHER_{}_HOST", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_HOST", chain.to_uppercase())) {
|
||||||
Ok(value) => {
|
cfg.host = value;
|
||||||
cfg.host = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
match env::var(format!("BAL_PUSHER_{}_PORT", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_PORT", chain.to_uppercase()))
|
||||||
Ok(value) => match value.parse::<u64>() {
|
&& let Ok(port_num) = value.parse::<u64>()
|
||||||
Ok(value) => match u16::try_from(value) {
|
{
|
||||||
Ok(port) => cfg.port = port,
|
if let Ok(port) = u16::try_from(port_num) {
|
||||||
Err(e) => {
|
cfg.port = port;
|
||||||
error!(
|
} else {
|
||||||
"Port value {} exceeds u16 range for chain {}: {}",
|
error!(
|
||||||
value, chain, e
|
"Port value {} exceeds u16 range for chain {}",
|
||||||
);
|
port_num, chain
|
||||||
}
|
);
|
||||||
},
|
|
||||||
Err(_) => {}
|
|
||||||
},
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
|
||||||
match env::var(format!("BAL_PUSHER_{}_DIR_PATH", chain.to_uppercase())) {
|
|
||||||
Ok(value) => {
|
|
||||||
cfg.dir_path = value;
|
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
match env::var(format!("BAL_PUSHER_{}_DB_FIELD", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_DIR_PATH", chain.to_uppercase())) {
|
||||||
Ok(value) => {
|
cfg.dir_path = value;
|
||||||
cfg.db_field = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
match env::var(format!("BAL_PUSHER_{}_COOKIE_FILE", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_DB_FIELD", chain.to_uppercase())) {
|
||||||
Ok(value) => {
|
cfg.db_field = value;
|
||||||
cfg.cookie_file = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
match env::var(format!("BAL_PUSHER_{}_RPC_USER", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_COOKIE_FILE", chain.to_uppercase())) {
|
||||||
Ok(value) => {
|
cfg.cookie_file = value;
|
||||||
cfg.rpc_user = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
match env::var(format!("BAL_PUSHER_{}_RPC_PASSWORD", chain.to_uppercase())) {
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_RPC_USER", chain.to_uppercase())) {
|
||||||
Ok(value) => {
|
cfg.rpc_user = value;
|
||||||
cfg.rpc_pass = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
println!(
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_RPC_PASSWORD", chain.to_uppercase())) {
|
||||||
"{}",
|
cfg.rpc_pass = value;
|
||||||
format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())
|
}
|
||||||
);
|
if let Ok(value) = env::var(format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())) {
|
||||||
match env::var(format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())) {
|
cfg.zmq_listener = value;
|
||||||
Ok(value) => {
|
|
||||||
println!("value:{}", value);
|
|
||||||
cfg.zmq_listener = value;
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
cfg.clone()
|
cfg.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_zmq_connection(endpoint: &str) -> bool {
|
|
||||||
trace!("check zmq connection");
|
|
||||||
let context = Context::new();
|
|
||||||
let socket = match context.socket(DEALER) {
|
|
||||||
Ok(sock) => sock,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if socket.connect(endpoint).is_err() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to send an empty message non-blocking
|
|
||||||
socket.send("", DONTWAIT).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this struct to monitor connection health
|
|
||||||
struct ConnectionMonitor {
|
|
||||||
last_message_time: Instant,
|
|
||||||
timeout: Duration,
|
|
||||||
consecutive_timeouts: u32,
|
|
||||||
max_consecutive_timeouts: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionMonitor {
|
|
||||||
fn new(timeout_secs: u64, max_timeouts: u32) -> Self {
|
|
||||||
Self {
|
|
||||||
last_message_time: Instant::now(),
|
|
||||||
timeout: Duration::from_secs(timeout_secs),
|
|
||||||
consecutive_timeouts: 0,
|
|
||||||
max_consecutive_timeouts: max_timeouts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self) {
|
|
||||||
self.last_message_time = Instant::now();
|
|
||||||
self.consecutive_timeouts = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_connection(&mut self) -> ConnectionStatus {
|
|
||||||
let elapsed = self.last_message_time.elapsed();
|
|
||||||
|
|
||||||
if elapsed > self.timeout {
|
|
||||||
self.consecutive_timeouts += 1;
|
|
||||||
|
|
||||||
if self.consecutive_timeouts >= self.max_consecutive_timeouts {
|
|
||||||
ConnectionStatus::Lost(elapsed)
|
|
||||||
} else {
|
|
||||||
ConnectionStatus::Warning(elapsed)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ConnectionStatus::Healthy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self) {
|
|
||||||
self.consecutive_timeouts = 0;
|
|
||||||
self.last_message_time = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ConnectionStatus {
|
|
||||||
Healthy,
|
|
||||||
Warning(Duration),
|
|
||||||
Lost(Duration),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut cfg = MyConfig::default();
|
let mut cfg = MyConfig::default();
|
||||||
|
|
||||||
let dbfile = env::var("BAL_PUSHER_DB_FILE").unwrap();
|
|
||||||
parse_env(&mut cfg);
|
parse_env(&mut cfg);
|
||||||
let mut args = std::env::args();
|
let mut args = std::env::args();
|
||||||
let _exe_name = args.next().unwrap();
|
let _exe_name = args.next().unwrap();
|
||||||
@@ -662,31 +611,48 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match socket.set_subscribe(b"") {
|
match socket.set_subscribe(b"") {
|
||||||
Ok(_) => {}
|
Ok(_) => {
|
||||||
|
info!("ZMQ subscribed to all topics on {}", zmq_address);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("ZMQ subscribe failed: {}, exiting", e);
|
error!("ZMQ subscribe failed: {}, exiting", e);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = main_result(&cfg, network_params).await;
|
if let Err(e) = main_result(&cfg, network_params).await {
|
||||||
|
error!("main_result failed on startup: {}", e);
|
||||||
|
}
|
||||||
info!("waiting new blocks..");
|
info!("waiting new blocks..");
|
||||||
let mut last_seq: Vec<u8> = [0; 4].to_vec();
|
|
||||||
let mut counter = 0;
|
|
||||||
let max = 100;
|
|
||||||
socket.set_rcvtimeo(5000).unwrap(); // 5 seconds timeout
|
socket.set_rcvtimeo(5000).unwrap(); // 5 seconds timeout
|
||||||
|
let mut consecutive_timeouts: u32 = 0;
|
||||||
loop {
|
loop {
|
||||||
let message = match socket.recv_multipart(0) {
|
let message = match socket.recv_multipart(0) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("ZMQ recv timeout or error: {}, retrying...", e);
|
consecutive_timeouts += 1;
|
||||||
|
if consecutive_timeouts.is_multiple_of(720) {
|
||||||
|
error!(
|
||||||
|
"No ZMQ messages for {}s ({} consecutive timeouts), is bitcoind ZMQ active on {}?",
|
||||||
|
consecutive_timeouts * 5,
|
||||||
|
consecutive_timeouts,
|
||||||
|
zmq_address
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
trace!("ZMQ recv timeout or error: {}, retrying...", e);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if consecutive_timeouts > 0 {
|
||||||
|
info!(
|
||||||
|
"ZMQ connection restored after {} consecutive timeouts",
|
||||||
|
consecutive_timeouts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
consecutive_timeouts = 0;
|
||||||
let topic = message[0].clone();
|
let topic = message[0].clone();
|
||||||
let body = message[1].clone();
|
let body = message[1].clone();
|
||||||
let seq = message[2].clone();
|
|
||||||
last_seq = seq;
|
|
||||||
debug!(
|
debug!(
|
||||||
"ZMQ:GET TOPIC: {}",
|
"ZMQ:GET TOPIC: {}",
|
||||||
String::from_utf8(topic.clone()).expect("invalid topic")
|
String::from_utf8(topic.clone()).expect("invalid topic")
|
||||||
@@ -694,18 +660,52 @@ async fn main() -> std::io::Result<()> {
|
|||||||
trace!("ZMQ:GET BODY: {}", hex::encode(&body));
|
trace!("ZMQ:GET BODY: {}", hex::encode(&body));
|
||||||
if topic == b"hashblock" {
|
if topic == b"hashblock" {
|
||||||
info!("NEW BLOCK: {}", hex::encode(&body));
|
info!("NEW BLOCK: {}", hex::encode(&body));
|
||||||
let _ = main_result(&cfg, network_params).await;
|
if let Err(e) = main_result(&cfg, network_params).await {
|
||||||
|
error!("main_result failed on new block: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(100)); // Sleep for 100ms
|
thread::sleep(Duration::from_millis(100)); // Sleep for 100ms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn seq_to_str(seq: &Vec<u8>) -> String {
|
|
||||||
if seq.len() == 4 {
|
#[cfg(test)]
|
||||||
let mut rdr = Cursor::new(seq);
|
mod tests {
|
||||||
let sequence = rdr
|
use super::*;
|
||||||
.read_u32::<LittleEndian>()
|
|
||||||
.expect("Failed to read integer");
|
#[test]
|
||||||
return sequence.to_string();
|
fn parse_host_port_https_default_port() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_host_port("https://welist.bitcoin-after.life"),
|
||||||
|
Some(("welist.bitcoin-after.life".to_string(), 443))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_port_explicit_port_and_path() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_host_port("https://example.com:8443/ping"),
|
||||||
|
Some(("example.com".to_string(), 8443))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_port_http_default_port() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_host_port("http://example.com"),
|
||||||
|
Some(("example.com".to_string(), 80))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_port_ipv6_literal_brackets_stripped() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_host_port("https://[2a13:2c0::1]:443"),
|
||||||
|
Some(("2a13:2c0::1".to_string(), 443))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_port_invalid_url() {
|
||||||
|
assert_eq!(parse_host_port("not a url"), None);
|
||||||
}
|
}
|
||||||
"Unknown".to_string()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use chrono::Utc;
|
|||||||
use hex_conservative::FromHex;
|
use hex_conservative::FromHex;
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json;
|
|
||||||
use sqlite::State;
|
use sqlite::State;
|
||||||
use sqlite::{Connection, Value};
|
use sqlite::{Connection, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -119,6 +118,7 @@ pub struct StatsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[expect(dead_code)]
|
||||||
struct ActixConfig {
|
struct ActixConfig {
|
||||||
max_body_size: usize,
|
max_body_size: usize,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
@@ -208,7 +208,7 @@ async fn echo_pub_key(data: web::Data<AppState>) -> impl Responder {
|
|||||||
"Failed to read public key file {}: {}",
|
"Failed to read public key file {}: {}",
|
||||||
data.cfg.pub_key_path, e
|
data.cfg.pub_key_path, e
|
||||||
);
|
);
|
||||||
HttpResponse::InternalServerError().body("Failed to read public key file")
|
HttpResponse::InternalServerError().body("error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +217,38 @@ async fn echo_version() -> impl Responder {
|
|||||||
HttpResponse::Ok().body(VERSION)
|
HttpResponse::Ok().body(VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_valid_ip(ip: &str) -> bool {
|
||||||
|
ip.parse::<std::net::IpAddr>().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_client_ip(req: &actix_web::HttpRequest) -> String {
|
||||||
|
if let Some(val) = req.headers().get("X-Real-IP")
|
||||||
|
&& let Ok(s) = val.to_str()
|
||||||
|
{
|
||||||
|
let ip = s.split(',').next().unwrap_or(s).trim();
|
||||||
|
if is_valid_ip(ip) {
|
||||||
|
debug!("client IP from X-Real-IP: {}", ip);
|
||||||
|
return ip.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(val) = req.headers().get("X-Forwarded-For")
|
||||||
|
&& let Ok(s) = val.to_str()
|
||||||
|
{
|
||||||
|
let ip = s.split(',').next().unwrap_or(s).trim();
|
||||||
|
if is_valid_ip(ip) {
|
||||||
|
debug!("client IP from X-Forwarded-For: {}", ip);
|
||||||
|
return ip.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fallback = req
|
||||||
|
.connection_info()
|
||||||
|
.peer_addr()
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
debug!("client IP from peer_addr fallback: {}", fallback);
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
|
||||||
async fn echo_info(
|
async fn echo_info(
|
||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
@@ -224,26 +256,15 @@ async fn echo_info(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("Unknown network");
|
return HttpResponse::NotFound().body("error");
|
||||||
}
|
}
|
||||||
info!("echo info!!!{}", param);
|
info!("echo info!!!{}", param);
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
debug!("network disabled {}", param);
|
debug!("network disabled {}", param);
|
||||||
return HttpResponse::BadRequest().body("network disabled");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
let remote_addr = req
|
let remote_addr = extract_client_ip(&req);
|
||||||
.headers()
|
|
||||||
.get("X-Real-IP")
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.and_then(|xff| xff.split(',').next())
|
|
||||||
.map(|ip| ip.trim().to_string())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
req.connection_info()
|
|
||||||
.peer_addr()
|
|
||||||
.unwrap_or("unknown")
|
|
||||||
.to_string()
|
|
||||||
});
|
|
||||||
let address = match netconfig.xpub {
|
let address = match netconfig.xpub {
|
||||||
false => {
|
false => {
|
||||||
let address = netconfig.address.to_string();
|
let address = netconfig.address.to_string();
|
||||||
@@ -257,7 +278,7 @@ async fn echo_info(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_info (lookup phase)");
|
error!("DB mutex poisoned in echo_info (lookup phase)");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match get_last_used_address_by_ip(
|
match get_last_used_address_by_ip(
|
||||||
@@ -266,31 +287,28 @@ async fn echo_info(
|
|||||||
&netconfig.address,
|
&netconfig.address,
|
||||||
&remote_addr,
|
&remote_addr,
|
||||||
) {
|
) {
|
||||||
Some(address) => return HttpResponse::Ok().json(InfoResponse {
|
Some(address) => {
|
||||||
address,
|
return HttpResponse::Ok().json(InfoResponse {
|
||||||
base_fee: netconfig.fixed_fee,
|
address,
|
||||||
chain: netconfig.network.to_string(),
|
base_fee: netconfig.fixed_fee,
|
||||||
info: data.cfg.info.to_string(),
|
chain: netconfig.network.to_string(),
|
||||||
version: VERSION.to_string(),
|
info: data.cfg.info.to_string(),
|
||||||
}),
|
version: VERSION.to_string(),
|
||||||
None => {
|
});
|
||||||
let next = get_next_address_index(&db, &netconfig.name, &netconfig.address);
|
|
||||||
next
|
|
||||||
}
|
}
|
||||||
|
None => get_next_address_index(&db, &netconfig.name, &netconfig.address),
|
||||||
}
|
}
|
||||||
}; // lock released
|
}; // lock released
|
||||||
|
|
||||||
// Derive address (CPU-bound, no lock held)
|
// Derive address (CPU-bound, no lock held)
|
||||||
let derived = match new_address_from_xpub(
|
let derived =
|
||||||
&netconfig.address, next_idx.1, netconfig.network
|
match new_address_from_xpub(&netconfig.address, next_idx.1, netconfig.network) {
|
||||||
) {
|
Ok(address) => address,
|
||||||
Ok(address) => address,
|
Err(e) => {
|
||||||
Err(e) => {
|
error!("Failed to derive address from xpub: {}", e);
|
||||||
error!("Failed to derive address from xpub: {}", e);
|
return HttpResponse::BadRequest().body("error");
|
||||||
return HttpResponse::BadRequest()
|
}
|
||||||
.body(format!("Failed to derive address: {}", e));
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lock #2: save the newly derived address
|
// Lock #2: save the newly derived address
|
||||||
{
|
{
|
||||||
@@ -298,7 +316,7 @@ async fn echo_info(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_info (save phase)");
|
error!("DB mutex poisoned in echo_info (save phase)");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
save_new_address(&db, next_idx.0, &derived.0, &derived.1, &remote_addr);
|
save_new_address(&db, next_idx.0, &derived.0, &derived.1, &remote_addr);
|
||||||
@@ -321,30 +339,30 @@ async fn echo_info(
|
|||||||
debug!("echo info reply: {}", json_data);
|
debug!("echo info reply: {}", json_data);
|
||||||
HttpResponse::Ok().json(info)
|
HttpResponse::Ok().json(info)
|
||||||
}
|
}
|
||||||
Err(err) => HttpResponse::InternalServerError().body(format!("error:{}", err)),
|
Err(_err) => HttpResponse::InternalServerError().body("error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("Unknown network");
|
return HttpResponse::NotFound().body("error");
|
||||||
}
|
}
|
||||||
info!("echo stats!!! {}", data.cfg.expose_stats);
|
info!("echo stats!!! {}", data.cfg.expose_stats);
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
debug!("network disabled {}", param);
|
debug!("network disabled {}", param);
|
||||||
return HttpResponse::BadRequest().body("network disabled");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
if !data.cfg.expose_stats {
|
if !data.cfg.expose_stats {
|
||||||
return HttpResponse::Forbidden().body("Stats not exposed");
|
return HttpResponse::Forbidden().body("error");
|
||||||
}
|
}
|
||||||
let mut stats: Vec<StatsResponse> = vec![];
|
let mut stats: Vec<StatsResponse> = vec![];
|
||||||
let db = match data.db.lock() {
|
let db = match data.db.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_stats");
|
error!("DB mutex poisoned in echo_stats");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut stmt = match db.prepare(
|
let mut stmt = match db.prepare(
|
||||||
@@ -353,24 +371,56 @@ async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl
|
|||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to prepare stats query: {}", e);
|
error!("Failed to prepare stats query: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = stmt.bind((1, Value::String(netconfig.name.clone()))) {
|
if let Err(e) = stmt.bind((1, Value::String(netconfig.name.clone()))) {
|
||||||
error!("Failed to bind chain in stats query: {}", e);
|
error!("Failed to bind chain in stats query: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
while let Ok(State::Row) = stmt.next() {
|
while let Ok(State::Row) = stmt.next() {
|
||||||
let report_date = stmt.read("report_date").unwrap_or("0".to_string());
|
let report_date = stmt.read("report_date").unwrap_or("0".to_string());
|
||||||
let chain = stmt.read("chain").unwrap_or("?".to_string());
|
let chain = stmt.read("chain").unwrap_or("?".to_string());
|
||||||
let totals = stmt.read("totals").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
let totals = stmt
|
||||||
let waiting = stmt.read("waiting").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.read("totals")
|
||||||
let sent = stmt.read("sent").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.unwrap_or("0".to_string())
|
||||||
let failed = stmt.read("failed").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.parse::<i64>()
|
||||||
let waiting_profit = stmt.read("waiting_profit").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let sent_profit = stmt.read("sent_profit").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
let waiting = stmt
|
||||||
let missed_profit = stmt.read("missed_profit").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.read("waiting")
|
||||||
let unique_inputs = stmt.read("unique_inputs").unwrap_or("0".to_string()).parse::<i64>().unwrap_or(0);
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let sent = stmt
|
||||||
|
.read("sent")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let failed = stmt
|
||||||
|
.read("failed")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let waiting_profit = stmt
|
||||||
|
.read("waiting_profit")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let sent_profit = stmt
|
||||||
|
.read("sent_profit")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let missed_profit = stmt
|
||||||
|
.read("missed_profit")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let unique_inputs = stmt
|
||||||
|
.read("unique_inputs")
|
||||||
|
.unwrap_or("0".to_string())
|
||||||
|
.parse::<i64>()
|
||||||
|
.unwrap_or(0);
|
||||||
stats.push(StatsResponse {
|
stats.push(StatsResponse {
|
||||||
report_date,
|
report_date,
|
||||||
chain,
|
chain,
|
||||||
@@ -384,13 +434,8 @@ async fn echo_stats(path: web::Path<String>, data: web::Data<AppState>) -> impl
|
|||||||
unique_inputs,
|
unique_inputs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
match serde_json::to_string(&stats) {
|
debug!("echo stats reply for chain: {}", netconfig.name);
|
||||||
Ok(json_data) => {
|
HttpResponse::Ok().json(stats)
|
||||||
debug!("echo info reply: {}", json_data);
|
|
||||||
HttpResponse::Ok().json(stats)
|
|
||||||
}
|
|
||||||
Err(err) => HttpResponse::InternalServerError().body(format!("error:{}", err)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
||||||
@@ -398,32 +443,33 @@ async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
|||||||
let strbody = match std::str::from_utf8(&body) {
|
let strbody = match std::str::from_utf8(&body) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return HttpResponse::BadRequest().body("Invalid UTF-8 body");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!("{}", strbody);
|
info!("{}", strbody);
|
||||||
|
|
||||||
if strbody.is_empty() || strbody.len() != 64 || !strbody.chars().all(|c| c.is_ascii_hexdigit()) {
|
if strbody.is_empty() || strbody.len() != 64 || !strbody.chars().all(|c| c.is_ascii_hexdigit())
|
||||||
return HttpResponse::BadRequest().body("Invalid txid");
|
{
|
||||||
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = match data.db.lock() {
|
let db = match data.db.lock() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_search");
|
error!("DB mutex poisoned in echo_search");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut statement = match db.prepare("SELECT * FROM tbl_tx WHERE txid = ? LIMIT 1") {
|
let mut statement = match db.prepare("SELECT * FROM tbl_tx WHERE txid = ? LIMIT 1") {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to prepare statement: {}", e);
|
error!("Failed to prepare statement: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = statement.bind((1, strbody)) {
|
if let Err(e) = statement.bind((1, strbody)) {
|
||||||
error!("Failed to bind parameter: {}", e);
|
error!("Failed to bind parameter: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(State::Row) = statement.next() {
|
if let Ok(State::Row) = statement.next() {
|
||||||
@@ -469,11 +515,14 @@ async fn echo_search(body: Bytes, data: web::Data<AppState>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
match serde_json::to_string(&response_data) {
|
match serde_json::to_string(&response_data) {
|
||||||
Ok(json_data) => HttpResponse::Ok().json(json_data),
|
Ok(json_data) => {
|
||||||
Err(_) => HttpResponse::BadRequest().body("Bad data received"),
|
debug!("echo search reply: {}", json_data);
|
||||||
|
HttpResponse::Ok().json(&response_data)
|
||||||
|
}
|
||||||
|
Err(_) => HttpResponse::BadRequest().body("error"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::BadRequest().body("Bad data received")
|
HttpResponse::BadRequest().body("error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,20 +534,19 @@ struct ParsedTx {
|
|||||||
ntxid: String,
|
ntxid: String,
|
||||||
raw_hex: String, // the original line
|
raw_hex: String, // the original line
|
||||||
locktime: String,
|
locktime: String,
|
||||||
inputs: Vec<(String, String)>, // (in_txid, in_vout)
|
inputs: Vec<(String, String)>, // (in_txid, in_vout)
|
||||||
outputs: Vec<(usize, String, u64)> // (idx, script_pubkey, amount_sat)
|
outputs: Vec<(usize, String, u64)>, // (idx, script_pubkey, amount_sat)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse all transactions from the request body **without** needing the DB lock.
|
/// Parse all transactions from the request body **without** needing the DB lock.
|
||||||
/// Returns `Ok(parsed_txs)` if at least one tx was valid, or `Err(HttpResponse)` for early failure.
|
/// Skips transactions that don't have a valid willexecutor output.
|
||||||
fn parse_request_transactions(
|
fn parse_request_transactions(
|
||||||
strbody: &str,
|
strbody: &str,
|
||||||
_req_time: i64,
|
_req_time: i64,
|
||||||
netconfig: &NetConfig,
|
netconfig: &NetConfig,
|
||||||
known_addresses: &HashSet<String>,
|
known_addresses: &HashSet<String>,
|
||||||
) -> Result<Vec<(ParsedTx, String, u64)>, HttpResponse> {
|
) -> Vec<(ParsedTx, String, u64)> {
|
||||||
let mut result: Vec<(ParsedTx, String, u64)> = Vec::new();
|
let mut result: Vec<(ParsedTx, String, u64)> = Vec::new();
|
||||||
let mut union_tx = true;
|
|
||||||
|
|
||||||
for line in strbody.split('\n') {
|
for line in strbody.split('\n') {
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
@@ -560,7 +608,7 @@ fn parse_request_transactions(
|
|||||||
if known_addresses.contains(&address) {
|
if known_addresses.contains(&address) {
|
||||||
address.clone()
|
address.clone()
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
netconfig.address.clone()
|
netconfig.address.clone()
|
||||||
@@ -579,13 +627,8 @@ fn parse_request_transactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
error!("willexecutor output not found for tx {}", txid);
|
trace!("willexecutor output not found for tx {}, skipping", txid);
|
||||||
return Err(HttpResponse::BadRequest().body("Bad data received"));
|
continue;
|
||||||
}
|
|
||||||
if !union_tx {
|
|
||||||
// This is only used for SQL building later; we track it in the caller
|
|
||||||
} else {
|
|
||||||
union_tx = false;
|
|
||||||
}
|
}
|
||||||
result.push((
|
result.push((
|
||||||
ParsedTx {
|
ParsedTx {
|
||||||
@@ -602,7 +645,7 @@ fn parse_request_transactions(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_push(
|
async fn echo_push(
|
||||||
@@ -614,24 +657,24 @@ async fn echo_push(
|
|||||||
let strbody = match std::str::from_utf8(&body) {
|
let strbody = match std::str::from_utf8(&body) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return HttpResponse::BadRequest().body("Invalid UTF-8 body");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let param = path.into_inner();
|
let param = path.into_inner();
|
||||||
if !NETWORKS.contains(¶m.as_str()) {
|
if !NETWORKS.contains(¶m.as_str()) {
|
||||||
return HttpResponse::NotFound().body("Unknown network");
|
return HttpResponse::NotFound().body("error");
|
||||||
}
|
}
|
||||||
let netconfig = data.cfg.get_net_config(¶m);
|
let netconfig = data.cfg.get_net_config(¶m);
|
||||||
if !netconfig.enabled {
|
if !netconfig.enabled {
|
||||||
trace!("network not enabled {}", &netconfig.name);
|
trace!("network not enabled {}", &netconfig.name);
|
||||||
return HttpResponse::BadRequest().body("Network not enabled");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
let req_time = match Utc::now().timestamp_nanos_opt() {
|
let req_time = match Utc::now().timestamp_nanos_opt() {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
error!("Invalid timestamp");
|
error!("Invalid timestamp");
|
||||||
return HttpResponse::BadRequest().body("Invalid timestamp");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -641,7 +684,7 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned acquiring addresses in echo_push");
|
error!("DB mutex poisoned acquiring addresses in echo_push");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if netconfig.xpub {
|
if netconfig.xpub {
|
||||||
@@ -649,7 +692,7 @@ async fn echo_push(
|
|||||||
Ok(addrs) => addrs,
|
Ok(addrs) => addrs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load addresses from xpub: {}", e);
|
error!("Failed to load addresses from xpub: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -658,14 +701,9 @@ async fn echo_push(
|
|||||||
}; // lock released here
|
}; // lock released here
|
||||||
|
|
||||||
// Parse all transactions (CPU-bound, no DB needed)
|
// Parse all transactions (CPU-bound, no DB needed)
|
||||||
let parsed = match parse_request_transactions(
|
let parsed = parse_request_transactions(strbody, req_time, netconfig, &known_addresses);
|
||||||
strbody, req_time, netconfig, &known_addresses,
|
|
||||||
) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(resp) => return resp,
|
|
||||||
};
|
|
||||||
if parsed.is_empty() {
|
if parsed.is_empty() {
|
||||||
return HttpResponse::Ok().body("thx");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let all_txids: Vec<String> = parsed.iter().map(|(p, _, _)| p.txid.clone()).collect();
|
let all_txids: Vec<String> = parsed.iter().map(|(p, _, _)| p.txid.clone()).collect();
|
||||||
@@ -676,14 +714,14 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_push duplicate check");
|
error!("DB mutex poisoned in echo_push duplicate check");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match check_duplicate_txids(&db, &all_txids) {
|
match check_duplicate_txids(&db, &all_txids) {
|
||||||
Ok(dups) => dups,
|
Ok(dups) => dups,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Duplicate check failed: {}", e);
|
error!("Duplicate check failed: {}", e);
|
||||||
return HttpResponse::InternalServerError().body("Database error");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; // lock released here
|
}; // lock released here
|
||||||
@@ -699,7 +737,7 @@ async fn echo_push(
|
|||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(_p) => {
|
Err(_p) => {
|
||||||
error!("DB mutex poisoned in echo_push insert phase");
|
error!("DB mutex poisoned in echo_push insert phase");
|
||||||
return HttpResponse::InternalServerError().body("DB mutex poisoned");
|
return HttpResponse::InternalServerError().body("error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -763,9 +801,15 @@ async fn echo_push(
|
|||||||
}
|
}
|
||||||
sqlouts.push_str(" SELECT ?, ?, ?, ?");
|
sqlouts.push_str(" SELECT ?, ?, ?, ?");
|
||||||
pouts.push((lineout, Value::String(parsed.txid.clone())));
|
pouts.push((lineout, Value::String(parsed.txid.clone())));
|
||||||
pouts.push((lineout + 1, Value::Integer(i64::try_from(*idx).unwrap_or(-1))));
|
pouts.push((
|
||||||
|
lineout + 1,
|
||||||
|
Value::Integer(i64::try_from(*idx).unwrap_or(-1)),
|
||||||
|
));
|
||||||
pouts.push((lineout + 2, Value::String(script.clone())));
|
pouts.push((lineout + 2, Value::String(script.clone())));
|
||||||
pouts.push((lineout + 3, Value::Integer(i64::try_from(*amount).unwrap_or(0))));
|
pouts.push((
|
||||||
|
lineout + 3,
|
||||||
|
Value::Integer(i64::try_from(*amount).unwrap_or(0)),
|
||||||
|
));
|
||||||
lineout += 4;
|
lineout += 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -780,7 +824,7 @@ async fn echo_push(
|
|||||||
|
|
||||||
if let Err(err) = execute_insert(&db, sqltxs, ptx, sqlinps, pinps, sqlouts, pouts) {
|
if let Err(err) = execute_insert(&db, sqltxs, ptx, sqlinps, pinps, sqlouts, pouts) {
|
||||||
error!("execute_insert failed: {}", err);
|
error!("execute_insert failed: {}", err);
|
||||||
return HttpResponse::BadRequest().body("Bad data received");
|
return HttpResponse::BadRequest().body("error");
|
||||||
}
|
}
|
||||||
} // lock released
|
} // lock released
|
||||||
|
|
||||||
@@ -862,7 +906,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let db = match open_db(&cfg.db_file) {
|
let db = match open_db(&cfg.db_file) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, e));
|
return Err(std::io::Error::other(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -877,15 +921,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
cfg: cfg.clone(),
|
cfg: cfg.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize networks
|
|
||||||
{
|
|
||||||
let db = data.db.lock().unwrap();
|
|
||||||
for network in NETWORKS {
|
|
||||||
let netconfig = data.cfg.get_net_config(network);
|
|
||||||
insert_xpub(&db, &netconfig.name.to_string(), &netconfig.address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bind_address = data.cfg.bind_address.clone();
|
let bind_address = data.cfg.bind_address.clone();
|
||||||
let bind_port = data.cfg.bind_port;
|
let bind_port = data.cfg.bind_port;
|
||||||
|
|
||||||
95
src/db.rs
95
src/db.rs
@@ -1,7 +1,9 @@
|
|||||||
use log::{error, info, trace};
|
use log::{error, info, trace, warn};
|
||||||
use sqlite::{Connection, Error, State, Value};
|
use sqlite::{Connection, Error, State, Value};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Check which txids are already present in the database in a single batch query.
|
/// Check which txids are already present in the database in a single batch query.
|
||||||
/// Returns a HashSet of txids that already exist (duplicates).
|
/// Returns a HashSet of txids that already exist (duplicates).
|
||||||
@@ -10,28 +12,28 @@ pub fn check_duplicate_txids(db: &Connection, txids: &[String]) -> Result<HashSe
|
|||||||
if txids.is_empty() {
|
if txids.is_empty() {
|
||||||
return Ok(HashSet::new());
|
return Ok(HashSet::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a single query with all txids using IN clause placeholders
|
// Build a single query with all txids using IN clause placeholders
|
||||||
// SQLite supports up to 1000 parameters per statement, so we chunk for safety
|
// SQLite supports up to 1000 parameters per statement, so we chunk for safety
|
||||||
let mut duplicates = HashSet::new();
|
let mut duplicates = HashSet::new();
|
||||||
let chunk_size = 500; // Safe chunk size for SQLite parameters
|
let chunk_size = 500; // Safe chunk size for SQLite parameters
|
||||||
|
|
||||||
for chunk in txids.chunks(chunk_size) {
|
for chunk in txids.chunks(chunk_size) {
|
||||||
let placeholders = chunk.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
let placeholders = chunk.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
||||||
let sql = format!("SELECT txid FROM tbl_tx WHERE txid IN ({})", placeholders);
|
let sql = format!("SELECT txid FROM tbl_tx WHERE txid IN ({})", placeholders);
|
||||||
let mut stmt = db.prepare(sql)?;
|
let mut stmt = db.prepare(sql)?;
|
||||||
|
|
||||||
for (i, txid) in chunk.iter().enumerate() {
|
for (i, txid) in chunk.iter().enumerate() {
|
||||||
stmt.bind((i + 1, Value::String(txid.clone())))?;
|
stmt.bind((i + 1, Value::String(txid.clone())))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Ok(State::Row) = stmt.next() {
|
while let Ok(State::Row) = stmt.next() {
|
||||||
if let Ok(txid) = stmt.read::<String, _>("txid") {
|
if let Ok(txid) = stmt.read::<String, _>("txid") {
|
||||||
duplicates.insert(txid);
|
duplicates.insert(txid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(duplicates)
|
Ok(duplicates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,12 +63,10 @@ pub fn open_db(path: &str) -> Result<Connection, String> {
|
|||||||
];
|
];
|
||||||
for prefix in &forbidden {
|
for prefix in &forbidden {
|
||||||
if path_str.starts_with(prefix) {
|
if path_str.starts_with(prefix) {
|
||||||
return Err(
|
return Err(format!(
|
||||||
format!(
|
"Absolute database path under {} is forbidden",
|
||||||
"Absolute database path under {} is forbidden",
|
prefix
|
||||||
prefix
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,24 +74,48 @@ pub fn open_db(path: &str) -> Result<Connection, String> {
|
|||||||
// If file exists, must be a regular file (not a symlink, device, etc.)
|
// If file exists, must be a regular file (not a symlink, device, etc.)
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
if p.is_symlink() {
|
if p.is_symlink() {
|
||||||
return Err(
|
return Err("Database path must not be a symlink".to_string());
|
||||||
"Database path must not be a symlink".to_string()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let metadata = std::fs::metadata(p)
|
let metadata = std::fs::metadata(p)
|
||||||
.map_err(|e| format!("Cannot access database file metadata: {}", e))?;
|
.map_err(|e| format!("Cannot access database file metadata: {}", e))?;
|
||||||
if !metadata.is_file() {
|
if !metadata.is_file() {
|
||||||
return Err(
|
return Err(
|
||||||
"Database path must point to a regular file, not a directory or device".to_string()
|
"Database path must point to a regular file, not a directory or device".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = sqlite::open(path)
|
let conn = sqlite::open(path).map_err(|e| format!("Failed to open SQLite database: {}", e))?;
|
||||||
.map_err(|e| format!("Failed to open SQLite database: {}", e))?;
|
|
||||||
|
// Set busy timeout BEFORE WAL mode so SQLite waits instead of failing immediately.
|
||||||
|
// This handles the race where two processes (server + pusher) open the same DB
|
||||||
|
// and both try to enable WAL mode concurrently.
|
||||||
|
conn.execute("PRAGMA busy_timeout = 5000;")
|
||||||
|
.map_err(|e| format!("Failed to set busy_timeout: {}", e))?;
|
||||||
|
|
||||||
|
// Retry WAL mode up to 5 times (handles concurrent open from bal-pusher).
|
||||||
|
let mut wal_ok = false;
|
||||||
|
for attempt in 0..5 {
|
||||||
|
match conn.execute("PRAGMA journal_mode = WAL;") {
|
||||||
|
Ok(_) => {
|
||||||
|
wal_ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"WAL mode attempt {}/5 failed: {}, retrying in 100ms...",
|
||||||
|
attempt + 1,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wal_ok {
|
||||||
|
// WAL might already be enabled by another process; this is not fatal.
|
||||||
|
warn!("Could not set WAL mode after retries — may already be enabled by another process");
|
||||||
|
}
|
||||||
|
|
||||||
conn.execute("PRAGMA journal_mode = WAL;")
|
|
||||||
.map_err(|e| format!("Failed to enable WAL mode: {}", e))?;
|
|
||||||
conn.execute("PRAGMA synchronous = NORMAL;")
|
conn.execute("PRAGMA synchronous = NORMAL;")
|
||||||
.map_err(|e| format!("Failed to set synchronous NORMAL: {}", e))?;
|
.map_err(|e| format!("Failed to set synchronous NORMAL: {}", e))?;
|
||||||
|
|
||||||
@@ -102,13 +126,15 @@ pub fn open_db(path: &str) -> Result<Connection, String> {
|
|||||||
/// in-memory lookup during transaction validation (replaces N+1 query).
|
/// in-memory lookup during transaction validation (replaces N+1 query).
|
||||||
pub fn get_all_addresses_by_xpub(db: &Connection, xpub: &str) -> Result<HashSet<String>, Error> {
|
pub fn get_all_addresses_by_xpub(db: &Connection, xpub: &str) -> Result<HashSet<String>, Error> {
|
||||||
let mut stmt = db.prepare(
|
let mut stmt = db.prepare(
|
||||||
"SELECT a.address FROM tbl_address a JOIN tbl_xpub x ON a.xpub = x.id WHERE x.xpub = ?"
|
"SELECT a.address FROM tbl_address a JOIN tbl_xpub x ON a.xpub = x.id WHERE x.xpub = ?",
|
||||||
)?;
|
)?;
|
||||||
stmt.bind((1, Value::String(xpub.to_string())))?;
|
stmt.bind((1, Value::String(xpub.to_string())))?;
|
||||||
let mut addresses = HashSet::new();
|
let mut addresses = HashSet::new();
|
||||||
while let Ok(State::Row) = stmt.next() {
|
while let Ok(State::Row) = stmt.next() {
|
||||||
match stmt.read::<String, _>("address") {
|
match stmt.read::<String, _>("address") {
|
||||||
Ok(addr) => { addresses.insert(addr); }
|
Ok(addr) => {
|
||||||
|
addresses.insert(addr);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to read address column: {}", e);
|
error!("Failed to read address column: {}", e);
|
||||||
}
|
}
|
||||||
@@ -134,9 +160,11 @@ pub fn create_database(db: &Connection) {
|
|||||||
let _ = db.execute("CREATE TABLE IF NOT EXISTS tbl_address (address TEXT PRIMARY_KEY, path TEXT NOT NULL, date_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP, xpub INTEGER,remote_address TEXT);");
|
let _ = db.execute("CREATE TABLE IF NOT EXISTS tbl_address (address TEXT PRIMARY_KEY, path TEXT NOT NULL, date_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP, xpub INTEGER,remote_address TEXT);");
|
||||||
|
|
||||||
let _ = db.execute("CREATE TABLE IF NOT EXISTS tbl_stats (report_date TEXT, chain TEXT, totals INTEGER, waiting INTEGER, sent INTEGER, failed INTEGER, waiting_profit INTEGER, sent_profit INTEGER, missed_profit INTEGER, unique_inputs INTEGER);");
|
let _ = db.execute("CREATE TABLE IF NOT EXISTS tbl_stats (report_date TEXT, chain TEXT, totals INTEGER, waiting INTEGER, sent INTEGER, failed INTEGER, waiting_profit INTEGER, sent_profit INTEGER, missed_profit INTEGER, unique_inputs INTEGER);");
|
||||||
let _ = db.execute("CREATE INDEX IF NOT EXISTS idx_stats_chain ON tbl_stats(chain);");
|
// UNIQUE index required for ON CONFLICT(chain) DO UPDATE in calculate_stats
|
||||||
|
let _ = db.execute("DROP INDEX IF EXISTS idx_stats_chain;");
|
||||||
|
let _ = db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_stats_chain ON tbl_stats(chain);");
|
||||||
|
|
||||||
let _ = db.execute("UPDATE tbl_tx set network='bitcoin' where network='mainnet');");
|
let _ = db.execute("UPDATE tbl_tx set network='bitcoin' where network='mainnet';");
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
pub fn get_xpub_id(db: &Connection, network: &String, xpub: &String) -> Option<i64>{
|
pub fn get_xpub_id(db: &Connection, network: &String, xpub: &String) -> Option<i64>{
|
||||||
@@ -150,16 +178,17 @@ pub fn create_database(db: &Connection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
pub fn insert_xpub(db: &Connection, network: &String, xpub: &String) {
|
pub fn insert_xpub(db: &Connection, network: &str, xpub: &str) {
|
||||||
if xpub != "" {
|
if !xpub.is_empty() {
|
||||||
trace!("going to insert: {} xpub:{}", network, xpub);
|
trace!("going to insert: {} xpub:{}", network, xpub);
|
||||||
let mut stmt = match db.prepare("INSERT INTO tbl_xpub(network,xpub) VALUES(?, ?);") {
|
let mut stmt =
|
||||||
Ok(s) => s,
|
match db.prepare("INSERT OR IGNORE INTO tbl_xpub(network,xpub) VALUES(?, ?);") {
|
||||||
Err(e) => {
|
Ok(s) => s,
|
||||||
error!("Failed to prepare xpub insert statement: {}", e);
|
Err(e) => {
|
||||||
return;
|
error!("Failed to prepare xpub insert statement: {}", e);
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
if let Err(e) = stmt.bind((1, Value::String(network.to_string()))) {
|
if let Err(e) = stmt.bind((1, Value::String(network.to_string()))) {
|
||||||
error!("Failed to bind network parameter for xpub insert: {}", e);
|
error!("Failed to bind network parameter for xpub insert: {}", e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
32
src/xpub.rs
32
src/xpub.rs
@@ -11,6 +11,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Mainnet (BIP44/BIP49/BIP84)
|
// Mainnet (BIP44/BIP49/BIP84)
|
||||||
|
#[allow(dead_code)]
|
||||||
enum BS58Prefix {
|
enum BS58Prefix {
|
||||||
Xpub,
|
Xpub,
|
||||||
Ypub,
|
Ypub,
|
||||||
@@ -102,44 +103,29 @@ fn calc_checksum(desc: &str) -> Result<String, String> {
|
|||||||
Ok(checksum)
|
Ok(checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bitcoincore_descriptor(xpub: &String) -> String {
|
pub fn get_bitcoincore_descriptor(xpub: &str) -> String {
|
||||||
let fingerprint = match calculate_fingerprint(xpub) {
|
let fingerprint = match calculate_fingerprint(xpub) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut bip = 84;
|
|
||||||
let cpub = xpub.to_string();
|
|
||||||
match &xpub[0..4] {
|
|
||||||
"vpub" => {
|
|
||||||
bip = 84;
|
|
||||||
}
|
|
||||||
"zpub" => {
|
|
||||||
bip = 84;
|
|
||||||
}
|
|
||||||
&_ => {
|
|
||||||
bip = 84;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let xpub_converted = match convert_xpub(xpub) {
|
let xpub_converted = match convert_xpub(xpub) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
Err(_) => return String::new(), // Invalid xpub, return empty descriptor
|
||||||
};
|
};
|
||||||
let descriptor = format!("wpkh([{}/84h/0h/0h]{}/0/*)", fingerprint, xpub_converted);
|
let descriptor = format!("wpkh([{}/84h/0h/0h]{}/0/*)", fingerprint, xpub_converted);
|
||||||
let descriptor = match calc_checksum(&descriptor) {
|
match calc_checksum(&descriptor) {
|
||||||
Ok(checksum) => {
|
Ok(checksum) => {
|
||||||
let clean_descriptor = descriptor.split('#').next().unwrap_or(&descriptor);
|
let clean_descriptor = descriptor.split('#').next().unwrap_or(&descriptor);
|
||||||
format!("{}#{}", clean_descriptor, checksum)
|
format!("{}#{}", clean_descriptor, checksum)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {}", err);
|
eprintln!("Error: {}", err);
|
||||||
"".to_string()
|
String::new()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
descriptor
|
|
||||||
//format!("{}#{}",descriptor,checksum)
|
|
||||||
}
|
}
|
||||||
fn convert_xpub(xpub: &String) -> Result<String, String> {
|
fn convert_xpub(xpub: &str) -> Result<String, String> {
|
||||||
if xpub.len() >= 4 && (&xpub[0..4] == "xpub" || &xpub[0..4] == "ypub" || &xpub[0..4] == "zpub")
|
if xpub.len() >= 4 && (&xpub[0..4] == "xpub" || &xpub[0..4] == "ypub" || &xpub[0..4] == "zpub")
|
||||||
{
|
{
|
||||||
convert_to(xpub, BS58Prefix::Xpub)
|
convert_to(xpub, BS58Prefix::Xpub)
|
||||||
@@ -165,7 +151,7 @@ fn base58check_decode(s: &str) -> Result<Vec<u8>, String> {
|
|||||||
return Err("Data troppo corta".to_string());
|
return Err("Data troppo corta".to_string());
|
||||||
}
|
}
|
||||||
let (payload, checksum) = data.split_at(data.len() - 4);
|
let (payload, checksum) = data.split_at(data.len() - 4);
|
||||||
let hash = Sha256::digest(&Sha256::digest(payload));
|
let hash = Sha256::digest(Sha256::digest(payload));
|
||||||
if hash[0..4] != checksum[..] {
|
if hash[0..4] != checksum[..] {
|
||||||
return Err("Checksum invalido".to_string());
|
return Err("Checksum invalido".to_string());
|
||||||
}
|
}
|
||||||
@@ -173,7 +159,7 @@ fn base58check_decode(s: &str) -> Result<Vec<u8>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn base58check_encode(data: &[u8]) -> String {
|
fn base58check_encode(data: &[u8]) -> String {
|
||||||
let checksum = &Sha256::digest(&Sha256::digest(data))[0..4];
|
let checksum = &Sha256::digest(Sha256::digest(data))[0..4];
|
||||||
let full = [data, checksum].concat();
|
let full = [data, checksum].concat();
|
||||||
bs58::encode(full).into_string()
|
bs58::encode(full).into_string()
|
||||||
}
|
}
|
||||||
@@ -205,7 +191,7 @@ pub fn new_address_from_xpub(
|
|||||||
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
||||||
let xpub = Xpub::from_str(&convert_to(zpub, BS58Prefix::Xpub)?)?;
|
let xpub = Xpub::from_str(&convert_to(zpub, BS58Prefix::Xpub)?)?;
|
||||||
let path = format!("m/0/{}", index);
|
let path = format!("m/0/{}", index);
|
||||||
let derivation_path = DerivationPath::from_str(&path.as_str())?;
|
let derivation_path = DerivationPath::from_str(path.as_str())?;
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let derived_xpub = xpub.derive_pub(&secp, &derivation_path)?;
|
let derived_xpub = xpub.derive_pub(&secp, &derivation_path)?;
|
||||||
let public_key = derived_xpub.public_key;
|
let public_key = derived_xpub.public_key;
|
||||||
|
|||||||
@@ -1,37 +1,35 @@
|
|||||||
use bal_server::db::open_db;
|
use bal_server::db::open_db;
|
||||||
use sqlite::State;
|
use sqlite::State;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_open_db_blocks_traversal() {
|
fn test_open_db_blocks_traversal() {
|
||||||
let res = open_db("../etc/passwd");
|
let res = open_db("../etc/passwd");
|
||||||
assert!(
|
assert!(res.is_err(), "Path with '..' should be rejected");
|
||||||
res.is_err(),
|
|
||||||
"Path with '..' should be rejected"
|
|
||||||
);
|
|
||||||
let err = match res {
|
let err = match res {
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
Ok(_) => panic!("Expected error for traversal path"),
|
Ok(_) => panic!("Expected error for traversal path"),
|
||||||
};
|
};
|
||||||
assert!(err.contains("'..'"), "Error should mention directory traversal: {}", err);
|
assert!(
|
||||||
|
err.contains("'..'"),
|
||||||
|
"Error should mention directory traversal: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_open_db_blocks_forbidden_absolute() {
|
fn test_open_db_blocks_forbidden_absolute() {
|
||||||
for path in ["/etc/passwd", "/proc/self/mem", "/dev/null", "/usr/bin/ls"] {
|
for path in ["/etc/passwd", "/proc/self/mem", "/dev/null", "/usr/bin/ls"] {
|
||||||
let res = open_db(path);
|
let res = open_db(path);
|
||||||
assert!(
|
assert!(res.is_err(), "Absolute path {} should be rejected", path);
|
||||||
res.is_err(),
|
|
||||||
"Absolute path {} should be rejected", path
|
|
||||||
);
|
|
||||||
let err = match res {
|
let err = match res {
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
Ok(_) => panic!("Expected error for forbidden path {}", path),
|
Ok(_) => panic!("Expected error for forbidden path {}", path),
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
err.contains("forbidden"),
|
err.contains("forbidden"),
|
||||||
"Error should mention forbidden prefix: {}", err
|
"Error should mention forbidden prefix: {}",
|
||||||
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,10 +39,7 @@ fn test_open_db_allows_relative() {
|
|||||||
let test_path = "tmp_test_bal.db";
|
let test_path = "tmp_test_bal.db";
|
||||||
let _ = fs::remove_file(test_path);
|
let _ = fs::remove_file(test_path);
|
||||||
let res = open_db(test_path);
|
let res = open_db(test_path);
|
||||||
assert!(
|
assert!(res.is_ok(), "Valid relative path should be allowed");
|
||||||
res.is_ok(),
|
|
||||||
"Valid relative path should be allowed"
|
|
||||||
);
|
|
||||||
let db = res.unwrap();
|
let db = res.unwrap();
|
||||||
drop(db);
|
drop(db);
|
||||||
let _ = fs::remove_file(test_path);
|
let _ = fs::remove_file(test_path);
|
||||||
@@ -61,10 +56,7 @@ fn test_open_db_wal_pragmas_set() {
|
|||||||
let mut stmt = db.prepare("PRAGMA journal_mode;").unwrap();
|
let mut stmt = db.prepare("PRAGMA journal_mode;").unwrap();
|
||||||
if let Ok(State::Row) = stmt.next() {
|
if let Ok(State::Row) = stmt.next() {
|
||||||
let mode: String = stmt.read(0).unwrap();
|
let mode: String = stmt.read(0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(mode, "wal", "SQLite journal mode should be WAL");
|
||||||
mode, "wal",
|
|
||||||
"SQLite journal mode should be WAL"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
panic!("Could not read journal_mode pragma");
|
panic!("Could not read journal_mode pragma");
|
||||||
}
|
}
|
||||||
@@ -81,20 +73,18 @@ fn test_open_db_rejects_symlink() {
|
|||||||
let _ = fs::remove_file(real);
|
let _ = fs::remove_file(real);
|
||||||
let _ = fs::remove_file(link);
|
let _ = fs::remove_file(link);
|
||||||
fs::File::create(real).unwrap();
|
fs::File::create(real).unwrap();
|
||||||
fs::soft_link(real, link).unwrap();
|
std::os::unix::fs::symlink(real, link).unwrap();
|
||||||
|
|
||||||
let res = open_db(link);
|
let res = open_db(link);
|
||||||
assert!(
|
assert!(res.is_err(), "Symlink DB path should be rejected");
|
||||||
res.is_err(),
|
|
||||||
"Symlink DB path should be rejected"
|
|
||||||
);
|
|
||||||
let err = match res {
|
let err = match res {
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
Ok(_) => panic!("Expected error for symlink"),
|
Ok(_) => panic!("Expected error for symlink"),
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
err.contains("symlink"),
|
err.contains("symlink"),
|
||||||
"Error should mention symlink: {}", err
|
"Error should mention symlink: {}",
|
||||||
|
err
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = fs::remove_file(real);
|
let _ = fs::remove_file(real);
|
||||||
|
|||||||
@@ -10,15 +10,21 @@ fn setup_db_with_xpub() -> sqlite::Connection {
|
|||||||
"CREATE TABLE tbl_address (address TEXT PRIMARY KEY, path TEXT, xpub INTEGER, remote_address TEXT);"
|
"CREATE TABLE tbl_address (address TEXT PRIMARY KEY, path TEXT, xpub INTEGER, remote_address TEXT);"
|
||||||
);
|
);
|
||||||
// Insert test xpub
|
// Insert test xpub
|
||||||
let mut stmt = db.prepare("INSERT INTO tbl_xpub(id, network, xpub) VALUES(?, ?, ?);").unwrap();
|
let mut stmt = db
|
||||||
|
.prepare("INSERT INTO tbl_xpub(id, network, xpub) VALUES(?, ?, ?);")
|
||||||
|
.unwrap();
|
||||||
stmt.bind((1, Value::Integer(1))).unwrap();
|
stmt.bind((1, Value::Integer(1))).unwrap();
|
||||||
stmt.bind((2, Value::String("testnet".to_string()))).unwrap();
|
stmt.bind((2, Value::String("testnet".to_string())))
|
||||||
stmt.bind((3, Value::String("tpub_test".to_string()))).unwrap();
|
.unwrap();
|
||||||
|
stmt.bind((3, Value::String("tpub_test".to_string())))
|
||||||
|
.unwrap();
|
||||||
let _ = stmt.next();
|
let _ = stmt.next();
|
||||||
drop(stmt);
|
drop(stmt);
|
||||||
// Insert test addresses
|
// Insert test addresses
|
||||||
for addr in ["addr1", "addr2", "addr3"] {
|
for addr in ["addr1", "addr2", "addr3"] {
|
||||||
let mut stmt = db.prepare("INSERT INTO tbl_address(address, path, xpub) VALUES(?, ?, ?);").unwrap();
|
let mut stmt = db
|
||||||
|
.prepare("INSERT INTO tbl_address(address, path, xpub) VALUES(?, ?, ?);")
|
||||||
|
.unwrap();
|
||||||
stmt.bind((1, Value::String(addr.to_string()))).unwrap();
|
stmt.bind((1, Value::String(addr.to_string()))).unwrap();
|
||||||
stmt.bind((2, Value::String("m/0/1".to_string()))).unwrap();
|
stmt.bind((2, Value::String("m/0/1".to_string()))).unwrap();
|
||||||
stmt.bind((3, Value::Integer(1))).unwrap();
|
stmt.bind((3, Value::Integer(1))).unwrap();
|
||||||
@@ -53,7 +59,10 @@ fn test_network_unknown_returns_404() {
|
|||||||
for n in networks {
|
for n in networks {
|
||||||
assert!(networks.contains(&n), "{} should be a valid network", n);
|
assert!(networks.contains(&n), "{} should be a valid network", n);
|
||||||
}
|
}
|
||||||
assert!(!networks.contains(&"attacker"), "attacker should not be a valid network");
|
assert!(
|
||||||
|
!networks.contains(&"attacker"),
|
||||||
|
"attacker should not be a valid network"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -36,11 +36,8 @@ fn test_db_null_unwrap_or() {
|
|||||||
|
|
||||||
let mut found_value = None;
|
let mut found_value = None;
|
||||||
let _ = db.iterate("SELECT * FROM test_stats;", |pairs| {
|
let _ = db.iterate("SELECT * FROM test_stats;", |pairs| {
|
||||||
let row: HashMap<_, _> = pairs
|
let row: HashMap<_, _> = pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect();
|
||||||
.into_iter()
|
let totals = row["totals"].unwrap_or("0").to_string();
|
||||||
.map(|(k, v)| (k.to_string(), v.map(|s| s)))
|
|
||||||
.collect();
|
|
||||||
let totals = row["totals"].clone().unwrap_or("0").to_string();
|
|
||||||
found_value = Some(totals);
|
found_value = Some(totals);
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gitignore_protection_env() {
|
fn test_gitignore_protection_env() {
|
||||||
@@ -23,30 +22,20 @@ fn test_gitignore_protection_env() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for pattern in required_patterns {
|
for pattern in required_patterns {
|
||||||
let has_exact = gitignore.contains(&pattern);
|
let has_wildcard = gitignore.contains("*.env.local") || gitignore.contains(".env.local");
|
||||||
let has_wildcard = gitignore.contains(&format!("*.env.local"))
|
|
||||||
|| gitignore.contains(&format!(".env.local"));
|
|
||||||
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
|
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
|
||||||
|
|
||||||
// For .env.local, either .env.local or *.env.local is acceptable
|
|
||||||
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
|
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
|
||||||
if is_env_local {
|
if is_env_local {
|
||||||
assert!(
|
assert!(
|
||||||
has_wildcard,
|
has_wildcard,
|
||||||
".gitignore must contain pattern '*.env.local' or '.env.local' to protect secrets",
|
".gitignore must contain pattern '*.env.local' or '.env.local' to protect secrets",
|
||||||
);
|
);
|
||||||
} else if pattern == ".env.production" || pattern == ".env.secret" {
|
} else if pattern == ".env.production"
|
||||||
assert!(
|
|| pattern == ".env.secret"
|
||||||
gitignore.contains(pattern),
|
|| pattern == "*.env"
|
||||||
".gitignore must contain pattern '{}' to protect secrets",
|
|| pattern == ".env"
|
||||||
pattern
|
{
|
||||||
);
|
|
||||||
} else if pattern == "*.env" {
|
|
||||||
assert!(
|
|
||||||
has_env,
|
|
||||||
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
|
||||||
);
|
|
||||||
} else if pattern == ".env" {
|
|
||||||
assert!(
|
assert!(
|
||||||
has_env,
|
has_env,
|
||||||
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
|
||||||
@@ -65,7 +54,6 @@ fn test_gitignore_protection_env() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_private_key_in_git() {
|
fn test_no_private_key_in_git() {
|
||||||
// Check that .gitignore includes private_key.pem
|
|
||||||
let gitignore = match fs::read_to_string(".gitignore") {
|
let gitignore = match fs::read_to_string(".gitignore") {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -88,20 +76,17 @@ fn test_no_private_key_in_git() {
|
|||||||
".gitignore must block chiave_privata.key"
|
".gitignore must block chiave_privata.key"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that no private key files are tracked by git
|
|
||||||
let output = std::process::Command::new("git")
|
let output = std::process::Command::new("git")
|
||||||
.args(&["ls-files", "*.pem", "*.key"])
|
.args(["ls-files", "*.pem", "*.key"])
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run git ls-files");
|
.expect("Failed to run git ls-files");
|
||||||
|
|
||||||
let tracked_keys = String::from_utf8(output.stdout).unwrap();
|
let tracked_keys = String::from_utf8(output.stdout).unwrap();
|
||||||
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
|
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
|
||||||
|
|
||||||
// Only non-empty entries and only public_key.pem should be tracked
|
|
||||||
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
|
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
|
||||||
if !tracked.contains("public_key.pem") {
|
if !tracked.contains("public_key.pem") {
|
||||||
assert!(
|
panic!(
|
||||||
false,
|
|
||||||
"Private key file is tracked by git: {}. Remove it with git rm --cached",
|
"Private key file is tracked by git: {}. Remove it with git rm --cached",
|
||||||
tracked
|
tracked
|
||||||
);
|
);
|
||||||
@@ -113,47 +98,41 @@ fn test_no_private_key_in_git() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_token_in_source_files() {
|
fn test_no_token_in_source_files() {
|
||||||
// Scan source files for hardcoded tokens
|
|
||||||
let mut found_issues = Vec::new();
|
let mut found_issues = Vec::new();
|
||||||
|
|
||||||
// Scan .sh files for hardcoded 40-char hex strings
|
|
||||||
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
|
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = path.extension()
|
||||||
if ext == "sh" {
|
&& ext == "sh"
|
||||||
let content = fs::read_to_string(&path).unwrap();
|
{
|
||||||
for (line_num, line) in content.lines().enumerate() {
|
let content = fs::read_to_string(&path).unwrap();
|
||||||
// Skip comments and example/template files
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
if line.trim().starts_with("#")
|
if line.trim().starts_with('#')
|
||||||
|| line.to_lowercase().contains("example")
|
|| line.to_lowercase().contains("example")
|
||||||
|| line.to_lowercase().contains("template")
|
|| line.to_lowercase().contains("template")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.trim().len() >= 40 {
|
||||||
|
let hex_chars: Vec<_> = line
|
||||||
|
.trim()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_hexdigit())
|
||||||
|
.collect();
|
||||||
|
if (40..=64).contains(&hex_chars.len())
|
||||||
|
&& (line.to_lowercase().contains("token")
|
||||||
|
|| line.to_lowercase().contains("api")
|
||||||
|
|| line.to_lowercase().contains("secret"))
|
||||||
{
|
{
|
||||||
continue;
|
found_issues.push(format!(
|
||||||
}
|
"Potential hardcoded token in {}: line {}: {}",
|
||||||
// Check for 40-64 hex chars that could be API tokens (not in .env.example comments)
|
path.display(),
|
||||||
if line.trim().len() >= 40 {
|
line_num + 1,
|
||||||
let hex_chars = line
|
line.trim()
|
||||||
.trim()
|
));
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_ascii_hexdigit())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if hex_chars.len() >= 40 && hex_chars.len() <= 64 {
|
|
||||||
// Check if it looks like it's part of a TOKEN assignment
|
|
||||||
if line.to_lowercase().contains("token")
|
|
||||||
|| line.to_lowercase().contains("api")
|
|
||||||
|| line.to_lowercase().contains("secret")
|
|
||||||
{
|
|
||||||
found_issues.push(format!(
|
|
||||||
"Potential hardcoded token in {}: line {}: {}",
|
|
||||||
path.display(),
|
|
||||||
line_num + 1,
|
|
||||||
line.trim()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,8 +144,7 @@ fn test_no_token_in_source_files() {
|
|||||||
for issue in &found_issues {
|
for issue in &found_issues {
|
||||||
println!(" {}", issue);
|
println!(" {}", issue);
|
||||||
}
|
}
|
||||||
assert!(
|
panic!(
|
||||||
false,
|
|
||||||
"Found potential hardcoded tokens in shell scripts: {:?}",
|
"Found potential hardcoded tokens in shell scripts: {:?}",
|
||||||
found_issues
|
found_issues
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user