Rewrite pezbridge-bot as notify-only, drop Noter key custody entirely

Renewing the automation key's spending allowance always needs 3-of-5
multisig approval regardless, so having this bot hold one signer's
(Noter's) private key on a VPS to auto-submit one of those three
signatures was never actually saving a real step - the other two
signers still had to sign manually every time. Once pwap-web shipped
a real multisig operations UI (and now wallet-android's Bridge screen
too), the bot's only remaining job is to detect a low allowance and
notify all 5 signers via Telegram - they each sign with their own
wallet through one of pwap-web, pezbridge-sign.pex.mom, or the wallet
app directly. Noter's mnemonic was removed from the VPS entirely
(shred -u) after this rewrite was deployed and verified.

Also adds gen_automation_key.rs (one-off key generation helper) and
the updated systemd unit with LoadCredential removed.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-07-14 07:38:24 -07:00
1 parent 042e1df314
commit eb821b2ff5
6 files changed
+741 -44

No files matched your search

+404 -44
View File
@@ -256,7 +256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
dependencies = [
"num-traits",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -482,7 +482,7 @@ dependencies = [
"bs58",
"hmac 0.12.1",
"k256",
"rand_core",
"rand_core 0.6.4",
"ripemd",
"secp256k1 0.27.0",
"sha2 0.10.9",
@@ -686,6 +686,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.9.1"
@@ -694,7 +700,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
@@ -871,6 +888,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@@ -899,7 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
@@ -911,7 +937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"typenum",
]
@@ -947,7 +973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
@@ -1253,7 +1279,7 @@ dependencies = [
"ed25519",
"hashbrown 0.15.5",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"sha2 0.10.9",
"subtle",
"zeroize",
@@ -1278,7 +1304,7 @@ dependencies = [
"generic-array",
"group",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"sec1",
"serdect",
"subtle",
@@ -1355,7 +1381,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -1388,7 +1414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
dependencies = [
"byteorder",
"rand",
"rand 0.8.5",
"rustc-hex",
"static_assertions",
]
@@ -1617,8 +1643,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -1629,18 +1657,32 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
name = "getrandom_or_panic"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9"
dependencies = [
"rand",
"rand_core",
"rand 0.8.5",
"rand_core 0.6.4",
]
[[package]]
@@ -1656,7 +1698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -1793,12 +1835,94 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
@@ -2023,6 +2147,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -2203,7 +2333,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -2247,7 +2377,7 @@ dependencies = [
"libsecp256k1-core",
"libsecp256k1-gen-ecmult",
"libsecp256k1-gen-genmult",
"rand",
"rand 0.8.5",
"serde",
"sha2 0.9.9",
"typenum",
@@ -2318,6 +2448,12 @@ dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.2.0"
@@ -2350,7 +2486,7 @@ checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d"
dependencies = [
"byteorder",
"keccak",
"rand_core",
"rand_core 0.6.4",
"zeroize",
]
@@ -2542,8 +2678,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9"
dependencies = [
"bitcoin_hashes 0.13.0",
"rand",
"rand_core",
"rand 0.8.5",
"rand_core 0.6.4",
"serde",
"unicode-normalization",
]
@@ -2613,7 +2749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@@ -2983,7 +3119,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -3089,6 +3225,62 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.2",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "quote"
version = "1.0.44"
@@ -3104,6 +3296,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radium"
version = "0.7.0"
@@ -3118,7 +3316,18 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20 0.10.1",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
@@ -3128,7 +3337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -3140,6 +3349,21 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -3198,6 +3422,44 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
@@ -3304,6 +3566,7 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"web-time",
"zeroize",
]
@@ -3367,6 +3630,12 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
@@ -3667,7 +3936,7 @@ dependencies = [
"curve25519-dalek",
"getrandom_or_panic",
"merlin",
"rand_core",
"rand_core 0.6.4",
"serde_bytes",
"sha2 0.10.9",
"subtle",
@@ -3732,7 +4001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
dependencies = [
"bitcoin_hashes 0.14.1",
"rand",
"rand 0.8.5",
"secp256k1-sys 0.10.1",
]
@@ -3872,6 +4141,18 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serdect"
version = "0.2.0"
@@ -3889,7 +4170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -3901,7 +4182,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.9.0",
"opaque-debug",
]
@@ -3913,7 +4194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -3959,7 +4240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -4025,7 +4306,7 @@ dependencies = [
"bip39",
"blake2-rfc",
"bs58",
"chacha20",
"chacha20 0.9.1",
"crossbeam-queue",
"derive_more 0.99.20",
"ed25519-zebra",
@@ -4048,7 +4329,7 @@ dependencies = [
"pbkdf2",
"pin-project",
"poly1305",
"rand",
"rand 0.8.5",
"rand_chacha",
"ruzstd 0.6.0",
"schnorrkel",
@@ -4079,7 +4360,7 @@ dependencies = [
"bip39",
"blake2-rfc",
"bs58",
"chacha20",
"chacha20 0.9.1",
"crossbeam-queue",
"derive_more 2.1.1",
"ed25519-zebra",
@@ -4102,7 +4383,7 @@ dependencies = [
"pbkdf2",
"pin-project",
"poly1305",
"rand",
"rand 0.8.5",
"rand_chacha",
"ruzstd 0.8.3",
"schnorrkel",
@@ -4145,7 +4426,7 @@ dependencies = [
"lru",
"parking_lot",
"pin-project",
"rand",
"rand 0.8.5",
"rand_chacha",
"serde",
"serde_json",
@@ -4181,7 +4462,7 @@ dependencies = [
"lru",
"parking_lot",
"pin-project",
"rand",
"rand 0.8.5",
"rand_chacha",
"serde",
"serde_json",
@@ -4213,7 +4494,7 @@ dependencies = [
"futures",
"httparse",
"log",
"rand",
"rand 0.8.5",
"sha1",
]
@@ -4272,7 +4553,7 @@ dependencies = [
"parking_lot",
"paste",
"primitive-types 0.12.2",
"rand",
"rand 0.8.5",
"scale-info",
"schnorrkel",
"secp256k1 0.28.2",
@@ -4402,7 +4683,7 @@ dependencies = [
"num-traits",
"parity-scale-codec",
"paste",
"rand",
"rand 0.8.5",
"scale-info",
"serde",
"simple-mermaid",
@@ -4459,7 +4740,7 @@ dependencies = [
"log",
"parity-scale-codec",
"parking_lot",
"rand",
"rand 0.8.5",
"smallvec",
"sp-core",
"sp-externalities",
@@ -4514,7 +4795,7 @@ dependencies = [
"nohash-hasher",
"parity-scale-codec",
"parking_lot",
"rand",
"rand 0.8.5",
"scale-info",
"schnellru",
"sp-core",
@@ -4842,6 +5123,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -5116,6 +5406,51 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
"url",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
@@ -5200,6 +5535,12 @@ dependencies = [
"hash-db",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
version = "1.6.3"
@@ -5208,7 +5549,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"digest 0.10.7",
"rand",
"rand 0.8.5",
"static_assertions",
]
@@ -5315,6 +5656,7 @@ dependencies = [
"hex",
"pezkuwi-subxt",
"pezkuwi-subxt-signer",
"reqwest",
"serde",
"serde_json",
"sp-core",
@@ -5366,9 +5708,9 @@ dependencies = [
"ark-serialize-derive",
"arrayref",
"digest 0.10.7",
"rand",
"rand 0.8.5",
"rand_chacha",
"rand_core",
"rand_core 0.6.4",
"sha2 0.10.9",
"sha3",
"zeroize",
@@ -5384,6 +5726,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -5595,6 +5946,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -5953,7 +6313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core",
"rand_core 0.6.4",
"serde",
"zeroize",
]
+8
View File
@@ -12,6 +12,10 @@ license = "Apache-2.0"
name = "usdt-bridge"
path = "src/main.rs"
[[bin]]
name = "pezbridge-bot"
path = "src/bin/pezbridge_bot.rs"
[dependencies]
# Async runtime
tokio = { version = "1.36", features = ["full"] }
@@ -54,6 +58,10 @@ chrono = { version = "0.4", features = ["serde"] }
# Async streams
futures = "0.3"
# Telegram Bot API HTTP client (pezbridge-bot only). rustls instead of native-tls/OpenSSL so
# this cross-compiles cleanly to a static musl binary (no OpenSSL sysroot needed for the target).
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
# Error handling
anyhow = "1.0"
thiserror = "1.0"
@@ -0,0 +1,53 @@
[Unit]
Description=PezbridgeBot - USDT bridge automation-key allowance renewal notifier (holds no key)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pezbridgebot
Group=pezbridgebot
WorkingDirectory=/opt/pezbridgebot/data
ExecStart=/opt/pezbridgebot/bin/pezbridge-bot
Environment=PEZBRIDGE_CONFIG=/opt/pezbridgebot/bin/pezbridge_bot_config.json
Environment=RUST_LOG=info
EnvironmentFile=/etc/pezbridgebot/telegram_token.env
Restart=on-failure
RestartSec=10
# --- Hardening ---
# Deployed 2026-07-13 on NEW-10 (89.116.25.180), simplified 2026-07-14: this process used to
# also hold Noter's (Validator_04's) mnemonic via LoadCredential so it could automate 1-of-3
# required signatures. That stopped being worth the risk once pwap-web shipped a real signing
# UI (any of the 5 signers can now approve directly with their own wallet extension at
# /multisig/pending) - the bot-held key only ever saved "2 humans click instead of 3", not
# enough to justify a private key living on a server. Noter's mnemonic was removed from this
# host entirely; this unit now only reads chain state and sends Telegram messages - it has no
# LoadCredential, no signing capability, and nothing worth exfiltrating. The hardening below is
# kept anyway as good baseline practice, not because there's a key to protect anymore.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
MemoryDenyWriteExecute=yes
SystemCallFilter=@system-service
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=
ReadWritePaths=/opt/pezbridgebot/data
UMask=0077
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,27 @@
use anyhow::Result;
fn main() -> Result<()> {
let (pair, phrase, _) = sp_core::sr25519::Pair::generate_with_phrase(None);
use sp_core::Pair as _;
let public = pair.public();
let account = sp_core::crypto::AccountId32::from(public.0);
let ss58 = sp_core::crypto::Ss58Codec::to_ss58check_with_version(
&account,
sp_core::crypto::Ss58AddressFormat::custom(42),
);
let out = serde_json::json!({
"warning": "CONTAINS PRIVATE SEED PHRASE - KEEP SECURE - NOT IN GIT - NOT IN LOGS",
"role": "usdt-bridge relayer automation key - LIMITED delegated allowance only via Assets.approve_transfer, never asset owner/admin",
"ss58_address_pezkuwi": ss58,
"scheme": "sr25519",
"mnemonic": phrase,
});
std::fs::write("/home/myhez/res/bridge_automation_key.json", serde_json::to_string_pretty(&out)?)?;
println!("Generated automation key.");
println!("Address (never the mnemonic): {}", ss58);
println!("Saved to /home/myhez/res/bridge_automation_key.json");
Ok(())
}
@@ -0,0 +1,9 @@
{
"pezkuwi_rpc": "wss://asset-hub-rpc.pezkuwichain.io",
"serok_chat_id": 5362991649,
"ops_chat_id": -5420200114,
"poll_interval_secs": 600,
"renewal_threshold": 3000000000,
"topup_amount": 10000000000,
"state_path": "/opt/pezbridgebot/data/pezbridge_bot_state.json"
}
+240
View File
@@ -0,0 +1,240 @@
//! PezbridgeBot - notifies signers when the USDT bridge automation key's `Assets.approve_transfer`
//! spending allowance needs renewal (see /home/myhez/res/validators-tiki.md and the
//! usdt-bridge-multisig-migration memory for the full custody model).
//!
//! This process holds NO private key and cannot sign or submit anything. An earlier version of
//! this bot held Noter's (Validator_04's) key so it could automate 1-of-3 required signatures -
//! that made sense when it was written, since pwap-web had no working signing UI at all and
//! every approval required a hand-written script. It stopped making sense once pwap-web's real
//! multisig operations UI shipped (shared/lib/multisig.ts + web/src/components/
//! MultisigOperations.tsx, at /multisig/pending): any of the 5 signers can now approve directly
//! with their own wallet extension, so the only thing a bot-held key still bought was "2 humans
//! click instead of 3" - not worth a private key sitting on a server for. Simplified back down
//! to detect-and-notify only; Noter's mnemonic was removed from this host entirely.
//!
//! Flow:
//! 1. Poll the automation key's remaining Assets.Approvals allowance for wUSDT (asset 1000).
//! 2. When it drops below `renewal_threshold`, send ONE Telegram notification (to Serok's chat
//! and the ops group) pointing signers at pwap-web's /multisig/pending page, where any 3 of
//! the 5 real signatories can approve the renewal with their own wallets.
//! 3. Suppress repeat notifications until the allowance is actually renewed (goes back above
//! threshold) - avoids spamming every poll cycle while humans coordinate.
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use sp_core::crypto::Ss58Codec;
use std::path::PathBuf;
use std::time::Duration;
use subxt::dynamic::{At, Value};
use subxt::{OnlineClient, SubstrateConfig};
use tracing::{error, info, warn};
const WUSDT_ASSET_ID: u32 = 1000;
const MULTISIG_ADDR: &str = "5GvwxmCDp3PC33KHoeWSgj3S7ocE7nzk1jiCCZMPSDBFeNcj";
const AUTOMATION_KEY_ADDR: &str = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM";
const MULTISIG_THRESHOLD: u16 = 3;
const PWAP_WEB_OPERATIONS_URL: &str = "https://app.pezkuwichain.io/multisig/pending";
// ============================================================================
// Config / state
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
struct BotConfig {
pezkuwi_rpc: String,
serok_chat_id: i64,
ops_chat_id: i64,
poll_interval_secs: u64,
renewal_threshold: u128,
topup_amount: u128,
state_path: PathBuf,
}
impl Default for BotConfig {
fn default() -> Self {
Self {
pezkuwi_rpc: "wss://asset-hub-rpc.pezkuwichain.io".to_string(),
serok_chat_id: 0,
ops_chat_id: 0,
poll_interval_secs: 600,
renewal_threshold: 3_000_000_000, // 3,000 wUSDT
topup_amount: 10_000_000_000, // 10,000 wUSDT
state_path: PathBuf::from("pezbridge_bot_state.json"),
}
}
}
fn load_config() -> Result<BotConfig> {
let path = std::env::var("PEZBRIDGE_CONFIG").unwrap_or_else(|_| "pezbridge_bot_config.json".to_string());
if std::path::Path::new(&path).exists() {
let content = std::fs::read_to_string(&path).context("reading config file")?;
Ok(serde_json::from_str(&content).context("parsing config file")?)
} else {
warn!("No config file at {path}, using defaults - serok_chat_id/ops_chat_id MUST be set for this to be useful");
Ok(BotConfig::default())
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct BotState {
/// Whether a notification has already been sent for the CURRENT low-allowance episode -
/// reset back to false once the allowance rises back above threshold (meaning humans
/// renewed it via pwap-web), so the next depletion gets its own fresh notification.
already_notified: bool,
}
fn load_state(path: &PathBuf) -> Result<BotState> {
if path.exists() {
let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content).unwrap_or_default())
} else {
Ok(BotState::default())
}
}
fn save_state(path: &PathBuf, state: &BotState) -> Result<()> {
std::fs::write(path, serde_json::to_string_pretty(state)?)?;
Ok(())
}
// ============================================================================
// Chain read
// ============================================================================
async fn get_approval_remaining(
client: &OnlineClient<SubstrateConfig>,
asset_id: u32,
owner: &str,
delegate: &str,
) -> Result<u128> {
let owner_bytes = sp_core::crypto::AccountId32::from_ss58check(owner)
.map_err(|e| anyhow!("invalid owner address: {e:?}"))?;
let delegate_bytes = sp_core::crypto::AccountId32::from_ss58check(delegate)
.map_err(|e| anyhow!("invalid delegate address: {e:?}"))?;
let storage_query = subxt::dynamic::storage(
"Assets",
"Approvals",
vec![
Value::primitive(asset_id.into()),
Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&owner_bytes)),
Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&delegate_bytes)),
],
);
let result = client.storage().at_latest().await?.fetch(&storage_query).await?;
if let Some(v) = result {
if let Some(amount) = v.to_value()?.at("amount").and_then(|a| a.as_u128()) {
return Ok(amount);
}
}
// No approval entry at all = the allowance is fully exhausted (or was never granted) -
// treat identically to "0 remaining" so it triggers a notification.
Ok(0)
}
// ============================================================================
// Telegram (send-only - no polling/callbacks needed anymore, there's no action this bot takes)
// ============================================================================
async fn tg_send_message(http: &reqwest::Client, token: &str, chat_id: i64, text: &str) -> Result<()> {
if chat_id == 0 {
return Ok(());
}
let url = format!("https://api.telegram.org/bot{token}/sendMessage");
let body = serde_json::json!({ "chat_id": chat_id, "text": text });
let resp = http.post(&url).json(&body).send().await?;
let json: serde_json::Value = resp.json().await?;
if json["ok"].as_bool() != Some(true) {
return Err(anyhow!("sendMessage failed: {json}"));
}
Ok(())
}
async fn notify(http: &reqwest::Client, token: &str, config: &BotConfig, msg: &str) {
info!("[pezbridge-bot] {msg}");
if let Err(e) = tg_send_message(http, token, config.serok_chat_id, msg).await {
error!("failed to notify Serok: {e:#}");
}
if let Err(e) = tg_send_message(http, token, config.ops_chat_id, msg).await {
error!("failed to notify ops chat: {e:#}");
}
}
// ============================================================================
// Core logic
// ============================================================================
async fn check_allowance_and_maybe_notify(
read_client: &OnlineClient<SubstrateConfig>,
http: &reqwest::Client,
token: &str,
config: &BotConfig,
state: &mut BotState,
) -> Result<()> {
let remaining = get_approval_remaining(read_client, WUSDT_ASSET_ID, MULTISIG_ADDR, AUTOMATION_KEY_ADDR).await?;
info!("Automation key remaining allowance: {:.2} wUSDT", remaining as f64 / 1_000_000.0);
if remaining >= config.renewal_threshold {
if state.already_notified {
info!("Allowance is back above threshold - renewal happened, resetting notification state");
state.already_notified = false;
save_state(&config.state_path, state)?;
}
return Ok(());
}
if state.already_notified {
return Ok(()); // already told everyone, don't spam while they coordinate
}
let topup = config.topup_amount;
let text = format!(
"\u{26A0}\u{FE0F} USDT Bridge - otomasyon anahtarı izni azaldı\n\n\
Kalan izin: {:.2} wUSDT\nEşik: {:.2} wUSDT\n\n\
Önerilen yenileme: {:.2} wUSDT (Assets.approve_transfer, multisig'den otomasyon anahtarına).\n\n\
{} imzacının (5'ten herhangi 3'ü) pwap-web üzerinden kendi cüzdanlarıyla onaylaması gerekiyor:\n{}",
remaining as f64 / 1_000_000.0,
config.renewal_threshold as f64 / 1_000_000.0,
topup as f64 / 1_000_000.0,
MULTISIG_THRESHOLD,
PWAP_WEB_OPERATIONS_URL
);
notify(http, token, config, &text).await;
state.already_notified = true;
save_state(&config.state_path, state)?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).init();
let config = load_config()?;
if config.serok_chat_id == 0 && config.ops_chat_id == 0 {
return Err(anyhow!("both serok_chat_id and ops_chat_id are unset - this bot would notify no one"));
}
let token = std::env::var("PEZBRIDGE_TELEGRAM_TOKEN").context("PEZBRIDGE_TELEGRAM_TOKEN env var not set")?;
info!("Connecting to {}...", config.pezkuwi_rpc);
let read_client = OnlineClient::<SubstrateConfig>::from_url(&config.pezkuwi_rpc)
.await
.context("failed to connect")?;
let http = reqwest::Client::new();
let mut state = load_state(&config.state_path)?;
notify(&http, &token, &config, "PezbridgeBot started (notify-only, holds no key).").await;
let mut poll_timer = tokio::time::interval(Duration::from_secs(config.poll_interval_secs));
loop {
poll_timer.tick().await;
if let Err(e) = check_allowance_and_maybe_notify(&read_client, &http, &token, &config, &mut state).await {
error!("allowance check failed: {e:#}");
}
}
}