Kick Contract #2
@ -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