feat(evm): add find_all_grants to Policy trait with shared auto_type queries

This commit is contained in:
hdbg
2026-03-10 18:56:31 +01:00
parent 4a5dd3eea7
commit b7c4f2e735
4 changed files with 224 additions and 23 deletions

View File

@@ -16,7 +16,7 @@ use crate::{
schema::{self, evm_transaction_log}, schema::{self, evm_transaction_log},
}, },
evm::policies::{ evm::policies::{
EvalContext, EvalViolation, FullGrant, Policy, SpecificMeaning, EvalContext, EvalViolation, FullGrant, Grant, Policy, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer, ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
}, },
}; };
@@ -87,6 +87,17 @@ pub enum CreationError {
Database(#[from] diesel::result::Error), Database(#[from] diesel::result::Error),
} }
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum ListGrantsError {
#[error("Database connection pool error")]
#[diagnostic(code(arbiter_server::evm::list_grants_error::pool))]
Pool(#[from] db::PoolError),
#[error("Database returned error")]
#[diagnostic(code(arbiter_server::evm::list_grants_error::database))]
Database(#[from] diesel::result::Error),
}
/// Controls whether a transaction should be executed or only validated /// Controls whether a transaction should be executed or only validated
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunKind { pub enum RunKind {
@@ -201,6 +212,27 @@ impl Engine {
Ok(id) Ok(id)
} }
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListGrantsError> {
let mut conn = self.db.get().await?;
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
grants.extend(
EtherTransfer::find_all_grants(&mut conn)
.await?
.into_iter()
.map(Grant::from),
);
grants.extend(
TokenTransfer::find_all_grants(&mut conn)
.await?
.into_iter()
.map(Grant::from),
);
Ok(grants)
}
pub async fn evaluate_transaction( pub async fn evaluate_transaction(
&self, &self,
wallet_id: i32, wallet_id: i32,

View File

@@ -97,6 +97,11 @@ pub trait Policy: Sized {
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<Option<Grant<Self::Settings>>>> + Send; ) -> impl Future<Output = QueryResult<Option<Grant<Self::Settings>>>> + Send;
// Return all non-revoked grants, eagerly loading policy-specific settings
fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<Vec<Grant<Self::Settings>>>> + Send;
// Records, updates or deletes rate limits // Records, updates or deletes rate limits
// In other words, records grant-specific things after transaction is executed // In other words, records grant-specific things after transaction is executed
fn record_transaction( fn record_transaction(
@@ -192,6 +197,19 @@ pub enum SpecificGrant {
TokenTransfer(token_transfers::Settings), TokenTransfer(token_transfers::Settings),
} }
/// Blanket conversion from a typed `Grant<S>` into `Grant<SpecificGrant>`.
/// Lets the engine collect across all policies into one `Vec<Grant<SpecificGrant>>`.
impl<S: Into<SpecificGrant>> From<Grant<S>> for Grant<SpecificGrant> {
fn from(g: Grant<S>) -> Self {
Grant {
id: g.id,
shared_grant_id: g.shared_grant_id,
shared: g.shared,
settings: g.settings.into(),
}
}
}
pub struct FullGrant<PolicyGrant> { pub struct FullGrant<PolicyGrant> {
pub basic: SharedGrantSettings, pub basic: SharedGrantSettings,
pub specific: PolicyGrant, pub specific: PolicyGrant,

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use alloy::primitives::{Address, U256}; use alloy::primitives::{Address, U256};
@@ -11,7 +12,7 @@ use crate::db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit, EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp, NewEvmEtherTransferLimit, SqliteTimestamp,
}; };
use crate::db::schema::{evm_ether_transfer_limit, evm_transaction_log}; use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
use crate::evm::policies::{ use crate::evm::policies::{
Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
}; };
@@ -23,6 +24,14 @@ use crate::{
evm::{policies::Policy, utils}, evm::{policies::Policy, utils},
}; };
#[diesel::auto_type]
fn grant_join() -> _ {
evm_ether_transfer_grant::table.inner_join(
evm_basic_grant::table
.on(evm_ether_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
}
use super::{DatabaseID, EvalContext, EvalViolation}; use super::{DatabaseID, EvalContext, EvalViolation};
// Plain ether transfer // Plain ether transfer
@@ -183,24 +192,12 @@ impl Policy for EtherTransfer {
context: &EvalContext, context: &EvalContext,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> diesel::result::QueryResult<Option<Grant<Self::Settings>>> { ) -> diesel::result::QueryResult<Option<Grant<Self::Settings>>> {
use crate::db::schema::{
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
};
let target_bytes = context.to.to_vec(); let target_bytes = context.to.to_vec();
// Find a grant where: // Find a grant where:
// 1. The basic grant's wallet_id and client_id match the context // 1. The basic grant's wallet_id and client_id match the context
// 2. Any of the grant's targets match the context's `to` address // 2. Any of the grant's targets match the context's `to` address
let grant: Option<(EvmBasicGrant, EvmEtherTransferGrant)> = evm_ether_transfer_grant::table let grant: Option<(EvmBasicGrant, EvmEtherTransferGrant)> = grant_join()
.inner_join(
evm_basic_grant::table
.on(evm_ether_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
.inner_join(
evm_ether_transfer_grant_target::table
.on(evm_ether_transfer_grant::id.eq(evm_ether_transfer_grant_target::grant_id)),
)
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id)) .filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
.filter(evm_basic_grant::client_id.eq(context.client_id)) .filter(evm_basic_grant::client_id.eq(context.client_id))
.filter(evm_ether_transfer_grant_target::address.eq(&target_bytes)) .filter(evm_ether_transfer_grant_target::address.eq(&target_bytes))
@@ -266,4 +263,75 @@ impl Policy for EtherTransfer {
Ok(()) Ok(())
} }
async fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Vec<Grant<Self::Settings>>> {
let grants: Vec<(EvmBasicGrant, EvmEtherTransferGrant)> = grant_join()
.filter(evm_basic_grant::revoked_at.is_null())
.select((EvmBasicGrant::as_select(), EvmEtherTransferGrant::as_select()))
.load(conn)
.await?;
if grants.is_empty() {
return Ok(Vec::new());
}
let grant_ids: Vec<i32> = grants.iter().map(|(_, g)| g.id).collect();
let limit_ids: Vec<i32> = grants.iter().map(|(_, g)| g.limit_id).collect();
let all_targets: Vec<EvmEtherTransferGrantTarget> = evm_ether_transfer_grant_target::table
.filter(evm_ether_transfer_grant_target::grant_id.eq_any(&grant_ids))
.select(EvmEtherTransferGrantTarget::as_select())
.load(conn)
.await?;
let all_limits: Vec<EvmEtherTransferLimit> = evm_ether_transfer_limit::table
.filter(evm_ether_transfer_limit::id.eq_any(&limit_ids))
.select(EvmEtherTransferLimit::as_select())
.load(conn)
.await?;
let mut targets_by_grant: HashMap<i32, Vec<EvmEtherTransferGrantTarget>> = HashMap::new();
for target in all_targets {
targets_by_grant.entry(target.grant_id).or_default().push(target);
}
let limits_by_id: HashMap<i32, EvmEtherTransferLimit> =
all_limits.into_iter().map(|l| (l.id, l)).collect();
grants
.into_iter()
.map(|(basic, specific)| {
let targets: Vec<Address> = targets_by_grant
.get(&specific.id)
.map(|v| v.as_slice())
.unwrap_or_default()
.iter()
.filter_map(|t| {
let arr: [u8; 20] = t.address.clone().try_into().ok()?;
Some(Address::from(arr))
})
.collect();
let limit = limits_by_id
.get(&specific.limit_id)
.ok_or(diesel::result::Error::NotFound)?;
Ok(Grant {
id: specific.id,
shared_grant_id: specific.basic_grant_id,
shared: SharedGrantSettings::try_from_model(basic)?,
settings: Settings {
target: targets,
limit: VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
.map_err(|e| diesel::result::Error::DeserializationError(Box::new(e)))?,
window: Duration::seconds(limit.window_secs as i64),
},
},
})
})
.collect()
}
} }

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use alloy::{ use alloy::{
primitives::{Address, U256}, primitives::{Address, U256},
sol_types::SolCall, sol_types::SolCall,
@@ -14,7 +16,8 @@ use crate::db::models::{
NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, SqliteTimestamp,
}; };
use crate::db::schema::{ use crate::db::schema::{
evm_token_transfer_grant, evm_token_transfer_log, evm_token_transfer_volume_limit, evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
evm_token_transfer_volume_limit,
}; };
use crate::evm::{ use crate::evm::{
abi::IERC20::transferCall, abi::IERC20::transferCall,
@@ -24,6 +27,14 @@ use crate::evm::{
use super::{DatabaseID, EvalContext, EvalViolation}; use super::{DatabaseID, EvalContext, EvalViolation};
#[diesel::auto_type]
fn grant_join() -> _ {
evm_token_transfer_grant::table.inner_join(
evm_basic_grant::table
.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning { pub struct Meaning {
token: &'static TokenInfo, token: &'static TokenInfo,
@@ -192,15 +203,9 @@ impl Policy for TokenTransfer {
context: &EvalContext, context: &EvalContext,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Option<Grant<Self::Settings>>> { ) -> QueryResult<Option<Grant<Self::Settings>>> {
use crate::db::schema::{evm_basic_grant, evm_token_transfer_grant};
let token_contract_bytes = context.to.to_vec(); let token_contract_bytes = context.to.to_vec();
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = evm_token_transfer_grant::table let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
.inner_join(
evm_basic_grant::table
.on(evm_token_transfer_grant::basic_grant_id.eq(evm_basic_grant::id)),
)
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id)) .filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
.filter(evm_basic_grant::client_id.eq(context.client_id)) .filter(evm_basic_grant::client_id.eq(context.client_id))
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes)) .filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
@@ -288,4 +293,82 @@ impl Policy for TokenTransfer {
Ok(()) Ok(())
} }
async fn find_all_grants(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Vec<Grant<Self::Settings>>> {
let grants: Vec<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
.filter(evm_basic_grant::revoked_at.is_null())
.select((EvmBasicGrant::as_select(), EvmTokenTransferGrant::as_select()))
.load(conn)
.await?;
if grants.is_empty() {
return Ok(Vec::new());
}
let grant_ids: Vec<i32> = grants.iter().map(|(_, g)| g.id).collect();
let all_volume_limits: Vec<EvmTokenTransferVolumeLimit> =
evm_token_transfer_volume_limit::table
.filter(evm_token_transfer_volume_limit::grant_id.eq_any(&grant_ids))
.select(EvmTokenTransferVolumeLimit::as_select())
.load(conn)
.await?;
let mut limits_by_grant: HashMap<i32, Vec<EvmTokenTransferVolumeLimit>> = HashMap::new();
for limit in all_volume_limits {
limits_by_grant.entry(limit.grant_id).or_default().push(limit);
}
grants
.into_iter()
.map(|(basic, specific)| {
let volume_limits: Vec<VolumeRateLimit> = limits_by_grant
.get(&specific.id)
.map(|v| v.as_slice())
.unwrap_or_default()
.iter()
.map(|row| {
Ok(VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&row.max_volume)
.map_err(|e| diesel::result::Error::DeserializationError(Box::new(e)))?,
window: Duration::seconds(row.window_secs as i64),
})
})
.collect::<QueryResult<Vec<_>>>()?;
let token_contract: [u8; 20] = specific
.token_contract
.clone()
.try_into()
.map_err(|_| diesel::result::Error::DeserializationError(
"Invalid token contract address length".into(),
))?;
let target: Option<Address> = match &specific.receiver {
None => None,
Some(bytes) => {
let arr: [u8; 20] = bytes.clone().try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid receiver address length".into(),
)
})?;
Some(Address::from(arr))
}
};
Ok(Grant {
id: specific.id,
shared_grant_id: specific.basic_grant_id,
shared: SharedGrantSettings::try_from_model(basic)?,
settings: Settings {
token_contract: Address::from(token_contract),
target,
volume_limits,
},
})
})
.collect()
}
} }