diff --git a/Cargo.lock b/Cargo.lock index 83aecb0..c658a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "detee-shared" version = "0.1.0" -source = "git+ssh://git@gitea.detee.cloud/testnet/proto?branch=credits-v2#6d377926408953e8da2c0f4c6625d4fb90ba7652" +source = "git+ssh://git@gitea.detee.cloud/testnet/proto?branch=credits_app#2fb91e5e876f28f4d7092c7fed0efc93f06111ac" dependencies = [ "bincode 2.0.1", "prost", diff --git a/Cargo.toml b/Cargo.toml index 7af54d1..c117b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ serde_yaml = "0.9.34" surrealdb = "2.2.2" tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] } tonic = { version = "0.12", features = ["tls"] } -detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto", branch = "credits-v2" } +detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto", branch = "credits_app" } ed25519-dalek = "2.1.1" bs58 = "0.5.1" tokio-stream = "0.1.17" diff --git a/src/db/app.rs b/src/db/app.rs index f224c03..f5d3f63 100644 --- a/src/db/app.rs +++ b/src/db/app.rs @@ -25,9 +25,9 @@ pub struct AppNode { pub region: String, pub city: String, pub ip: String, - pub avail_mem_mb: u32, + pub avail_mem_mib: u32, pub avail_vcpus: u32, - pub avail_storage_gbs: u32, + pub avail_storage_mib: u32, pub avail_ports: u32, pub max_ports_per_app: u32, pub price: u64, @@ -72,9 +72,9 @@ pub struct NewAppReq { pub mr_enclave: String, pub hratls_pubkey: String, pub ports: Vec, - pub memory_mb: u32, + pub memory_mib: u32, pub vcpus: u32, - pub disk_size_gb: u32, + pub disk_size_mib: u32, pub locked_nano: u64, pub price_per_unit: u64, pub error: String, @@ -165,12 +165,12 @@ impl NewAppReq { ->$app_node CONTENT {{ created_at: time::now(), app_name: $app_name, package_url: $package_url, - mr_enclave: $mr_enclave, hratls_pubkey: $hratls_pubkey, ports: {:?}, memory_mb: {}, - vcpus: {}, disk_size_gb: {}, locked_nano: {locked_nano}, price_per_unit: {}, error: '', + mr_enclave: $mr_enclave, hratls_pubkey: $hratls_pubkey, ports: {:?}, memory_mib: {}, + vcpus: {}, disk_size_mib: {}, locked_nano: {locked_nano}, price_per_unit: {}, error: '', }}; COMMIT TRANSACTION;", - self.ports, self.memory_mb, self.vcpus, self.disk_size_gb, self.price_per_unit); + self.ports, self.memory_mib, self.vcpus, self.disk_size_mib, self.price_per_unit); log::trace!("submit_new_app_req query: {tx_query}"); @@ -212,9 +212,9 @@ pub struct AppNodeWithReports { pub region: String, pub city: String, pub ip: String, - pub avail_mem_mb: u32, + pub avail_mem_mib: u32, pub avail_vcpus: u32, - pub avail_storage_gbs: u32, + pub avail_storage_mib: u32, pub avail_ports: u32, pub max_ports_per_app: u32, pub price: u64, @@ -233,13 +233,13 @@ impl AppNodeWithReports { avail_ports >= {} && max_ports_per_app >= {} && avail_vcpus >= {} && - avail_mem_mb >= {} && - avail_storage_gbs >= {} ", + avail_mem_mib >= {} && + avail_storage_mib >= {} ", filters.free_ports, filters.free_ports, filters.vcpus, - filters.memory_mb, - filters.storage_gb + filters.memory_mib, + filters.storage_mib ); // TODO: bind all strings @@ -278,8 +278,8 @@ pub struct ActiveApp { pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, pub vcpus: u32, - pub memory_mb: u32, - pub disk_size_gb: u32, + pub memory_mib: u32, + pub disk_size_mib: u32, pub created_at: Datetime, pub price_per_unit: u64, pub locked_nano: u64, @@ -299,8 +299,8 @@ impl From for DeletedApp { mapped_ports: value.mapped_ports, host_ipv4: value.host_ipv4, vcpus: value.vcpus, - memory_mb: value.memory_mb, - disk_size_gb: value.disk_size_gb, + memory_mib: value.memory_mib, + disk_size_mib: value.disk_size_mib, created_at: value.created_at, price_per_unit: value.price_per_unit, mr_enclave: value.mr_enclave, @@ -311,15 +311,6 @@ impl From for DeletedApp { } impl ActiveApp { - pub fn price_per_minute(&self) -> u64 { - (self.total_units() * self.price_per_unit as f64) as u64 - } - - fn total_units(&self) -> f64 { - // TODO: Optimize this based on price of hardware. - (self.vcpus as f64 * 5f64) + (self.memory_mb as f64 / 200f64) + (self.disk_size_gb as f64) - } - pub async fn get_by_uuid(db: &Surreal, uuid: &str) -> Result, Error> { let contract: Option = db .query("select * from $active_app_id;".to_string()) @@ -352,8 +343,8 @@ impl ActiveApp { mapped_ports, host_ipv4: new_app_res.ip_address, vcpus: new_app_req.vcpus, - memory_mb: new_app_req.memory_mb, - disk_size_gb: new_app_req.disk_size_gb, + memory_mib: new_app_req.memory_mib, + disk_size_mib: new_app_req.disk_size_mib, created_at: new_app_req.created_at.clone(), price_per_unit: new_app_req.price_per_unit, locked_nano: new_app_req.locked_nano, @@ -367,7 +358,7 @@ impl ActiveApp { let locked_nano = active_app.locked_nano; let _: Vec = db.insert(()).relation(active_app).await?; - NewAppReq::delete(&db, &new_app_res.uuid).await?; + NewAppReq::delete(db, &new_app_res.uuid).await?; db.query(format!("UPDATE {ACCOUNT}:{admin_account} SET tmp_locked -= {locked_nano};")) .await?; @@ -498,8 +489,8 @@ pub struct ActiveAppWithNode { pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, pub vcpus: u32, - pub memory_mb: u32, - pub disk_size_gb: u32, + pub memory_mib: u32, + pub disk_size_mib: u32, pub created_at: Datetime, pub price_per_unit: u64, pub locked_nano: u64, @@ -519,8 +510,8 @@ impl From for ActiveApp { mapped_ports: val.mapped_ports, host_ipv4: val.host_ipv4, vcpus: val.vcpus, - memory_mb: val.memory_mb, - disk_size_gb: val.disk_size_gb, + memory_mib: val.memory_mib, + disk_size_mib: val.disk_size_mib, created_at: val.created_at, price_per_unit: val.price_per_unit, locked_nano: val.locked_nano, @@ -599,8 +590,8 @@ impl ActiveAppWithNode { pub struct AppNodeResources { pub avail_ports: u32, pub avail_vcpus: u32, - pub avail_mem_mb: u32, - pub avail_storage_gbs: u32, + pub avail_mem_mib: u32, + pub avail_storage_mib: u32, pub max_ports_per_app: u32, } @@ -628,9 +619,9 @@ impl From<&old_brain::BrainData> for Vec { region: old_node.region.clone(), city: old_node.city.clone(), ip: old_node.ip.clone(), - avail_mem_mb: old_node.avail_mem_mb, + avail_mem_mib: old_node.avail_mem_mib, avail_vcpus: old_node.avail_vcpus, - avail_storage_gbs: old_node.avail_storage_mb, + avail_storage_mib: old_node.avail_storage_mib, avail_ports: old_node.avail_no_of_port, max_ports_per_app: old_node.max_ports_per_app, price: old_node.price, @@ -664,9 +655,9 @@ impl From<&old_brain::BrainData> for Vec { app_node: RecordId::from((APP_NODE, old_c.node_pubkey.clone())), mapped_ports, host_ipv4: old_c.host_ipv4.clone(), - disk_size_gb: old_c.disk_size_mb * 1024, + disk_size_mib: old_c.disk_size_mib, vcpus: old_c.vcpus, - memory_mb: old_c.memory_mb, + memory_mib: old_c.memory_mib, price_per_unit: old_c.price_per_unit, locked_nano: old_c.locked_nano, created_at: old_c.created_at.into(), @@ -692,8 +683,8 @@ pub struct DeletedApp { pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, pub vcpus: u32, - pub memory_mb: u32, - pub disk_size_gb: u32, + pub memory_mib: u32, + pub disk_size_mib: u32, pub created_at: Datetime, pub price_per_unit: u64, pub mr_enclave: String, diff --git a/src/db/vm.rs b/src/db/vm.rs index cf42193..3d4fef7 100644 --- a/src/db/vm.rs +++ b/src/db/vm.rs @@ -1013,9 +1013,9 @@ impl From<&old_brain::BrainData> for Vec { region: old_node.region.clone(), city: old_node.city.clone(), ip: old_node.ip.clone(), - avail_mem_mib: old_node.avail_mem_mb, + avail_mem_mib: old_node.avail_mem_mib, avail_vcpus: old_node.avail_vcpus, - avail_storage_mib: old_node.avail_storage_gbs, + avail_storage_mib: old_node.avail_storage_mib, avail_ipv4: old_node.avail_ipv4, avail_ipv6: old_node.avail_ipv6, avail_ports: old_node.avail_ports, @@ -1045,9 +1045,9 @@ impl From<&old_brain::BrainData> for Vec { mapped_ports, public_ipv4: old_c.public_ipv4.clone(), public_ipv6: old_c.public_ipv6.clone(), - disk_size_mib: old_c.disk_size_gb, + disk_size_mib: old_c.disk_size_mib, vcpus: old_c.vcpus, - memory_mib: old_c.memory_mb, + memory_mib: old_c.memory_mib, dtrfs_sha: old_c.dtrfs_sha.clone(), kernel_sha: old_c.kernel_sha.clone(), price_per_unit: old_c.price_per_unit, diff --git a/src/grpc/app.rs b/src/grpc/app.rs index bbd22ac..0d7087c 100644 --- a/src/grpc/app.rs +++ b/src/grpc/app.rs @@ -52,9 +52,9 @@ impl BrainAppDaemon for AppDaemonServer { ip: req.main_ip, price: req.price, - avail_mem_mb: 0, + avail_mem_mib: 0, avail_vcpus: 0, - avail_storage_gbs: 0, + avail_storage_mib: 0, avail_ports: 0, max_ports_per_app: 0, offline_minutes: 0, diff --git a/src/grpc/types.rs b/src/grpc/types.rs index b773d8e..11b719e 100644 --- a/src/grpc/types.rs +++ b/src/grpc/types.rs @@ -280,8 +280,8 @@ impl From for AppContract { node_pubkey: value.app_node.id.key().to_string(), public_ipv4: value.host_ipv4, resource: Some(AppResource { - memory_mb: value.memory_mb, - disk_size_gb: value.disk_size_gb, + memory_mib: value.memory_mib, + disk_size_mib: value.disk_size_mib, vcpus: value.vcpus, ports: value.mapped_ports.iter().map(|(_, g)| *g).collect(), }), @@ -322,9 +322,9 @@ impl From for db::NewAppReq { mr_enclave, hratls_pubkey: val.hratls_pubkey, ports: resource.ports, - memory_mb: resource.memory_mb, + memory_mib: resource.memory_mib, vcpus: resource.vcpus, - disk_size_gb: resource.disk_size_gb, + disk_size_mib: resource.disk_size_mib, locked_nano: val.locked_nano, price_per_unit: val.price_per_unit, error: String::new(), @@ -337,8 +337,8 @@ impl From for NewAppReq { fn from(value: db::NewAppReq) -> Self { let resource = AppResource { vcpus: value.vcpus, - memory_mb: value.memory_mb, - disk_size_gb: value.disk_size_gb, + memory_mib: value.memory_mib, + disk_size_mib: value.disk_size_mib, ports: value.ports, }; let mr_enclave = Some(hex::decode(value.mr_enclave).unwrap_or_default()); @@ -382,8 +382,8 @@ impl From for db::AppNodeResources { Self { avail_ports: value.avail_no_of_port, avail_vcpus: value.avail_vcpus, - avail_mem_mb: value.avail_memory_mb, - avail_storage_gbs: value.avail_storage_gb, + avail_mem_mib: value.avail_memory_mib, + avail_storage_mib: value.avail_storage_mib, max_ports_per_app: value.max_ports_per_app, } } diff --git a/src/old_brain.rs b/src/old_brain.rs index d4dc2ee..cae6efc 100644 --- a/src/old_brain.rs +++ b/src/old_brain.rs @@ -37,9 +37,9 @@ pub struct VmNode { pub region: String, pub city: String, pub ip: String, - pub avail_mem_mb: u32, + pub avail_mem_mib: u32, pub avail_vcpus: u32, - pub avail_storage_gbs: u32, + pub avail_storage_mib: u32, pub avail_ipv4: u32, pub avail_ipv6: u32, pub avail_ports: u32, @@ -60,9 +60,9 @@ pub struct VmContract { pub exposed_ports: Vec, pub public_ipv4: String, pub public_ipv6: String, - pub disk_size_gb: u32, + pub disk_size_mib: u32, pub vcpus: u32, - pub memory_mb: u32, + pub memory_mib: u32, pub kernel_sha: String, pub dtrfs_sha: String, pub created_at: chrono::DateTime, @@ -82,9 +82,9 @@ pub struct AppContract { pub node_pubkey: String, pub mapped_ports: Vec<(u16, u16)>, pub host_ipv4: String, - pub disk_size_mb: u32, + pub disk_size_mib: u32, pub vcpus: u32, - pub memory_mb: u32, + pub memory_mib: u32, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, // price per unit per minute @@ -105,9 +105,9 @@ pub struct AppNode { pub region: String, pub city: String, pub ip: String, - pub avail_mem_mb: u32, + pub avail_mem_mib: u32, pub avail_vcpus: u32, - pub avail_storage_mb: u32, + pub avail_storage_mib: u32, pub avail_no_of_port: u32, pub max_ports_per_app: u32, // nanotokens per unit per minute diff --git a/surql/functions.sql b/surql/functions.sql index 739db37..75f263e 100644 --- a/surql/functions.sql +++ b/surql/functions.sql @@ -33,8 +33,8 @@ DEFINE FUNCTION OVERWRITE fn::app_price_per_minute( LET $app = (select * from $app_id)[0]; RETURN (($app.vcpus * 5) + - ($app.memory_mb / 200) + - ($app.disk_size_gb / 10)) + ($app.memory_mib / 200) + + ($app.disk_size_mib / 10)) * $app.price_per_unit; }; diff --git a/surql/tables.sql b/surql/tables.sql index bf99783..5ab6655 100644 --- a/surql/tables.sql +++ b/surql/tables.sql @@ -88,9 +88,9 @@ DEFINE FIELD country ON TABLE app_node TYPE string; DEFINE FIELD region ON TABLE app_node TYPE string; DEFINE FIELD city ON TABLE app_node TYPE string; DEFINE FIELD ip ON TABLE app_node TYPE string; -DEFINE FIELD avail_mem_mb ON TABLE app_node TYPE int; +DEFINE FIELD avail_mem_mib ON TABLE app_node TYPE int; DEFINE FIELD avail_vcpus ON TABLE app_node TYPE int; -DEFINE FIELD avail_storage_gbs ON TABLE app_node TYPE int; +DEFINE FIELD avail_storage_mib ON TABLE app_node TYPE int; DEFINE FIELD avail_ports ON TABLE app_node TYPE int; DEFINE FIELD max_ports_per_app ON TABLE app_node TYPE int; DEFINE FIELD price ON TABLE app_node TYPE int; @@ -102,9 +102,9 @@ DEFINE FIELD package_url ON TABLE new_app_req TYPE string; DEFINE FIELD mr_enclave ON TABLE new_app_req TYPE string; DEFINE FIELD hratls_pubkey ON TABLE new_app_req TYPE string; DEFINE FIELD ports ON TABLE new_app_req TYPE array; -DEFINE FIELD memory_mb ON TABLE new_app_req TYPE int; +DEFINE FIELD memory_mib ON TABLE new_app_req TYPE int; DEFINE FIELD vcpus ON TABLE new_app_req TYPE int; -DEFINE FIELD disk_size_gb ON TABLE new_app_req TYPE int; +DEFINE FIELD disk_size_mib ON TABLE new_app_req TYPE int; DEFINE FIELD locked_nano ON TABLE new_app_req TYPE int; DEFINE FIELD price_per_unit ON TABLE new_app_req TYPE int; DEFINE FIELD error ON TABLE new_app_req TYPE string; @@ -115,8 +115,8 @@ DEFINE FIELD app_name ON TABLE active_app TYPE string; DEFINE FIELD mapped_ports ON TABLE active_app TYPE array<[int, int]>; DEFINE FIELD host_ipv4 ON TABLE active_app TYPE string; DEFINE FIELD vcpus ON TABLE active_app TYPE int; -DEFINE FIELD memory_mb ON TABLE active_app TYPE int; -DEFINE FIELD disk_size_gb ON TABLE active_app TYPE int; +DEFINE FIELD memory_mib ON TABLE active_app TYPE int; +DEFINE FIELD disk_size_mib ON TABLE active_app TYPE int; DEFINE FIELD created_at ON TABLE active_app TYPE datetime; DEFINE FIELD price_per_unit ON TABLE active_app TYPE int; DEFINE FIELD locked_nano ON TABLE active_app TYPE int; @@ -130,8 +130,8 @@ DEFINE FIELD app_name ON TABLE deleted_app TYPE string; DEFINE FIELD mapped_ports ON TABLE deleted_app TYPE array<[int, int]>; DEFINE FIELD host_ipv4 ON TABLE deleted_app TYPE string; DEFINE FIELD vcpus ON TABLE deleted_app TYPE int; -DEFINE FIELD memory_mb ON TABLE deleted_app TYPE int; -DEFINE FIELD disk_size_gb ON TABLE deleted_app TYPE int; +DEFINE FIELD memory_mib ON TABLE deleted_app TYPE int; +DEFINE FIELD disk_size_mib 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 DEFAULT time::now(); DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int; diff --git a/tests/db_tx_new_app_test.rs b/tests/db_tx_new_app_test.rs index b87e484..83ef753 100644 --- a/tests/db_tx_new_app_test.rs +++ b/tests/db_tx_new_app_test.rs @@ -9,25 +9,25 @@ async fn test_new_app_db_tx() { let db = prepare_test_db().await.unwrap(); let req = NewAppReq { - package_url: "https://registry.detee.ltd/sgx/packages/actix-app-info_package_2025-04-16_21-59-38.tar.gz".to_string(), - node_pubkey: "AH3SpV6ZjXMGSSe6xGH2ekUZxyUhnesAFz4LjX7PnvVn".to_string(), - resource: Some( - AppResource { - memory_mb: 1500, - disk_size_gb: 2, - vcpus: 1, - ports: vec![ 8080 ], - }, - ), - uuid: "".to_string(), - admin_pubkey: "H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc".to_string(), - price_per_unit: 200000, - locked_nano: 152400000, - hratls_pubkey: "7E0F887AA6BB9104EEC1066F454D4C2D9063D676715F55F919D3FBCEDC63240B".to_string(), - public_package_mr_enclave: Some( - vec![ 128, 0, 97, 103, 165, 103, 68, 203, 240, 145, 153, 254, 34, 129, 75, 140, 8, 186, 63, 226, 144, 129, 201, 187, 175, 66, 80, 1, 151, 114, 183, 159, ], - ), - app_name: "lively-ferret".to_string(), + package_url: "https://registry.detee.ltd/sgx/packages/actix-app-info_package_2025-04-16_21-59-38.tar.gz".to_string(), + node_pubkey: "AH3SpV6ZjXMGSSe6xGH2ekUZxyUhnesAFz4LjX7PnvVn".to_string(), + resource: Some( + AppResource { + memory_mib: 1500, + disk_size_mib: 2000, + vcpus: 1, + ports: vec![ 8080 ], + }, + ), + uuid: "".to_string(), + admin_pubkey: "H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc".to_string(), + price_per_unit: 200000, + locked_nano: 152400000, + hratls_pubkey: "7E0F887AA6BB9104EEC1066F454D4C2D9063D676715F55F919D3FBCEDC63240B".to_string(), + public_package_mr_enclave: Some( + vec![ 128, 0, 97, 103, 165, 103, 68, 203, 240, 145, 153, 254, 34, 129, 75, 140, 8, 186, 63, 226, 144, 129, 201, 187, 175, 66, 80, 1, 151, 114, 183, 159, ], + ), + app_name: "lively-ferret".to_string(), }; let db_req: db::NewAppReq = req.into(); diff --git a/tests/grpc_app_cli_test.rs b/tests/grpc_app_cli_test.rs index ef2acd7..d72fe1f 100644 --- a/tests/grpc_app_cli_test.rs +++ b/tests/grpc_app_cli_test.rs @@ -93,6 +93,7 @@ async fn test_app_creation() { db.select::>((ACTIVE_APP, new_app_resp.uuid)).await.unwrap(); assert!(active_app.is_some()); + tokio::time::sleep(std::time::Duration::from_millis(300)).await; let acc_db: db::Account = db.select((ACCOUNT, key.pubkey.clone())).await.unwrap().unwrap(); assert_eq!(acc_db.balance, airdrop_amount * TOKEN_DECIMAL - (locking_nano + 100)); assert_eq!(acc_db.tmp_locked, 0); diff --git a/tests/grpc_app_daemon_test.rs b/tests/grpc_app_daemon_test.rs index 4c4c2f0..d9593b2 100644 --- a/tests/grpc_app_daemon_test.rs +++ b/tests/grpc_app_daemon_test.rs @@ -102,8 +102,8 @@ async fn test_app_daemon_resource_msg() { node_pubkey: daemon_pubkey, avail_no_of_port: 5, avail_vcpus: 4, - avail_memory_mb: 8192, - avail_storage_gb: 100, + avail_memory_mib: 8192, + avail_storage_mib: 10_0000, max_ports_per_app: 5, }; @@ -123,17 +123,17 @@ async fn test_app_daemon_resource_msg() { let app_node_opt: Option = db.select((APP_NODE, daemon_key.pubkey)).await.unwrap(); assert!(app_node_opt.is_some()); let db::AppNode { - avail_mem_mb, + avail_mem_mib, avail_vcpus, - avail_storage_gbs, + avail_storage_mib, avail_ports, max_ports_per_app, .. } = app_node_opt.unwrap(); - assert_eq!(avail_mem_mb, req_data.avail_memory_mb); + assert_eq!(avail_mem_mib, req_data.avail_memory_mib); assert_eq!(avail_vcpus, req_data.avail_vcpus); - assert_eq!(avail_storage_gbs, req_data.avail_storage_gb); + assert_eq!(avail_storage_mib, req_data.avail_storage_mib); assert_eq!(avail_ports, req_data.avail_no_of_port); assert_eq!(max_ports_per_app, req_data.max_ports_per_app); } diff --git a/tests/mock_data.yaml b/tests/mock_data.yaml index f285555..caf0ac5 100644 --- a/tests/mock_data.yaml +++ b/tests/mock_data.yaml @@ -488,9 +488,9 @@ app_nodes: region: Hesse city: Frankfurt am Main ip: 212.95.45.139 - avail_mem_mb: 16000 + avail_mem_mib: 16000 avail_vcpus: 16 - avail_storage_mb: 200000 + avail_storage_mib: 200000 avail_no_of_port: 20000 max_ports_per_app: 9 price: 20000 @@ -506,9 +506,9 @@ app_contracts: - - 28667 - 8080 host_ipv4: 212.95.45.139 - disk_size_mb: 1000 + disk_size_mib: 1000 vcpus: 1 - memory_mb: 1000 + memory_mib: 1000 created_at: 2025-04-21T11:27:28.833236909Z updated_at: 2025-04-21T11:27:28.833237729Z price_per_unit: 200000