3 Commits

Author SHA1 Message Date
hdbg
f461d945cb feat(evm): add EVM grants screen with create UI and list
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
2026-03-28 16:34:02 +01:00
hdbg
aa2df4adcb refactor(proto): restructure wallet access messages for improved data organization 2026-03-28 13:45:48 +01:00
hdbg
43412094b7 refactor(server::evm): removed repetetive errors and error variants 2026-03-28 12:54:14 +01:00
26 changed files with 1058 additions and 340 deletions

View File

@@ -0,0 +1,11 @@
---
name: Widget decomposition and provider subscriptions
description: Prefer splitting screens into multiple focused files/widgets; each widget subscribes to its own relevant providers
type: feedback
---
Split screens into multiple smaller widgets across multiple files. Each widget should subscribe only to the providers it needs (`ref.watch` at lowest possible level), rather than having one large screen widget that watches everything and passes data down as parameters.
**Why:** Reduces unnecessary rebuilds; improves readability; each file has one clear responsibility.
**How to apply:** When building a new screen, identify which sub-widgets need their own provider subscriptions and extract them into separate files (e.g., `widgets/grant_card.dart` watches enrichment providers itself, rather than the screen doing it and passing resolved strings down).

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ scripts/__pycache__/
.DS_Store .DS_Store
.cargo/config.toml .cargo/config.toml
.vscode/ .vscode/
docs/

View File

@@ -132,17 +132,22 @@ message SdkClientConnectionCancel {
bytes pubkey = 1; bytes pubkey = 1;
} }
message WalletAccess {
int32 wallet_id = 1;
int32 sdk_client_id = 2;
}
message SdkClientWalletAccess { message SdkClientWalletAccess {
int32 client_id = 1; int32 id = 1;
int32 wallet_id = 2; WalletAccess access = 2;
} }
message SdkClientGrantWalletAccess { message SdkClientGrantWalletAccess {
repeated SdkClientWalletAccess accesses = 1; repeated WalletAccess accesses = 1;
} }
message SdkClientRevokeWalletAccess { message SdkClientRevokeWalletAccess {
repeated SdkClientWalletAccess accesses = 1; repeated int32 accesses = 1;
} }
message ListWalletAccessResponse { message ListWalletAccessResponse {

View File

@@ -1,28 +1,21 @@
use std::path::PathBuf; use tonic_prost_build::configure;
use tonic_prost_build::{Config, configure};
static PROTOBUF_DIR: &str = "../../../protobufs"; static PROTOBUF_DIR: &str = "../../../protobufs";
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); println!("cargo::rerun-if-changed={PROTOBUF_DIR}");
let protoc_path = protoc_bin_vendored::protoc_bin_path()?;
let protoc_include = protoc_bin_vendored::include_path()?;
let mut config = Config::new();
config.protoc_executable(protoc_path);
let protos = [
PathBuf::from(format!("{}/arbiter.proto", PROTOBUF_DIR)),
PathBuf::from(format!("{}/user_agent.proto", PROTOBUF_DIR)),
PathBuf::from(format!("{}/client.proto", PROTOBUF_DIR)),
PathBuf::from(format!("{}/evm.proto", PROTOBUF_DIR)),
];
let includes = [PathBuf::from(PROTOBUF_DIR), protoc_include];
configure() configure()
.message_attribute(".", "#[derive(::kameo::Reply)]") .message_attribute(".", "#[derive(::kameo::Reply)]")
.compile_with_config(config, &protos, &includes)?; .compile_protos(
&[
format!("{}/arbiter.proto", PROTOBUF_DIR),
format!("{}/user_agent.proto", PROTOBUF_DIR),
format!("{}/client.proto", PROTOBUF_DIR),
format!("{}/evm.proto", PROTOBUF_DIR),
],
&[PROTOBUF_DIR.to_string()],
)
.unwrap();
Ok(()) Ok(())
} }

View File

@@ -9,12 +9,12 @@ use rand::{SeedableRng, rng, rngs::StdRng};
use crate::{ use crate::{
actors::keyholder::{CreateNew, Decrypt, KeyHolder}, actors::keyholder::{CreateNew, Decrypt, KeyHolder},
db::{ db::{
self, DatabasePool, self, DatabaseError, DatabasePool,
models::{self, SqliteTimestamp}, models::{self, SqliteTimestamp},
schema, schema,
}, },
evm::{ evm::{
self, ListGrantsError, RunKind, self, RunKind,
policies::{ policies::{
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer, ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
@@ -33,11 +33,7 @@ pub enum SignTransactionError {
#[error("Database error: {0}")] #[error("Database error: {0}")]
#[diagnostic(code(arbiter::evm::sign::database))] #[diagnostic(code(arbiter::evm::sign::database))]
Database(#[from] diesel::result::Error), Database(#[from] DatabaseError),
#[error("Database pool error: {0}")]
#[diagnostic(code(arbiter::evm::sign::pool))]
Pool(#[from] db::PoolError),
#[error("Keyholder error: {0}")] #[error("Keyholder error: {0}")]
#[diagnostic(code(arbiter::evm::sign::keyholder))] #[diagnostic(code(arbiter::evm::sign::keyholder))]
@@ -68,15 +64,7 @@ pub enum Error {
#[error("Database error: {0}")] #[error("Database error: {0}")]
#[diagnostic(code(arbiter::evm::database))] #[diagnostic(code(arbiter::evm::database))]
Database(#[from] diesel::result::Error), Database(#[from] DatabaseError),
#[error("Database pool error: {0}")]
#[diagnostic(code(arbiter::evm::database_pool))]
DatabasePool(#[from] db::PoolError),
#[error("Grant creation error: {0}")]
#[diagnostic(code(arbiter::evm::creation))]
Creation(#[from] evm::CreationError),
} }
#[derive(Actor)] #[derive(Actor)]
@@ -116,7 +104,7 @@ impl EvmActor {
.await .await
.map_err(|_| Error::KeyholderSend)?; .map_err(|_| Error::KeyholderSend)?;
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let wallet_id = insert_into(schema::evm_wallet::table) let wallet_id = insert_into(schema::evm_wallet::table)
.values(&models::NewEvmWallet { .values(&models::NewEvmWallet {
address: address.as_slice().to_vec(), address: address.as_slice().to_vec(),
@@ -124,18 +112,20 @@ impl EvmActor {
}) })
.returning(schema::evm_wallet::id) .returning(schema::evm_wallet::id)
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await
.map_err(DatabaseError::from)?;
Ok((wallet_id, address)) Ok((wallet_id, address))
} }
#[message] #[message]
pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> { pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let rows: Vec<models::EvmWallet> = schema::evm_wallet::table let rows: Vec<models::EvmWallet> = schema::evm_wallet::table
.select(models::EvmWallet::as_select()) .select(models::EvmWallet::as_select())
.load(&mut conn) .load(&mut conn)
.await?; .await
.map_err(DatabaseError::from)?;
Ok(rows Ok(rows
.into_iter() .into_iter()
@@ -151,7 +141,7 @@ impl EvmActor {
&mut self, &mut self,
basic: SharedGrantSettings, basic: SharedGrantSettings,
grant: SpecificGrant, grant: SpecificGrant,
) -> Result<i32, evm::CreationError> { ) -> Result<i32, DatabaseError> {
match grant { match grant {
SpecificGrant::EtherTransfer(settings) => { SpecificGrant::EtherTransfer(settings) => {
self.engine self.engine
@@ -174,22 +164,23 @@ impl EvmActor {
#[message] #[message]
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> { pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
diesel::update(schema::evm_basic_grant::table) diesel::update(schema::evm_basic_grant::table)
.filter(schema::evm_basic_grant::id.eq(grant_id)) .filter(schema::evm_basic_grant::id.eq(grant_id))
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now())) .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
.execute(&mut conn) .execute(&mut conn)
.await?; .await
.map_err(DatabaseError::from)?;
Ok(()) Ok(())
} }
#[message] #[message]
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> { pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.engine.list_all_grants().await { Ok(self
Ok(grants) => Ok(grants), .engine
Err(ListGrantsError::Database(db)) => Err(Error::Database(db)), .list_all_grants()
Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)), .await
} .map_err(DatabaseError::from)?)
} }
#[message] #[message]
@@ -199,13 +190,14 @@ impl EvmActor {
wallet_address: Address, wallet_address: Address,
transaction: TxEip1559, transaction: TxEip1559,
) -> Result<SpecificMeaning, SignTransactionError> { ) -> Result<SpecificMeaning, SignTransactionError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let wallet = schema::evm_wallet::table let wallet = schema::evm_wallet::table
.select(models::EvmWallet::as_select()) .select(models::EvmWallet::as_select())
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice())) .filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
.first(&mut conn) .first(&mut conn)
.await .await
.optional()? .optional()
.map_err(DatabaseError::from)?
.ok_or(SignTransactionError::WalletNotFound)?; .ok_or(SignTransactionError::WalletNotFound)?;
let wallet_access = schema::evm_wallet_access::table let wallet_access = schema::evm_wallet_access::table
.select(models::EvmWalletAccess::as_select()) .select(models::EvmWalletAccess::as_select())
@@ -213,7 +205,8 @@ impl EvmActor {
.filter(schema::evm_wallet_access::client_id.eq(client_id)) .filter(schema::evm_wallet_access::client_id.eq(client_id))
.first(&mut conn) .first(&mut conn)
.await .await
.optional()? .optional()
.map_err(DatabaseError::from)?
.ok_or(SignTransactionError::WalletNotFound)?; .ok_or(SignTransactionError::WalletNotFound)?;
drop(conn); drop(conn);
@@ -232,13 +225,14 @@ impl EvmActor {
wallet_address: Address, wallet_address: Address,
mut transaction: TxEip1559, mut transaction: TxEip1559,
) -> Result<Signature, SignTransactionError> { ) -> Result<Signature, SignTransactionError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let wallet = schema::evm_wallet::table let wallet = schema::evm_wallet::table
.select(models::EvmWallet::as_select()) .select(models::EvmWallet::as_select())
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice())) .filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
.first(&mut conn) .first(&mut conn)
.await .await
.optional()? .optional()
.map_err(DatabaseError::from)?
.ok_or(SignTransactionError::WalletNotFound)?; .ok_or(SignTransactionError::WalletNotFound)?;
let wallet_access = schema::evm_wallet_access::table let wallet_access = schema::evm_wallet_access::table
.select(models::EvmWalletAccess::as_select()) .select(models::EvmWalletAccess::as_select())
@@ -246,7 +240,8 @@ impl EvmActor {
.filter(schema::evm_wallet_access::client_id.eq(client_id)) .filter(schema::evm_wallet_access::client_id.eq(client_id))
.first(&mut conn) .first(&mut conn)
.await .await
.optional()? .optional()
.map_err(DatabaseError::from)?
.ok_or(SignTransactionError::WalletNotFound)?; .ok_or(SignTransactionError::WalletNotFound)?;
drop(conn); drop(conn);

View File

@@ -3,11 +3,6 @@ use crate::{
db::{self, models::KeyType}, db::{self, models::KeyType},
}; };
pub struct EvmAccessEntry {
pub wallet_id: i32,
pub sdk_client_id: i32,
}
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. /// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum AuthPublicKey { pub enum AuthPublicKey {

View File

@@ -13,9 +13,10 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer; use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
use crate::actors::keyholder::KeyHolderState; use crate::actors::keyholder::KeyHolderState;
use crate::actors::user_agent::EvmAccessEntry;
use crate::actors::user_agent::session::Error; use crate::actors::user_agent::session::Error;
use crate::db::models::{ProgramClient, ProgramClientMetadata}; use crate::db::models::{
CoreEvmWalletAccess, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::db::schema::evm_wallet_access; use crate::db::schema::evm_wallet_access;
use crate::evm::policies::{Grant, SpecificGrant}; use crate::evm::policies::{Grant, SpecificGrant};
use crate::safe_cell::SafeCell; use crate::safe_cell::SafeCell;
@@ -304,8 +305,6 @@ impl UserAgentSession {
} }
} }
#[messages] #[messages]
impl UserAgentSession { impl UserAgentSession {
#[message] #[message]
@@ -360,20 +359,16 @@ impl UserAgentSession {
#[message] #[message]
pub(crate) async fn handle_grant_evm_wallet_access( pub(crate) async fn handle_grant_evm_wallet_access(
&mut self, &mut self,
entries: Vec<EvmAccessEntry>, entries: Vec<NewEvmWalletAccess>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
conn.transaction(|conn| { conn.transaction(|conn| {
Box::pin(async move { Box::pin(async move {
use crate::db::models::NewEvmWalletAccess;
use crate::db::schema::evm_wallet_access; use crate::db::schema::evm_wallet_access;
for entry in entries { for entry in entries {
diesel::insert_into(evm_wallet_access::table) diesel::insert_into(evm_wallet_access::table)
.values(&NewEvmWalletAccess { .values(&entry)
wallet_id: entry.wallet_id,
client_id: entry.sdk_client_id,
})
.on_conflict_do_nothing() .on_conflict_do_nothing()
.execute(conn) .execute(conn)
.await?; .await?;
@@ -389,7 +384,7 @@ impl UserAgentSession {
#[message] #[message]
pub(crate) async fn handle_revoke_evm_wallet_access( pub(crate) async fn handle_revoke_evm_wallet_access(
&mut self, &mut self,
entries: Vec<EvmAccessEntry>, entries: Vec<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
conn.transaction(|conn| { conn.transaction(|conn| {
@@ -397,11 +392,7 @@ impl UserAgentSession {
use crate::db::schema::evm_wallet_access; use crate::db::schema::evm_wallet_access;
for entry in entries { for entry in entries {
diesel::delete(evm_wallet_access::table) diesel::delete(evm_wallet_access::table)
.filter( .filter(evm_wallet_access::wallet_id.eq(entry))
evm_wallet_access::wallet_id
.eq(entry.wallet_id)
.and(evm_wallet_access::client_id.eq(entry.sdk_client_id)),
)
.execute(conn) .execute(conn)
.await?; .await?;
} }
@@ -414,19 +405,15 @@ impl UserAgentSession {
} }
#[message] #[message]
pub(crate) async fn handle_list_wallet_access(&mut self) -> Result<Vec<EvmAccessEntry>, Error> { pub(crate) async fn handle_list_wallet_access(
&mut self,
) -> Result<Vec<EvmWalletAccess>, Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
use crate::db::schema::evm_wallet_access; use crate::db::schema::evm_wallet_access;
let access_entries = evm_wallet_access::table let access_entries = evm_wallet_access::table
.select((evm_wallet_access::wallet_id, evm_wallet_access::client_id)) .select(EvmWalletAccess::as_select())
.load::<(i32, i32)>(&mut conn) .load::<_>(&mut conn)
.await? .await?;
.into_iter()
.map(|(wallet_id, sdk_client_id)| EvmAccessEntry {
wallet_id,
sdk_client_id,
})
.collect();
Ok(access_entries) Ok(access_entries)
} }
} }

View File

@@ -193,6 +193,12 @@ pub struct EvmWallet {
omit(id, created_at), omit(id, created_at),
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
#[view(
CoreEvmWalletAccess,
derive(Insertable),
omit(created_at),
attributes_with = "deriveless"
)]
pub struct EvmWalletAccess { pub struct EvmWalletAccess {
pub id: i32, pub id: i32,
pub wallet_id: i32, pub wallet_id: i32,

View File

@@ -8,10 +8,11 @@ use alloy::{
use chrono::Utc; use chrono::Utc;
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use tracing_subscriber::registry::Data;
use crate::{ use crate::{
db::{ db::{
self, self, DatabaseError,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
}, },
@@ -30,12 +31,8 @@ mod utils;
/// Errors that can only occur once the transaction meaning is known (during policy evaluation) /// Errors that can only occur once the transaction meaning is known (during policy evaluation)
#[derive(Debug, thiserror::Error, miette::Diagnostic)] #[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum PolicyError { pub enum PolicyError {
#[error("Database connection pool error")] #[error("Database error")]
#[diagnostic(code(arbiter_server::evm::policy_error::pool))] Error(#[from] crate::db::DatabaseError),
Pool(#[from] db::PoolError),
#[error("Database returned error")]
#[diagnostic(code(arbiter_server::evm::policy_error::database))]
Database(#[from] diesel::result::Error),
#[error("Transaction violates policy: {0:?}")] #[error("Transaction violates policy: {0:?}")]
#[diagnostic(code(arbiter_server::evm::policy_error::violation))] #[diagnostic(code(arbiter_server::evm::policy_error::violation))]
Violations(Vec<EvalViolation>), Violations(Vec<EvalViolation>),
@@ -57,16 +54,6 @@ pub enum VetError {
Evaluated(SpecificMeaning, #[source] PolicyError), Evaluated(SpecificMeaning, #[source] PolicyError),
} }
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum SignError {
#[error("Database connection pool error")]
#[diagnostic(code(arbiter_server::evm::database_error))]
Pool(#[from] db::PoolError),
#[error("Database returned error")]
#[diagnostic(code(arbiter_server::evm::database_error))]
Database(#[from] diesel::result::Error),
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)] #[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum AnalyzeError { pub enum AnalyzeError {
#[error("Engine doesn't support granting permissions for contract creation")] #[error("Engine doesn't support granting permissions for contract creation")]
@@ -78,28 +65,6 @@ pub enum AnalyzeError {
UnsupportedTransactionType, UnsupportedTransactionType,
} }
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum CreationError {
#[error("Database connection pool error")]
#[diagnostic(code(arbiter_server::evm::creation_error::database_error))]
Pool(#[from] db::PoolError),
#[error("Database returned error")]
#[diagnostic(code(arbiter_server::evm::creation_error::database_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 {
@@ -167,16 +132,22 @@ impl Engine {
meaning: &P::Meaning, meaning: &P::Meaning,
run_kind: RunKind, run_kind: RunKind,
) -> Result<(), PolicyError> { ) -> Result<(), PolicyError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let grant = P::try_find_grant(&context, &mut conn) let grant = P::try_find_grant(&context, &mut conn)
.await? .await
.map_err(DatabaseError::from)?
.ok_or(PolicyError::NoMatchingGrant)?; .ok_or(PolicyError::NoMatchingGrant)?;
let mut violations = let mut violations =
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn) check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
.await?; .await
violations.extend(P::evaluate(&context, meaning, &grant, &mut conn).await?); .map_err(DatabaseError::from)?;
violations.extend(
P::evaluate(&context, meaning, &grant, &mut conn)
.await
.map_err(DatabaseError::from)?,
);
if !violations.is_empty() { if !violations.is_empty() {
return Err(PolicyError::Violations(violations)); return Err(PolicyError::Violations(violations));
@@ -200,7 +171,8 @@ impl Engine {
QueryResult::Ok(()) QueryResult::Ok(())
}) })
}) })
.await?; .await
.map_err(DatabaseError::from)?;
} }
Ok(()) Ok(())
@@ -215,7 +187,7 @@ impl Engine {
pub async fn create_grant<P: Policy>( pub async fn create_grant<P: Policy>(
&self, &self,
full_grant: FullGrant<P::Settings>, full_grant: FullGrant<P::Settings>,
) -> Result<i32, CreationError> { ) -> Result<i32, DatabaseError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let id = conn let id = conn
@@ -261,7 +233,7 @@ impl Engine {
Ok(id) Ok(id)
} }
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListGrantsError> { pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, DatabaseError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new(); let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();

View File

@@ -45,10 +45,15 @@ use crate::{
user_agent::{ user_agent::{
OutOfBand, UserAgentConnection, UserAgentSession, OutOfBand, UserAgentConnection, UserAgentSession,
session::connection::{ session::connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess, HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess, HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete,
HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess,
HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess,
HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
}, },
}, },
}, },
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess},
grpc::{Convert, TryConvert, request_tracker::RequestTracker}, grpc::{Convert, TryConvert, request_tracker::RequestTracker},
}; };
mod auth; mod auth;
@@ -383,7 +388,8 @@ async fn dispatch_inner(
} }
UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => { UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => {
let entries = accesses.try_convert()?; let entries: Vec<NewEvmWalletAccess> =
accesses.into_iter().map(|a| a.convert()).collect();
match actor.ask(HandleGrantEvmWalletAccess { entries }).await { match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
Ok(()) => { Ok(()) => {
@@ -398,9 +404,7 @@ async fn dispatch_inner(
} }
UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => { UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => {
let entries = accesses.try_convert()?; match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await {
match actor.ask(HandleRevokeEvmWalletAccess { entries }).await {
Ok(()) => { Ok(()) => {
info!("Successfully revoked wallet access"); info!("Successfully revoked wallet access");
return Ok(None); return Ok(None);

View File

@@ -1,23 +1,21 @@
use alloy::primitives::{Address, U256};
use arbiter_proto::proto::evm::{ use arbiter_proto::proto::evm::{
EtherTransferSettings as ProtoEtherTransferSettings, EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
SpecificGrant as ProtoSpecificGrant, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
TokenTransferSettings as ProtoTokenTransferSettings,
TransactionRateLimit as ProtoTransactionRateLimit,
VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}; };
use arbiter_proto::proto::user_agent::SdkClientWalletAccess; use arbiter_proto::proto::user_agent::{SdkClientWalletAccess, WalletAccess};
use alloy::primitives::{Address, U256};
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, TimeZone, Utc};
use prost_types::Timestamp as ProtoTimestamp; use prost_types::Timestamp as ProtoTimestamp;
use tonic::Status; use tonic::Status;
use crate::actors::user_agent::EvmAccessEntry; use crate::db::models::{CoreEvmWalletAccess, NewEvmWallet, NewEvmWalletAccess};
use crate::grpc::Convert;
use crate::{ use crate::{
evm::policies::{ evm::policies::{
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
ether_transfer, token_transfers, token_transfers,
}, },
grpc::TryConvert, grpc::TryConvert,
}; };
@@ -79,8 +77,14 @@ impl TryConvert for ProtoSharedSettings {
Ok(SharedGrantSettings { Ok(SharedGrantSettings {
wallet_access_id: self.wallet_access_id, wallet_access_id: self.wallet_access_id,
chain: self.chain_id, chain: self.chain_id,
valid_from: self.valid_from.map(ProtoTimestamp::try_convert).transpose()?, valid_from: self
valid_until: self.valid_until.map(ProtoTimestamp::try_convert).transpose()?, .valid_from
.map(ProtoTimestamp::try_convert)
.transpose()?,
valid_until: self
.valid_until
.map(ProtoTimestamp::try_convert)
.transpose()?,
max_gas_fee_per_gas: self max_gas_fee_per_gas: self
.max_gas_fee_per_gas .max_gas_fee_per_gas
.as_deref() .as_deref()
@@ -136,17 +140,29 @@ impl TryConvert for ProtoSpecificGrant {
} }
} }
impl TryConvert for Vec<SdkClientWalletAccess> { impl Convert for WalletAccess {
type Output = Vec<EvmAccessEntry>; type Output = NewEvmWalletAccess;
fn convert(self) -> Self::Output {
NewEvmWalletAccess {
wallet_id: self.wallet_id,
client_id: self.sdk_client_id,
}
}
}
impl TryConvert for SdkClientWalletAccess {
type Output = CoreEvmWalletAccess;
type Error = Status; type Error = Status;
fn try_convert(self) -> Result<Vec<EvmAccessEntry>, Status> { fn try_convert(self) -> Result<CoreEvmWalletAccess, Status> {
Ok(self let Some(access) = self.access else {
.into_iter() return Err(Status::invalid_argument("Missing wallet access entry"));
.map(|SdkClientWalletAccess { client_id, wallet_id }| EvmAccessEntry { };
wallet_id, Ok(CoreEvmWalletAccess {
sdk_client_id: client_id, wallet_id: access.wallet_id,
client_id: access.sdk_client_id,
id: self.id,
}) })
.collect())
} }
} }

View File

@@ -5,13 +5,13 @@ use arbiter_proto::proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}, },
user_agent::SdkClientWalletAccess as ProtoSdkClientWalletAccess, user_agent::{SdkClientWalletAccess as ProtoSdkClientWalletAccess, WalletAccess},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use prost_types::Timestamp as ProtoTimestamp; use prost_types::Timestamp as ProtoTimestamp;
use crate::{ use crate::{
actors::user_agent::EvmAccessEntry, db::models::EvmWalletAccess,
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit}, evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
grpc::Convert, grpc::Convert,
}; };
@@ -96,13 +96,16 @@ impl Convert for SpecificGrant {
} }
} }
impl Convert for EvmAccessEntry { impl Convert for EvmWalletAccess {
type Output = ProtoSdkClientWalletAccess; type Output = ProtoSdkClientWalletAccess;
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
ProtoSdkClientWalletAccess { Self::Output {
client_id: self.sdk_client_id, id: self.id,
access: Some(WalletAccess {
wallet_id: self.wallet_id, wallet_id: self.wallet_id,
sdk_client_id: self.client_id,
}),
} }
} }
} }

View File

@@ -29,17 +29,27 @@ Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
Future<int> createEvmGrant( Future<int> createEvmGrant(
Connection connection, { Connection connection, {
required int clientId, required SharedSettings sharedSettings,
required int walletId,
required Int64 chainId,
DateTime? validFrom,
DateTime? validUntil,
List<int>? maxGasFeePerGas,
List<int>? maxPriorityFeePerGas,
TransactionRateLimit? rateLimit,
required SpecificGrant specific, required SpecificGrant specific,
}) async { }) async {
throw UnimplementedError('EVM grant creation is not yet implemented.'); final request = UserAgentRequest(
evmGrantCreate: EvmGrantCreateRequest(
shared: sharedSettings,
specific: specific,
),
);
final resp = await connection.ask(request);
if (!resp.hasEvmGrantCreate()) {
throw Exception(
'Expected EVM grant create response, got ${resp.whichPayload()}',
);
}
final result = resp.evmGrantCreate;
return result.grantId;
} }
Future<void> deleteEvmGrant(Connection connection, int grantId) async { Future<void> deleteEvmGrant(Connection connection, int grantId) async {

View File

@@ -15,11 +15,25 @@ Future<Set<int>> readClientWalletAccess(
); );
} }
return { return {
for (final access in response.listWalletAccessResponse.accesses) for (final entry in response.listWalletAccessResponse.accesses)
if (access.clientId == clientId) access.walletId, if (entry.access.sdkClientId == clientId) entry.access.walletId,
}; };
} }
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
Connection connection,
) async {
final response = await connection.ask(
UserAgentRequest(listWalletAccess: Empty()),
);
if (!response.hasListWalletAccessResponse()) {
throw Exception(
'Expected list wallet access response, got ${response.whichPayload()}',
);
}
return response.listWalletAccessResponse.accesses.toList(growable: false);
}
Future<void> writeClientWalletAccess( Future<void> writeClientWalletAccess(
Connection connection, { Connection connection, {
required int clientId, required int clientId,
@@ -36,7 +50,7 @@ Future<void> writeClientWalletAccess(
grantWalletAccess: SdkClientGrantWalletAccess( grantWalletAccess: SdkClientGrantWalletAccess(
accesses: [ accesses: [
for (final walletId in toGrant) for (final walletId in toGrant)
SdkClientWalletAccess(clientId: clientId, walletId: walletId), WalletAccess(sdkClientId: clientId, walletId: walletId),
], ],
), ),
), ),
@@ -49,7 +63,7 @@ Future<void> writeClientWalletAccess(
revokeWalletAccess: SdkClientRevokeWalletAccess( revokeWalletAccess: SdkClientRevokeWalletAccess(
accesses: [ accesses: [
for (final walletId in toRevoke) for (final walletId in toRevoke)
SdkClientWalletAccess(clientId: clientId, walletId: walletId), walletId
], ],
), ),
), ),

View File

@@ -1072,14 +1072,81 @@ class SdkClientConnectionCancel extends $pb.GeneratedMessage {
void clearPubkey() => $_clearField(1); void clearPubkey() => $_clearField(1);
} }
class SdkClientWalletAccess extends $pb.GeneratedMessage { class WalletAccess extends $pb.GeneratedMessage {
factory SdkClientWalletAccess({ factory WalletAccess({
$core.int? clientId,
$core.int? walletId, $core.int? walletId,
$core.int? sdkClientId,
}) { }) {
final result = create(); final result = create();
if (clientId != null) result.clientId = clientId;
if (walletId != null) result.walletId = walletId; if (walletId != null) result.walletId = walletId;
if (sdkClientId != null) result.sdkClientId = sdkClientId;
return result;
}
WalletAccess._();
factory WalletAccess.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WalletAccess.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WalletAccess',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
createEmptyInstance: create)
..aI(1, _omitFieldNames ? '' : 'walletId')
..aI(2, _omitFieldNames ? '' : 'sdkClientId')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WalletAccess clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WalletAccess copyWith(void Function(WalletAccess) updates) =>
super.copyWith((message) => updates(message as WalletAccess))
as WalletAccess;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WalletAccess create() => WalletAccess._();
@$core.override
WalletAccess createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WalletAccess getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WalletAccess>(create);
static WalletAccess? _defaultInstance;
@$pb.TagNumber(1)
$core.int get walletId => $_getIZ(0);
@$pb.TagNumber(1)
set walletId($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1)
$core.bool hasWalletId() => $_has(0);
@$pb.TagNumber(1)
void clearWalletId() => $_clearField(1);
@$pb.TagNumber(2)
$core.int get sdkClientId => $_getIZ(1);
@$pb.TagNumber(2)
set sdkClientId($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2)
$core.bool hasSdkClientId() => $_has(1);
@$pb.TagNumber(2)
void clearSdkClientId() => $_clearField(2);
}
class SdkClientWalletAccess extends $pb.GeneratedMessage {
factory SdkClientWalletAccess({
$core.int? id,
WalletAccess? access,
}) {
final result = create();
if (id != null) result.id = id;
if (access != null) result.access = access;
return result; return result;
} }
@@ -1097,8 +1164,9 @@ class SdkClientWalletAccess extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
createEmptyInstance: create) createEmptyInstance: create)
..aI(1, _omitFieldNames ? '' : 'clientId') ..aI(1, _omitFieldNames ? '' : 'id')
..aI(2, _omitFieldNames ? '' : 'walletId') ..aOM<WalletAccess>(2, _omitFieldNames ? '' : 'access',
subBuilder: WalletAccess.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -1122,27 +1190,29 @@ class SdkClientWalletAccess extends $pb.GeneratedMessage {
static SdkClientWalletAccess? _defaultInstance; static SdkClientWalletAccess? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get clientId => $_getIZ(0); $core.int get id => $_getIZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set clientId($core.int value) => $_setSignedInt32(0, value); set id($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasClientId() => $_has(0); $core.bool hasId() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearClientId() => $_clearField(1); void clearId() => $_clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.int get walletId => $_getIZ(1); WalletAccess get access => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set walletId($core.int value) => $_setSignedInt32(1, value); set access(WalletAccess value) => $_setField(2, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasWalletId() => $_has(1); $core.bool hasAccess() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearWalletId() => $_clearField(2); void clearAccess() => $_clearField(2);
@$pb.TagNumber(2)
WalletAccess ensureAccess() => $_ensure(1);
} }
class SdkClientGrantWalletAccess extends $pb.GeneratedMessage { class SdkClientGrantWalletAccess extends $pb.GeneratedMessage {
factory SdkClientGrantWalletAccess({ factory SdkClientGrantWalletAccess({
$core.Iterable<SdkClientWalletAccess>? accesses, $core.Iterable<WalletAccess>? accesses,
}) { }) {
final result = create(); final result = create();
if (accesses != null) result.accesses.addAll(accesses); if (accesses != null) result.accesses.addAll(accesses);
@@ -1163,8 +1233,8 @@ class SdkClientGrantWalletAccess extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
createEmptyInstance: create) createEmptyInstance: create)
..pPM<SdkClientWalletAccess>(1, _omitFieldNames ? '' : 'accesses', ..pPM<WalletAccess>(1, _omitFieldNames ? '' : 'accesses',
subBuilder: SdkClientWalletAccess.create) subBuilder: WalletAccess.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -1189,12 +1259,12 @@ class SdkClientGrantWalletAccess extends $pb.GeneratedMessage {
static SdkClientGrantWalletAccess? _defaultInstance; static SdkClientGrantWalletAccess? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$pb.PbList<SdkClientWalletAccess> get accesses => $_getList(0); $pb.PbList<WalletAccess> get accesses => $_getList(0);
} }
class SdkClientRevokeWalletAccess extends $pb.GeneratedMessage { class SdkClientRevokeWalletAccess extends $pb.GeneratedMessage {
factory SdkClientRevokeWalletAccess({ factory SdkClientRevokeWalletAccess({
$core.Iterable<SdkClientWalletAccess>? accesses, $core.Iterable<$core.int>? accesses,
}) { }) {
final result = create(); final result = create();
if (accesses != null) result.accesses.addAll(accesses); if (accesses != null) result.accesses.addAll(accesses);
@@ -1215,8 +1285,7 @@ class SdkClientRevokeWalletAccess extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'), const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.user_agent'),
createEmptyInstance: create) createEmptyInstance: create)
..pPM<SdkClientWalletAccess>(1, _omitFieldNames ? '' : 'accesses', ..p<$core.int>(1, _omitFieldNames ? '' : 'accesses', $pb.PbFieldType.K3)
subBuilder: SdkClientWalletAccess.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -1242,7 +1311,7 @@ class SdkClientRevokeWalletAccess extends $pb.GeneratedMessage {
static SdkClientRevokeWalletAccess? _defaultInstance; static SdkClientRevokeWalletAccess? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$pb.PbList<SdkClientWalletAccess> get accesses => $_getList(0); $pb.PbList<$core.int> get accesses => $_getList(0);
} }
class ListWalletAccessResponse extends $pb.GeneratedMessage { class ListWalletAccessResponse extends $pb.GeneratedMessage {

View File

@@ -418,19 +418,40 @@ final $typed_data.Uint8List sdkClientConnectionCancelDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChlTZGtDbGllbnRDb25uZWN0aW9uQ2FuY2VsEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5'); 'ChlTZGtDbGllbnRDb25uZWN0aW9uQ2FuY2VsEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5');
@$core.Deprecated('Use walletAccessDescriptor instead')
const WalletAccess$json = {
'1': 'WalletAccess',
'2': [
{'1': 'wallet_id', '3': 1, '4': 1, '5': 5, '10': 'walletId'},
{'1': 'sdk_client_id', '3': 2, '4': 1, '5': 5, '10': 'sdkClientId'},
],
};
/// Descriptor for `WalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletAccessDescriptor = $convert.base64Decode(
'CgxXYWxsZXRBY2Nlc3MSGwoJd2FsbGV0X2lkGAEgASgFUgh3YWxsZXRJZBIiCg1zZGtfY2xpZW'
'50X2lkGAIgASgFUgtzZGtDbGllbnRJZA==');
@$core.Deprecated('Use sdkClientWalletAccessDescriptor instead') @$core.Deprecated('Use sdkClientWalletAccessDescriptor instead')
const SdkClientWalletAccess$json = { const SdkClientWalletAccess$json = {
'1': 'SdkClientWalletAccess', '1': 'SdkClientWalletAccess',
'2': [ '2': [
{'1': 'client_id', '3': 1, '4': 1, '5': 5, '10': 'clientId'}, {'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
{'1': 'wallet_id', '3': 2, '4': 1, '5': 5, '10': 'walletId'}, {
'1': 'access',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.WalletAccess',
'10': 'access'
},
], ],
}; };
/// Descriptor for `SdkClientWalletAccess`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SdkClientWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientWalletAccessDescriptor = $convert.base64Decode( final $typed_data.Uint8List sdkClientWalletAccessDescriptor = $convert.base64Decode(
'ChVTZGtDbGllbnRXYWxsZXRBY2Nlc3MSGwoJY2xpZW50X2lkGAEgASgFUghjbGllbnRJZBIbCg' 'ChVTZGtDbGllbnRXYWxsZXRBY2Nlc3MSDgoCaWQYASABKAVSAmlkEjgKBmFjY2VzcxgCIAEoCz'
'l3YWxsZXRfaWQYAiABKAVSCHdhbGxldElk'); 'IgLmFyYml0ZXIudXNlcl9hZ2VudC5XYWxsZXRBY2Nlc3NSBmFjY2Vzcw==');
@$core.Deprecated('Use sdkClientGrantWalletAccessDescriptor instead') @$core.Deprecated('Use sdkClientGrantWalletAccessDescriptor instead')
const SdkClientGrantWalletAccess$json = { const SdkClientGrantWalletAccess$json = {
@@ -441,7 +462,7 @@ const SdkClientGrantWalletAccess$json = {
'3': 1, '3': 1,
'4': 3, '4': 3,
'5': 11, '5': 11,
'6': '.arbiter.user_agent.SdkClientWalletAccess', '6': '.arbiter.user_agent.WalletAccess',
'10': 'accesses' '10': 'accesses'
}, },
], ],
@@ -450,29 +471,22 @@ const SdkClientGrantWalletAccess$json = {
/// Descriptor for `SdkClientGrantWalletAccess`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SdkClientGrantWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientGrantWalletAccessDescriptor = final $typed_data.Uint8List sdkClientGrantWalletAccessDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChpTZGtDbGllbnRHcmFudFdhbGxldEFjY2VzcxJFCghhY2Nlc3NlcxgBIAMoCzIpLmFyYml0ZX' 'ChpTZGtDbGllbnRHcmFudFdhbGxldEFjY2VzcxI8CghhY2Nlc3NlcxgBIAMoCzIgLmFyYml0ZX'
'IudXNlcl9hZ2VudC5TZGtDbGllbnRXYWxsZXRBY2Nlc3NSCGFjY2Vzc2Vz'); 'IudXNlcl9hZ2VudC5XYWxsZXRBY2Nlc3NSCGFjY2Vzc2Vz');
@$core.Deprecated('Use sdkClientRevokeWalletAccessDescriptor instead') @$core.Deprecated('Use sdkClientRevokeWalletAccessDescriptor instead')
const SdkClientRevokeWalletAccess$json = { const SdkClientRevokeWalletAccess$json = {
'1': 'SdkClientRevokeWalletAccess', '1': 'SdkClientRevokeWalletAccess',
'2': [ '2': [
{ {'1': 'accesses', '3': 1, '4': 3, '5': 5, '10': 'accesses'},
'1': 'accesses',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.user_agent.SdkClientWalletAccess',
'10': 'accesses'
},
], ],
}; };
/// Descriptor for `SdkClientRevokeWalletAccess`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SdkClientRevokeWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientRevokeWalletAccessDescriptor = final $typed_data.Uint8List sdkClientRevokeWalletAccessDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChtTZGtDbGllbnRSZXZva2VXYWxsZXRBY2Nlc3MSRQoIYWNjZXNzZXMYASADKAsyKS5hcmJpdG' 'ChtTZGtDbGllbnRSZXZva2VXYWxsZXRBY2Nlc3MSGgoIYWNjZXNzZXMYASADKAVSCGFjY2Vzc2'
'VyLnVzZXJfYWdlbnQuU2RrQ2xpZW50V2FsbGV0QWNjZXNzUghhY2Nlc3Nlcw=='); 'Vz');
@$core.Deprecated('Use listWalletAccessResponseDescriptor instead') @$core.Deprecated('Use listWalletAccessResponseDescriptor instead')
const ListWalletAccessResponse$json = { const ListWalletAccessResponse$json = {

View File

@@ -5,6 +5,7 @@ import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/experimental/mutation.dart'; import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'evm_grants.freezed.dart'; part 'evm_grants.freezed.dart';
@@ -73,14 +74,7 @@ class EvmGrants extends _$EvmGrants {
Future<int> executeCreateEvmGrant( Future<int> executeCreateEvmGrant(
MutationTarget ref, { MutationTarget ref, {
required int clientId, required SharedSettings sharedSettings,
required int walletId,
required Int64 chainId,
DateTime? validFrom,
DateTime? validUntil,
List<int>? maxGasFeePerGas,
List<int>? maxPriorityFeePerGas,
TransactionRateLimit? rateLimit,
required SpecificGrant specific, required SpecificGrant specific,
}) { }) {
return createEvmGrantMutation.run(ref, (tsx) async { return createEvmGrantMutation.run(ref, (tsx) async {
@@ -91,14 +85,7 @@ Future<int> executeCreateEvmGrant(
final grantId = await createEvmGrant( final grantId = await createEvmGrant(
connection, connection,
clientId: clientId, sharedSettings: sharedSettings,
walletId: walletId,
chainId: chainId,
validFrom: validFrom,
validUntil: validUntil,
maxGasFeePerGas: maxGasFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
rateLimit: rateLimit,
specific: specific, specific: specific,
); );

View File

@@ -0,0 +1,22 @@
import 'package:arbiter/features/connection/evm/wallet_access.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'wallet_access_list.g.dart';
@riverpod
Future<List<SdkClientWalletAccess>?> walletAccessList(Ref ref) async {
final connection = await ref.watch(connectionManagerProvider.future);
if (connection == null) {
return null;
}
try {
return await listAllWalletAccesses(connection);
} catch (e, st) {
talker.handle(e, st);
rethrow;
}
}

View File

@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wallet_access_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(walletAccessList)
final walletAccessListProvider = WalletAccessListProvider._();
final class WalletAccessListProvider
extends
$FunctionalProvider<
AsyncValue<List<SdkClientWalletAccess>?>,
List<SdkClientWalletAccess>?,
FutureOr<List<SdkClientWalletAccess>?>
>
with
$FutureModifier<List<SdkClientWalletAccess>?>,
$FutureProvider<List<SdkClientWalletAccess>?> {
WalletAccessListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'walletAccessListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$walletAccessListHash();
@$internal
@override
$FutureProviderElement<List<SdkClientWalletAccess>?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SdkClientWalletAccess>?> create(Ref ref) {
return walletAccessList(ref);
}
}
String _$walletAccessListHash() => r'c06006d6792ae463105a539723e9bb396192f96b';

View File

@@ -19,6 +19,7 @@ class Router extends RootStackRouter {
children: [ children: [
AutoRoute(page: EvmRoute.page, path: 'evm'), AutoRoute(page: EvmRoute.page, path: 'evm'),
AutoRoute(page: ClientsRoute.page, path: 'clients'), AutoRoute(page: ClientsRoute.page, path: 'clients'),
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
AutoRoute(page: AboutRoute.page, path: 'about'), AutoRoute(page: AboutRoute.page, path: 'about'),
], ],
), ),

View File

@@ -9,7 +9,7 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:arbiter/proto/user_agent.pb.dart' as _i14; import 'package:arbiter/proto/user_agent.pb.dart' as _i15;
import 'package:arbiter/screens/bootstrap.dart' as _i2; import 'package:arbiter/screens/bootstrap.dart' as _i2;
import 'package:arbiter/screens/dashboard.dart' as _i7; import 'package:arbiter/screens/dashboard.dart' as _i7;
import 'package:arbiter/screens/dashboard/about.dart' as _i1; import 'package:arbiter/screens/dashboard/about.dart' as _i1;
@@ -17,23 +17,24 @@ import 'package:arbiter/screens/dashboard/clients/details.dart' as _i3;
import 'package:arbiter/screens/dashboard/clients/details/client_details.dart' import 'package:arbiter/screens/dashboard/clients/details/client_details.dart'
as _i4; as _i4;
import 'package:arbiter/screens/dashboard/clients/table.dart' as _i5; import 'package:arbiter/screens/dashboard/clients/table.dart' as _i5;
import 'package:arbiter/screens/dashboard/evm/evm.dart' as _i8; import 'package:arbiter/screens/dashboard/evm/evm.dart' as _i9;
import 'package:arbiter/screens/dashboard/evm/grants/grant_create.dart' as _i6; import 'package:arbiter/screens/dashboard/evm/grants/grant_create.dart' as _i6;
import 'package:arbiter/screens/server_connection.dart' as _i9; import 'package:arbiter/screens/dashboard/evm/grants/grants.dart' as _i8;
import 'package:arbiter/screens/server_info_setup.dart' as _i10; import 'package:arbiter/screens/server_connection.dart' as _i10;
import 'package:arbiter/screens/vault_setup.dart' as _i11; import 'package:arbiter/screens/server_info_setup.dart' as _i11;
import 'package:auto_route/auto_route.dart' as _i12; import 'package:arbiter/screens/vault_setup.dart' as _i12;
import 'package:flutter/material.dart' as _i13; import 'package:auto_route/auto_route.dart' as _i13;
import 'package:flutter/material.dart' as _i14;
/// generated route for /// generated route for
/// [_i1.AboutScreen] /// [_i1.AboutScreen]
class AboutRoute extends _i12.PageRouteInfo<void> { class AboutRoute extends _i13.PageRouteInfo<void> {
const AboutRoute({List<_i12.PageRouteInfo>? children}) const AboutRoute({List<_i13.PageRouteInfo>? children})
: super(AboutRoute.name, initialChildren: children); : super(AboutRoute.name, initialChildren: children);
static const String name = 'AboutRoute'; static const String name = 'AboutRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i1.AboutScreen(); return const _i1.AboutScreen();
@@ -43,13 +44,13 @@ class AboutRoute extends _i12.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.Bootstrap] /// [_i2.Bootstrap]
class Bootstrap extends _i12.PageRouteInfo<void> { class Bootstrap extends _i13.PageRouteInfo<void> {
const Bootstrap({List<_i12.PageRouteInfo>? children}) const Bootstrap({List<_i13.PageRouteInfo>? children})
: super(Bootstrap.name, initialChildren: children); : super(Bootstrap.name, initialChildren: children);
static const String name = 'Bootstrap'; static const String name = 'Bootstrap';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i2.Bootstrap(); return const _i2.Bootstrap();
@@ -59,11 +60,11 @@ class Bootstrap extends _i12.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.ClientDetails] /// [_i3.ClientDetails]
class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> { class ClientDetails extends _i13.PageRouteInfo<ClientDetailsArgs> {
ClientDetails({ ClientDetails({
_i13.Key? key, _i14.Key? key,
required _i14.SdkClientEntry client, required _i15.SdkClientEntry client,
List<_i12.PageRouteInfo>? children, List<_i13.PageRouteInfo>? children,
}) : super( }) : super(
ClientDetails.name, ClientDetails.name,
args: ClientDetailsArgs(key: key, client: client), args: ClientDetailsArgs(key: key, client: client),
@@ -72,7 +73,7 @@ class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> {
static const String name = 'ClientDetails'; static const String name = 'ClientDetails';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<ClientDetailsArgs>(); final args = data.argsAs<ClientDetailsArgs>();
@@ -84,9 +85,9 @@ class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> {
class ClientDetailsArgs { class ClientDetailsArgs {
const ClientDetailsArgs({this.key, required this.client}); const ClientDetailsArgs({this.key, required this.client});
final _i13.Key? key; final _i14.Key? key;
final _i14.SdkClientEntry client; final _i15.SdkClientEntry client;
@override @override
String toString() { String toString() {
@@ -106,11 +107,11 @@ class ClientDetailsArgs {
/// generated route for /// generated route for
/// [_i4.ClientDetailsScreen] /// [_i4.ClientDetailsScreen]
class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> { class ClientDetailsRoute extends _i13.PageRouteInfo<ClientDetailsRouteArgs> {
ClientDetailsRoute({ ClientDetailsRoute({
_i13.Key? key, _i14.Key? key,
required int clientId, required int clientId,
List<_i12.PageRouteInfo>? children, List<_i13.PageRouteInfo>? children,
}) : super( }) : super(
ClientDetailsRoute.name, ClientDetailsRoute.name,
args: ClientDetailsRouteArgs(key: key, clientId: clientId), args: ClientDetailsRouteArgs(key: key, clientId: clientId),
@@ -120,7 +121,7 @@ class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> {
static const String name = 'ClientDetailsRoute'; static const String name = 'ClientDetailsRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
@@ -136,7 +137,7 @@ class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> {
class ClientDetailsRouteArgs { class ClientDetailsRouteArgs {
const ClientDetailsRouteArgs({this.key, required this.clientId}); const ClientDetailsRouteArgs({this.key, required this.clientId});
final _i13.Key? key; final _i14.Key? key;
final int clientId; final int clientId;
@@ -158,13 +159,13 @@ class ClientDetailsRouteArgs {
/// generated route for /// generated route for
/// [_i5.ClientsScreen] /// [_i5.ClientsScreen]
class ClientsRoute extends _i12.PageRouteInfo<void> { class ClientsRoute extends _i13.PageRouteInfo<void> {
const ClientsRoute({List<_i12.PageRouteInfo>? children}) const ClientsRoute({List<_i13.PageRouteInfo>? children})
: super(ClientsRoute.name, initialChildren: children); : super(ClientsRoute.name, initialChildren: children);
static const String name = 'ClientsRoute'; static const String name = 'ClientsRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i5.ClientsScreen(); return const _i5.ClientsScreen();
@@ -174,13 +175,13 @@ class ClientsRoute extends _i12.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i6.CreateEvmGrantScreen] /// [_i6.CreateEvmGrantScreen]
class CreateEvmGrantRoute extends _i12.PageRouteInfo<void> { class CreateEvmGrantRoute extends _i13.PageRouteInfo<void> {
const CreateEvmGrantRoute({List<_i12.PageRouteInfo>? children}) const CreateEvmGrantRoute({List<_i13.PageRouteInfo>? children})
: super(CreateEvmGrantRoute.name, initialChildren: children); : super(CreateEvmGrantRoute.name, initialChildren: children);
static const String name = 'CreateEvmGrantRoute'; static const String name = 'CreateEvmGrantRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i6.CreateEvmGrantScreen(); return const _i6.CreateEvmGrantScreen();
@@ -190,13 +191,13 @@ class CreateEvmGrantRoute extends _i12.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i7.DashboardRouter] /// [_i7.DashboardRouter]
class DashboardRouter extends _i12.PageRouteInfo<void> { class DashboardRouter extends _i13.PageRouteInfo<void> {
const DashboardRouter({List<_i12.PageRouteInfo>? children}) const DashboardRouter({List<_i13.PageRouteInfo>? children})
: super(DashboardRouter.name, initialChildren: children); : super(DashboardRouter.name, initialChildren: children);
static const String name = 'DashboardRouter'; static const String name = 'DashboardRouter';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i7.DashboardRouter(); return const _i7.DashboardRouter();
@@ -205,29 +206,45 @@ class DashboardRouter extends _i12.PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [_i8.EvmScreen] /// [_i8.EvmGrantsScreen]
class EvmRoute extends _i12.PageRouteInfo<void> { class EvmGrantsRoute extends _i13.PageRouteInfo<void> {
const EvmRoute({List<_i12.PageRouteInfo>? children}) const EvmGrantsRoute({List<_i13.PageRouteInfo>? children})
: super(EvmRoute.name, initialChildren: children); : super(EvmGrantsRoute.name, initialChildren: children);
static const String name = 'EvmRoute'; static const String name = 'EvmGrantsRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i8.EvmScreen(); return const _i8.EvmGrantsScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i9.ServerConnectionScreen] /// [_i9.EvmScreen]
class EvmRoute extends _i13.PageRouteInfo<void> {
const EvmRoute({List<_i13.PageRouteInfo>? children})
: super(EvmRoute.name, initialChildren: children);
static const String name = 'EvmRoute';
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i9.EvmScreen();
},
);
}
/// generated route for
/// [_i10.ServerConnectionScreen]
class ServerConnectionRoute class ServerConnectionRoute
extends _i12.PageRouteInfo<ServerConnectionRouteArgs> { extends _i13.PageRouteInfo<ServerConnectionRouteArgs> {
ServerConnectionRoute({ ServerConnectionRoute({
_i13.Key? key, _i14.Key? key,
String? arbiterUrl, String? arbiterUrl,
List<_i12.PageRouteInfo>? children, List<_i13.PageRouteInfo>? children,
}) : super( }) : super(
ServerConnectionRoute.name, ServerConnectionRoute.name,
args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl), args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl),
@@ -236,13 +253,13 @@ class ServerConnectionRoute
static const String name = 'ServerConnectionRoute'; static const String name = 'ServerConnectionRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<ServerConnectionRouteArgs>( final args = data.argsAs<ServerConnectionRouteArgs>(
orElse: () => const ServerConnectionRouteArgs(), orElse: () => const ServerConnectionRouteArgs(),
); );
return _i9.ServerConnectionScreen( return _i10.ServerConnectionScreen(
key: args.key, key: args.key,
arbiterUrl: args.arbiterUrl, arbiterUrl: args.arbiterUrl,
); );
@@ -253,7 +270,7 @@ class ServerConnectionRoute
class ServerConnectionRouteArgs { class ServerConnectionRouteArgs {
const ServerConnectionRouteArgs({this.key, this.arbiterUrl}); const ServerConnectionRouteArgs({this.key, this.arbiterUrl});
final _i13.Key? key; final _i14.Key? key;
final String? arbiterUrl; final String? arbiterUrl;
@@ -274,33 +291,33 @@ class ServerConnectionRouteArgs {
} }
/// generated route for /// generated route for
/// [_i10.ServerInfoSetupScreen] /// [_i11.ServerInfoSetupScreen]
class ServerInfoSetupRoute extends _i12.PageRouteInfo<void> { class ServerInfoSetupRoute extends _i13.PageRouteInfo<void> {
const ServerInfoSetupRoute({List<_i12.PageRouteInfo>? children}) const ServerInfoSetupRoute({List<_i13.PageRouteInfo>? children})
: super(ServerInfoSetupRoute.name, initialChildren: children); : super(ServerInfoSetupRoute.name, initialChildren: children);
static const String name = 'ServerInfoSetupRoute'; static const String name = 'ServerInfoSetupRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i10.ServerInfoSetupScreen(); return const _i11.ServerInfoSetupScreen();
}, },
); );
} }
/// generated route for /// generated route for
/// [_i11.VaultSetupScreen] /// [_i12.VaultSetupScreen]
class VaultSetupRoute extends _i12.PageRouteInfo<void> { class VaultSetupRoute extends _i13.PageRouteInfo<void> {
const VaultSetupRoute({List<_i12.PageRouteInfo>? children}) const VaultSetupRoute({List<_i13.PageRouteInfo>? children})
: super(VaultSetupRoute.name, initialChildren: children); : super(VaultSetupRoute.name, initialChildren: children);
static const String name = 'VaultSetupRoute'; static const String name = 'VaultSetupRoute';
static _i12.PageInfo page = _i12.PageInfo( static _i13.PageInfo page = _i13.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i11.VaultSetupScreen(); return const _i12.VaultSetupScreen();
}, },
); );
} }

View File

@@ -9,7 +9,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
const breakpoints = MaterialAdaptiveBreakpoints(); const breakpoints = MaterialAdaptiveBreakpoints();
final routes = [const EvmRoute(), const ClientsRoute(), const AboutRoute()]; final routes = [
const EvmRoute(),
const ClientsRoute(),
const EvmGrantsRoute(),
const AboutRoute(),
];
@RoutePage() @RoutePage()
class DashboardRouter extends StatelessWidget { class DashboardRouter extends StatelessWidget {
@@ -38,6 +43,11 @@ class DashboardRouter extends StatelessWidget {
selectedIcon: Icon(Icons.devices_other), selectedIcon: Icon(Icons.devices_other),
label: "Clients", label: "Clients",
), ),
NavigationDestination(
icon: Icon(Icons.policy_outlined),
selectedIcon: Icon(Icons.policy),
label: "Grants",
),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.info_outline), icon: Icon(Icons.info_outline),
selectedIcon: Icon(Icons.info), selectedIcon: Icon(Icons.info),

View File

@@ -1,12 +1,16 @@
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/evm/evm_grants.dart'; import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hooks_riverpod/experimental/mutation.dart'; import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
import 'package:sizer/sizer.dart'; import 'package:sizer/sizer.dart';
@RoutePage() @RoutePage()
@@ -15,11 +19,10 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final wallets = ref.watch(evmProvider).asData?.value ?? const <WalletEntry>[];
final createMutation = ref.watch(createEvmGrantMutation); final createMutation = ref.watch(createEvmGrantMutation);
final selectedWalletIndex = useState<int?>(wallets.isEmpty ? null : 0); final selectedClientId = useState<int?>(null);
final clientIdController = useTextEditingController(); final selectedWalletAccessId = useState<int?>(null);
final chainIdController = useTextEditingController(text: '1'); final chainIdController = useTextEditingController(text: '1');
final gasFeeController = useTextEditingController(); final gasFeeController = useTextEditingController();
final priorityFeeController = useTextEditingController(); final priorityFeeController = useTextEditingController();
@@ -40,14 +43,13 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
]); ]);
Future<void> submit() async { Future<void> submit() async {
final selectedWallet = selectedWalletIndex.value; final accessId = selectedWalletAccessId.value;
if (selectedWallet == null) { if (accessId == null) {
_showCreateMessage(context, 'At least one wallet is required.'); _showCreateMessage(context, 'Select a client and wallet access.');
return; return;
} }
try { try {
final clientId = int.parse(clientIdController.text.trim());
final chainId = Int64.parseInt(chainIdController.text.trim()); final chainId = Int64.parseInt(chainIdController.text.trim());
final rateLimit = _buildRateLimit( final rateLimit = _buildRateLimit(
txCountController.text, txCountController.text,
@@ -83,16 +85,25 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
_ => throw Exception('Unsupported grant type.'), _ => throw Exception('Unsupported grant type.'),
}; };
final sharedSettings = SharedSettings(
walletAccessId: accessId,
chainId: chainId,
);
if (validFrom.value != null) {
sharedSettings.validFrom = _toTimestamp(validFrom.value!);
}
if (validUntil.value != null) {
sharedSettings.validUntil = _toTimestamp(validUntil.value!);
}
final gasBytes = _optionalBigIntBytes(gasFeeController.text);
if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes;
final priorityBytes = _optionalBigIntBytes(priorityFeeController.text);
if (priorityBytes != null) sharedSettings.maxPriorityFeePerGas = priorityBytes;
if (rateLimit != null) sharedSettings.rateLimit = rateLimit;
await executeCreateEvmGrant( await executeCreateEvmGrant(
ref, ref,
clientId: clientId, sharedSettings: sharedSettings,
walletId: selectedWallet + 1,
chainId: chainId,
validFrom: validFrom.value,
validUntil: validUntil.value,
maxGasFeePerGas: _optionalBigIntBytes(gasFeeController.text),
maxPriorityFeePerGas: _optionalBigIntBytes(priorityFeeController.text),
rateLimit: rateLimit,
specific: specific, specific: specific,
); );
if (!context.mounted) { if (!context.mounted) {
@@ -113,22 +124,23 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
child: ListView( child: ListView(
padding: EdgeInsets.fromLTRB(2.4.w, 2.h, 2.4.w, 3.2.h), padding: EdgeInsets.fromLTRB(2.4.w, 2.h, 2.4.w, 3.2.h),
children: [ children: [
_CreateIntroCard(walletCount: wallets.length), const _CreateIntroCard(),
SizedBox(height: 1.8.h), SizedBox(height: 1.8.h),
_CreateSection( _CreateSection(
title: 'Shared grant options', title: 'Shared grant options',
children: [ children: [
_WalletPickerField( _ClientPickerField(
wallets: wallets, selectedClientId: selectedClientId.value,
selectedIndex: selectedWalletIndex.value, onChanged: (clientId) {
onChanged: (value) => selectedWalletIndex.value = value, selectedClientId.value = clientId;
selectedWalletAccessId.value = null;
},
), ),
_NumberInputField( _WalletAccessPickerField(
controller: clientIdController, selectedClientId: selectedClientId.value,
label: 'Client ID', selectedAccessId: selectedWalletAccessId.value,
hint: '42', onChanged: (accessId) =>
helper: selectedWalletAccessId.value = accessId,
'Manual for now. The app does not yet expose a client picker.',
), ),
_NumberInputField( _NumberInputField(
controller: chainIdController, controller: chainIdController,
@@ -204,9 +216,7 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
} }
class _CreateIntroCard extends StatelessWidget { class _CreateIntroCard extends StatelessWidget {
const _CreateIntroCard({required this.walletCount}); const _CreateIntroCard();
final int walletCount;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -222,7 +232,7 @@ class _CreateIntroCard extends StatelessWidget {
border: Border.all(color: const Color(0x1A17324A)), border: Border.all(color: const Color(0x1A17324A)),
), ),
child: Text( child: Text(
'Compose shared constraints once, then switch between Ether and token transfer rules. $walletCount wallet${walletCount == 1 ? '' : 's'} currently available.', 'Pick a client, then select one of the wallet accesses already granted to it. Compose shared constraints once, then switch between Ether and token transfer rules.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.5), style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.5),
), ),
); );
@@ -266,37 +276,98 @@ class _CreateSection extends StatelessWidget {
} }
} }
class _WalletPickerField extends StatelessWidget { class _ClientPickerField extends ConsumerWidget {
const _WalletPickerField({ const _ClientPickerField({
required this.wallets, required this.selectedClientId,
required this.selectedIndex,
required this.onChanged, required this.onChanged,
}); });
final List<WalletEntry> wallets; final int? selectedClientId;
final int? selectedIndex;
final ValueChanged<int?> onChanged; final ValueChanged<int?> onChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final clients =
ref.watch(sdkClientsProvider).asData?.value ?? const <SdkClientEntry>[];
return DropdownButtonFormField<int>( return DropdownButtonFormField<int>(
initialValue: selectedIndex, value: clients.any((c) => c.id == selectedClientId)
? selectedClientId
: null,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Wallet', labelText: 'Client',
helperText:
'Uses the current wallet order. The API still does not expose stable wallet IDs directly.',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
items: [ items: [
for (var i = 0; i < wallets.length; i++) for (final c in clients)
DropdownMenuItem( DropdownMenuItem(
value: i, value: c.id,
child: Text( child: Text(
'Wallet ${(i + 1).toString().padLeft(2, '0')} · ${_shortAddress(wallets[i].address)}', c.info.name.isEmpty ? 'Client #${c.id}' : c.info.name,
), ),
), ),
], ],
onChanged: wallets.isEmpty ? null : onChanged, onChanged: clients.isEmpty ? null : onChanged,
);
}
}
class _WalletAccessPickerField extends ConsumerWidget {
const _WalletAccessPickerField({
required this.selectedClientId,
required this.selectedAccessId,
required this.onChanged,
});
final int? selectedClientId;
final int? selectedAccessId;
final ValueChanged<int?> onChanged;
@override
Widget build(BuildContext context, WidgetRef ref) {
final allAccesses =
ref.watch(walletAccessListProvider).asData?.value ??
const <SdkClientWalletAccess>[];
final wallets =
ref.watch(evmProvider).asData?.value ?? const <WalletEntry>[];
final walletById = <int, WalletEntry>{
for (final w in wallets) w.id: w,
};
final accesses = selectedClientId == null
? const <SdkClientWalletAccess>[]
: allAccesses
.where((a) => a.access.sdkClientId == selectedClientId)
.toList();
final effectiveValue =
accesses.any((a) => a.id == selectedAccessId) ? selectedAccessId : null;
return DropdownButtonFormField<int>(
value: effectiveValue,
decoration: InputDecoration(
labelText: 'Wallet access',
helperText: selectedClientId == null
? 'Select a client first'
: accesses.isEmpty
? 'No wallet accesses for this client'
: null,
border: const OutlineInputBorder(),
),
items: [
for (final a in accesses)
DropdownMenuItem(
value: a.id,
child: Text(() {
final wallet = walletById[a.access.walletId];
return wallet != null
? _shortAddress(wallet.address)
: 'Wallet #${a.access.walletId}';
}()),
),
],
onChanged: accesses.isEmpty ? null : onChanged,
); );
} }
} }
@@ -735,6 +806,13 @@ class _VolumeLimitValue {
} }
} }
Timestamp _toTimestamp(DateTime value) {
final utc = value.toUtc();
return Timestamp()
..seconds = Int64(utc.millisecondsSinceEpoch ~/ 1000)
..nanos = (utc.microsecondsSinceEpoch % 1000000) * 1000;
}
TransactionRateLimit? _buildRateLimit(String countText, String windowText) { TransactionRateLimit? _buildRateLimit(String countText, String windowText) {
if (countText.trim().isEmpty || windowText.trim().isEmpty) { if (countText.trim().isEmpty || windowText.trim().isEmpty) {
return null; return null;

View File

@@ -0,0 +1,231 @@
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/router.gr.dart';
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
import 'package:arbiter/theme/palette.dart';
import 'package:arbiter/widgets/page_header.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {
return message.substring('Exception: '.length);
}
return message;
}
// ─── State panel ──────────────────────────────────────────────────────────────
class _StatePanel extends StatelessWidget {
const _StatePanel({
required this.icon,
required this.title,
required this.body,
this.actionLabel,
this.onAction,
this.busy = false,
});
final IconData icon;
final String title;
final String body;
final String? actionLabel;
final Future<void> Function()? onAction;
final bool busy;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: Palette.cream.withValues(alpha: 0.92),
border: Border.all(color: Palette.line),
),
child: Padding(
padding: EdgeInsets.all(2.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (busy)
SizedBox(
width: 2.8.h,
height: 2.8.h,
child: const CircularProgressIndicator(strokeWidth: 2.5),
)
else
Icon(icon, size: 34, color: Palette.coral),
SizedBox(height: 1.8.h),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 1.h),
Text(
body,
style: theme.textTheme.bodyLarge?.copyWith(
color: Palette.ink.withValues(alpha: 0.72),
height: 1.5,
),
),
if (actionLabel != null && onAction != null) ...[
SizedBox(height: 2.h),
OutlinedButton.icon(
onPressed: () => onAction!(),
icon: const Icon(Icons.refresh),
label: Text(actionLabel!),
),
],
],
),
),
);
}
}
// ─── Grant list ───────────────────────────────────────────────────────────────
class _GrantList extends StatelessWidget {
const _GrantList({required this.grants});
final List<GrantEntry> grants;
@override
Widget build(BuildContext context) {
return Column(
children: [
for (var i = 0; i < grants.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == grants.length - 1 ? 0 : 1.8.h,
),
child: GrantCard(grant: grants[i]),
),
],
);
}
}
// ─── Screen ───────────────────────────────────────────────────────────────────
@RoutePage()
class EvmGrantsScreen extends ConsumerWidget {
const EvmGrantsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Screen watches only the grant list for top-level state decisions
final grantsAsync = ref.watch(evmGrantsProvider);
Future<void> refresh() async {
ref.invalidate(walletAccessListProvider);
ref.invalidate(evmGrantsProvider);
}
void showMessage(String message) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
);
}
Future<void> safeRefresh() async {
try {
await refresh();
} catch (e) {
showMessage(_formatError(e));
}
}
final grantsState = grantsAsync.asData?.value;
final grants = grantsState?.grants;
final content = switch (grantsAsync) {
AsyncLoading() when grantsState == null => const _StatePanel(
icon: Icons.hourglass_top,
title: 'Loading grants',
body: 'Pulling grant registry from Arbiter.',
busy: true,
),
AsyncError(:final error) => _StatePanel(
icon: Icons.sync_problem,
title: 'Grant registry unavailable',
body: _formatError(error),
actionLabel: 'Retry',
onAction: safeRefresh,
),
AsyncData(:final value) when value == null => _StatePanel(
icon: Icons.portable_wifi_off,
title: 'No active server connection',
body: 'Reconnect to Arbiter to list EVM grants.',
actionLabel: 'Refresh',
onAction: safeRefresh,
),
_ when grants != null && grants.isEmpty => _StatePanel(
icon: Icons.policy_outlined,
title: 'No grants yet',
body: 'Create a grant to allow SDK clients to sign transactions.',
actionLabel: 'Create grant',
onAction: () async => context.router.push(const CreateEvmGrantRoute()),
),
_ => _GrantList(grants: grants ?? const []),
};
return Scaffold(
body: SafeArea(
child: RefreshIndicator.adaptive(
color: Palette.ink,
backgroundColor: Colors.white,
onRefresh: safeRefresh,
child: ListView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
padding: EdgeInsets.fromLTRB(2.4.w, 2.4.h, 2.4.w, 3.2.h),
children: [
PageHeader(
title: 'EVM Grants',
isBusy: grantsAsync.isLoading,
actions: [
FilledButton.icon(
onPressed: () =>
context.router.push(const CreateEvmGrantRoute()),
icon: const Icon(Icons.add_rounded),
label: const Text('Create grant'),
),
SizedBox(width: 1.w),
OutlinedButton.icon(
onPressed: safeRefresh,
style: OutlinedButton.styleFrom(
foregroundColor: Palette.ink,
side: BorderSide(color: Palette.line),
padding: EdgeInsets.symmetric(
horizontal: 1.4.w,
vertical: 1.2.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Refresh'),
),
],
),
SizedBox(height: 1.8.h),
content,
],
),
),
),
);
}
}

View File

@@ -0,0 +1,225 @@
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/theme/palette.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
String _shortAddress(List<int> bytes) {
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
}
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {
return message.substring('Exception: '.length);
}
return message;
}
class GrantCard extends ConsumerWidget {
const GrantCard({super.key, required this.grant});
final GrantEntry grant;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Enrichment lookups — each watch scopes rebuilds to this card only
final walletAccesses =
ref.watch(walletAccessListProvider).asData?.value ?? const [];
final wallets = ref.watch(evmProvider).asData?.value ?? const [];
final clients = ref.watch(sdkClientsProvider).asData?.value ?? const [];
final revoking = ref.watch(revokeEvmGrantMutation) is MutationPending;
final isEther =
grant.specific.whichGrant() == SpecificGrant_Grant.etherTransfer;
final accent = isEther ? Palette.coral : Palette.token;
final typeLabel = isEther ? 'Ether' : 'Token';
final theme = Theme.of(context);
final muted = Palette.ink.withValues(alpha: 0.62);
// Resolve wallet_access_id → wallet address + client name
final accessById = <int, SdkClientWalletAccess>{
for (final a in walletAccesses) a.id: a,
};
final walletById = <int, WalletEntry>{
for (final w in wallets) w.id: w,
};
final clientNameById = <int, String>{
for (final c in clients) c.id: c.info.name,
};
final accessId = grant.shared.walletAccessId;
final access = accessById[accessId];
final wallet = access != null ? walletById[access.access.walletId] : null;
final walletLabel = wallet != null
? _shortAddress(wallet.address)
: 'Access #$accessId';
final clientLabel = () {
if (access == null) return '';
final name = clientNameById[access.access.sdkClientId] ?? '';
return name.isEmpty ? 'Client #${access.access.sdkClientId}' : name;
}();
void showError(String message) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
);
}
Future<void> revoke() async {
try {
await executeRevokeEvmGrant(ref, grantId: grant.id);
} catch (e) {
showError(_formatError(e));
}
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: Palette.cream.withValues(alpha: 0.92),
border: Border.all(color: Palette.line),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Accent strip
Container(
width: 0.8.w,
decoration: BoxDecoration(
color: accent,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(24),
),
),
),
// Card body
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 1.6.w,
vertical: 1.4.h,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row 1: type badge · chain · spacer · revoke button
Row(
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.4.h,
),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Text(
typeLabel,
style: theme.textTheme.labelSmall?.copyWith(
color: accent,
fontWeight: FontWeight.w800,
),
),
),
SizedBox(width: 1.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.4.h,
),
decoration: BoxDecoration(
color: Palette.ink.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Chain ${grant.shared.chainId}',
style: theme.textTheme.labelSmall?.copyWith(
color: muted,
fontWeight: FontWeight.w700,
),
),
),
const Spacer(),
if (revoking)
SizedBox(
width: 1.8.h,
height: 1.8.h,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Palette.coral,
),
)
else
OutlinedButton.icon(
onPressed: revoke,
style: OutlinedButton.styleFrom(
foregroundColor: Palette.coral,
side: BorderSide(
color: Palette.coral.withValues(alpha: 0.4),
),
padding: EdgeInsets.symmetric(
horizontal: 1.w,
vertical: 0.6.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.block_rounded, size: 16),
label: const Text('Revoke'),
),
],
),
SizedBox(height: 0.8.h),
// Row 2: wallet address · client name
Row(
children: [
Text(
walletLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: Palette.ink,
fontFamily: 'monospace',
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
child: Text(
'·',
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
),
),
Expanded(
child: Text(
clientLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
),
),
],
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -5,4 +5,5 @@ class Palette {
static const coral = Color(0xFFE26254); static const coral = Color(0xFFE26254);
static const cream = Color(0xFFFFFAF4); static const cream = Color(0xFFFFFAF4);
static const line = Color(0x1A15263C); static const line = Color(0x1A15263C);
static const token = Color(0xFF5C6BC0);
} }