From eb37ee0a0ca7c29074f3741745961e891a17f77f Mon Sep 17 00:00:00 2001 From: hdbg Date: Sun, 22 Mar 2026 12:00:33 +0100 Subject: [PATCH] refactor(client): redesign of wallet handle --- server/crates/arbiter-client/Cargo.toml | 5 +- server/crates/arbiter-client/src/client.rs | 76 +++++++++ server/crates/arbiter-client/src/lib.rs | 8 +- server/crates/arbiter-client/src/signer.rs | 153 ------------------ server/crates/arbiter-client/src/transport.rs | 3 - .../crates/arbiter-client/src/wallets/evm.rs | 89 ++++++++++ .../crates/arbiter-client/src/wallets/mod.rs | 2 + 7 files changed, 177 insertions(+), 159 deletions(-) create mode 100644 server/crates/arbiter-client/src/client.rs delete mode 100644 server/crates/arbiter-client/src/signer.rs create mode 100644 server/crates/arbiter-client/src/wallets/evm.rs create mode 100644 server/crates/arbiter-client/src/wallets/mod.rs diff --git a/server/crates/arbiter-client/Cargo.toml b/server/crates/arbiter-client/Cargo.toml index 163c3f1..f5e353b 100644 --- a/server/crates/arbiter-client/Cargo.toml +++ b/server/crates/arbiter-client/Cargo.toml @@ -8,9 +8,12 @@ license = "Apache-2.0" [lints] workspace = true +[features] +evm = ["dep:alloy"] + [dependencies] arbiter-proto.path = "../arbiter-proto" -alloy.workspace = true +alloy = { workspace = true, optional = true } tonic.workspace = true tonic.features = ["tls-aws-lc"] tokio.workspace = true diff --git a/server/crates/arbiter-client/src/client.rs b/server/crates/arbiter-client/src/client.rs new file mode 100644 index 0000000..64d0d04 --- /dev/null +++ b/server/crates/arbiter-client/src/client.rs @@ -0,0 +1,76 @@ +use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl}; +use std::sync::Arc; +use tokio::sync::{Mutex, mpsc}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::transport::ClientTlsConfig; + +use crate::{ + auth::{ConnectError, authenticate}, + storage::{FileSigningKeyStorage, SigningKeyStorage}, + transport::{BUFFER_LENGTH, ClientTransport}, +}; + +#[cfg(feature = "evm")] +use crate::wallets::evm::ArbiterEvmWallet; + +#[derive(Debug, thiserror::Error)] +pub enum ClientError { + #[error("gRPC error")] + Grpc(#[from] tonic::Status), + + #[error("Connection closed by server")] + ConnectionClosed, +} + +pub struct ArbiterClient { + #[allow(dead_code)] + transport: Arc>, +} + +impl ArbiterClient { + pub async fn connect(url: ArbiterUrl) -> Result { + let storage = FileSigningKeyStorage::from_default_location()?; + Self::connect_with_storage(url, &storage).await + } + + pub async fn connect_with_storage( + url: ArbiterUrl, + storage: &S, + ) -> Result { + let key = storage.load_or_create()?; + Self::connect_with_key(url, key).await + } + + pub async fn connect_with_key( + url: ArbiterUrl, + key: ed25519_dalek::SigningKey, + ) -> Result { + let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); + let tls = ClientTlsConfig::new().trust_anchor(anchor); + + 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: Arc::new(Mutex::new(transport)), + }) + } + + #[cfg(feature = "evm")] + pub async fn evm_wallets(&self) -> Result, ClientError> { + todo!("fetch EVM wallet list from server") + } +} diff --git a/server/crates/arbiter-client/src/lib.rs b/server/crates/arbiter-client/src/lib.rs index f98d107..1be4c38 100644 --- a/server/crates/arbiter-client/src/lib.rs +++ b/server/crates/arbiter-client/src/lib.rs @@ -1,8 +1,12 @@ mod auth; -mod signer; +mod client; mod storage; mod transport; +pub mod wallets; pub use auth::ConnectError; -pub use signer::ArbiterSigner; +pub use client::{ArbiterClient, ClientError}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; + +#[cfg(feature = "evm")] +pub use wallets::evm::ArbiterEvmWallet; diff --git a/server/crates/arbiter-client/src/signer.rs b/server/crates/arbiter-client/src/signer.rs deleted file mode 100644 index 3ef001a..0000000 --- a/server/crates/arbiter-client/src/signer.rs +++ /dev/null @@ -1,153 +0,0 @@ -use alloy::{ - consensus::SignableTransaction, - network::TxSigner, - primitives::{Address, B256, ChainId, Signature}, - signers::{Error, Result, Signer}, -}; -use arbiter_proto::{ - proto::arbiter_service_client::ArbiterServiceClient, - url::ArbiterUrl, -}; -use async_trait::async_trait; -use tokio::sync::{Mutex, mpsc}; -use tokio_stream::wrappers::ReceiverStream; -use tonic::transport::ClientTlsConfig; - -use crate::{ - auth::{ConnectError, authenticate}, - storage::{FileSigningKeyStorage, SigningKeyStorage}, - transport::{BUFFER_LENGTH, ClientSignError, ClientTransport}, -}; - -pub struct ArbiterSigner { - transport: Mutex, - address: Option
, - chain_id: Option, -} - -impl ArbiterSigner { - pub async fn connect_grpc(url: ArbiterUrl) -> std::result::Result { - let storage = FileSigningKeyStorage::from_default_location()?; - Self::connect_grpc_with_storage(url, &storage).await - } - - pub async fn connect_grpc_with_storage( - url: ArbiterUrl, - storage: &S, - ) -> std::result::Result { - 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 { - 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
{ - self.address - } - - pub fn set_wallet_address(&mut self, address: Option
) { - 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) -> 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
{ - let wallet_address = self - .address - .ok_or_else(|| Error::other(ClientSignError::WalletAddressNotConfigured))?; - - Ok(wallet_address) - } -} - -#[async_trait] -impl Signer for ArbiterSigner { - async fn sign_hash(&self, _hash: &B256) -> Result { - 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 { - self.chain_id - } - - fn set_chain_id(&mut self, chain_id: Option) { - self.chain_id = chain_id; - } -} - -#[async_trait] -impl TxSigner for ArbiterSigner { - fn address(&self) -> Address { - self.address.unwrap_or(Address::ZERO) - } - - async fn sign_transaction( - &self, - tx: &mut dyn SignableTransaction, - ) -> Result { - 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", - )) - } -} diff --git a/server/crates/arbiter-client/src/transport.rs b/server/crates/arbiter-client/src/transport.rs index 768d3e3..d56a9f8 100644 --- a/server/crates/arbiter-client/src/transport.rs +++ b/server/crates/arbiter-client/src/transport.rs @@ -18,9 +18,6 @@ pub(crate) enum ClientSignError { #[error("Connection closed by server")] ConnectionClosed, - - #[error("Wallet address is not configured")] - WalletAddressNotConfigured, } pub(crate) struct ClientTransport { diff --git a/server/crates/arbiter-client/src/wallets/evm.rs b/server/crates/arbiter-client/src/wallets/evm.rs new file mode 100644 index 0000000..32ae735 --- /dev/null +++ b/server/crates/arbiter-client/src/wallets/evm.rs @@ -0,0 +1,89 @@ +use alloy::{ + consensus::SignableTransaction, + network::TxSigner, + primitives::{Address, B256, ChainId, Signature}, + signers::{Error, Result, Signer}, +}; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::transport::ClientTransport; + +pub struct ArbiterEvmWallet { + transport: Arc>, + address: Address, + chain_id: Option, +} + +impl ArbiterEvmWallet { + pub(crate) fn new(transport: Arc>, 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) -> 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(()) + } +} + +#[async_trait] +impl Signer for ArbiterEvmWallet { + async fn sign_hash(&self, _hash: &B256) -> Result { + Err(Error::other( + "hash-only signing is not supported for ArbiterEvmWallet; use transaction signing", + )) + } + + fn address(&self) -> Address { + self.address + } + + fn chain_id(&self) -> Option { + self.chain_id + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.chain_id = chain_id; + } +} + +#[async_trait] +impl TxSigner for ArbiterEvmWallet { + fn address(&self) -> Address { + self.address + } + + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> Result { + let _transport = self.transport.lock().await; + self.validate_chain_id(tx)?; + + Err(Error::other( + "transaction signing is not supported by current arbiter.client protocol", + )) + } +} diff --git a/server/crates/arbiter-client/src/wallets/mod.rs b/server/crates/arbiter-client/src/wallets/mod.rs new file mode 100644 index 0000000..b2c917e --- /dev/null +++ b/server/crates/arbiter-client/src/wallets/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "evm")] +pub mod evm;