Admin and opertor features (#3)

all admin features listing accounts and contracts with access controll and test
ban a user by operator
tests for all features
mock data for test

Reviewed-on: #3
Co-authored-by: Noor <noormohammedb@protonmail.com>
Co-committed-by: Noor <noormohammedb@protonmail.com>
Signed-off-by: Noor <noormohammedb@protonmail.com>
This commit is contained in:
Noor 2025-05-25 21:40:45 +00:00
parent d1e85ec03e
commit 0ec1b61d8b
Signed by: noormohammedb
GPG Key ID: D83EFB8B3B967146
13 changed files with 967 additions and 73 deletions

@ -24,6 +24,7 @@ pub const OLD_BRAIN_DATA_PATH: &str = "./saved_data.yaml";
pub const ACCOUNT: &str = "account";
pub const KICK: &str = "kick";
pub const BAN: &str = "ban";
pub const VM_NODE: &str = "vm_node";
pub const ACTIVE_VM: &str = "active_vm";

@ -404,6 +404,12 @@ impl ActiveAppWithNode {
None => Ok(vec![]),
}
}
pub async fn list_all(db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
let mut query_response = db.query(format!("SELECT * FROM {ACTIVE_APP} FETCH out;")).await?;
let active_apps: Vec<Self> = query_response.take(0)?;
Ok(active_apps)
}
}
#[derive(Debug, Serialize)]

@ -1,5 +1,5 @@
use super::Error;
use crate::constants::{ACCOUNT, KICK, MIN_ESCROW, TOKEN_DECIMAL};
use crate::constants::{ACCOUNT, BAN, KICK, MIN_ESCROW, TOKEN_DECIMAL, VM_NODE};
use crate::db::prelude::*;
use crate::old_brain;
use serde::{Deserialize, Serialize};
@ -75,6 +75,43 @@ impl Account {
op_account.save(db).await?;
Ok(())
}
pub async fn slash_account(
db: &Surreal<Client>,
account: &str,
slash_amount: u64,
) -> Result<(), Error> {
let tx_query = "
BEGIN TRANSACTION;
LET $account = $account_input;
UPDATE $account SET escrow -= $slash_amount;
IF $account.escrow < 0 {{
THROW 'Insufficient escrow.'
}};
COMMIT TRANSACTION;";
let mut query_resp = db
.query(tx_query)
.bind(("account_input", RecordId::from((ACCOUNT, account))))
.bind(("slash_amount", slash_amount.saturating_mul(TOKEN_DECIMAL)))
.await?;
log::trace!("query_resp: {query_resp:?}");
let query_error = query_resp.take_errors();
if !query_error.is_empty() {
log::error!("slash_account query error: {query_error:?}");
return Err(Error::FailedToSlashOperator(account.to_string()));
}
Ok(())
}
pub async fn list_accounts(db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
let accounts: Vec<Account> = db.select(ACCOUNT).await?;
Ok(accounts)
}
}
impl Account {
@ -86,20 +123,27 @@ impl Account {
let mut query_response = db
.query(format!(
"(select operator->ban[0] as ban
from vm_node:{node}
where operator->ban->account contains account:{user}
from $vm_node_input
where operator->ban->account contains $user_account_input
).ban;"
))
.bind(("vm_node_input", RecordId::from((VM_NODE, node))))
.bind(("user_account_input", RecordId::from((ACCOUNT, user))))
.query(format!(
"(select operator->ban[0] as ban
from app_node:{node}
where operator->ban->account contains account:{user}
from $app_node_input
where operator->ban->account contains $user_account_input
).ban;"
))
.bind(("app_node_input", RecordId::from((APP_NODE, node))))
.bind(("user_account_input", RecordId::from((ACCOUNT, user))))
.await?;
let vm_node_ban: Option<Self> = query_response.take(0)?;
let app_node_ban: Option<Self> = query_response.take(1)?;
let vm_node_ban: Option<Ban> = query_response.take(0).unwrap();
let app_node_ban: Option<Ban> = query_response.take(1).unwrap();
log::trace!("vm_node_ban: {vm_node_ban:?}");
log::trace!("app_node_ban: {app_node_ban:?}");
Ok(vm_node_ban.is_some() || app_node_ban.is_some())
}
@ -128,12 +172,49 @@ impl From<&old_brain::BrainData> for Vec<Account> {
#[derive(Debug, Serialize, Deserialize)]
pub struct Ban {
id: RecordId,
#[serde(rename = "in")]
from_account: RecordId,
pub from_account: RecordId,
#[serde(rename = "out")]
to_account: RecordId,
created_at: Datetime,
pub to_account: RecordId,
pub created_at: Datetime,
}
impl Ban {
pub async fn get_record(
db: &Surreal<Client>,
op_wallet: &str,
user_wallet: &str,
) -> Result<Option<Self>, Error> {
let query =
format!("SELECT * FROM {BAN} WHERE in = $operator_input AND out = $user_input;");
let mut response = db
.query(query)
.bind(("operator_input", RecordId::from((ACCOUNT, op_wallet))))
.bind(("user_input", RecordId::from((ACCOUNT, user_wallet))))
.await?;
let ban_record: Option<Self> = response.take(0)?;
Ok(ban_record)
}
pub async fn create(
db: &Surreal<Client>,
op_wallet: &str,
user_wallet: &str,
) -> Result<(), Error> {
if Self::get_record(db, op_wallet, user_wallet).await?.is_some() {
log::error!("User {user_wallet} is already banned by {op_wallet}");
return Err(Error::AlreadyBanned(op_wallet.to_string()));
}
let _: Vec<Self> = db
.insert(BAN)
.relation(Self {
from_account: RecordId::from((ACCOUNT, op_wallet)),
to_account: RecordId::from((ACCOUNT, user_wallet)),
created_at: Default::default(),
})
.await?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]

@ -45,6 +45,10 @@ pub enum Error {
FailedToDeleteContract(String),
#[error("Failed to kick contract {0}")]
FailedKickContract(String),
#[error("Already banned {0}")]
AlreadyBanned(String),
#[error("Failed to slash operator {0}")]
FailedToSlashOperator(String),
}
pub mod prelude {

@ -814,6 +814,12 @@ impl ActiveVmWithNode {
pub fn price_per_minute(&self) -> u64 {
self.total_units() * self.price_per_unit
}
pub async fn list_all(db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
let mut query_response = db.query(format!("SELECT * FROM {ACTIVE_VM} FETCH out;")).await?;
let active_vms: Vec<Self> = query_response.take(0)?;
Ok(active_vms)
}
}
// TODO: delete all of these From implementation after migration 0 gets executed

@ -152,11 +152,11 @@ impl BrainGeneralCli for GeneralCliServer {
}
}
async fn ban_user(&self, _req: Request<BanUserReq>) -> Result<Response<Empty>, Status> {
todo!();
// let req = check_sig_from_req(req)?;
// self.data.ban_user(&req.operator_wallet, &req.user_wallet);
// Ok(Response::new(Empty {}))
async fn ban_user(&self, req: Request<BanUserReq>) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?;
log::info!("Banning user: {}, by: {}", req.user_wallet, req.operator_wallet);
db::Ban::create(&self.db, &req.operator_wallet, &req.user_wallet).await?;
Ok(Response::new(Empty {}))
}
// admin commands
@ -164,69 +164,66 @@ impl BrainGeneralCli for GeneralCliServer {
async fn airdrop(&self, req: Request<AirdropReq>) -> Result<Response<Empty>, Status> {
check_admin_key(&req)?;
let req = check_sig_from_req(req)?;
log::info!("Airdropping {} tokens to {}", req.tokens, req.pubkey);
db::Account::airdrop(&self.db, &req.pubkey, req.tokens).await?;
Ok(Response::new(Empty {}))
}
async fn slash(&self, _req: Request<SlashReq>) -> Result<Response<Empty>, Status> {
todo!();
// check_admin_key(&req)?;
// let req = check_sig_from_req(req)?;
// self.data.slash_account(&req.pubkey, req.tokens);
// Ok(Response::new(Empty {}))
async fn slash(&self, req: Request<SlashReq>) -> Result<Response<Empty>, Status> {
check_admin_key(&req)?;
let req = check_sig_from_req(req)?;
db::Account::slash_account(&self.db, &req.pubkey, req.tokens).await?;
Ok(Response::new(Empty {}))
}
async fn list_accounts(
&self,
_req: Request<Empty>,
req: Request<Empty>,
) -> Result<Response<Self::ListAccountsStream>, Status> {
todo!();
// check_admin_key(&req)?;
// let _ = check_sig_from_req(req)?;
// let accounts = self.data.list_accounts();
// let (tx, rx) = mpsc::channel(6);
// tokio::spawn(async move {
// for account in accounts {
// let _ = tx.send(Ok(account.into())).await;
// }
// });
// let output_stream = ReceiverStream::new(rx);
// Ok(Response::new(Box::pin(output_stream) as Self::ListAccountsStream))
check_admin_key(&req)?;
check_sig_from_req(req)?;
let accounts = db::Account::list_accounts(&self.db).await?;
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for account in accounts {
let _ = tx.send(Ok(account.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(Box::pin(output_stream) as Self::ListAccountsStream))
}
async fn list_all_vm_contracts(
&self,
_req: Request<Empty>,
req: Request<Empty>,
) -> Result<Response<Self::ListAllVmContractsStream>, Status> {
todo!();
// check_admin_key(&req)?;
// let _ = check_sig_from_req(req)?;
// let contracts = self.data.list_all_contracts();
// let (tx, rx) = mpsc::channel(6);
// tokio::spawn(async move {
// for contract in contracts {
// let _ = tx.send(Ok(contract.into())).await;
// }
// });
// let output_stream = ReceiverStream::new(rx);
// Ok(Response::new(Box::pin(output_stream) as Self::ListAllVmContractsStream))
check_admin_key(&req)?;
check_sig_from_req(req)?;
let contracts = db::ActiveVmWithNode::list_all(&self.db).await?;
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for contract in contracts {
let _ = tx.send(Ok(contract.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(Box::pin(output_stream) as Self::ListAllVmContractsStream))
}
async fn list_all_app_contracts(
&self,
_req: tonic::Request<Empty>,
req: tonic::Request<Empty>,
) -> Result<tonic::Response<Self::ListAllAppContractsStream>, Status> {
todo!();
// check_admin_key(&req)?;
// let _ = check_sig_from_req(req)?;
// let contracts = self.data.list_all_app_contracts();
// let (tx, rx) = mpsc::channel(6);
// tokio::spawn(async move {
// for contract in contracts {
// let _ = tx.send(Ok(contract.into())).await;
// }
// });
// let output_stream = ReceiverStream::new(rx);
// Ok(Response::new(Box::pin(output_stream)))
check_admin_key(&req)?;
check_sig_from_req(req)?;
let contracts = db::ActiveAppWithNode::list_all(&self.db).await?;
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for contract in contracts {
let _ = tx.send(Ok(contract.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(Box::pin(output_stream)))
}
}

@ -2,7 +2,7 @@ use crate::constants::{ACCOUNT, APP_NODE, ID_ALPHABET, NEW_APP_REQ, NEW_VM_REQ,
use crate::db::prelude as db;
use detee_shared::app_proto::AppNodeListResp;
use detee_shared::common_proto::MappedPort;
use detee_shared::general_proto::{AccountBalance, ListOperatorsResp};
use detee_shared::general_proto::{Account, AccountBalance, ListOperatorsResp};
use detee_shared::{app_proto::*, vm_proto::*};
use nanoid::nanoid;
@ -14,6 +14,16 @@ impl From<db::Account> for AccountBalance {
}
}
impl From<db::Account> for Account {
fn from(account: db::Account) -> Self {
Account {
pubkey: account.id.to_string(),
balance: account.balance,
tmp_locked: account.tmp_locked,
}
}
}
impl From<NewVmReq> for db::NewVmReq {
fn from(new_vm_req: NewVmReq) -> Self {
Self {

@ -129,14 +129,14 @@ DEFINE FIELD vcpus ON TABLE deleted_app TYPE int;
DEFINE FIELD memory_mb ON TABLE deleted_app TYPE int;
DEFINE FIELD disk_size_gb ON TABLE deleted_app TYPE int;
DEFINE FIELD created_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime DEFAULT time::now();;
DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime DEFAULT time::now();
DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int;
DEFINE FIELD mr_enclave ON TABLE deleted_app TYPE string;
DEFINE FIELD package_url ON TABLE deleted_app TYPE string;
DEFINE FIELD hratls_pubkey ON TABLE deleted_app TYPE string;
DEFINE TABLE ban TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE ban TYPE datetime;
DEFINE FIELD created_at ON TABLE ban TYPE datetime DEFAULT time::now();;
DEFINE TABLE kick TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE kick TYPE datetime;

@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use detee_shared::general_proto::brain_general_cli_server::BrainGeneralCliServer;
use detee_shared::vm_proto::brain_vm_cli_server::BrainVmCliServer;
use detee_shared::vm_proto::brain_vm_daemon_server::BrainVmDaemonServer;
@ -31,14 +31,15 @@ pub async fn prepare_test_db() -> Result<Surreal<Client>> {
let db = surreal_brain::db::db_connection(&db_url, &db_user, &db_pass, db_ns, db_name).await?;
DB_STATE
.get_or_init(|| async {
let old_brain_data = surreal_brain::old_brain::BrainData::load_from_disk()
.map_err(|e| anyhow!(e.to_string()))?;
let raw_mock_data = std::fs::read_to_string("tests/mock_data.yaml")?;
let mock_data: surreal_brain::old_brain::BrainData =
serde_yaml::from_str(&raw_mock_data)?;
db.query(format!("REMOVE DATABASE {db_name}")).await?;
for schema in DB_SCHEMA_FILES.map(std::fs::read_to_string) {
db.query(schema?).await?;
}
surreal_brain::db::migration0(&db, &old_brain_data).await?;
surreal_brain::db::migration0(&db, &mock_data).await?;
Ok::<(), anyhow::Error>(())
})
.await;

@ -32,6 +32,17 @@ impl Key {
Key { sg_key: sk, pubkey }
}
pub fn from(private_key: &str) -> Self {
let signing_key: SigningKey = bs58::decode(private_key)
.into_vec()
.expect("Failed to decode private key")
.as_slice()
.try_into()
.expect("Failed to convert to SigningKey");
let pubkey = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
Key { sg_key: signing_key, pubkey }
}
pub fn sign_request<T: std::fmt::Debug>(&self, req: T) -> Result<Request<T>> {
let pubkey = self.pubkey.clone();
let timestamp = chrono::Utc::now().to_rfc3339();

@ -1,10 +1,12 @@
use super::test_utils::{admin_keys, Key};
use anyhow::{anyhow, Result};
use detee_shared::app_proto;
use detee_shared::common_proto::Empty;
use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient;
use detee_shared::general_proto::{AirdropReq, ReportNodeReq};
use detee_shared::general_proto::{Account, AirdropReq, RegOperatorReq, ReportNodeReq};
use detee_shared::vm_proto;
use detee_shared::vm_proto::brain_vm_cli_client::BrainVmCliClient;
use futures::StreamExt;
use surreal_brain::constants::{ACTIVE_VM, NEW_VM_REQ};
use surreal_brain::db::prelude as db;
use surrealdb::engine::remote::ws::Client;
@ -77,3 +79,85 @@ pub async fn report_node(
Ok(client_gen_cli.report_node(key.sign_request(report_req)?).await?)
}
pub async fn register_operator(brain_channel: &Channel, key: &Key, escrow: u64) -> Result<()> {
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let reg_req =
RegOperatorReq { pubkey: key.pubkey.clone(), escrow, email: "foo@bar.com".to_string() };
cli_client.register_operator(key.sign_request(reg_req.clone()).unwrap()).await?;
Ok(())
}
pub async fn list_accounts(brain_channel: &Channel, admin_key: &Key) -> Result<Vec<Account>> {
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let mut stream =
cli_client.list_accounts(admin_key.sign_request(Empty {}).unwrap()).await?.into_inner();
let mut accounts = Vec::new();
while let Some(stream_data) = stream.next().await {
match stream_data {
Ok(account) => {
accounts.push(account);
}
Err(e) => {
panic!("Error while listing accounts: {e:?}");
}
}
}
Ok(accounts)
}
pub async fn list_all_vm_contracts(
brain_channel: &Channel,
admin_key: &Key,
) -> Result<Vec<vm_proto::VmContract>> {
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let mut stream = cli_client
.list_all_vm_contracts(admin_key.sign_request(Empty {}).unwrap())
.await?
.into_inner();
let mut vm_contracts = Vec::new();
while let Some(stream_data) = stream.next().await {
match stream_data {
Ok(vm_contract) => {
vm_contracts.push(vm_contract);
}
Err(e) => {
panic!("Error while listing vm_contracts: {e:?}");
}
}
}
Ok(vm_contracts)
}
pub async fn list_all_app_contracts(
brain_channel: &Channel,
admin_key: &Key,
) -> Result<Vec<app_proto::AppContract>> {
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let mut stream = cli_client
.list_all_app_contracts(admin_key.sign_request(Empty {}).unwrap())
.await?
.into_inner();
let mut app_contracts = Vec::new();
while let Some(stream_data) = stream.next().await {
match stream_data {
Ok(app_contract) => {
app_contracts.push(app_contract);
}
Err(e) => {
panic!("Error while listing app_contracts: {e:?}");
}
}
}
Ok(app_contracts)
}

@ -2,14 +2,18 @@ use common::prepare_test_env::{
prepare_test_db, run_service_for_stream, run_service_in_background,
};
use common::test_utils::{admin_keys, Key};
use common::vm_cli_utils::{airdrop, create_new_vm, report_node};
use common::vm_cli_utils::{
airdrop, create_new_vm, list_accounts, list_all_app_contracts, list_all_vm_contracts,
register_operator, report_node,
};
use common::vm_daemon_utils::{mock_vm_daemon, register_vm_node};
use detee_shared::common_proto::{Empty, Pubkey};
use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient;
use detee_shared::general_proto::AirdropReq;
use detee_shared::general_proto::{AirdropReq, BanUserReq, SlashReq};
use detee_shared::vm_proto::brain_vm_daemon_client::BrainVmDaemonClient;
use futures::StreamExt;
use surreal_brain::constants::{TOKEN_DECIMAL, VM_NODE};
use surreal_brain::constants::{ACCOUNT, ACTIVE_APP, ACTIVE_VM, BAN, TOKEN_DECIMAL, VM_NODE};
use surreal_brain::db::prelude as db;
use surreal_brain::db::vm::VmNodeWithReports;
mod common;
@ -209,6 +213,37 @@ async fn test_inspect_operator() {
assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey);
}
#[tokio::test]
async fn test_register_operator() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let key = Key::new();
let min_escrew_error = register_operator(&brain_channel, &key, 10).await.err().unwrap();
assert!(min_escrew_error.to_string().contains("Minimum escrow amount is 5000"));
let no_balance = register_operator(&brain_channel, &key, 5000).await.err().unwrap();
assert!(no_balance.to_string().contains("Insufficient funds, deposit more tokens"));
airdrop(&brain_channel, &key.pubkey, 1000).await.unwrap();
let no_balance = register_operator(&brain_channel, &key, 5000).await.err().unwrap();
assert!(no_balance.to_string().contains("Insufficient funds, deposit more tokens"));
airdrop(&brain_channel, &key.pubkey, 7000).await.unwrap();
register_operator(&brain_channel, &key, 6000).await.unwrap();
let operator_account: Option<db::Account> =
db_conn.select((ACCOUNT, key.pubkey)).await.unwrap();
assert!(operator_account.is_some());
let account = operator_account.unwrap();
assert_eq!(account.escrow, 6000 * TOKEN_DECIMAL);
assert_eq!(account.balance, 2000 * TOKEN_DECIMAL);
assert_eq!(account.tmp_locked, 0);
}
#[tokio::test]
async fn test_kick_contract() {
// TODO: implement seed data to test
@ -221,14 +256,16 @@ async fn test_kick_contract() {
// 6. refund amount calculation
// 7. refund of multiple contract kick in a day for same user
/*
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.filter_module("tungstenite", log::LevelFilter::Debug)
.filter_module("tokio_tungstenite", log::LevelFilter::Debug)
.init();
*/
let db_conn = prepare_test_db().await.unwrap();
let contract_uuid = "e3d01f252b2a410b80e312f44e474334";
let contract_uuid = "5af49a714c64a82ef50e574b023b2a0ef0405ed";
let operator_wallet = "7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB";
let reason = "'; THROW 'Injected error'; --"; // sql injection query
@ -244,3 +281,165 @@ async fn test_kick_contract() {
}
}
}
#[tokio::test]
async fn test_ban_user() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let op_key = Key::from("6f3WcQuv8sPrxAsSh24TdXkauwVd8vFUyCCfbNqxszvR"); // "4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ"
let user_key = Key::new();
let node_pubkey = "7fujZQeTme52RdXTLmQST5jBgAbvzic5iERtH5EWoYjk";
/*
base58 = "6f3WcQuv8sPrxAsSh24TdXkauwVd8vFUyCCfbNqxszvR"
pubkey = "4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ"
base58 = "7fujZQeTme52RdXTLmQST5jBgAbvzic5iERtH5EWoYjk"
pubkey = "G2PetsFM8peG77rf9UTfVD7HEHNPmdjV3cebBpKuR3QT"
*/
let user_wallet = user_key.pubkey.clone();
let operator_wallet = op_key.pubkey.clone();
let _ = cli_client
.ban_user(
op_key
.sign_request(BanUserReq {
operator_wallet: operator_wallet.clone(),
user_wallet: user_wallet.clone(),
})
.unwrap(),
)
.await
.unwrap();
let ban_reconrd: Option<db::Ban> = db_conn
.query(format!("SELECT * FROM {BAN} WHERE in = account:{operator_wallet} AND out = account:{user_wallet};"))
.await.unwrap().take(0).unwrap();
assert!(ban_reconrd.is_some());
let already_banned = cli_client
.ban_user(
op_key
.sign_request(BanUserReq {
operator_wallet: operator_wallet.clone(),
user_wallet: user_wallet.clone(),
})
.unwrap(),
)
.await
.err()
.unwrap();
assert!(already_banned.message().contains("Already banned"));
airdrop(&brain_channel, &user_wallet, 10).await.unwrap();
let operator_banned_you =
create_new_vm(&db_conn, &user_key, node_pubkey, &brain_channel).await.err().unwrap();
assert!(operator_banned_you.to_string().contains("This operator banned you"));
}
#[tokio::test]
async fn test_slash_operator() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone());
let op_key = Key::new();
let admin = admin_keys()[0].clone();
let escrew = 5500;
let slash_amt = 2500;
airdrop(&brain_channel, &op_key.pubkey, 10000).await.unwrap();
register_operator(&brain_channel, &op_key, escrew).await.unwrap();
let raw_slash_req = SlashReq { pubkey: op_key.pubkey.clone(), tokens: 2500 };
let other_key = Key::new();
let admin_error = cli_client
.slash(other_key.sign_request(raw_slash_req.clone()).unwrap())
.await
.err()
.unwrap();
assert!(admin_error.message().contains("This operation is reserved to admin accounts"));
let admin_error =
cli_client.slash(op_key.sign_request(raw_slash_req.clone()).unwrap()).await.err().unwrap();
assert!(admin_error.message().contains("This operation is reserved to admin accounts"));
cli_client.slash(admin.sign_request(raw_slash_req.clone()).unwrap()).await.unwrap();
let operator_account: Option<db::Account> =
db_conn.select((ACCOUNT, op_key.pubkey)).await.unwrap();
assert!(operator_account.is_some());
let account = operator_account.unwrap();
assert_eq!(account.escrow, (escrew - slash_amt) * TOKEN_DECIMAL);
}
#[tokio::test]
async fn test_admin_list_account() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let admin_key = admin_keys()[0].clone();
let unauthenticated = list_accounts(&brain_channel, &Key::new()).await.err().unwrap();
assert!(unauthenticated.to_string().contains("reserved to admin accounts"));
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; // sync time for other db opeartion
let acc_in_db = db_conn.select::<Vec<db::Account>>(ACCOUNT).await.unwrap();
let accounts = list_accounts(&brain_channel, &admin_key).await.unwrap();
assert_eq!(accounts.len(), acc_in_db.len());
airdrop(&brain_channel, &Key::new().pubkey, 10).await.unwrap();
let acc_in_db = db_conn.select::<Vec<db::Account>>(ACCOUNT).await.unwrap();
let accounts = list_accounts(&brain_channel, &admin_key).await.unwrap();
assert_eq!(accounts.len(), acc_in_db.len());
}
#[tokio::test]
async fn test_admin_list_all_vm_contracts() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let admin_key = admin_keys()[0].clone();
let unauthenticated = list_all_vm_contracts(&brain_channel, &Key::new()).await.err().unwrap();
assert!(unauthenticated.to_string().contains("reserved to admin accounts"));
let vm_in_db = db_conn.select::<Vec<db::ActiveVm>>(ACTIVE_VM).await.unwrap();
let vm_contracts = list_all_vm_contracts(&brain_channel, &admin_key).await.unwrap();
assert_eq!(vm_contracts.len(), vm_in_db.len());
// TODO: mock vm daemon and deploy a new vm, then list again
}
#[tokio::test]
async fn test_admin_list_all_app_contracts() {
let db_conn = prepare_test_db().await.unwrap();
let brain_channel = run_service_for_stream().await.unwrap();
let admin_key = admin_keys()[0].clone();
let unauthenticated = list_all_app_contracts(&brain_channel, &Key::new()).await.err().unwrap();
assert!(unauthenticated.to_string().contains("reserved to admin accounts"));
let app_contracts = list_all_app_contracts(&brain_channel, &admin_key).await.unwrap();
assert_eq!(app_contracts.len(), 1);
let app_in_db = db_conn.select::<Vec<db::ActiveApp>>(ACTIVE_APP).await.unwrap();
let app_contracts = list_all_app_contracts(&brain_channel, &admin_key).await.unwrap();
assert_eq!(app_contracts.len(), app_in_db.len());
// TODO: mock app daemon and deploy a new app, then list again
}

494
tests/mock_data.yaml Normal file

@ -0,0 +1,494 @@
accounts:
DXXkYSnhP3ijsHYxkedcuMomEyc122WaAbkDX7SaGuUS:
balance: 20293420000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL:
balance: 25949200000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
Cnkvn3WuHYfTzh1YK1TAv2VD25sNvstJNnQtxjcdQSL7:
balance: 4794480000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS:
balance: 4672207240000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
E3bgXsWvgichXeC6AqULJCZDp7FbEdTxBD67UaYVWf9y:
balance: 21121600000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
HQyGWpiteHbxjszngZvmiX7ZFZAmF6nFjEraBa1M6bbM:
balance: 979410300000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc:
balance: 976000000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
45Pyv9hRfub43NyRrYv95MhZs1Wrm8sj3RhBvA3F1Bvr:
balance: 1670441080000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
49JBVzmgsQbUURHzAWax2gxo6jmukqbEQzP97YeeNQyu:
balance: 1076960680000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB:
balance: 3271040000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK:
balance: 554454460000
tmp_locked: 547200000
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
3BNggj8ZTsoSjfAGdPfmcU2Gobm2qcTEBg9iHXEUPe1t:
balance: 9978460000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
B981xPHmHthfKr15J9uJ64qd9zt2KsdiEuDRR7UUCGWi:
balance: 99980200000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
CLYyE6id5876DW69LHDynuH6TjJPvWRBTQC5XDZ6jfT1:
balance: 25000000000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
db5ZB6uDbF1mUUgeggBZ9XKbi3mUfX6WHkBpbwUHJpB:
balance: 25000000000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
fY3NNjvFTeR1FBh5nXV3ujX7zZqrm3eBUWGEiG75TK1:
balance: 1000000000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
FBMWVqME3t1i4R6zWyDQGUuiTeruZ1TxLhTmhaEcFypZ:
balance: 181560160000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX:
balance: 74660380000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ:
balance: 25949200000
tmp_locked: 0
kicked_for: []
last_kick: 1970-01-01T00:00:00Z
banned_by: []
operators:
BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS:
escrow: 5096692000000
email: first_on_detee@proton.me
banned_users: []
vm_nodes:
- HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv
- 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4
- Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu
- 4QbUXDM915RUFnHm3NiysLXFLk1WRGZvABwLNzx4tTEW
- DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb
app_nodes: []
x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK:
escrow: 5499700480000
email: gheo@detee.ltd
banned_users: []
vm_nodes:
- 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f
- 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9
app_nodes: []
7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB:
escrow: 888888888899999
email: ""
banned_users: []
vm_nodes: []
app_nodes:
- BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg
4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ:
escrow: 5499700480000
email: "test_mock@operator"
banned_users: []
vm_nodes:
- 7fujZQeTme52RdXTLmQST5jBgAbvzic5iERtH5EWoYjk
app_nodes: []
vm_nodes:
- public_key: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9
operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK
country: GB
region: England
city: London
ip: 173.234.17.2
avail_mem_mb: 26000
avail_vcpus: 28
avail_storage_gbs: 680
avail_ipv4: 2
avail_ipv6: 65516
avail_ports: 19999
max_ports_per_vm: 5
price: 20000
reports: {}
offline_minutes: 0
- public_key: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu
operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS
country: FR
region: Île-de-France
city: Paris
ip: 156.146.63.215
avail_mem_mb: 123000
avail_vcpus: 46
avail_storage_gbs: 440
avail_ipv4: 2
avail_ipv6: 0
avail_ports: 20000
max_ports_per_vm: 5
price: 20000
reports: {}
offline_minutes: 0
- public_key: 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f
operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK
country: CA
region: Quebec
city: Montréal
ip: 184.107.169.199
avail_mem_mb: 30000
avail_vcpus: 31
avail_storage_gbs: 700
avail_ipv4: 0
avail_ipv6: 0
avail_ports: 20000
max_ports_per_vm: 5
price: 18000
reports: {}
offline_minutes: 0
- public_key: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb
operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS
country: CA
region: British Columbia
city: Vancouver
ip: 149.22.95.1
avail_mem_mb: 109000
avail_vcpus: 45
avail_storage_gbs: 400
avail_ipv4: 25
avail_ipv6: 0
avail_ports: 20000
max_ports_per_vm: 5
price: 20000
reports: {}
offline_minutes: 0
- public_key: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4
operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS
country: US
region: California
city: San Jose
ip: 149.36.48.99
avail_mem_mb: 120000
avail_vcpus: 41
avail_storage_gbs: 390
avail_ipv4: 23
avail_ipv6: 0
avail_ports: 19999
max_ports_per_vm: 5
price: 20000
reports: {}
offline_minutes: 0
- public_key: HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv
operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS
country: CA
region: British Columbia
city: Vancouver
ip: 149.22.95.28
avail_mem_mb: 125000
avail_vcpus: 46
avail_storage_gbs: 400
avail_ipv4: 26
avail_ipv6: 0
avail_ports: 20000
max_ports_per_vm: 5
price: 20000
reports: {}
offline_minutes: 0
- public_key: 7fujZQeTme52RdXTLmQST5jBgAbvzic5iERtH5EWoYjk
operator_wallet: 4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ
country: GB
region: England
city: London
ip: 193.234.17.2
avail_mem_mb: 28000
avail_vcpus: 24
avail_storage_gbs: 1680
avail_ipv4: 1
avail_ipv6: 0
avail_ports: 19999
max_ports_per_vm: 10
price: 24000
reports: {}
offline_minutes: 0
vm_contracts:
- uuid: 958165e3-dea8-407d-8c42-dd17002ef79c
hostname: detee-landing-fr
admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL
node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu
exposed_ports: []
public_ipv4: 156.146.63.216
public_ipv6: ""
disk_size_gb: 10
vcpus: 2
memory_mb: 3000
kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151
dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b
created_at: 2025-02-28T23:19:41.769423466Z
updated_at: 2025-04-12T12:11:58.516768949Z
price_per_unit: 20000
locked_nano: 14875500000
collected_at: 2025-04-20T00:34:15.461165181Z
- uuid: e807a2fd-cf90-4a14-bc3a-89ce6dc59033
hostname: detee-landing-gb
admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL
node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9
exposed_ports: []
public_ipv4: 173.234.136.154
public_ipv6: ""
disk_size_gb: 10
vcpus: 2
memory_mb: 3000
kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151
dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b
created_at: 2025-03-06T19:51:39.595163157Z
updated_at: 2025-03-06T19:51:39.595163842Z
price_per_unit: 20000
locked_nano: 14875500000
collected_at: 2025-04-20T00:34:15.461181545Z
- uuid: 23094406-2307-4332-a642-acee718d0186
hostname: heroic-door
admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX
node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9
exposed_ports:
- 38288
public_ipv4: ""
public_ipv6: ""
disk_size_gb: 10
vcpus: 1
memory_mb: 1000
kernel_sha: 14e225e4aaf84cc2e0b5f64206121186ddebc4b378b886da3b2f7515dfd41692
dtrfs_sha: 03ce24dbbe917fdd4f6347e61036805ddbdded5044c272bab188ef9333093bee
created_at: 2025-03-12T16:28:24.749161605Z
updated_at: 2025-03-12T16:28:24.749162477Z
price_per_unit: 20000
locked_nano: 14134140000
collected_at: 2025-04-20T00:34:15.461191231Z
- uuid: 1f49a71c-f68c-4c64-a82e-f50e0ba0b574
hostname: astromech-wrench
admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL
node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb
exposed_ports: []
public_ipv4: 149.22.95.2
public_ipv6: ""
disk_size_gb: 10
vcpus: 2
memory_mb: 3000
kernel_sha: 3a68709138bed09c16671949cf1f03acee95a08381ba84fc70fb586001fa6767
dtrfs_sha: 0bb93443f65c9f4379ed469f94794f5c1bf14d8905b0b2c56a125df4a9ebe83e
created_at: 2025-03-20T14:40:25.557753393Z
updated_at: 2025-03-20T14:40:25.557754242Z
price_per_unit: 20000
locked_nano: 11865620000
collected_at: 2025-04-20T00:34:15.461201690Z
- uuid: 16577f1c-9867-4a17-80a8-6cf0490f1270
hostname: sofenty
admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL
node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu
exposed_ports: []
public_ipv4: 156.146.63.217
public_ipv6: ""
disk_size_gb: 10
vcpus: 2
memory_mb: 3000
kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542
dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45
created_at: 2025-04-07T22:57:57.646151746Z
updated_at: 2025-04-07T22:57:57.646152630Z
price_per_unit: 20000
locked_nano: 11867500000
collected_at: 2025-04-20T00:34:15.461211040Z
- uuid: 4b6e25ca-87ac-478b-8f16-aa8f5c44c704
hostname: cloaked-mailbox
admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX
node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb
exposed_ports: []
public_ipv4: 149.22.95.2
public_ipv6: ""
disk_size_gb: 30
vcpus: 1
memory_mb: 1000
kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542
dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45
created_at: 2025-04-12T13:44:56.957037550Z
updated_at: 2025-04-12T13:44:56.957038546Z
price_per_unit: 20000
locked_nano: 11177760000
collected_at: 2025-04-20T00:34:15.461219779Z
- uuid: eb1a13ed-d782-4b71-8860-73540129cb7d
hostname: twenty
admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL
node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4
exposed_ports: []
public_ipv4: 149.36.48.100
public_ipv6: ""
disk_size_gb: 10
vcpus: 4
memory_mb: 4000
kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542
dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45
created_at: 2025-04-15T00:46:35.622165457Z
updated_at: 2025-04-15T00:46:35.622166372Z
price_per_unit: 20000
locked_nano: 15570720000
collected_at: 2025-04-20T00:34:15.461230948Z
- uuid: 1bf36309-3774-4825-b023-b2a0ef0405ed
hostname: shadowy-hobo
admin_pubkey: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK
node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4
exposed_ports:
- 46393
public_ipv4: ""
public_ipv6: ""
disk_size_gb: 10
vcpus: 1
memory_mb: 1000
kernel_sha: e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919
dtrfs_sha: d207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990
created_at: 2025-04-16T20:37:57.176592933Z
updated_at: 2025-04-16T20:37:57.176594069Z
price_per_unit: 20000
locked_nano: 12730960000
collected_at: 2025-04-20T00:34:15.461240342Z
- uuid: 5af49a71-4c64-a82e-f50e574-b023-b2a0ef0405ed
hostname: hallow-hobo
admin_pubkey: 4qFJJJdRrSB9hCn8rrvYTXHLJg371ab36PJmZ4uxHjGQ
node_pubkey: 7fujZQeTme52RdXTLmQST5jBgAbvzic5iERtH5EWoYjk
exposed_ports:
- 46393
public_ipv4: ""
public_ipv6: ""
disk_size_gb: 10
vcpus: 1
memory_mb: 1000
kernel_sha: e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919
dtrfs_sha: d207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990
created_at: 2025-04-16T20:37:57.176592933Z
updated_at: 2025-04-16T20:37:57.176594069Z
price_per_unit: 20000
locked_nano: 12730960000
collected_at: 2025-04-20T00:34:15.461240342Z
app_nodes:
- node_pubkey: BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg
operator_wallet: 7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB
country: DE
region: Hesse
city: Frankfurt am Main
ip: 212.95.45.139
avail_mem_mb: 16000
avail_vcpus: 16
avail_storage_mb: 200000
avail_no_of_port: 20000
max_ports_per_app: 9
price: 20000
offline_minutes: 0
app_contracts:
- uuid: e3d01f25-2b2a-410b-80e3-12f44e474334
package_url: https://registry.detee.ltd/sgx/packages/base_package_2025-04-17_11-01-08.tar.gz
admin_pubkey: H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc
node_pubkey: BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg
mapped_ports:
- - 27158
- 34500
- - 28667
- 8080
host_ipv4: 212.95.45.139
disk_size_mb: 1000
vcpus: 1
memory_mb: 1000
created_at: 2025-04-21T11:27:28.833236909Z
updated_at: 2025-04-21T11:27:28.833237729Z
price_per_unit: 200000
locked_nano: 121200000
collected_at: 2025-04-21T11:28:24.905665571Z
hratls_pubkey: 7E0F887AA6BB9104EEC1066F454D4C2D9063D676715F55F919D3FBCEDC63240B
public_package_mr_enclave:
- 52
- 183
- 102
- 210
- 251
- 219
- 218
- 140
- 168
- 118
- 10
- 193
- 98
- 240
- 147
- 124
- 240
- 189
- 46
- 95
- 138
- 172
- 15
- 246
- 227
- 114
- 70
- 159
- 232
- 212
- 9
- 234
app_name: diligent-seahorse