feat(evm): add grant management for EVM wallets
This commit is contained in:
1
server/Cargo.lock
generated
1
server/Cargo.lock
generated
@@ -727,6 +727,7 @@ dependencies = [
|
||||
"memsafe",
|
||||
"miette",
|
||||
"pem",
|
||||
"prost-types",
|
||||
"rand 0.10.0",
|
||||
"rcgen",
|
||||
"restructed",
|
||||
|
||||
@@ -50,6 +50,7 @@ rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
spki.workspace = true
|
||||
alloy.workspace = true
|
||||
prost-types.workspace = true
|
||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -15,9 +15,9 @@ use crate::{
|
||||
schema,
|
||||
},
|
||||
evm::{
|
||||
self, RunKind,
|
||||
self, ListGrantsError, RunKind,
|
||||
policies::{
|
||||
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||
},
|
||||
},
|
||||
@@ -194,19 +194,12 @@ impl EvmActor {
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_list_grants(
|
||||
&mut self,
|
||||
wallet_id: Option<i32>,
|
||||
) -> Result<Vec<EvmBasicGrant>, Error> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let mut query = schema::evm_basic_grant::table
|
||||
.select(EvmBasicGrant::as_select())
|
||||
.filter(schema::evm_basic_grant::revoked_at.is_null())
|
||||
.into_boxed();
|
||||
if let Some(wid) = wallet_id {
|
||||
query = query.filter(schema::evm_basic_grant::wallet_id.eq(wid));
|
||||
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||
match self.engine.list_all_grants().await {
|
||||
Ok(grants) => Ok(grants),
|
||||
Err(ListGrantsError::Database(db)) => Err(Error::Database(db)),
|
||||
Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)),
|
||||
}
|
||||
Ok(query.load(&mut conn).await?)
|
||||
}
|
||||
|
||||
#[message]
|
||||
|
||||
@@ -34,7 +34,7 @@ smlang::statemachine!(
|
||||
custom_error: true,
|
||||
transitions: {
|
||||
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
|
||||
Init + BootstrapAuthRequest(BootstrapAuthRequest) [async verify_bootstrap_token] / provide_key_bootstrap = AuthOk(AuthPublicKey),
|
||||
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
|
||||
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
|
||||
}
|
||||
);
|
||||
@@ -136,9 +136,9 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
#[allow(missing_docs)]
|
||||
#[allow(clippy::result_unit_err)]
|
||||
async fn verify_bootstrap_token(
|
||||
&self,
|
||||
BootstrapAuthRequest { pubkey, token }: &BootstrapAuthRequest,
|
||||
) -> Result<bool, Self::Error> {
|
||||
&mut self,
|
||||
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
|
||||
) -> Result<AuthPublicKey, Self::Error> {
|
||||
let token_ok: bool = self
|
||||
.conn
|
||||
.actors
|
||||
@@ -157,16 +157,15 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
return Err(Error::InvalidBootstrapToken);
|
||||
}
|
||||
|
||||
register_key(&self.conn.db, pubkey).await?;
|
||||
register_key(&self.conn.db, &pubkey).await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
self.conn
|
||||
.transport
|
||||
.send(Ok(Response::AuthOk))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
|
||||
fn provide_key_bootstrap(
|
||||
&mut self,
|
||||
event_data: BootstrapAuthRequest,
|
||||
) -> Result<AuthPublicKey, Self::Error> {
|
||||
Ok(event_data.pubkey)
|
||||
Ok(pubkey)
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use alloy::primitives::Address;
|
||||
use arbiter_proto::transport::Bi;
|
||||
use arbiter_proto::{transport::Bi};
|
||||
use kameo::actor::Spawn as _;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
|
||||
db::{self, models::KeyType},
|
||||
db::{self, models::KeyType}, evm::policies::{Grant, SpecificGrant},
|
||||
evm::policies::SharedGrantSettings,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
@@ -109,6 +110,16 @@ pub enum Request {
|
||||
ClientConnectionResponse {
|
||||
approved: bool,
|
||||
},
|
||||
|
||||
ListGrants,
|
||||
EvmGrantCreate {
|
||||
client_id: i32,
|
||||
shared: SharedGrantSettings,
|
||||
specific: SpecificGrant,
|
||||
},
|
||||
EvmGrantDelete {
|
||||
grant_id: i32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -123,6 +134,10 @@ pub enum Response {
|
||||
ClientConnectionCancel,
|
||||
EvmWalletCreate(Result<(), evm::Error>),
|
||||
EvmWalletList(Vec<Address>),
|
||||
|
||||
ListGrants(Vec<Grant<SpecificGrant>>),
|
||||
EvmGrantCreate(Result<i32, evm::Error>),
|
||||
EvmGrantDelete(Result<(), evm::Error>),
|
||||
}
|
||||
|
||||
pub type Transport = Box<dyn Bi<Request, Result<Response, TransportResponseError>> + Send>;
|
||||
|
||||
@@ -7,7 +7,8 @@ use tracing::{error, info};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
use crate::actors::{
|
||||
evm::{Generate, ListWallets},
|
||||
evm::{Generate, ListWallets, UseragentListGrants},
|
||||
evm::{UseragentCreateGrant, UseragentDeleteGrant},
|
||||
keyholder::{self, Bootstrap, TryUnseal},
|
||||
user_agent::{
|
||||
BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState,
|
||||
@@ -40,6 +41,7 @@ impl UserAgentSession {
|
||||
self.handle_bootstrap_encrypted_key(nonce, ciphertext, associated_data)
|
||||
.await
|
||||
}
|
||||
Request::ListGrants => self.handle_grant_list().await,
|
||||
Request::QueryVaultState => self.handle_query_vault_state().await,
|
||||
Request::EvmWalletCreate => self.handle_evm_wallet_create().await,
|
||||
Request::EvmWalletList => self.handle_evm_wallet_list().await,
|
||||
@@ -48,6 +50,12 @@ impl UserAgentSession {
|
||||
| Request::ClientConnectionResponse { .. } => {
|
||||
Err(TransportResponseError::UnexpectedRequestPayload)
|
||||
}
|
||||
Request::EvmGrantCreate {
|
||||
client_id,
|
||||
shared,
|
||||
specific,
|
||||
} => self.handle_grant_create(client_id, shared, specific).await,
|
||||
Request::EvmGrantDelete { grant_id } => self.handle_grant_delete(grant_id).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,3 +294,56 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAgentSession {
|
||||
async fn handle_grant_list(&mut self) -> Output {
|
||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||
Ok(grants) => Ok(Response::ListGrants(grants)),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant list failed");
|
||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_grant_create(
|
||||
&mut self,
|
||||
client_id: i32,
|
||||
basic: crate::evm::policies::SharedGrantSettings,
|
||||
grant: crate::evm::policies::SpecificGrant,
|
||||
) -> Output {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(UseragentCreateGrant {
|
||||
client_id,
|
||||
basic,
|
||||
grant,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant create failed");
|
||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_grant_delete(&mut self, grant_id: i32) -> Output {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(UseragentDeleteGrant { grant_id })
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant delete failed");
|
||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ pub enum EvalViolation {
|
||||
|
||||
pub type DatabaseID = i32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Grant<PolicySettings> {
|
||||
pub id: DatabaseID,
|
||||
pub shared_grant_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
|
||||
@@ -145,6 +146,7 @@ pub struct VolumeRateLimit {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SharedGrantSettings {
|
||||
pub wallet_id: i32,
|
||||
pub client_id: i32,
|
||||
pub chain: ChainId,
|
||||
|
||||
pub valid_from: Option<DateTime<Utc>>,
|
||||
@@ -160,6 +162,7 @@ impl SharedGrantSettings {
|
||||
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
||||
Ok(Self {
|
||||
wallet_id: model.wallet_id,
|
||||
client_id: model.client_id,
|
||||
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
||||
valid_from: model.valid_from.map(Into::into),
|
||||
valid_until: model.valid_until.map(Into::into),
|
||||
@@ -197,6 +200,7 @@ impl SharedGrantSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SpecificGrant {
|
||||
EtherTransfer(ether_transfer::Settings),
|
||||
TokenTransfer(token_transfers::Settings),
|
||||
|
||||
@@ -51,9 +51,10 @@ impl From<Meaning> for SpecificMeaning {
|
||||
}
|
||||
|
||||
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
target: Vec<Address>,
|
||||
limit: VolumeRateLimit,
|
||||
pub target: Vec<Address>,
|
||||
pub limit: VolumeRateLimit,
|
||||
}
|
||||
|
||||
impl From<Settings> for SpecificGrant {
|
||||
|
||||
@@ -74,6 +74,7 @@ fn shared() -> SharedGrantSettings {
|
||||
max_gas_fee_per_gas: None,
|
||||
max_priority_fee_per_gas: None,
|
||||
rate_limit: None,
|
||||
client_id: CLIENT_ID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,11 @@ impl From<Meaning> for SpecificMeaning {
|
||||
}
|
||||
|
||||
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
token_contract: Address,
|
||||
target: Option<Address>,
|
||||
volume_limits: Vec<VolumeRateLimit>,
|
||||
pub token_contract: Address,
|
||||
pub target: Option<Address>,
|
||||
pub volume_limits: Vec<VolumeRateLimit>,
|
||||
}
|
||||
impl From<Settings> for SpecificGrant {
|
||||
fn from(val: Settings) -> SpecificGrant {
|
||||
|
||||
@@ -93,6 +93,7 @@ fn shared() -> SharedGrantSettings {
|
||||
max_gas_fee_per_gas: None,
|
||||
max_priority_fee_per_gas: None,
|
||||
rate_limit: None,
|
||||
client_id: CLIENT_ID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
use arbiter_proto::{
|
||||
proto::{
|
||||
self,
|
||||
evm::{
|
||||
EvmError as ProtoEvmError, WalletCreateResponse, WalletEntry, WalletList,
|
||||
WalletListResponse, wallet_create_response::Result as WalletCreateResult,
|
||||
EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
|
||||
EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
|
||||
EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
|
||||
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
|
||||
SpecificGrant as ProtoGrantSpecificGrant,
|
||||
TokenTransferSettings as ProtoTokenTransferSettings,
|
||||
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList,
|
||||
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
||||
evm_grant_list_response::Result as EvmGrantListResult,
|
||||
specific_grant::Grant as ProtoSpecificGrantType,
|
||||
wallet_create_response::Result as WalletCreateResult,
|
||||
wallet_list_response::Result as WalletListResult,
|
||||
},
|
||||
user_agent::{
|
||||
AuthChallenge as ProtoAuthChallenge,
|
||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
|
||||
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
||||
BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel,
|
||||
ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType,
|
||||
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
|
||||
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
||||
VaultState as ProtoVaultState,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
},
|
||||
@@ -23,13 +32,26 @@ use arbiter_proto::{
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt as _;
|
||||
use prost_types::Timestamp;
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::{Status, Streaming};
|
||||
|
||||
use crate::actors::user_agent::{
|
||||
self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse,
|
||||
TransportResponseError, UnsealError, VaultState,
|
||||
use crate::{
|
||||
actors::user_agent::{
|
||||
self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse,
|
||||
TransportResponseError, UnsealError, VaultState,
|
||||
},
|
||||
evm::{
|
||||
self,
|
||||
policies::{Grant, SpecificGrant},
|
||||
policies::{
|
||||
SharedGrantSettings, TransactionRateLimit, VolumeRateLimit, ether_transfer,
|
||||
token_transfers,
|
||||
},
|
||||
},
|
||||
};
|
||||
use alloy::primitives::{Address, U256};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
|
||||
pub struct GrpcTransport {
|
||||
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
|
||||
@@ -46,19 +68,17 @@ impl GrpcTransport {
|
||||
|
||||
fn request_to_domain(request: UserAgentRequest) -> Result<DomainRequest, Status> {
|
||||
match request.payload {
|
||||
Some(UserAgentRequestPayload::AuthChallengeRequest(
|
||||
ProtoAuthChallengeRequest {
|
||||
pubkey,
|
||||
bootstrap_token,
|
||||
key_type,
|
||||
},
|
||||
)) => Ok(DomainRequest::AuthChallengeRequest {
|
||||
Some(UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
||||
pubkey,
|
||||
bootstrap_token,
|
||||
key_type,
|
||||
})) => Ok(DomainRequest::AuthChallengeRequest {
|
||||
pubkey: parse_auth_pubkey(key_type, pubkey)?,
|
||||
bootstrap_token,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::AuthChallengeSolution(
|
||||
ProtoAuthChallengeSolution { signature },
|
||||
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
||||
Some(UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
||||
signature,
|
||||
})) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
||||
Some(UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey })) => {
|
||||
let client_pubkey: [u8; 32] = client_pubkey
|
||||
.as_slice()
|
||||
@@ -77,29 +97,42 @@ impl GrpcTransport {
|
||||
ciphertext,
|
||||
associated_data,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::BootstrapEncryptedKey(
|
||||
ProtoBootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
},
|
||||
)) => Ok(DomainRequest::BootstrapEncryptedKey {
|
||||
Some(UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
})) => Ok(DomainRequest::BootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::QueryVaultState(_)) => {
|
||||
Ok(DomainRequest::QueryVaultState)
|
||||
}
|
||||
Some(UserAgentRequestPayload::QueryVaultState(_)) => Ok(DomainRequest::QueryVaultState),
|
||||
Some(UserAgentRequestPayload::EvmWalletCreate(_)) => Ok(DomainRequest::EvmWalletCreate),
|
||||
Some(UserAgentRequestPayload::EvmWalletList(_)) => Ok(DomainRequest::EvmWalletList),
|
||||
Some(UserAgentRequestPayload::ClientConnectionResponse(
|
||||
ClientConnectionResponse { approved },
|
||||
)) => Ok(DomainRequest::ClientConnectionResponse { approved }),
|
||||
Some(_) => Err(Status::invalid_argument(
|
||||
"Unexpected user-agent request payload",
|
||||
Some(UserAgentRequestPayload::ClientConnectionResponse(ClientConnectionResponse {
|
||||
approved,
|
||||
})) => Ok(DomainRequest::ClientConnectionResponse { approved }),
|
||||
|
||||
Some(UserAgentRequestPayload::EvmGrantList(_)) => Ok(DomainRequest::ListGrants),
|
||||
Some(UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest {
|
||||
client_id,
|
||||
shared,
|
||||
specific,
|
||||
})) => {
|
||||
let shared = parse_shared_settings(client_id, shared)?;
|
||||
let specific = parse_specific_grant(specific)?;
|
||||
Ok(DomainRequest::EvmGrantCreate {
|
||||
client_id,
|
||||
shared,
|
||||
specific,
|
||||
})
|
||||
}
|
||||
Some(UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id })) => {
|
||||
Ok(DomainRequest::EvmGrantDelete { grant_id })
|
||||
}
|
||||
None => Err(Status::invalid_argument(
|
||||
"Missing user-agent request payload",
|
||||
)),
|
||||
None => Err(Status::invalid_argument("Missing user-agent request payload")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +206,29 @@ impl GrpcTransport {
|
||||
})),
|
||||
})
|
||||
}
|
||||
DomainResponse::ListGrants(grants) => {
|
||||
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
|
||||
result: Some(EvmGrantListResult::Grants(EvmGrantList {
|
||||
grants: grants.into_iter().map(grant_to_proto).collect(),
|
||||
})),
|
||||
})
|
||||
}
|
||||
DomainResponse::EvmGrantCreate(result) => {
|
||||
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
|
||||
result: Some(match result {
|
||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
||||
Err(_) => EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
DomainResponse::EvmGrantDelete(result) => {
|
||||
UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse {
|
||||
result: Some(match result {
|
||||
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
||||
Err(_) => EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
UserAgentResponse {
|
||||
@@ -191,7 +247,9 @@ impl GrpcTransport {
|
||||
TransportResponseError::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument("client_pubkey must be 32 bytes")
|
||||
}
|
||||
TransportResponseError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
TransportResponseError::StateTransitionFailed => {
|
||||
Status::internal("State machine error")
|
||||
}
|
||||
TransportResponseError::KeyHolderActorUnreachable => {
|
||||
Status::internal("Vault is not available")
|
||||
}
|
||||
@@ -238,6 +296,171 @@ impl Bi<DomainRequest, Result<DomainResponse, TransportResponseError>> for GrpcT
|
||||
}
|
||||
}
|
||||
|
||||
fn grant_to_proto(grant: Grant<SpecificGrant>) -> proto::evm::GrantEntry {
|
||||
GrantEntry {
|
||||
id: grant.id,
|
||||
specific: Some(match grant.settings {
|
||||
SpecificGrant::EtherTransfer(settings) => ProtoSpecificGrant {
|
||||
grant: Some(ProtoSpecificGrantType::EtherTransfer(
|
||||
ProtoEtherTransferSettings {
|
||||
targets: settings
|
||||
.target
|
||||
.into_iter()
|
||||
.map(|addr| addr.as_slice().to_vec())
|
||||
.collect(),
|
||||
limit: Some(proto::evm::VolumeRateLimit {
|
||||
max_volume: settings.limit.max_volume.to_be_bytes_vec(),
|
||||
window_secs: settings.limit.window.num_seconds(),
|
||||
}),
|
||||
},
|
||||
)),
|
||||
},
|
||||
SpecificGrant::TokenTransfer(settings) => ProtoSpecificGrant {
|
||||
grant: Some(ProtoSpecificGrantType::TokenTransfer(
|
||||
ProtoTokenTransferSettings {
|
||||
token_contract: settings.token_contract.as_slice().to_vec(),
|
||||
target: settings.target.map(|addr| addr.as_slice().to_vec()),
|
||||
volume_limits: settings
|
||||
.volume_limits
|
||||
.into_iter()
|
||||
.map(|vrl| proto::evm::VolumeRateLimit {
|
||||
max_volume: vrl.max_volume.to_be_bytes_vec(),
|
||||
window_secs: vrl.window.num_seconds(),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}),
|
||||
client_id: grant.shared.client_id,
|
||||
shared: Some(proto::evm::SharedSettings {
|
||||
wallet_id: grant.shared.wallet_id,
|
||||
chain_id: grant.shared.chain,
|
||||
valid_from: grant.shared.valid_from.map(|dt| Timestamp {
|
||||
seconds: dt.timestamp(),
|
||||
nanos: 0,
|
||||
}),
|
||||
valid_until: grant.shared.valid_until.map(|dt| Timestamp {
|
||||
seconds: dt.timestamp(),
|
||||
nanos: 0,
|
||||
}),
|
||||
max_gas_fee_per_gas: grant
|
||||
.shared
|
||||
.max_gas_fee_per_gas
|
||||
.map(|fee| fee.to_be_bytes_vec()),
|
||||
max_priority_fee_per_gas: grant
|
||||
.shared
|
||||
.max_priority_fee_per_gas
|
||||
.map(|fee| fee.to_be_bytes_vec()),
|
||||
rate_limit: grant
|
||||
.shared
|
||||
.rate_limit
|
||||
.map(|limit| proto::evm::TransactionRateLimit {
|
||||
count: limit.count,
|
||||
window_secs: limit.window.num_seconds(),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_volume_rate_limit(vrl: ProtoVolumeRateLimit) -> Result<VolumeRateLimit, Status> {
|
||||
Ok(VolumeRateLimit {
|
||||
max_volume: U256::from_be_slice(&vrl.max_volume),
|
||||
window: chrono::Duration::seconds(vrl.window_secs),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_shared_settings(
|
||||
client_id: i32,
|
||||
proto: Option<ProtoSharedSettings>,
|
||||
) -> Result<SharedGrantSettings, Status> {
|
||||
let s = proto.ok_or_else(|| Status::invalid_argument("missing shared settings"))?;
|
||||
let parse_u256 = |b: Vec<u8>| -> Result<U256, Status> {
|
||||
if b.is_empty() {
|
||||
Err(Status::invalid_argument("U256 bytes must not be empty"))
|
||||
} else {
|
||||
Ok(U256::from_be_slice(&b))
|
||||
}
|
||||
};
|
||||
let parse_ts = |ts: prost_types::Timestamp| -> Result<DateTime<Utc>, Status> {
|
||||
Utc.timestamp_opt(ts.seconds, ts.nanos as u32)
|
||||
.single()
|
||||
.ok_or_else(|| Status::invalid_argument("invalid timestamp"))
|
||||
};
|
||||
Ok(SharedGrantSettings {
|
||||
wallet_id: s.wallet_id,
|
||||
client_id,
|
||||
chain: s.chain_id,
|
||||
valid_from: s.valid_from.map(parse_ts).transpose()?,
|
||||
valid_until: s.valid_until.map(parse_ts).transpose()?,
|
||||
max_gas_fee_per_gas: s.max_gas_fee_per_gas.map(parse_u256).transpose()?,
|
||||
max_priority_fee_per_gas: s.max_priority_fee_per_gas.map(parse_u256).transpose()?,
|
||||
rate_limit: s.rate_limit.map(|rl| TransactionRateLimit {
|
||||
count: rl.count,
|
||||
window: chrono::Duration::seconds(rl.window_secs),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_specific_grant(proto: Option<proto::evm::SpecificGrant>) -> Result<SpecificGrant, Status> {
|
||||
use proto::evm::specific_grant::Grant as ProtoGrant;
|
||||
let g = proto
|
||||
.and_then(|sg| sg.grant)
|
||||
.ok_or_else(|| Status::invalid_argument("missing specific grant"))?;
|
||||
match g {
|
||||
ProtoGrant::EtherTransfer(s) => {
|
||||
let limit = parse_volume_rate_limit(
|
||||
s.limit
|
||||
.ok_or_else(|| Status::invalid_argument("missing ether transfer limit"))?,
|
||||
)?;
|
||||
let target = s
|
||||
.targets
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
if b.len() == 20 {
|
||||
Ok(Address::from_slice(&b))
|
||||
} else {
|
||||
Err(Status::invalid_argument(
|
||||
"ether transfer target must be 20 bytes",
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
|
||||
target,
|
||||
limit,
|
||||
}))
|
||||
}
|
||||
ProtoGrant::TokenTransfer(s) => {
|
||||
if s.token_contract.len() != 20 {
|
||||
return Err(Status::invalid_argument("token_contract must be 20 bytes"));
|
||||
}
|
||||
let target = s
|
||||
.target
|
||||
.map(|b| {
|
||||
if b.len() == 20 {
|
||||
Ok(Address::from_slice(&b))
|
||||
} else {
|
||||
Err(Status::invalid_argument(
|
||||
"token transfer target must be 20 bytes",
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
let volume_limits = s
|
||||
.volume_limits
|
||||
.into_iter()
|
||||
.map(parse_volume_rate_limit)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(SpecificGrant::TokenTransfer(token_transfers::Settings {
|
||||
token_contract: Address::from_slice(&s.token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_auth_pubkey(key_type: i32, pubkey: Vec<u8>) -> Result<AuthPublicKey, Status> {
|
||||
match ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified) {
|
||||
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#![deny(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic
|
||||
)]
|
||||
|
||||
use crate::context::ServerContext;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user