From 952ea971ca2bec6961db1ccdc8b1fc467a57e8b8 Mon Sep 17 00:00:00 2001 From: Noor Date: Tue, 20 May 2025 14:04:07 +0530 Subject: [PATCH] kick contract transaction one single transaction query to execute all the kick operation Remove unused fields from deleted_app schema a basic test for kick and possibilities --- src/db/app.rs | 1 + src/db/general.rs | 181 +++++++++++++++++++++++-------------- surql/tables.sql | 2 - tests/grpc_general_test.rs | 34 +++++++ 4 files changed, 146 insertions(+), 72 deletions(-) diff --git a/src/db/app.rs b/src/db/app.rs index b4dade4..b11955b 100644 --- a/src/db/app.rs +++ b/src/db/app.rs @@ -98,6 +98,7 @@ impl NewAppReq { } pub async fn submit(self, db: &Surreal) -> Result, Error> { + // TODO: handle financial transaction let new_app_req: Vec = db.insert(NEW_APP_REQ).relation(self).await?; Ok(new_app_req) } diff --git a/src/db/general.rs b/src/db/general.rs index 4ffd49c..b31beb0 100644 --- a/src/db/general.rs +++ b/src/db/general.rs @@ -151,10 +151,9 @@ pub struct Kick { impl Kick { pub async fn kicked_in_a_day(db: &Surreal, account: &str) -> Result, 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};" + "select * from {KICK} where out = {ACCOUNT}:{account} and created_at > time::now() - 24h;" )) .await?; let kicks: Vec = result.take(0)?; @@ -289,39 +288,58 @@ impl WrapperContract { contract_uuid: &str, reason: &str, ) -> Result { - 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(); + let ( + 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(); - ( - 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::::into(active_app.clone()).price_per_minute(); + ( + active_vm.vm_node.operator, + active_vm.admin, + active_vm.id, + active_vm.collected_at, + price_per_minute, + "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) = ActiveAppWithNode::get_by_uuid(db, contract_uuid).await? { + let price_per_minute = Into::::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); - }; + ( + active_app.app_node.operator, + active_app.admin, + active_app.id, + active_app.collected_at, + price_per_minute, + "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 { + 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); } let mut minutes_to_refund = @@ -333,50 +351,73 @@ impl WrapperContract { 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() { - refund_amount = 0; - } + log::debug!("Removing {refund_amount} escrow from {} and giving it to {}", operator, admin); - let mut operator_account = Account::get(db, &node_operator).await?; - let mut admin_account = Account::get(db, &admin).await?; + let transaction_query = format!( + " + 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 { - refund_amount = operator_account.escrow; - } + -- move contract into deleted state - log::debug!( - "Removing {refund_amount} escrow from {} and giving it to {}", - node_operator, - admin + RELATE $admin->{deleted_table}->$node + SET id = $id, + {platform_specific_query} + 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); - operator_account.escrow = operator_account.escrow.saturating_sub(refund_amount); + log::trace!("kick_contract transaction_query: {}", &transaction_query); + let refunded: Option = db.query(transaction_query).await?.take(14)?; + let refunded_amount = refunded.ok_or(Error::FailedToCreateDBEntry("Refund".to_string()))?; - 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) + Ok(refunded_amount) } } diff --git a/surql/tables.sql b/surql/tables.sql index 4e9c02e..35ecc1c 100644 --- a/surql/tables.sql +++ b/surql/tables.sql @@ -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 deleted_at ON TABLE deleted_app TYPE datetime; 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 package_url ON TABLE deleted_app TYPE string; DEFINE FIELD hratls_pubkey ON TABLE deleted_app TYPE string; diff --git a/tests/grpc_general_test.rs b/tests/grpc_general_test.rs index a5b9537..299fff8 100644 --- a/tests/grpc_general_test.rs +++ b/tests/grpc_general_test.rs @@ -211,3 +211,37 @@ async fn test_inspect_operator() { assert!(!inspect_response.vm_nodes.is_empty()); 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()); +}