Compare commits
5 Commits
f015d345f4
...
terrors-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efb11d2271 | ||
| 2148faa376 | |||
|
|
eb37ee0a0c | ||
|
|
1f07fd6a98 | ||
|
|
e135519c06 |
@@ -28,6 +28,10 @@ url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-
|
|||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
backend = "cargo:cargo-audit"
|
backend = "cargo:cargo-audit"
|
||||||
|
|
||||||
|
[[tools."cargo:cargo-edit"]]
|
||||||
|
version = "0.13.9"
|
||||||
|
backend = "cargo:cargo-edit"
|
||||||
|
|
||||||
[[tools."cargo:cargo-features"]]
|
[[tools."cargo:cargo-features"]]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
backend = "cargo:cargo-features"
|
backend = "cargo:cargo-features"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ protoc = "29.6"
|
|||||||
"cargo:cargo-insta" = "1.46.3"
|
"cargo:cargo-insta" = "1.46.3"
|
||||||
python = "3.14.3"
|
python = "3.14.3"
|
||||||
ast-grep = "0.42.0"
|
ast-grep = "0.42.0"
|
||||||
|
"cargo:cargo-edit" = "0.13.9"
|
||||||
|
|
||||||
[tasks.codegen]
|
[tasks.codegen]
|
||||||
sources = ['protobufs/*.proto']
|
sources = ['protobufs/*.proto']
|
||||||
|
|||||||
151
server/Cargo.lock
generated
151
server/Cargo.lock
generated
@@ -67,13 +67,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alloy-chains"
|
name = "alloy-chains"
|
||||||
version = "0.2.31"
|
version = "0.2.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757"
|
checksum = "9247f0a399ef71aeb68f497b2b8fb348014f742b50d3b83b1e00dfe1b7d64b3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy-primitives",
|
"alloy-primitives",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"strum",
|
"strum 0.27.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -165,7 +165,7 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -471,7 +471,7 @@ dependencies = [
|
|||||||
"alloy-rlp",
|
"alloy-rlp",
|
||||||
"alloy-serde",
|
"alloy-serde",
|
||||||
"alloy-sol-types",
|
"alloy-sol-types",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@@ -578,7 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249"
|
checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -624,7 +624,7 @@ checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy-json-rpc",
|
"alloy-json-rpc",
|
||||||
"alloy-transport",
|
"alloy-transport",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -686,6 +686,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
|
"terrors",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -753,7 +754,7 @@ dependencies = [
|
|||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"smlang",
|
"smlang",
|
||||||
"spki",
|
"spki",
|
||||||
"strum",
|
"strum 0.28.0",
|
||||||
"test-log",
|
"test-log",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1077,9 +1078,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.16.1"
|
version = "1.16.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"untrusted 0.7.1",
|
"untrusted 0.7.1",
|
||||||
@@ -1088,9 +1089,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.38.0"
|
version = "0.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
@@ -1285,19 +1286,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "borsh"
|
name = "borsh"
|
||||||
version = "1.6.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
|
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh-derive",
|
"borsh-derive",
|
||||||
|
"bytes",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "borsh-derive"
|
name = "borsh-derive"
|
||||||
version = "1.6.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
@@ -1811,15 +1813,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel-async"
|
name = "diesel-async"
|
||||||
version = "0.7.4"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13096fb8dae53f2d411c4b523bec85f45552ed3044a2ab4d85fb2092d9cb4f34"
|
checksum = "b95864e58597509106f1fddfe0600de7e589e1fddddd87f54eee0a49fd111bbc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bb8",
|
"bb8",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
"scoped-futures",
|
"scoped-futures",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -2049,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2869,20 +2872,11 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
@@ -3193,7 +3187,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3270,9 +3264,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.5"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
|
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num_enum_derive",
|
"num_enum_derive",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@@ -3280,9 +3274,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum_derive"
|
name = "num_enum_derive"
|
||||||
version = "0.7.5"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3674,7 +3668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
@@ -3695,7 +3689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
@@ -3777,9 +3771,9 @@ checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.13.1"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6"
|
checksum = "14104c5a24d9bcf7eb2c24753e0f49fe14555d8bd565ea3d38e4b4303267259d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -4292,7 +4286,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4323,9 +4317,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.9"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -4708,7 +4702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4769,7 +4763,16 @@ version = "0.27.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros 0.27.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros 0.28.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4784,6 +4787,18 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -4881,7 +4896,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4894,6 +4909,11 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terrors"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "git+https://github.com/CleverWild/terrors#a0867fd9ca3fbb44c32e92113a917f1577b5716a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "test-log"
|
name = "test-log"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -5026,9 +5046,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
@@ -5113,7 +5133,7 @@ dependencies = [
|
|||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5127,32 +5147,32 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.0+spec-1.1.0"
|
version = "1.0.1+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.4+spec-1.1.0"
|
version = "0.25.5+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"toml_datetime 1.0.0+spec-1.1.0",
|
"toml_datetime 1.0.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.9+spec-1.1.0"
|
version = "1.0.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5916,6 +5936,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
@@ -6083,18 +6112,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = ["crates/*"]
|
||||||
"crates/*",
|
|
||||||
]
|
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
@@ -9,23 +7,23 @@ disallowed-methods = "deny"
|
|||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tonic = { version = "0.14.3", features = [
|
tonic = { version = "0.14.5", features = [
|
||||||
"deflate",
|
"deflate",
|
||||||
"gzip",
|
"gzip",
|
||||||
"tls-connect-info",
|
"tls-connect-info",
|
||||||
"zstd",
|
"zstd",
|
||||||
] }
|
] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.36", features = ["aws-lc-rs"] }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||||
smlang = "0.8.0"
|
smlang = "0.8.0"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
futures = "0.3.31"
|
futures = "0.3.32"
|
||||||
tokio-stream = { version = "0.1.18", features = ["full"] }
|
tokio-stream = { version = "0.1.18", features = ["full"] }
|
||||||
kameo = "0.19.2"
|
kameo = "0.19.2"
|
||||||
prost-types = { version = "0.14.3", features = ["chrono"] }
|
prost-types = { version = "0.14.3", features = ["chrono"] }
|
||||||
@@ -43,3 +41,4 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
|||||||
rsa = { version = "0.9", features = ["sha2"] }
|
rsa = { version = "0.9", features = ["sha2"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
spki = "0.7"
|
spki = "0.7"
|
||||||
|
terrors = { version = "0.5", git = "https://github.com/CleverWild/terrors" }
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ license = "Apache-2.0"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
evm = ["dep:alloy"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arbiter-proto.path = "../arbiter-proto"
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
alloy.workspace = true
|
alloy = { workspace = true, optional = true }
|
||||||
tonic.workspace = true
|
tonic.workspace = true
|
||||||
tonic.features = ["tls-aws-lc"]
|
tonic.features = ["tls-aws-lc"]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@@ -18,6 +21,7 @@ tokio-stream.workspace = true
|
|||||||
ed25519-dalek.workspace = true
|
ed25519-dalek.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
http = "1.4.0"
|
http = "1.4.0"
|
||||||
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
|
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
terrors.workspace = true
|
||||||
103
server/crates/arbiter-client/src/auth.rs
Normal file
103
server/crates/arbiter-client/src/auth.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use arbiter_proto::{
|
||||||
|
format_challenge,
|
||||||
|
proto::client::{
|
||||||
|
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientRequest,
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use ed25519_dalek::Signer as _;
|
||||||
|
use terrors::OneOf;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{
|
||||||
|
ConnectError, MissingAuthChallengeError, UnexpectedAuthResponseError, map_auth_code_error,
|
||||||
|
},
|
||||||
|
transport::{ClientTransport, next_request_id},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn send_auth_challenge_request(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
) -> std::result::Result<(), ConnectError> {
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
request_id: next_request_id(),
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_auth_challenge(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> {
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| OneOf::new(MissingAuthChallengeError))?;
|
||||||
|
|
||||||
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or_else(|| OneOf::new(MissingAuthChallengeError))?;
|
||||||
|
match payload {
|
||||||
|
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
|
||||||
|
ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
|
||||||
|
_ => Err(OneOf::new(UnexpectedAuthResponseError)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_auth_challenge_solution(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
challenge: arbiter_proto::proto::client::AuthChallenge,
|
||||||
|
) -> std::result::Result<(), ConnectError> {
|
||||||
|
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
|
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
request_id: next_request_id(),
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
||||||
|
AuthChallengeSolution { signature },
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_auth_confirmation(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
) -> std::result::Result<(), ConnectError> {
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| OneOf::new(UnexpectedAuthResponseError))?;
|
||||||
|
|
||||||
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or_else(|| OneOf::new(UnexpectedAuthResponseError))?;
|
||||||
|
match payload {
|
||||||
|
ClientResponsePayload::AuthResult(result)
|
||||||
|
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
|
||||||
|
_ => Err(OneOf::new(UnexpectedAuthResponseError)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn authenticate(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
) -> std::result::Result<(), ConnectError> {
|
||||||
|
send_auth_challenge_request(transport, key).await?;
|
||||||
|
let challenge = receive_auth_challenge(transport).await?;
|
||||||
|
send_auth_challenge_solution(transport, key, challenge).await?;
|
||||||
|
receive_auth_confirmation(transport).await
|
||||||
|
}
|
||||||
82
server/crates/arbiter-client/src/client.rs
Normal file
82
server/crates/arbiter-client/src/client.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use terrors::{Broaden as _, OneOf};
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use tonic::transport::ClientTlsConfig;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::authenticate,
|
||||||
|
errors::ConnectError,
|
||||||
|
storage::{FileSigningKeyStorage, SigningKeyStorage},
|
||||||
|
transport::{BUFFER_LENGTH, ClientTransport},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
use crate::errors::{ClientConnectionClosedError, ClientError};
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
use crate::wallets::evm::ArbiterEvmWallet;
|
||||||
|
|
||||||
|
pub struct ArbiterClient {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArbiterClient {
|
||||||
|
pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> {
|
||||||
|
let storage = FileSigningKeyStorage::from_default_location().broaden()?;
|
||||||
|
Self::connect_with_storage(url, &storage).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_with_storage<S: SigningKeyStorage>(
|
||||||
|
url: ArbiterUrl,
|
||||||
|
storage: &S,
|
||||||
|
) -> Result<Self, ConnectError> {
|
||||||
|
let key = storage.load_or_create().broaden()?;
|
||||||
|
Self::connect_with_key(url, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_with_key(
|
||||||
|
url: ArbiterUrl,
|
||||||
|
key: ed25519_dalek::SigningKey,
|
||||||
|
) -> Result<Self, ConnectError> {
|
||||||
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)
|
||||||
|
.map_err(OneOf::new)?
|
||||||
|
.to_owned();
|
||||||
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|
||||||
|
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))
|
||||||
|
.map_err(OneOf::new)?
|
||||||
|
.tls_config(tls)
|
||||||
|
.map_err(OneOf::new)?
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
|
.map_err(OneOf::new)?;
|
||||||
|
|
||||||
|
let mut client = ArbiterServiceClient::new(channel);
|
||||||
|
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
||||||
|
let response_stream = client
|
||||||
|
.client(ReceiverStream::new(rx))
|
||||||
|
.await
|
||||||
|
.map_err(OneOf::new)?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
let mut transport = ClientTransport {
|
||||||
|
sender: tx,
|
||||||
|
receiver: response_stream,
|
||||||
|
};
|
||||||
|
|
||||||
|
authenticate(&mut transport, &key).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
transport: Arc::new(Mutex::new(transport)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> {
|
||||||
|
let _ = &self.transport;
|
||||||
|
Err(OneOf::new(ClientConnectionClosedError))
|
||||||
|
}
|
||||||
|
}
|
||||||
127
server/crates/arbiter-client/src/errors.rs
Normal file
127
server/crates/arbiter-client/src/errors.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use terrors::OneOf;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
use alloy::{primitives::ChainId, signers::Error as AlloySignerError};
|
||||||
|
|
||||||
|
pub type StorageError = OneOf<(std::io::Error, InvalidKeyLengthError)>;
|
||||||
|
|
||||||
|
pub type ConnectError = OneOf<(
|
||||||
|
tonic::transport::Error,
|
||||||
|
http::uri::InvalidUri,
|
||||||
|
webpki::Error,
|
||||||
|
tonic::Status,
|
||||||
|
MissingAuthChallengeError,
|
||||||
|
ApprovalDeniedError,
|
||||||
|
NoUserAgentsOnlineError,
|
||||||
|
UnexpectedAuthResponseError,
|
||||||
|
std::io::Error,
|
||||||
|
InvalidKeyLengthError,
|
||||||
|
)>;
|
||||||
|
|
||||||
|
pub type ClientError = OneOf<(tonic::Status, ClientConnectionClosedError)>;
|
||||||
|
|
||||||
|
pub(crate) type ClientTransportError =
|
||||||
|
OneOf<(TransportChannelClosedError, TransportConnectionClosedError)>;
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
pub(crate) type EvmWalletError = OneOf<(
|
||||||
|
EvmChainIdMismatchError,
|
||||||
|
EvmHashSigningUnsupportedError,
|
||||||
|
EvmTransactionSigningUnsupportedError,
|
||||||
|
)>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
|
||||||
|
pub struct InvalidKeyLengthError {
|
||||||
|
pub expected: usize,
|
||||||
|
pub actual: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Auth challenge was not returned by server")]
|
||||||
|
pub struct MissingAuthChallengeError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Client approval denied by User Agent")]
|
||||||
|
pub struct ApprovalDeniedError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("No User Agents online to approve client")]
|
||||||
|
pub struct NoUserAgentsOnlineError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Unexpected auth response payload")]
|
||||||
|
pub struct UnexpectedAuthResponseError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Connection closed by server")]
|
||||||
|
pub struct ClientConnectionClosedError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Transport channel closed")]
|
||||||
|
pub struct TransportChannelClosedError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Connection closed by server")]
|
||||||
|
pub struct TransportConnectionClosedError;
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("Transaction chain id mismatch: signer {signer}, tx {tx}")]
|
||||||
|
pub struct EvmChainIdMismatchError {
|
||||||
|
pub signer: ChainId,
|
||||||
|
pub tx: ChainId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("hash-only signing is not supported for ArbiterEvmWallet; use transaction signing")]
|
||||||
|
pub struct EvmHashSigningUnsupportedError;
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
|
#[error("transaction signing is not supported by current arbiter.client protocol")]
|
||||||
|
pub struct EvmTransactionSigningUnsupportedError;
|
||||||
|
|
||||||
|
pub(crate) fn map_auth_code_error(code: i32) -> ConnectError {
|
||||||
|
use arbiter_proto::proto::client::AuthResult;
|
||||||
|
|
||||||
|
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
|
||||||
|
AuthResult::ApprovalDenied => OneOf::new(ApprovalDeniedError),
|
||||||
|
AuthResult::NoUserAgentsOnline => OneOf::new(NoUserAgentsOnlineError),
|
||||||
|
AuthResult::Unspecified
|
||||||
|
| AuthResult::Success
|
||||||
|
| AuthResult::InvalidKey
|
||||||
|
| AuthResult::InvalidSignature
|
||||||
|
| AuthResult::Internal => OneOf::new(UnexpectedAuthResponseError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
impl From<EvmChainIdMismatchError> for AlloySignerError {
|
||||||
|
fn from(value: EvmChainIdMismatchError) -> Self {
|
||||||
|
AlloySignerError::TransactionChainIdMismatch {
|
||||||
|
signer: value.signer,
|
||||||
|
tx: value.tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
impl From<EvmHashSigningUnsupportedError> for AlloySignerError {
|
||||||
|
fn from(_value: EvmHashSigningUnsupportedError) -> Self {
|
||||||
|
AlloySignerError::other(
|
||||||
|
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
impl From<EvmTransactionSigningUnsupportedError> for AlloySignerError {
|
||||||
|
fn from(_value: EvmTransactionSigningUnsupportedError) -> Self {
|
||||||
|
AlloySignerError::other(
|
||||||
|
"transaction signing is not supported by current arbiter.client protocol",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,455 +1,13 @@
|
|||||||
use alloy::{
|
mod auth;
|
||||||
consensus::SignableTransaction,
|
mod client;
|
||||||
network::TxSigner,
|
mod errors;
|
||||||
primitives::{Address, B256, ChainId, Signature},
|
mod storage;
|
||||||
signers::{Error, Result, Signer},
|
mod transport;
|
||||||
};
|
pub mod wallets;
|
||||||
use arbiter_proto::{
|
|
||||||
format_challenge, home_path,
|
pub use client::ArbiterClient;
|
||||||
proto::{
|
pub use errors::{ClientError, ConnectError, StorageError};
|
||||||
arbiter_service_client::ArbiterServiceClient,
|
pub use storage::{FileSigningKeyStorage, SigningKeyStorage};
|
||||||
client::{
|
|
||||||
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientRequest, ClientResponse,
|
#[cfg(feature = "evm")]
|
||||||
client_request::Payload as ClientRequestPayload,
|
pub use wallets::evm::ArbiterEvmWallet;
|
||||||
client_response::Payload as ClientResponsePayload,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
url::ArbiterUrl,
|
|
||||||
};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use ed25519_dalek::Signer as _;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
|
||||||
use tokio::sync::{Mutex, mpsc};
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
|
||||||
use tonic::transport::ClientTlsConfig;
|
|
||||||
|
|
||||||
const BUFFER_LENGTH: usize = 16;
|
|
||||||
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
|
|
||||||
|
|
||||||
fn next_request_id() -> i32 {
|
|
||||||
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConnectError {
|
|
||||||
#[error("Could not establish connection")]
|
|
||||||
Connection(#[from] tonic::transport::Error),
|
|
||||||
|
|
||||||
#[error("Invalid server URI")]
|
|
||||||
InvalidUri(#[from] http::uri::InvalidUri),
|
|
||||||
|
|
||||||
#[error("Invalid CA certificate")]
|
|
||||||
InvalidCaCert(#[from] webpki::Error),
|
|
||||||
|
|
||||||
#[error("gRPC error")]
|
|
||||||
Grpc(#[from] tonic::Status),
|
|
||||||
|
|
||||||
#[error("Auth challenge was not returned by server")]
|
|
||||||
MissingAuthChallenge,
|
|
||||||
|
|
||||||
#[error("Client approval denied by User Agent")]
|
|
||||||
ApprovalDenied,
|
|
||||||
|
|
||||||
#[error("No User Agents online to approve client")]
|
|
||||||
NoUserAgentsOnline,
|
|
||||||
|
|
||||||
#[error("Unexpected auth response payload")]
|
|
||||||
UnexpectedAuthResponse,
|
|
||||||
|
|
||||||
#[error("Signing key storage error")]
|
|
||||||
Storage(#[from] StorageError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum StorageError {
|
|
||||||
#[error("I/O error")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
|
|
||||||
InvalidKeyLength { expected: usize, actual: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SigningKeyStorage {
|
|
||||||
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FileSigningKeyStorage {
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileSigningKeyStorage {
|
|
||||||
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
|
|
||||||
|
|
||||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
|
||||||
Self { path: path.into() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_default_location() -> std::result::Result<Self, StorageError> {
|
|
||||||
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
|
||||||
let bytes = std::fs::read(path)?;
|
|
||||||
let raw: [u8; 32] =
|
|
||||||
bytes
|
|
||||||
.try_into()
|
|
||||||
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
|
|
||||||
expected: 32,
|
|
||||||
actual: v.len(),
|
|
||||||
})?;
|
|
||||||
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SigningKeyStorage for FileSigningKeyStorage {
|
|
||||||
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.path.exists() {
|
|
||||||
return Self::read_key(&self.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
|
||||||
let raw_key = key.to_bytes();
|
|
||||||
|
|
||||||
// Use create_new to prevent accidental overwrite if another process creates the key first.
|
|
||||||
match std::fs::OpenOptions::new()
|
|
||||||
.create_new(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&self.path)
|
|
||||||
{
|
|
||||||
Ok(mut file) => {
|
|
||||||
use std::io::Write as _;
|
|
||||||
file.write_all(&raw_key)?;
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
|
||||||
Self::read_key(&self.path)
|
|
||||||
}
|
|
||||||
Err(err) => Err(StorageError::Io(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
enum ClientSignError {
|
|
||||||
#[error("Transport channel closed")]
|
|
||||||
ChannelClosed,
|
|
||||||
|
|
||||||
#[error("Connection closed by server")]
|
|
||||||
ConnectionClosed,
|
|
||||||
|
|
||||||
#[error("Wallet address is not configured")]
|
|
||||||
WalletAddressNotConfigured,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ClientTransport {
|
|
||||||
sender: mpsc::Sender<ClientRequest>,
|
|
||||||
receiver: tonic::Streaming<ClientResponse>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientTransport {
|
|
||||||
async fn send(&mut self, request: ClientRequest) -> std::result::Result<(), ClientSignError> {
|
|
||||||
self.sender
|
|
||||||
.send(request)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ClientSignError::ChannelClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
|
|
||||||
match self.receiver.message().await {
|
|
||||||
Ok(Some(resp)) => Ok(resp),
|
|
||||||
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
|
||||||
Err(_) => Err(ClientSignError::ConnectionClosed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ArbiterSigner {
|
|
||||||
transport: Mutex<ClientTransport>,
|
|
||||||
address: Option<Address>,
|
|
||||||
chain_id: Option<ChainId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArbiterSigner {
|
|
||||||
pub async fn connect_grpc(url: ArbiterUrl) -> std::result::Result<Self, ConnectError> {
|
|
||||||
let storage = FileSigningKeyStorage::from_default_location()?;
|
|
||||||
Self::connect_grpc_with_storage(url, &storage).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_grpc_with_storage<S: SigningKeyStorage>(
|
|
||||||
url: ArbiterUrl,
|
|
||||||
storage: &S,
|
|
||||||
) -> std::result::Result<Self, ConnectError> {
|
|
||||||
let key = storage.load_or_create()?;
|
|
||||||
Self::connect_grpc_with_key(url, key).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_grpc_with_key(
|
|
||||||
url: ArbiterUrl,
|
|
||||||
key: ed25519_dalek::SigningKey,
|
|
||||||
) -> std::result::Result<Self, ConnectError> {
|
|
||||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
|
||||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
|
||||||
|
|
||||||
// NOTE: We intentionally keep the same URL construction strategy as the user-agent crate
|
|
||||||
// to avoid behavior drift between the two clients.
|
|
||||||
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))?
|
|
||||||
.tls_config(tls)?
|
|
||||||
.connect()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut client = ArbiterServiceClient::new(channel);
|
|
||||||
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
|
||||||
let response_stream = client.client(ReceiverStream::new(rx)).await?.into_inner();
|
|
||||||
|
|
||||||
let mut transport = ClientTransport {
|
|
||||||
sender: tx,
|
|
||||||
receiver: response_stream,
|
|
||||||
};
|
|
||||||
|
|
||||||
authenticate(&mut transport, &key).await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
transport: Mutex::new(transport),
|
|
||||||
address: None,
|
|
||||||
chain_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wallet_address(&self) -> Option<Address> {
|
|
||||||
self.address
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_wallet_address(&mut self, address: Option<Address>) {
|
|
||||||
self.address = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_wallet_address(mut self, address: Address) -> Self {
|
|
||||||
self.address = Some(address);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
|
|
||||||
self.chain_id = Some(chain_id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_chain_id(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<()> {
|
|
||||||
if let Some(chain_id) = self.chain_id
|
|
||||||
&& !tx.set_chain_id_checked(chain_id)
|
|
||||||
{
|
|
||||||
return Err(Error::TransactionChainIdMismatch {
|
|
||||||
signer: chain_id,
|
|
||||||
tx: tx.chain_id().unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_wallet_address(&self) -> Result<Address> {
|
|
||||||
let wallet_address = self
|
|
||||||
.address
|
|
||||||
.ok_or_else(|| Error::other(ClientSignError::WalletAddressNotConfigured))?;
|
|
||||||
|
|
||||||
Ok(wallet_address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_auth_result(code: i32) -> ConnectError {
|
|
||||||
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
|
|
||||||
AuthResult::ApprovalDenied => ConnectError::ApprovalDenied,
|
|
||||||
AuthResult::NoUserAgentsOnline => ConnectError::NoUserAgentsOnline,
|
|
||||||
AuthResult::Unspecified
|
|
||||||
| AuthResult::Success
|
|
||||||
| AuthResult::InvalidKey
|
|
||||||
| AuthResult::InvalidSignature
|
|
||||||
| AuthResult::Internal => ConnectError::UnexpectedAuthResponse,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_auth_challenge_request(
|
|
||||||
transport: &mut ClientTransport,
|
|
||||||
key: &ed25519_dalek::SigningKey,
|
|
||||||
) -> std::result::Result<(), ConnectError> {
|
|
||||||
transport
|
|
||||||
.send(ClientRequest {
|
|
||||||
request_id: next_request_id(),
|
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
|
||||||
AuthChallengeRequest {
|
|
||||||
pubkey: key.verifying_key().to_bytes().to_vec(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_auth_challenge(
|
|
||||||
transport: &mut ClientTransport,
|
|
||||||
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> {
|
|
||||||
let response = transport
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ConnectError::MissingAuthChallenge)?;
|
|
||||||
|
|
||||||
let payload = response.payload.ok_or(ConnectError::MissingAuthChallenge)?;
|
|
||||||
match payload {
|
|
||||||
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
|
|
||||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
|
||||||
_ => Err(ConnectError::UnexpectedAuthResponse),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_auth_challenge_solution(
|
|
||||||
transport: &mut ClientTransport,
|
|
||||||
key: &ed25519_dalek::SigningKey,
|
|
||||||
challenge: arbiter_proto::proto::client::AuthChallenge,
|
|
||||||
) -> std::result::Result<(), ConnectError> {
|
|
||||||
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
|
||||||
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
|
||||||
|
|
||||||
transport
|
|
||||||
.send(ClientRequest {
|
|
||||||
request_id: next_request_id(),
|
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
|
||||||
AuthChallengeSolution { signature },
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_auth_confirmation(
|
|
||||||
transport: &mut ClientTransport,
|
|
||||||
) -> std::result::Result<(), ConnectError> {
|
|
||||||
let response = transport
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ConnectError::UnexpectedAuthResponse)?;
|
|
||||||
|
|
||||||
let payload = response
|
|
||||||
.payload
|
|
||||||
.ok_or(ConnectError::UnexpectedAuthResponse)?;
|
|
||||||
match payload {
|
|
||||||
ClientResponsePayload::AuthResult(result)
|
|
||||||
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
|
||||||
_ => Err(ConnectError::UnexpectedAuthResponse),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn authenticate(
|
|
||||||
transport: &mut ClientTransport,
|
|
||||||
key: &ed25519_dalek::SigningKey,
|
|
||||||
) -> std::result::Result<(), ConnectError> {
|
|
||||||
send_auth_challenge_request(transport, key).await?;
|
|
||||||
let challenge = receive_auth_challenge(transport).await?;
|
|
||||||
send_auth_challenge_solution(transport, key, challenge).await?;
|
|
||||||
receive_auth_confirmation(transport).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Signer for ArbiterSigner {
|
|
||||||
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
|
|
||||||
Err(Error::other(
|
|
||||||
"hash-only signing is not supported for ArbiterSigner; use transaction signing",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn address(&self) -> Address {
|
|
||||||
self.address.unwrap_or(Address::ZERO)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn chain_id(&self) -> Option<ChainId> {
|
|
||||||
self.chain_id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
|
|
||||||
self.chain_id = chain_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl TxSigner<Signature> for ArbiterSigner {
|
|
||||||
fn address(&self) -> Address {
|
|
||||||
self.address.unwrap_or(Address::ZERO)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sign_transaction(
|
|
||||||
&self,
|
|
||||||
tx: &mut dyn SignableTransaction<Signature>,
|
|
||||||
) -> Result<Signature> {
|
|
||||||
let _transport = self.transport.lock().await;
|
|
||||||
self.validate_chain_id(tx)?;
|
|
||||||
let _ = self.ensure_wallet_address()?;
|
|
||||||
|
|
||||||
Err(Error::other(
|
|
||||||
"transaction signing is not supported by current arbiter.client protocol",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
|
||||||
|
|
||||||
fn unique_temp_key_path() -> std::path::PathBuf {
|
|
||||||
let nanos = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.expect("clock should be after unix epoch")
|
|
||||||
.as_nanos();
|
|
||||||
std::env::temp_dir().join(format!(
|
|
||||||
"arbiter-client-key-{}-{}.bin",
|
|
||||||
std::process::id(),
|
|
||||||
nanos
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn file_storage_creates_and_reuses_key() {
|
|
||||||
let path = unique_temp_key_path();
|
|
||||||
let storage = FileSigningKeyStorage::new(path.clone());
|
|
||||||
|
|
||||||
let key_a = storage
|
|
||||||
.load_or_create()
|
|
||||||
.expect("first load_or_create should create key");
|
|
||||||
let key_b = storage
|
|
||||||
.load_or_create()
|
|
||||||
.expect("second load_or_create should read same key");
|
|
||||||
|
|
||||||
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
|
|
||||||
assert!(path.exists());
|
|
||||||
|
|
||||||
std::fs::remove_file(path).expect("temp key file should be removable");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn file_storage_rejects_invalid_key_length() {
|
|
||||||
let path = unique_temp_key_path();
|
|
||||||
std::fs::write(&path, [42u8; 31]).expect("should write invalid key file");
|
|
||||||
let storage = FileSigningKeyStorage::new(path.clone());
|
|
||||||
|
|
||||||
let err = storage
|
|
||||||
.load_or_create()
|
|
||||||
.expect_err("storage should reject non-32-byte key file");
|
|
||||||
|
|
||||||
match err {
|
|
||||||
StorageError::InvalidKeyLength { expected, actual } => {
|
|
||||||
assert_eq!(expected, 32);
|
|
||||||
assert_eq!(actual, 31);
|
|
||||||
}
|
|
||||||
other => panic!("unexpected error: {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::remove_file(path).expect("temp key file should be removable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
130
server/crates/arbiter-client/src/storage.rs
Normal file
130
server/crates/arbiter-client/src/storage.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use arbiter_proto::home_path;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use terrors::OneOf;
|
||||||
|
|
||||||
|
use crate::errors::{InvalidKeyLengthError, StorageError};
|
||||||
|
|
||||||
|
pub trait SigningKeyStorage {
|
||||||
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileSigningKeyStorage {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSigningKeyStorage {
|
||||||
|
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
|
||||||
|
|
||||||
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self { path: path.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_default_location() -> std::result::Result<Self, StorageError> {
|
||||||
|
Ok(Self::new(
|
||||||
|
home_path()
|
||||||
|
.map_err(OneOf::new)?
|
||||||
|
.join(Self::DEFAULT_FILE_NAME),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
||||||
|
let bytes = std::fs::read(path).map_err(OneOf::new)?;
|
||||||
|
let raw: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
|
||||||
|
OneOf::new(InvalidKeyLengthError {
|
||||||
|
expected: 32,
|
||||||
|
actual: v.len(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SigningKeyStorage for FileSigningKeyStorage {
|
||||||
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(OneOf::new)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.path.exists() {
|
||||||
|
return Self::read_key(&self.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let raw_key = key.to_bytes();
|
||||||
|
|
||||||
|
// Use create_new to prevent accidental overwrite if another process creates the key first.
|
||||||
|
match std::fs::OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&self.path)
|
||||||
|
{
|
||||||
|
Ok(mut file) => {
|
||||||
|
use std::io::Write as _;
|
||||||
|
file.write_all(&raw_key).map_err(OneOf::new)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
|
Self::read_key(&self.path)
|
||||||
|
}
|
||||||
|
Err(err) => Err(OneOf::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{FileSigningKeyStorage, SigningKeyStorage};
|
||||||
|
use crate::errors::InvalidKeyLengthError;
|
||||||
|
|
||||||
|
fn unique_temp_key_path() -> std::path::PathBuf {
|
||||||
|
let nanos = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("clock should be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"arbiter-client-key-{}-{}.bin",
|
||||||
|
std::process::id(),
|
||||||
|
nanos
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_storage_creates_and_reuses_key() {
|
||||||
|
let path = unique_temp_key_path();
|
||||||
|
let storage = FileSigningKeyStorage::new(path.clone());
|
||||||
|
|
||||||
|
let key_a = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect("first load_or_create should create key");
|
||||||
|
let key_b = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect("second load_or_create should read same key");
|
||||||
|
|
||||||
|
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
|
||||||
|
assert!(path.exists());
|
||||||
|
|
||||||
|
std::fs::remove_file(path).expect("temp key file should be removable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_storage_rejects_invalid_key_length() {
|
||||||
|
let path = unique_temp_key_path();
|
||||||
|
std::fs::write(&path, [42u8; 31]).expect("should write invalid key file");
|
||||||
|
let storage = FileSigningKeyStorage::new(path.clone());
|
||||||
|
|
||||||
|
let err = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect_err("storage should reject non-32-byte key file");
|
||||||
|
|
||||||
|
match err.narrow::<InvalidKeyLengthError, _>() {
|
||||||
|
Ok(invalid_len) => {
|
||||||
|
assert_eq!(invalid_len.expected, 32);
|
||||||
|
assert_eq!(invalid_len.actual, 31);
|
||||||
|
}
|
||||||
|
Err(other) => panic!("unexpected io error: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(path).expect("temp key file should be removable");
|
||||||
|
}
|
||||||
|
}
|
||||||
42
server/crates/arbiter-client/src/transport.rs
Normal file
42
server/crates/arbiter-client/src/transport.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
use terrors::OneOf;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::errors::{
|
||||||
|
ClientTransportError, TransportChannelClosedError, TransportConnectionClosedError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const BUFFER_LENGTH: usize = 16;
|
||||||
|
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
|
||||||
|
|
||||||
|
pub(crate) fn next_request_id() -> i32 {
|
||||||
|
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ClientTransport {
|
||||||
|
pub(crate) sender: mpsc::Sender<ClientRequest>,
|
||||||
|
pub(crate) receiver: tonic::Streaming<ClientResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientTransport {
|
||||||
|
pub(crate) async fn send(
|
||||||
|
&mut self,
|
||||||
|
request: ClientRequest,
|
||||||
|
) -> std::result::Result<(), ClientTransportError> {
|
||||||
|
self.sender
|
||||||
|
.send(request)
|
||||||
|
.await
|
||||||
|
.map_err(|_| OneOf::new(TransportChannelClosedError))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn recv(
|
||||||
|
&mut self,
|
||||||
|
) -> std::result::Result<ClientResponse, ClientTransportError> {
|
||||||
|
match self.receiver.message().await {
|
||||||
|
Ok(Some(resp)) => Ok(resp),
|
||||||
|
Ok(None) => Err(OneOf::new(TransportConnectionClosedError)),
|
||||||
|
Err(_) => Err(OneOf::new(TransportConnectionClosedError)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
server/crates/arbiter-client/src/wallets/evm.rs
Normal file
97
server/crates/arbiter-client/src/wallets/evm.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use alloy::{
|
||||||
|
consensus::SignableTransaction,
|
||||||
|
network::TxSigner,
|
||||||
|
primitives::{Address, B256, ChainId, Signature},
|
||||||
|
signers::{Result, Signer},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use terrors::OneOf;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{
|
||||||
|
EvmChainIdMismatchError, EvmHashSigningUnsupportedError,
|
||||||
|
EvmTransactionSigningUnsupportedError, EvmWalletError,
|
||||||
|
},
|
||||||
|
transport::ClientTransport,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ArbiterEvmWallet {
|
||||||
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
|
address: Address,
|
||||||
|
chain_id: Option<ChainId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArbiterEvmWallet {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||||
|
Self {
|
||||||
|
transport,
|
||||||
|
address,
|
||||||
|
chain_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
|
||||||
|
self.chain_id = Some(chain_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_chain_id(
|
||||||
|
&self,
|
||||||
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
|
) -> std::result::Result<(), EvmWalletError> {
|
||||||
|
if let Some(chain_id) = self.chain_id
|
||||||
|
&& !tx.set_chain_id_checked(chain_id)
|
||||||
|
{
|
||||||
|
return Err(OneOf::new(EvmChainIdMismatchError {
|
||||||
|
signer: chain_id,
|
||||||
|
tx: tx.chain_id().unwrap(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Signer for ArbiterEvmWallet {
|
||||||
|
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
|
||||||
|
Err(EvmWalletError::new(EvmHashSigningUnsupportedError).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain_id(&self) -> Option<ChainId> {
|
||||||
|
self.chain_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
|
||||||
|
self.chain_id = chain_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
|
) -> Result<Signature> {
|
||||||
|
let _transport = self.transport.lock().await;
|
||||||
|
self.validate_chain_id(tx)
|
||||||
|
.map_err(OneOf::into::<alloy::signers::Error>)?;
|
||||||
|
|
||||||
|
Err(EvmWalletError::new(EvmTransactionSigningUnsupportedError).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
server/crates/arbiter-client/src/wallets/mod.rs
Normal file
2
server/crates/arbiter-client/src/wallets/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#[cfg(feature = "evm")]
|
||||||
|
pub mod evm;
|
||||||
@@ -10,7 +10,7 @@ tonic.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
tonic-prost = "0.14.3"
|
tonic-prost = "0.14.5"
|
||||||
prost = "0.14.3"
|
prost = "0.14.3"
|
||||||
kameo.workspace = true
|
kameo.workspace = true
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
@@ -24,7 +24,7 @@ async-trait.workspace = true
|
|||||||
tokio-stream.workspace = true
|
tokio-stream.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-prost-build = "0.14.3"
|
tonic-prost-build = "0.14.5"
|
||||||
protoc-bin-vendored = "3"
|
protoc-bin-vendored = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ license = "Apache-2.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
|
diesel = { version = "2.3.7", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
|
||||||
diesel-async = { version = "0.7.4", features = [
|
diesel-async = { version = "0.8.0", features = [
|
||||||
"bb8",
|
"bb8",
|
||||||
"migrations",
|
"migrations",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
@@ -44,7 +44,7 @@ x25519-dalek.workspace = true
|
|||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
||||||
restructed = "0.2.2"
|
restructed = "0.2.2"
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
pem = "3.0.6"
|
pem = "3.0.6"
|
||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
|
|||||||
Reference in New Issue
Block a user