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
This commit is contained in:
		
							parent
							
								
									dd8710f4a0
								
							
						
					
					
						commit
						952ea971ca
					
				| @ -98,6 +98,7 @@ impl NewAppReq { | ||||
|     } | ||||
| 
 | ||||
|     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?; | ||||
|         Ok(new_app_req) | ||||
|     } | ||||
|  | ||||
| @ -151,10 +151,9 @@ pub struct Kick { | ||||
| 
 | ||||
| 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};" | ||||
|                 "select * from {KICK} where out = {ACCOUNT}:{account} and created_at > time::now() - 24h;" | ||||
|             )) | ||||
|             .await?; | ||||
|         let kicks: Vec<Self> = result.take(0)?; | ||||
| @ -289,39 +288,58 @@ impl WrapperContract { | ||||
|         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(); | ||||
|         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::<ActiveApp>::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::<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); | ||||
|             }; | ||||
|             ( | ||||
|                 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<u64> = 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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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()); | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user