Kick Contract #2

Merged
ghe0 merged 6 commits from general_and_admin into main 2025-05-23 13:14:54 +00:00
4 changed files with 146 additions and 72 deletions
Showing only changes of commit 952ea971ca - Show all commits

@ -98,6 +98,7 @@ impl NewAppReq {
} }
pub async fn submit(self, db: &Surreal<Client>) -> Result<Vec<Self>, Error> { pub async fn submit(self, db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
// TODO: handle financial transaction
let new_app_req: Vec<Self> = db.insert(NEW_APP_REQ).relation(self).await?; let new_app_req: Vec<Self> = db.insert(NEW_APP_REQ).relation(self).await?;
Ok(new_app_req) Ok(new_app_req)
} }

@ -151,10 +151,9 @@ pub struct Kick {
impl Kick { impl Kick {
pub async fn kicked_in_a_day(db: &Surreal<Client>, account: &str) -> Result<Vec<Self>, Error> { 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 let mut result = db
.query(format!( .query(format!(
"select * from {KICK} where in = {ACCOUNT}:{account} and created_at > {yesterday};" "select * from {KICK} where out = {ACCOUNT}:{account} and created_at > time::now() - 24h;"
)) ))
.await?; .await?;
let kicks: Vec<Self> = result.take(0)?; let kicks: Vec<Self> = result.take(0)?;
@ -289,39 +288,58 @@ impl WrapperContract {
contract_uuid: &str, contract_uuid: &str,
reason: &str, reason: &str,
) -> Result<u64, Error> { ) -> Result<u64, Error> {
let (node_operator, admin, contract_id, node_id, collected_at, price_per_mint, is_vm) = let (
if let Some(active_vm) = ActiveVmWithNode::get_by_uuid(db, contract_uuid).await? { operator_id,
admin_id,
contract_id,
collected_at,
price_per_mint,
deleted_table,
platform_specific_query,
) = if let Some(active_vm) = ActiveVmWithNode::get_by_uuid(db, contract_uuid).await? {
let price_per_minute = active_vm.price_per_minute(); let price_per_minute = active_vm.price_per_minute();
( (
active_vm.vm_node.operator.to_string(), active_vm.vm_node.operator,
active_vm.admin.key().to_string(), active_vm.admin,
active_vm.id, active_vm.id,
active_vm.vm_node.id,
active_vm.collected_at, active_vm.collected_at,
price_per_minute, price_per_minute,
true, "deleted_vm",
"
hostname = $contract.hostname,
public_ipv4 = $contract.public_ipv4,
public_ipv6 = $contract.public_ipv6,
dtrfs_sha = $contract.dtrfs_sha,
kernel_sha = $contract.kernel_sha,
",
) )
} else if let Some(active_app) = } else if let Some(active_app) = ActiveAppWithNode::get_by_uuid(db, contract_uuid).await? {
ActiveAppWithNode::get_by_uuid(db, contract_uuid).await? let price_per_minute = Into::<ActiveApp>::into(active_app.clone()).price_per_minute();
{
let price_per_minute =
Into::<ActiveApp>::into(active_app.clone()).price_per_minute();
( (
active_app.app_node.operator.to_string(), active_app.app_node.operator,
active_app.admin.key().to_string(), active_app.admin,
active_app.id, active_app.id,
active_app.app_node.id,
active_app.collected_at, active_app.collected_at,
price_per_minute, price_per_minute,
false, "deleted_app",
"
app_name = $contract.app_name,
host_ipv4 = $contract.host_ipv4,
mr_enclave = $contract.mr_enclave,
package_url= $contract.package_url,
hratls_pubkey = $contract.hratls_pubkey,
",
) )
} else { } else {
return Err(Error::ContractNotFound); return Err(Error::ContractNotFound);
}; };
if node_operator != operator_wallet { let operator = operator_id.key().to_string();
let admin = admin_id.key().to_string();
if operator != operator_wallet {
return Err(Error::AccessDenied); return Err(Error::AccessDenied);
} }
let mut minutes_to_refund = let mut minutes_to_refund =
@ -333,50 +351,73 @@ impl WrapperContract {
minutes_to_refund = one_week_minute; minutes_to_refund = one_week_minute;
} }
let mut refund_amount = minutes_to_refund * price_per_mint; let refund_amount = minutes_to_refund * price_per_mint;
if !Kick::kicked_in_a_day(db, &admin).await?.is_empty() { log::debug!("Removing {refund_amount} escrow from {} and giving it to {}", operator, admin);
refund_amount = 0;
}
let mut operator_account = Account::get(db, &node_operator).await?; let transaction_query = format!(
let mut admin_account = Account::get(db, &admin).await?; "
BEGIN TRANSACTION;
LET $contract = {contract_id};
LET $operator_account = {operator_id};
LET $reason = '{reason}';
LET $refund_amount = {refund_amount};
LET $deleted_contract = {deleted_table}:{contract_uuid};
LET $id = record::id($contract.id);
LET $admin = $contract.in;
LET $node = $contract.out;
if operator_account.escrow < refund_amount { -- move contract into deleted state
refund_amount = operator_account.escrow;
}
log::debug!( RELATE $admin->{deleted_table}->$node
"Removing {refund_amount} escrow from {} and giving it to {}", SET id = $id,
node_operator, {platform_specific_query}
admin mapped_ports = $contract.mapped_ports,
disk_size_gb = $contract.disk_size_gb,
vcpus = $contract.vcpus,
memory_mb = $contract.memory_mb,
created_at = $contract.created_at,
deleted_at = time::now(),
price_per_unit = $contract.price_per_unit
;
DELETE $contract;
-- calculating refund amount
LET $refund = IF SELECT * FROM {KICK} WHERE out = $admin.id AND created_at > time::now() - 24h {{
0
}} ELSE IF $operator_account.escrow <= $refund_amount {{
$operator_account.escrow
}} ELSE {{
$refund_amount;
}};
RELATE $operator_account->{KICK}->$admin
SET id = $id,
reason = $reason,
contract = $deleted_contract,
node = $node,
created_at = time::now()
;
-- update balances
UPDATE $operator_account SET escrow -= $refund;
IF $operator_account.escrow < 0 {{
THROW 'Insufficient funds.'
}};
UPDATE $admin SET balance += $refund;
SELECT * FROM $refund;
COMMIT TRANSACTION;
",
); );
admin_account.balance = admin_account.balance.saturating_add(refund_amount); log::trace!("kick_contract transaction_query: {}", &transaction_query);
operator_account.escrow = operator_account.escrow.saturating_sub(refund_amount); let refunded: Option<u64> = db.query(transaction_query).await?.take(14)?;
let refunded_amount = refunded.ok_or(Error::FailedToCreateDBEntry("Refund".to_string()))?;
let kick = Kick { Ok(refunded_amount)
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)
} }
} }

@ -131,8 +131,6 @@ DEFINE FIELD disk_size_gb ON TABLE deleted_app TYPE int;
DEFINE FIELD created_at ON TABLE deleted_app TYPE datetime; DEFINE FIELD created_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime; DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int; DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int;
DEFINE FIELD locked_nano ON TABLE deleted_app TYPE int;
DEFINE FIELD collected_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD mr_enclave ON TABLE deleted_app TYPE string; DEFINE FIELD mr_enclave ON TABLE deleted_app TYPE string;
DEFINE FIELD package_url 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 FIELD hratls_pubkey ON TABLE deleted_app TYPE string;

@ -211,3 +211,37 @@ async fn test_inspect_operator() {
assert!(!inspect_response.vm_nodes.is_empty()); assert!(!inspect_response.vm_nodes.is_empty());
assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey); assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey);
} }
#[tokio::test]
async fn test_kick_contract() {
// TODO: implement seed data to test
// possibilities
// 1. vm contract
// 2. app contract
// 3. non existent contract
// 4. other operator's contract
// 5. contract collected more than a week
// 6. refund amount calculation
// 7. refund of multiple contract kick in a day for same user
env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.filter_module("tungstenite", log::LevelFilter::Debug)
.filter_module("tokio_tungstenite", log::LevelFilter::Debug)
.init();
let db_conn = prepare_test_db().await.unwrap();
let operator_wallet = "BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS";
let contract_uuid = "26577f1c98674a1780a86cf0490f1270";
let reason = "test reason";
let kick_response = surreal_brain::db::general::WrapperContract::kick_contract(
&db_conn,
&operator_wallet,
&contract_uuid,
&reason,
)
.await;
dbg!(kick_response.unwrap());
}