brain/src/db/general.rs
Noor 16fd64ac13
kick contract implemented
app pricing calculation
add node in kick schema and type
improved handling
Clone on all app types
handle expected error on kick contract
validate both app and vm contracts
2025-05-15 19:15:44 +05:30

383 lines
12 KiB
Rust

use super::Error;
use crate::constants::{ACCOUNT, KICK, MIN_ESCROW, TOKEN_DECIMAL};
use crate::db::prelude::*;
use crate::old_brain;
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Client;
use surrealdb::sql::Datetime;
use surrealdb::{RecordId, Surreal};
#[derive(Debug, Serialize, Deserialize)]
pub struct Account {
pub id: RecordId,
pub balance: u64,
pub tmp_locked: u64,
pub escrow: u64,
pub email: String,
}
impl Account {
pub async fn get(db: &Surreal<Client>, address: &str) -> Result<Self, Error> {
let id = (ACCOUNT, address);
let account: Option<Self> = db.select(id).await?;
let account = match account {
Some(account) => account,
None => {
Self { id: id.into(), balance: 0, tmp_locked: 0, escrow: 0, email: String::new() }
}
};
Ok(account)
}
pub async fn get_or_create(db: &Surreal<Client>, address: &str) -> Result<Self, Error> {
let id = (ACCOUNT, address);
match db.select(id).await? {
Some(account) => Ok(account),
None => {
let account: Option<Self> = db.create(id).await?;
account.ok_or(Error::FailedToCreateDBEntry(ACCOUNT.to_string()))
}
}
}
pub async fn airdrop(db: &Surreal<Client>, account: &str, tokens: u64) -> Result<(), Error> {
let tokens = tokens.saturating_mul(1_000_000_000);
let _ = db
.query(format!("upsert account:{account} SET balance = (balance || 0) + {tokens};"))
.await?;
Ok(())
}
pub async fn save(self, db: &Surreal<Client>) -> Result<Option<Self>, Error> {
let account: Option<Self> = db.upsert(self.id.clone()).content(self).await?;
Ok(account)
}
pub async fn operator_reg(
db: &Surreal<Client>,
wallet: &str,
email: &str,
escrow: u64,
) -> Result<(), Error> {
if escrow < MIN_ESCROW {
return Err(Error::MinimalEscrow);
}
let mut op_account = Self::get(db, wallet).await?;
let escrow = escrow.saturating_mul(TOKEN_DECIMAL);
let op_total_balance = op_account.balance.saturating_add(op_account.escrow);
if op_total_balance < escrow {
return Err(Error::InsufficientFunds);
}
op_account.email = email.to_string();
op_account.balance = op_total_balance.saturating_sub(escrow);
op_account.escrow = escrow;
op_account.save(db).await?;
Ok(())
}
}
impl Account {
pub async fn is_banned_by_node(
db: &Surreal<Client>,
user: &str,
node: &str,
) -> Result<bool, Error> {
let mut query_response = db
.query(format!(
"(select operator->ban[0] as ban
from vm_node:{node}
where operator->ban->account contains account:{user}
).ban;"
))
.query(format!(
"(select operator->ban[0] as ban
from app_node:{node}
where operator->ban->account contains account:{user}
).ban;"
))
.await?;
let vm_node_ban: Option<Self> = query_response.take(0)?;
let app_node_ban: Option<Self> = query_response.take(1)?;
Ok(vm_node_ban.is_some() || app_node_ban.is_some())
}
}
impl From<&old_brain::BrainData> for Vec<Account> {
fn from(old_data: &old_brain::BrainData) -> Self {
let mut accounts = Vec::new();
for old_account in old_data.accounts.iter() {
let mut a = Account {
id: RecordId::from(("account", old_account.key())),
balance: old_account.value().balance,
tmp_locked: old_account.value().tmp_locked,
escrow: 0,
email: String::new(),
};
if let Some(operator) = old_data.operators.get(old_account.key()) {
a.escrow = operator.escrow;
a.email = operator.email.clone();
}
accounts.push(a);
}
accounts
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Ban {
id: RecordId,
#[serde(rename = "in")]
from_account: RecordId,
#[serde(rename = "out")]
to_account: RecordId,
created_at: Datetime,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Kick {
id: RecordId,
#[serde(rename = "in")]
from_account: RecordId,
#[serde(rename = "out")]
to_account: RecordId,
created_at: Datetime,
reason: String,
contract: RecordId,
node: RecordId,
}
impl Kick {
pub async fn kicked_in_a_day(db: &Surreal<Client>, account: &str) -> Result<Vec<Self>, Error> {
let yesterday = chrono::Utc::now() - chrono::Duration::days(1);
let mut result = db
.query(format!(
"select * from {KICK} where in = {ACCOUNT}:{account} and created_at > {yesterday};"
))
.await?;
let kicks: Vec<Self> = result.take(0)?;
Ok(kicks)
}
pub async fn submit(self, db: &Surreal<Client>) -> Result<(), Error> {
let _: Vec<Self> = db.insert(KICK).relation(self).await?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Report {
#[serde(rename = "in")]
from_account: RecordId,
#[serde(rename = "out")]
to_node: RecordId,
created_at: Datetime,
pub reason: String,
pub contract_id: String,
}
impl Report {
// TODO: test this functionality and remove this comment
pub async fn create(
db: &Surreal<Client>,
from_account: RecordId,
to_node: RecordId,
reason: String,
contract_id: String,
) -> Result<(), Error> {
let _: Vec<Self> = db
.insert("report")
.relation(Report {
from_account,
to_node,
created_at: Datetime::default(),
reason,
contract_id,
})
.await?;
Ok(())
}
}
/// This is the operator obtained from the DB,
/// however the relation is defined using OperatorRelation
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Operator {
pub account: RecordId,
pub app_nodes: u64,
pub vm_nodes: u64,
pub email: String,
pub escrow: u64,
pub reports: u64,
}
impl Operator {
pub async fn list(db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
let mut result = db
.query(
"array::distinct(array::flatten( [
(select operator from vm_node group by operator).operator,
(select operator from app_node group by operator).operator
]));"
.to_string(),
)
.await?;
let operator_accounts: Vec<RecordId> = result.take(0)?;
let mut operators: Vec<Self> = Vec::new();
for account in operator_accounts.iter() {
if let Some(operator) = Self::inspect(db, &account.key().to_string()).await? {
operators.push(operator);
}
}
Ok(operators)
}
pub async fn inspect(db: &Surreal<Client>, account: &str) -> Result<Option<Self>, Error> {
let mut result = db
.query(format!(
"$vm_nodes = (select id from vm_node where operator = account:{account}).id;
$app_nodes = (select id from app_node where operator = account:{account}).id;
select *,
id as account,
email,
escrow,
$vm_nodes.len() as vm_nodes,
$app_nodes.len() as app_nodes,
(select id from report where $vm_nodes contains out).len() +
(select id from report where $app_nodes contains out).len()
as reports
from account where id = account:{account};"
))
.await?;
let operator: Option<Self> = result.take(2)?;
Ok(operator)
}
pub async fn inspect_nodes(
db: &Surreal<Client>,
account: &str,
) -> Result<(Option<Self>, Vec<VmNodeWithReports>, Vec<AppNodeWithReports>), Error> {
let operator = Self::inspect(db, account).await?;
let mut result = db
.query(format!(
"select *, operator, <-report.* as reports from vm_node
where operator = account:{account};"
))
.query(format!(
"select *, operator, <-report.* as reports from app_node
where operator = account:{account};"
))
.await?;
let vm_nodes: Vec<VmNodeWithReports> = result.take(0)?;
let app_nodes: Vec<AppNodeWithReports> = result.take(1)?;
Ok((operator, vm_nodes, app_nodes))
}
}
pub enum WrapperContract {
Vm(ActiveVmWithNode),
App(ActiveAppWithNode),
}
impl WrapperContract {
pub async fn kick_contract(
db: &Surreal<Client>,
operator_wallet: &str,
contract_uuid: &str,
reason: &str,
) -> Result<u64, Error> {
let (node_operator, admin, contract_id, node_id, collected_at, price_per_mint, is_vm) =
if let Some(active_vm) = ActiveVmWithNode::get_by_uuid(db, contract_uuid).await? {
let price_per_minute = active_vm.price_per_minute();
(
active_vm.vm_node.operator.to_string(),
active_vm.admin.key().to_string(),
active_vm.id,
active_vm.vm_node.id,
active_vm.collected_at,
price_per_minute,
true,
)
} else if let Some(active_app) =
ActiveAppWithNode::get_by_uuid(db, contract_uuid).await?
{
let price_per_minute =
Into::<ActiveApp>::into(active_app.clone()).price_per_minute();
(
active_app.app_node.operator.to_string(),
active_app.admin.key().to_string(),
active_app.id,
active_app.app_node.id,
active_app.collected_at,
price_per_minute,
false,
)
} else {
return Err(Error::ContractNotFound);
};
if node_operator != operator_wallet {
return Err(Error::AccessDenied);
}
let mut minutes_to_refund =
chrono::Utc::now().signed_duration_since(*collected_at).num_minutes().unsigned_abs();
let one_week_minute = 10080;
if minutes_to_refund > one_week_minute {
minutes_to_refund = one_week_minute;
}
let mut refund_amount = minutes_to_refund * price_per_mint;
if !Kick::kicked_in_a_day(db, &admin).await?.is_empty() {
refund_amount = 0;
}
let mut operator_account = Account::get(db, &node_operator).await?;
let mut admin_account = Account::get(db, &admin).await?;
if operator_account.escrow < refund_amount {
refund_amount = operator_account.escrow;
}
log::debug!(
"Removing {refund_amount} escrow from {} and giving it to {}",
node_operator,
admin
);
admin_account.balance = admin_account.balance.saturating_add(refund_amount);
operator_account.escrow = operator_account.escrow.saturating_sub(refund_amount);
let kick = Kick {
id: RecordId::from((KICK, contract_uuid)),
from_account: operator_account.id.clone(),
to_account: admin_account.id.clone(),
created_at: Datetime::default(),
reason: reason.to_string(),
contract: contract_id.clone(),
node: node_id,
};
kick.submit(db).await?;
operator_account.save(db).await?;
admin_account.save(db).await?;
let contract_id = contract_id.to_string();
if is_vm && !ActiveVm::delete(db, &contract_id).await? {
return Err(Error::FailedToDeleteContract(format!("vm:{contract_id}")));
} else if !is_vm && !ActiveApp::delete(db, &contract_id).await? {
return Err(Error::FailedToDeleteContract(format!("app:{contract_id}")));
}
Ok(refund_amount)
}
}