From d1e85ec03ea2d8372a389edf7d90b061c2f115ab Mon Sep 17 00:00:00 2001 From: Noor Date: Fri, 23 May 2025 13:14:53 +0000 Subject: [PATCH] Kick Contract (#2) Production grade kick contract code for financial transaction tested for both vm and app modified schema to include node in kick, default time for deleted_app and removed unwanted fields app data migration for testing kick contract for active_app fixed some tests register operator Reviewed-on: https://gitea.detee.cloud/testnet/brain/pulls/2 Co-authored-by: Noor Co-committed-by: Noor Signed-off-by: Noor --- saved_data.yaml | 569 +++++++++++++++++-------------- src/constants.rs | 7 +- src/db/app.rs | 126 +++++-- src/db/general.rs | 160 ++++++++- src/db/mod.rs | 36 +- src/db/vm.rs | 46 ++- src/grpc/app.rs | 2 +- src/grpc/general.rs | 55 ++- src/grpc/mod.rs | 8 +- src/grpc/types.rs | 14 +- surql/functions.sql | 10 + surql/tables.sql | 5 +- tests/common/prepare_test_env.rs | 5 +- tests/common/test_utils.rs | 15 + tests/common/vm_cli_utils.rs | 7 +- tests/common/vm_daemon_utils.rs | 12 +- tests/grpc_general_test.rs | 59 +++- tests/grpc_vm_cli_test.rs | 12 +- tests/grpc_vm_daemon_test.rs | 4 +- 19 files changed, 786 insertions(+), 366 deletions(-) diff --git a/saved_data.yaml b/saved_data.yaml index cd4dcab..26195ad 100644 --- a/saved_data.yaml +++ b/saved_data.yaml @@ -113,275 +113,328 @@ operators: email: first_on_detee@proton.me banned_users: [] vm_nodes: - - HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv - - 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 - - Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu - - 4QbUXDM915RUFnHm3NiysLXFLk1WRGZvABwLNzx4tTEW - - DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb + - HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv + - 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 + - Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu + - 4QbUXDM915RUFnHm3NiysLXFLk1WRGZvABwLNzx4tTEW + - DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb app_nodes: [] x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK: escrow: 5499700480000 email: gheo@detee.ltd banned_users: [] vm_nodes: - - 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f - - 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 + - 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f + - 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 app_nodes: [] 7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB: - escrow: 0 - email: '' + escrow: 888888888899999 + email: "" banned_users: [] vm_nodes: [] app_nodes: - - BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg + - BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg vm_nodes: -- public_key: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 - operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK - country: GB - region: England - city: London - ip: 173.234.17.2 - avail_mem_mb: 26000 - avail_vcpus: 28 - avail_storage_gbs: 680 - avail_ipv4: 2 - avail_ipv6: 65516 - avail_ports: 19999 - max_ports_per_vm: 5 - price: 20000 - reports: {} - offline_minutes: 0 -- public_key: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu - operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS - country: FR - region: Île-de-France - city: Paris - ip: 156.146.63.215 - avail_mem_mb: 123000 - avail_vcpus: 46 - avail_storage_gbs: 440 - avail_ipv4: 2 - avail_ipv6: 0 - avail_ports: 20000 - max_ports_per_vm: 5 - price: 20000 - reports: {} - offline_minutes: 0 -- public_key: 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f - operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK - country: CA - region: Quebec - city: Montréal - ip: 184.107.169.199 - avail_mem_mb: 30000 - avail_vcpus: 31 - avail_storage_gbs: 700 - avail_ipv4: 0 - avail_ipv6: 0 - avail_ports: 20000 - max_ports_per_vm: 5 - price: 18000 - reports: {} - offline_minutes: 0 -- public_key: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb - operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS - country: CA - region: British Columbia - city: Vancouver - ip: 149.22.95.1 - avail_mem_mb: 109000 - avail_vcpus: 45 - avail_storage_gbs: 400 - avail_ipv4: 25 - avail_ipv6: 0 - avail_ports: 20000 - max_ports_per_vm: 5 - price: 20000 - reports: {} - offline_minutes: 0 -- public_key: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 - operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS - country: US - region: California - city: San Jose - ip: 149.36.48.99 - avail_mem_mb: 120000 - avail_vcpus: 41 - avail_storage_gbs: 390 - avail_ipv4: 23 - avail_ipv6: 0 - avail_ports: 19999 - max_ports_per_vm: 5 - price: 20000 - reports: {} - offline_minutes: 0 -- public_key: HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv - operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS - country: CA - region: British Columbia - city: Vancouver - ip: 149.22.95.28 - avail_mem_mb: 125000 - avail_vcpus: 46 - avail_storage_gbs: 400 - avail_ipv4: 26 - avail_ipv6: 0 - avail_ports: 20000 - max_ports_per_vm: 5 - price: 20000 - reports: {} - offline_minutes: 0 + - public_key: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 + operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK + country: GB + region: England + city: London + ip: 173.234.17.2 + avail_mem_mb: 26000 + avail_vcpus: 28 + avail_storage_gbs: 680 + avail_ipv4: 2 + avail_ipv6: 65516 + avail_ports: 19999 + max_ports_per_vm: 5 + price: 20000 + reports: {} + offline_minutes: 0 + - public_key: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu + operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS + country: FR + region: Île-de-France + city: Paris + ip: 156.146.63.215 + avail_mem_mb: 123000 + avail_vcpus: 46 + avail_storage_gbs: 440 + avail_ipv4: 2 + avail_ipv6: 0 + avail_ports: 20000 + max_ports_per_vm: 5 + price: 20000 + reports: {} + offline_minutes: 0 + - public_key: 2Uf5pxhxKTUm6gRMnpbJHYDuyA6BWUfFsdmPyWfbMV1f + operator_wallet: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK + country: CA + region: Quebec + city: Montréal + ip: 184.107.169.199 + avail_mem_mb: 30000 + avail_vcpus: 31 + avail_storage_gbs: 700 + avail_ipv4: 0 + avail_ipv6: 0 + avail_ports: 20000 + max_ports_per_vm: 5 + price: 18000 + reports: {} + offline_minutes: 0 + - public_key: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb + operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS + country: CA + region: British Columbia + city: Vancouver + ip: 149.22.95.1 + avail_mem_mb: 109000 + avail_vcpus: 45 + avail_storage_gbs: 400 + avail_ipv4: 25 + avail_ipv6: 0 + avail_ports: 20000 + max_ports_per_vm: 5 + price: 20000 + reports: {} + offline_minutes: 0 + - public_key: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 + operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS + country: US + region: California + city: San Jose + ip: 149.36.48.99 + avail_mem_mb: 120000 + avail_vcpus: 41 + avail_storage_gbs: 390 + avail_ipv4: 23 + avail_ipv6: 0 + avail_ports: 19999 + max_ports_per_vm: 5 + price: 20000 + reports: {} + offline_minutes: 0 + - public_key: HiyMp21zaBVbRCjDsD5hEjQnHeHv4e1gpUR6pVfHTKqv + operator_wallet: BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS + country: CA + region: British Columbia + city: Vancouver + ip: 149.22.95.28 + avail_mem_mb: 125000 + avail_vcpus: 46 + avail_storage_gbs: 400 + avail_ipv4: 26 + avail_ipv6: 0 + avail_ports: 20000 + max_ports_per_vm: 5 + price: 20000 + reports: {} + offline_minutes: 0 vm_contracts: -- uuid: 958165e3-dea8-407d-8c42-dd17002ef79c - hostname: detee-landing-fr - admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL - node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu - exposed_ports: [] - public_ipv4: 156.146.63.216 - public_ipv6: '' - disk_size_gb: 10 - vcpus: 2 - memory_mb: 3000 - kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151 - dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b - created_at: 2025-02-28T23:19:41.769423466Z - updated_at: 2025-04-12T12:11:58.516768949Z - price_per_unit: 20000 - locked_nano: 14875500000 - collected_at: 2025-04-20T00:34:15.461165181Z -- uuid: e807a2fd-cf90-4a14-bc3a-89ce6dc59033 - hostname: detee-landing-gb - admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL - node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 - exposed_ports: [] - public_ipv4: 173.234.136.154 - public_ipv6: '' - disk_size_gb: 10 - vcpus: 2 - memory_mb: 3000 - kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151 - dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b - created_at: 2025-03-06T19:51:39.595163157Z - updated_at: 2025-03-06T19:51:39.595163842Z - price_per_unit: 20000 - locked_nano: 14875500000 - collected_at: 2025-04-20T00:34:15.461181545Z -- uuid: 23094406-2307-4332-a642-acee718d0186 - hostname: heroic-door - admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX - node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 - exposed_ports: - - 38288 - public_ipv4: '' - public_ipv6: '' - disk_size_gb: 10 - vcpus: 1 - memory_mb: 1000 - kernel_sha: 14e225e4aaf84cc2e0b5f64206121186ddebc4b378b886da3b2f7515dfd41692 - dtrfs_sha: 03ce24dbbe917fdd4f6347e61036805ddbdded5044c272bab188ef9333093bee - created_at: 2025-03-12T16:28:24.749161605Z - updated_at: 2025-03-12T16:28:24.749162477Z - price_per_unit: 20000 - locked_nano: 14134140000 - collected_at: 2025-04-20T00:34:15.461191231Z -- uuid: 1f49a71c-f68c-4c64-a82e-f50e0ba0b574 - hostname: astromech-wrench - admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL - node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb - exposed_ports: [] - public_ipv4: 149.22.95.2 - public_ipv6: '' - disk_size_gb: 10 - vcpus: 2 - memory_mb: 3000 - kernel_sha: 3a68709138bed09c16671949cf1f03acee95a08381ba84fc70fb586001fa6767 - dtrfs_sha: 0bb93443f65c9f4379ed469f94794f5c1bf14d8905b0b2c56a125df4a9ebe83e - created_at: 2025-03-20T14:40:25.557753393Z - updated_at: 2025-03-20T14:40:25.557754242Z - price_per_unit: 20000 - locked_nano: 11865620000 - collected_at: 2025-04-20T00:34:15.461201690Z -- uuid: 16577f1c-9867-4a17-80a8-6cf0490f1270 - hostname: sofenty - admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL - node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu - exposed_ports: [] - public_ipv4: 156.146.63.217 - public_ipv6: '' - disk_size_gb: 10 - vcpus: 2 - memory_mb: 3000 - kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 - dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 - created_at: 2025-04-07T22:57:57.646151746Z - updated_at: 2025-04-07T22:57:57.646152630Z - price_per_unit: 20000 - locked_nano: 11867500000 - collected_at: 2025-04-20T00:34:15.461211040Z -- uuid: 4b6e25ca-87ac-478b-8f16-aa8f5c44c704 - hostname: cloaked-mailbox - admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX - node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb - exposed_ports: [] - public_ipv4: 149.22.95.2 - public_ipv6: '' - disk_size_gb: 30 - vcpus: 1 - memory_mb: 1000 - kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 - dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 - created_at: 2025-04-12T13:44:56.957037550Z - updated_at: 2025-04-12T13:44:56.957038546Z - price_per_unit: 20000 - locked_nano: 11177760000 - collected_at: 2025-04-20T00:34:15.461219779Z -- uuid: eb1a13ed-d782-4b71-8860-73540129cb7d - hostname: twenty - admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL - node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 - exposed_ports: [] - public_ipv4: 149.36.48.100 - public_ipv6: '' - disk_size_gb: 10 - vcpus: 4 - memory_mb: 4000 - kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 - dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 - created_at: 2025-04-15T00:46:35.622165457Z - updated_at: 2025-04-15T00:46:35.622166372Z - price_per_unit: 20000 - locked_nano: 15570720000 - collected_at: 2025-04-20T00:34:15.461230948Z -- uuid: 1bf36309-3774-4825-b023-b2a0ef0405ed - hostname: shadowy-hobo - admin_pubkey: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK - node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 - exposed_ports: - - 46393 - public_ipv4: '' - public_ipv6: '' - disk_size_gb: 10 - vcpus: 1 - memory_mb: 1000 - kernel_sha: e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919 - dtrfs_sha: d207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990 - created_at: 2025-04-16T20:37:57.176592933Z - updated_at: 2025-04-16T20:37:57.176594069Z - price_per_unit: 20000 - locked_nano: 12730960000 - collected_at: 2025-04-20T00:34:15.461240342Z + - uuid: 958165e3-dea8-407d-8c42-dd17002ef79c + hostname: detee-landing-fr + admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL + node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu + exposed_ports: [] + public_ipv4: 156.146.63.216 + public_ipv6: "" + disk_size_gb: 10 + vcpus: 2 + memory_mb: 3000 + kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151 + dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b + created_at: 2025-02-28T23:19:41.769423466Z + updated_at: 2025-04-12T12:11:58.516768949Z + price_per_unit: 20000 + locked_nano: 14875500000 + collected_at: 2025-04-20T00:34:15.461165181Z + - uuid: e807a2fd-cf90-4a14-bc3a-89ce6dc59033 + hostname: detee-landing-gb + admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL + node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 + exposed_ports: [] + public_ipv4: 173.234.136.154 + public_ipv6: "" + disk_size_gb: 10 + vcpus: 2 + memory_mb: 3000 + kernel_sha: 3ec4fc5aa5729f515967ec71be4a851622785c0080f7191b1b07717149840151 + dtrfs_sha: 3f6b3e5740f249eedfb2f7248c521a551be8b2676f7fcb040f3f3bc840a5004b + created_at: 2025-03-06T19:51:39.595163157Z + updated_at: 2025-03-06T19:51:39.595163842Z + price_per_unit: 20000 + locked_nano: 14875500000 + collected_at: 2025-04-20T00:34:15.461181545Z + - uuid: 23094406-2307-4332-a642-acee718d0186 + hostname: heroic-door + admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX + node_pubkey: 7Xw3RxbP5pvfjZ8U6yA3HHVSS9YXjKH5Vkas3JRbQYd9 + exposed_ports: + - 38288 + public_ipv4: "" + public_ipv6: "" + disk_size_gb: 10 + vcpus: 1 + memory_mb: 1000 + kernel_sha: 14e225e4aaf84cc2e0b5f64206121186ddebc4b378b886da3b2f7515dfd41692 + dtrfs_sha: 03ce24dbbe917fdd4f6347e61036805ddbdded5044c272bab188ef9333093bee + created_at: 2025-03-12T16:28:24.749161605Z + updated_at: 2025-03-12T16:28:24.749162477Z + price_per_unit: 20000 + locked_nano: 14134140000 + collected_at: 2025-04-20T00:34:15.461191231Z + - uuid: 1f49a71c-f68c-4c64-a82e-f50e0ba0b574 + hostname: astromech-wrench + admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL + node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb + exposed_ports: [] + public_ipv4: 149.22.95.2 + public_ipv6: "" + disk_size_gb: 10 + vcpus: 2 + memory_mb: 3000 + kernel_sha: 3a68709138bed09c16671949cf1f03acee95a08381ba84fc70fb586001fa6767 + dtrfs_sha: 0bb93443f65c9f4379ed469f94794f5c1bf14d8905b0b2c56a125df4a9ebe83e + created_at: 2025-03-20T14:40:25.557753393Z + updated_at: 2025-03-20T14:40:25.557754242Z + price_per_unit: 20000 + locked_nano: 11865620000 + collected_at: 2025-04-20T00:34:15.461201690Z + - uuid: 16577f1c-9867-4a17-80a8-6cf0490f1270 + hostname: sofenty + admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL + node_pubkey: Du3UfPSUUZmA5thQmc9Vrxdy7UimpygcpDsQNnwRQPtu + exposed_ports: [] + public_ipv4: 156.146.63.217 + public_ipv6: "" + disk_size_gb: 10 + vcpus: 2 + memory_mb: 3000 + kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 + dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 + created_at: 2025-04-07T22:57:57.646151746Z + updated_at: 2025-04-07T22:57:57.646152630Z + price_per_unit: 20000 + locked_nano: 11867500000 + collected_at: 2025-04-20T00:34:15.461211040Z + - uuid: 4b6e25ca-87ac-478b-8f16-aa8f5c44c704 + hostname: cloaked-mailbox + admin_pubkey: DwfL5iFu32xh2YMCUxg63oEAThLRqehDAumiP9q6zuuX + node_pubkey: DgkbsrwttkZXvzxY5kDwQQoDd79GLmZ5tc7fYJUFkQQb + exposed_ports: [] + public_ipv4: 149.22.95.2 + public_ipv6: "" + disk_size_gb: 30 + vcpus: 1 + memory_mb: 1000 + kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 + dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 + created_at: 2025-04-12T13:44:56.957037550Z + updated_at: 2025-04-12T13:44:56.957038546Z + price_per_unit: 20000 + locked_nano: 11177760000 + collected_at: 2025-04-20T00:34:15.461219779Z + - uuid: eb1a13ed-d782-4b71-8860-73540129cb7d + hostname: twenty + admin_pubkey: FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL + node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 + exposed_ports: [] + public_ipv4: 149.36.48.100 + public_ipv6: "" + disk_size_gb: 10 + vcpus: 4 + memory_mb: 4000 + kernel_sha: e49c8587287b21df7600c04326fd7393524453918c14d67f73757dc769a13542 + dtrfs_sha: b5f408d00e2b93dc594fed3a7f2466a9878802ff1c7ae502247471cd06728a45 + created_at: 2025-04-15T00:46:35.622165457Z + updated_at: 2025-04-15T00:46:35.622166372Z + price_per_unit: 20000 + locked_nano: 15570720000 + collected_at: 2025-04-20T00:34:15.461230948Z + - uuid: 1bf36309-3774-4825-b023-b2a0ef0405ed + hostname: shadowy-hobo + admin_pubkey: x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK + node_pubkey: 3zRxiGRnf46vd3zAEmpaYBJocTV9oJB6yXf5GZFR1Sq4 + exposed_ports: + - 46393 + public_ipv4: "" + public_ipv6: "" + disk_size_gb: 10 + vcpus: 1 + memory_mb: 1000 + kernel_sha: e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919 + dtrfs_sha: d207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990 + created_at: 2025-04-16T20:37:57.176592933Z + updated_at: 2025-04-16T20:37:57.176594069Z + price_per_unit: 20000 + locked_nano: 12730960000 + collected_at: 2025-04-20T00:34:15.461240342Z app_nodes: -- node_pubkey: BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg - operator_wallet: 7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB - country: DE - region: Hesse - city: Frankfurt am Main - ip: 212.95.45.139 - avail_mem_mb: 16000 - avail_vcpus: 16 - avail_storage_mb: 200000 - avail_no_of_port: 20000 - max_ports_per_app: 9 - price: 20000 - offline_minutes: 0 -app_contracts: [] + - node_pubkey: BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg + operator_wallet: 7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB + country: DE + region: Hesse + city: Frankfurt am Main + ip: 212.95.45.139 + avail_mem_mb: 16000 + avail_vcpus: 16 + avail_storage_mb: 200000 + avail_no_of_port: 20000 + max_ports_per_app: 9 + price: 20000 + offline_minutes: 0 +app_contracts: + - uuid: e3d01f25-2b2a-410b-80e3-12f44e474334 + package_url: https://registry.detee.ltd/sgx/packages/base_package_2025-04-17_11-01-08.tar.gz + admin_pubkey: H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc + node_pubkey: BiqoPUEoAxYxMRXUmyofoS9H1TBQgQqvLJ6MbWh88AQg + mapped_ports: + - - 27158 + - 34500 + - - 28667 + - 8080 + host_ipv4: 212.95.45.139 + disk_size_mb: 1000 + vcpus: 1 + memory_mb: 1000 + created_at: 2025-04-21T11:27:28.833236909Z + updated_at: 2025-04-21T11:27:28.833237729Z + price_per_unit: 200000 + locked_nano: 121200000 + collected_at: 2025-04-21T11:28:24.905665571Z + hratls_pubkey: 7E0F887AA6BB9104EEC1066F454D4C2D9063D676715F55F919D3FBCEDC63240B + public_package_mr_enclave: + - 52 + - 183 + - 102 + - 210 + - 251 + - 219 + - 218 + - 140 + - 168 + - 118 + - 10 + - 193 + - 98 + - 240 + - 147 + - 124 + - 240 + - 189 + - 46 + - 95 + - 138 + - 172 + - 15 + - 246 + - 227 + - 114 + - 70 + - 159 + - 232 + - 212 + - 9 + - 234 + app_name: diligent-seahorse diff --git a/src/constants.rs b/src/constants.rs index 751f441..33cc61f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -5,7 +5,7 @@ pub const CERT_PATH: &str = "/etc/detee/brain/brain-crt.pem"; pub const CERT_KEY_PATH: &str = "/etc/detee/brain/brain-key.pem"; pub const CONFIG_PATH: &str = "/etc/detee/brain/config.ini"; -pub const DB_SCHEMA_FILE: &str = "interim_tables.surql"; +pub const DB_SCHEMA_FILES: [&str; 2] = ["surql/tables.sql", "surql/functions.sql"]; pub static ADMIN_ACCOUNTS: LazyLock> = LazyLock::new(|| { let default_admin_keys = vec![ @@ -23,6 +23,8 @@ pub static ADMIN_ACCOUNTS: LazyLock> = LazyLock::new(|| { pub const OLD_BRAIN_DATA_PATH: &str = "./saved_data.yaml"; pub const ACCOUNT: &str = "account"; +pub const KICK: &str = "kick"; + pub const VM_NODE: &str = "vm_node"; pub const ACTIVE_VM: &str = "active_vm"; pub const VM_UPDATE_EVENT: &str = "vm_update_event"; @@ -42,3 +44,6 @@ pub const ID_ALPHABET: [char; 62] = [ 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ]; + +pub const MIN_ESCROW: u64 = 5000; +pub const TOKEN_DECIMAL: u64 = 1_000_000_000; diff --git a/src/db/app.rs b/src/db/app.rs index 478d87d..2245313 100644 --- a/src/db/app.rs +++ b/src/db/app.rs @@ -12,7 +12,7 @@ use surrealdb::sql::Datetime; use surrealdb::{Notification, RecordId, Surreal}; use tokio_stream::StreamExt; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct AppNode { pub id: RecordId, pub operator: RecordId, @@ -32,8 +32,9 @@ pub struct AppNode { impl AppNode { pub async fn register(self, db: &Surreal) -> Result { db::Account::get_or_create(db, &self.operator.key().to_string()).await?; - let app_node: Option = db.upsert(self.id.clone()).content(self).await?; - app_node.ok_or(Error::FailedToCreateDBEntry) + let app_node_id = self.id.clone(); + let app_node: Option = db.upsert(app_node_id.clone()).content(self).await?; + app_node.ok_or(Error::FailedToCreateDBEntry(format!("{APP_NODE}:{app_node_id}"))) } } @@ -54,7 +55,7 @@ impl From for AppDaemonMsg { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct NewAppReq { pub id: RecordId, #[serde(rename = "in")] @@ -97,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) } @@ -164,7 +166,7 @@ impl AppNodeWithReports { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ActiveApp { pub id: RecordId, #[serde(rename = "in")] @@ -172,11 +174,11 @@ pub struct ActiveApp { #[serde(rename = "out")] pub app_node: RecordId, pub app_name: String, - pub mapped_ports: Vec<(u64, u64)>, + pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, - pub vcpus: u64, - pub memory_mb: u64, - pub disk_size_gb: u64, + pub vcpus: u32, + pub memory_mb: u32, + pub disk_size_gb: u32, pub created_at: Datetime, pub price_per_unit: u64, pub locked_nano: u64, @@ -210,6 +212,15 @@ 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 activate(db: &Surreal, id: &str) -> Result<(), Error> { let new_app_req = match NewAppReq::get(db, id).await? { Some(r) => r, @@ -223,9 +234,9 @@ impl ActiveApp { app_name: new_app_req.app_name, mapped_ports: vec![], host_ipv4: String::new(), - vcpus: new_app_req.vcpu as u64, - memory_mb: new_app_req.memory_mb as u64, - disk_size_gb: new_app_req.disk_mb as u64, + vcpus: new_app_req.vcpu, + memory_mb: new_app_req.memory_mb, + disk_size_gb: new_app_req.disk_mb, created_at: new_app_req.created_at.clone(), price_per_unit: new_app_req.price_per_unit, locked_nano: new_app_req.locked_nano, @@ -293,7 +304,7 @@ impl ActiveApp { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ActiveAppWithNode { pub id: RecordId, #[serde(rename = "in")] @@ -301,11 +312,11 @@ pub struct ActiveAppWithNode { #[serde(rename = "out")] pub app_node: AppNode, pub app_name: String, - pub mapped_ports: Vec<(u64, u64)>, + pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, - pub vcpus: u64, - pub memory_mb: u64, - pub disk_size_gb: u64, + pub vcpus: u32, + pub memory_mb: u32, + pub disk_size_gb: u32, pub created_at: Datetime, pub price_per_unit: u64, pub locked_nano: u64, @@ -315,10 +326,37 @@ pub struct ActiveAppWithNode { pub hratls_pubkey: String, } +impl From for ActiveApp { + fn from(val: ActiveAppWithNode) -> Self { + Self { + id: val.id, + admin: val.admin, + app_node: val.app_node.id, + app_name: val.app_name, + 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, + created_at: val.created_at, + price_per_unit: val.price_per_unit, + locked_nano: val.locked_nano, + collected_at: val.collected_at, + mr_enclave: val.mr_enclave, + package_url: val.package_url, + hratls_pubkey: val.hratls_pubkey, + } + } +} + impl ActiveAppWithNode { pub async fn get_by_uuid(db: &Surreal, uuid: &str) -> Result, Error> { - let contract: Option = - db.query(format!("select * from {ACTIVE_APP}:{uuid} fetch out;")).await?.take(0)?; + let contract: Option = db + .query(format!("select * from {ACTIVE_APP} where id = $uuid_input fetch out;")) + .bind(("uuid_input", RecordId::from((ACTIVE_APP, uuid)))) + .await? + .take(0)?; + Ok(contract) } @@ -393,7 +431,7 @@ impl From<&old_brain::BrainData> for Vec { let mut nodes = Vec::new(); for old_node in old_data.app_nodes.iter() { nodes.push(AppNode { - id: RecordId::from(("app_node", old_node.node_pubkey.clone())), + id: RecordId::from((APP_NODE, old_node.node_pubkey.clone())), operator: RecordId::from((ACCOUNT, old_node.operator_wallet.clone())), country: old_node.country.clone(), region: old_node.region.clone(), @@ -412,6 +450,46 @@ impl From<&old_brain::BrainData> for Vec { } } +impl From<&old_brain::BrainData> for Vec { + fn from(old_data: &old_brain::BrainData) -> Self { + let mut contracts = Vec::new(); + for old_c in old_data.app_contracts.iter() { + let mut mapped_ports = Vec::new(); + for port in old_c.mapped_ports.clone().into_iter().map(|(b, c)| (b as u32, c as u32)) { + mapped_ports.push(port); + } + + let mr_enclave_hex = old_c + .public_package_mr_enclave + .clone() + .unwrap_or_default() + .iter() + .map(|byte| format!("{byte:02X}")) + .collect(); + + contracts.push(ActiveApp { + id: RecordId::from((ACTIVE_APP, old_c.uuid.replace("-", ""))), + admin: RecordId::from((ACCOUNT, old_c.admin_pubkey.clone())), + 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, + vcpus: old_c.vcpus, + memory_mb: old_c.memory_mb, + price_per_unit: old_c.price_per_unit, + locked_nano: old_c.locked_nano, + created_at: old_c.created_at.into(), + collected_at: old_c.collected_at.into(), + app_name: old_c.app_name.clone(), + mr_enclave: mr_enclave_hex, + package_url: old_c.package_url.clone(), + hratls_pubkey: old_c.hratls_pubkey.clone(), + }); + } + contracts + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct DeletedApp { pub id: RecordId, @@ -420,11 +498,11 @@ pub struct DeletedApp { #[serde(rename = "out")] pub app_node: RecordId, pub app_name: String, - pub mapped_ports: Vec<(u64, u64)>, + pub mapped_ports: Vec<(u32, u32)>, pub host_ipv4: String, - pub vcpus: u64, - pub memory_mb: u64, - pub disk_size_gb: u64, + pub vcpus: u32, + pub memory_mb: u32, + pub disk_size_gb: u32, pub created_at: Datetime, pub price_per_unit: u64, pub locked_nano: u64, diff --git a/src/db/general.rs b/src/db/general.rs index 62774ae..7597e7a 100644 --- a/src/db/general.rs +++ b/src/db/general.rs @@ -1,7 +1,6 @@ -use crate::constants::ACCOUNT; -use crate::db::prelude::*; - use super::Error; +use crate::constants::{ACCOUNT, KICK, MIN_ESCROW, TOKEN_DECIMAL}; +use crate::db::prelude::*; use crate::old_brain; use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Client; @@ -37,7 +36,7 @@ impl Account { Some(account) => Ok(account), None => { let account: Option = db.create(id).await?; - account.ok_or(Error::FailedToCreateDBEntry) + account.ok_or(Error::FailedToCreateDBEntry(ACCOUNT.to_string())) } } } @@ -49,6 +48,33 @@ impl Account { .await?; Ok(()) } + + pub async fn save(self, db: &Surreal) -> Result, Error> { + let account: Option = db.upsert(self.id.clone()).content(self).await?; + Ok(account) + } + + pub async fn operator_reg( + db: &Surreal, + wallet: &str, + email: &str, + escrow: u64, + ) -> Result<(), Error> { + if escrow < MIN_ESCROW { + return Err(Error::MinimalEscrow); + } + let mut op_account = Self::get(db, wallet).await?; + let escrow = escrow.saturating_mul(TOKEN_DECIMAL); + let op_total_balance = op_account.balance.saturating_add(op_account.escrow); + if op_total_balance < escrow { + return Err(Error::InsufficientFunds); + } + op_account.email = email.to_string(); + op_account.balance = op_total_balance.saturating_sub(escrow); + op_account.escrow = escrow; + op_account.save(db).await?; + Ok(()) + } } impl Account { @@ -84,7 +110,7 @@ impl From<&old_brain::BrainData> for Vec { let mut accounts = Vec::new(); for old_account in old_data.accounts.iter() { let mut a = Account { - id: RecordId::from(("account", old_account.key())), + id: RecordId::from((ACCOUNT, old_account.key())), balance: old_account.value().balance, tmp_locked: old_account.value().tmp_locked, escrow: 0, @@ -120,6 +146,24 @@ pub struct Kick { created_at: Datetime, reason: String, contract: RecordId, + node: RecordId, +} + +impl Kick { + pub async fn kicked_in_a_day(db: &Surreal, account: &str) -> Result, Error> { + let mut result = db + .query(format!( + "select * from {KICK} where out = {ACCOUNT}:{account} and created_at > time::now() - 24h;" + )) + .await?; + let kicks: Vec = result.take(0)?; + Ok(kicks) + } + + pub async fn submit(self, db: &Surreal) -> Result<(), Error> { + let _: Vec = db.insert(KICK).relation(self).await?; + Ok(()) + } } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -158,7 +202,7 @@ impl Report { /// This is the operator obtained from the DB, /// however the relation is defined using OperatorRelation -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Operator { pub account: RecordId, pub app_nodes: u64, @@ -231,3 +275,107 @@ impl Operator { Ok((operator, vm_nodes, app_nodes)) } } + +pub async fn kick_contract( + db: &Surreal, + operator_wallet: &str, + contract_uuid: &str, + reason: &str, +) -> Result { + let (contract_id, operator_id, admin_id, app_or_vm) = + if let Some(active_vm) = ActiveVmWithNode::get_by_uuid(db, contract_uuid).await? { + (active_vm.id, active_vm.vm_node.operator, active_vm.admin, "vm") + } else if let Some(active_app) = ActiveAppWithNode::get_by_uuid(db, contract_uuid).await? { + (active_app.id, active_app.app_node.operator, active_app.admin, "app") + } else { + return Err(Error::ContractNotFound); + }; + + if operator_id.key().to_string() != operator_wallet { + return Err(Error::AccessDenied); + } + + log::debug!("Kicking contract {contract_id} by operator {operator_id} for reason: '{reason}'",); + + let transaction_query = format!( + " + BEGIN TRANSACTION; + LET $contract = {contract_id}; + LET $operator_account = {operator_id}; + LET $reason = $reason_str_input; + LET $contract_id = record::id($contract.id); + LET $admin = $contract.in; + LET $node = $contract.out; + + LET $active_contract = (select * from $contract)[0]; + LET $deleted_contract = $active_contract.patch([{{ + 'op': 'replace', + 'path': 'id', + 'value': type::record('deleted_{app_or_vm}:' + $contract_id) + }}]); + LET $deleted_contract = (INSERT RELATION INTO deleted_{app_or_vm} ( $deleted_contract ) RETURN AFTER)[0]; + + -- calculating refund minutes + LET $one_week_minutes = duration::mins(1w); + LET $uncollected_minutes = (time::now() - $active_contract.collected_at).mins(); + + LET $minutes_to_refund = if $uncollected_minutes > $one_week_minutes {{ + $one_week_minutes; + }} ELSE {{ + $uncollected_minutes; + }}; + + -- calculating refund amount + LET $prince_per_minute = fn::{app_or_vm}_price_per_minute($active_contract.id); + + LET $refund_amount = $prince_per_minute * $minutes_to_refund; + + 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 = $contract_id, + reason = $reason, + contract = $deleted_contract.id, + node = $node.id, + created_at = time::now() + ; + DELETE $active_contract.id; + + -- update balances + UPDATE $operator_account SET escrow -= $refund; + IF $operator_account.escrow < 0 {{ + THROW 'Insufficient funds.' + }}; + UPDATE $admin SET balance += $refund; + + $refund; + + COMMIT TRANSACTION; + ", + ); + + log::trace!("kick_contract transaction_query: {}", &transaction_query); + + let mut query_res = + db.query(transaction_query).bind(("reason_str_input", reason.to_string())).await?; + + log::trace!("transaction_query response: {:?}", &query_res); + + let query_error = query_res.take_errors(); + if !query_error.is_empty() { + log::error!("kick_contract query error: {query_error:?}"); + return Err(Error::FailedKickContract(contract_id.to_string())); + } + + let refunded: Option = query_res.take(20)?; + let refunded_amount = refunded.ok_or(Error::FailedToCreateDBEntry("Refund".to_string()))?; + log::info!("Refunded: {refunded_amount} to {admin_id}"); + + Ok(refunded_amount) +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 500f7b3..cd06be1 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,7 +2,10 @@ pub mod app; pub mod general; pub mod vm; -use crate::constants::{APP_NODE, DELETED_APP, DELETED_VM, NEW_APP_REQ, NEW_VM_REQ, UPDATE_VM_REQ}; +use crate::constants::{ + APP_NODE, DB_SCHEMA_FILES, DELETED_APP, DELETED_VM, MIN_ESCROW, NEW_APP_REQ, NEW_VM_REQ, + UPDATE_VM_REQ, +}; use crate::old_brain; use prelude::*; use serde::{Deserialize, Serialize}; @@ -22,16 +25,26 @@ pub enum Error { StdIo(#[from] std::io::Error), #[error(transparent)] TimeOut(#[from] tokio::time::error::Elapsed), - #[error("Failed to create account")] - FailedToCreateDBEntry, + #[error("Failed to create {0}")] + FailedToCreateDBEntry(String), #[error("Unknown Table: {0}")] UnknownTable(String), #[error("Daemon channel got closed: {0}")] AppDaemonConnection(#[from] tokio::sync::mpsc::error::SendError), #[error("AppDaemon Error {0}")] NewAppDaemonResp(String), + #[error("Minimum escrow amount is {MIN_ESCROW}")] + MinimalEscrow, #[error("Insufficient funds, deposit more tokens")] InsufficientFunds, + #[error("Contract not found")] + ContractNotFound, + #[error("Access denied")] + AccessDenied, + #[error("Failed to delete contract {0}")] + FailedToDeleteContract(String), + #[error("Failed to kick contract {0}")] + FailedKickContract(String), } pub mod prelude { @@ -63,10 +76,15 @@ pub async fn migration0( let accounts: Vec = old_data.into(); let vm_nodes: Vec = old_data.into(); let app_nodes: Vec = old_data.into(); - let vm_contracts: Vec = old_data.into(); + let active_vm: Vec = old_data.into(); + let active_app: Vec = old_data.into(); - let schema = std::fs::read_to_string(crate::constants::DB_SCHEMA_FILE)?; - db.query(schema).await?; + for schema_data in DB_SCHEMA_FILES.map(|path| (std::fs::read_to_string(path), path)) { + let schema_file = schema_data.1; + println!("Loading schema from {schema_file}"); + let schema = schema_data.0?; + db.query(schema).await?; + } println!("Inserting accounts..."); let _: Vec = db.insert(()).content(accounts).await?; @@ -74,8 +92,10 @@ pub async fn migration0( let _: Vec = db.insert(()).content(vm_nodes).await?; println!("Inserting app nodes..."); let _: Vec = db.insert(()).content(app_nodes).await?; - println!("Inserting vm contracts..."); - let _: Vec = db.insert("vm_contract").relation(vm_contracts).await?; + println!("Inserting active vm contracts..."); + let _: Vec = db.insert(()).relation(active_vm).await?; + println!("Inserting app contracts..."); + let _: Vec = db.insert(()).relation(active_app).await?; Ok(()) } diff --git a/src/db/vm.rs b/src/db/vm.rs index 82acfc2..c9ddcee 100644 --- a/src/db/vm.rs +++ b/src/db/vm.rs @@ -219,7 +219,7 @@ impl NewVmReq { ->vm_node:{vm_node} CONTENT {{ created_at: time::now(), hostname: '{}', vcpus: {}, memory_mb: {}, disk_size_gb: {}, - extra_ports: {}, public_ipv4: {:?}, public_ipv6: {:?}, + extra_ports: {:?}, public_ipv4: {:?}, public_ipv6: {:?}, dtrfs_url: '{}', dtrfs_sha: '{}', kernel_url: '{}', kernel_sha: '{}', price_per_unit: {}, locked_nano: {locked_nano}, error: '' }}; @@ -229,7 +229,7 @@ impl NewVmReq { self.vcpus, self.memory_mb, self.disk_size_gb, - format!("{:?}", self.extra_ports,), + self.extra_ports, self.public_ipv4, self.public_ipv6, self.dtrfs_url, @@ -242,13 +242,11 @@ impl NewVmReq { let mut query_resp = db.query(query).await?; let resp_err = query_resp.take_errors(); - if let Some(insufficient_funds_error) = resp_err.get(&1) { - if let surrealdb::Error::Api(surrealdb::error::Api::Query(tx_query_error)) = - insufficient_funds_error - { - log::error!("Transaction error: {}", tx_query_error); - return Err(Error::InsufficientFunds); - } + if let Some(surrealdb::Error::Api(surrealdb::error::Api::Query(tx_query_error))) = + resp_err.get(&1) + { + log::error!("Transaction error: {tx_query_error}"); + return Err(Error::InsufficientFunds); } Ok(()) @@ -728,10 +726,36 @@ pub struct ActiveVmWithNode { pub collected_at: Datetime, } +impl From for ActiveVm { + fn from(val: ActiveVmWithNode) -> Self { + Self { + id: val.id, + admin: val.admin, + vm_node: val.vm_node.id, + hostname: val.hostname, + mapped_ports: val.mapped_ports, + public_ipv4: val.public_ipv4, + public_ipv6: val.public_ipv6, + disk_size_gb: val.disk_size_gb, + vcpus: val.vcpus, + memory_mb: val.memory_mb, + dtrfs_sha: val.dtrfs_sha, + kernel_sha: val.kernel_sha, + created_at: val.created_at, + price_per_unit: val.price_per_unit, + locked_nano: val.locked_nano, + collected_at: val.collected_at, + } + } +} + impl ActiveVmWithNode { pub async fn get_by_uuid(db: &Surreal, uuid: &str) -> Result, Error> { - let contract: Option = - db.query(format!("select * from {ACTIVE_VM}:{uuid} fetch out;")).await?.take(0)?; + let contract: Option = db + .query(format!("select * from {ACTIVE_VM} where id = $uuid_input fetch out;")) + .bind(("uuid_input", RecordId::from((ACTIVE_VM, uuid)))) + .await? + .take(0)?; Ok(contract) } diff --git a/src/grpc/app.rs b/src/grpc/app.rs index 1cf31ee..78be5af 100644 --- a/src/grpc/app.rs +++ b/src/grpc/app.rs @@ -115,7 +115,7 @@ impl BrainAppDaemon for AppDaemonServer { let mut req_stream = req.into_inner(); let pubkey: String; if let Some(Ok(msg)) = req_stream.next().await { - log::debug!("App daemon_messages received auth message: {:?}", msg); + log::debug!("App daemon_messages received auth message: {msg:?}"); if let Some(daemon_message_app::Msg::Auth(auth)) = msg.msg { pubkey = auth.pubkey.clone(); check_sig_from_parts( diff --git a/src/grpc/general.rs b/src/grpc/general.rs index 925e6e0..cf3bf2b 100644 --- a/src/grpc/general.rs +++ b/src/grpc/general.rs @@ -107,24 +107,49 @@ impl BrainGeneralCli for GeneralCliServer { async fn register_operator( &self, - _req: Request, + req: Request, ) -> Result, Status> { - todo!(); - // let req = check_sig_from_req(req)?; - // info!("Regitering new operator: {req:?}"); - // match self.data.register_operator(req) { - // Ok(()) => Ok(Response::new(Empty {})), - // Err(e) => Err(Status::failed_precondition(e.to_string())), - // } + let req = check_sig_from_req(req)?; + log::info!("Regitering new operator: {req:?}"); + match db::Account::operator_reg(&self.db, &req.pubkey, &req.email, req.escrow).await { + Ok(()) => Ok(Response::new(Empty {})), + Err(e) if matches!(e, db::Error::InsufficientFunds | db::Error::MinimalEscrow) => { + Err(Status::failed_precondition(e.to_string())) + } + Err(e) => { + log::info!("Failed to register operator: {e:?}"); + Err(Status::unknown( + "Unknown error. Please try again or contact the DeTEE devs team.", + )) + } + } } - async fn kick_contract(&self, _req: Request) -> Result, Status> { - todo!(); - // let req = check_sig_from_req(req)?; - // match self.data.kick_contract(&req.operator_wallet, &req.contract_uuid, &req.reason).await { - // Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })), - // Err(e) => Err(Status::permission_denied(e.to_string())), - // } + async fn kick_contract(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + log::info!("Kicking contract: {}, by: {}", req.contract_uuid, req.operator_wallet); + match db::kick_contract(&self.db, &req.operator_wallet, &req.contract_uuid, &req.reason) + .await + { + Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })), + Err(e) + if matches!( + e, + db::Error::ContractNotFound + | db::Error::AccessDenied + | db::Error::FailedToDeleteContract(_) + ) => + { + log::warn!("Failed to kick contract: {e:?}"); + Err(Status::failed_precondition(e.to_string())) + } + Err(e) => { + log::error!("Failed to kick contract: {e:?}"); + Err(Status::unknown( + "Unknown error. Please try again or contact the DeTEE devs team.", + )) + } + } } async fn ban_user(&self, _req: Request) -> Result, Status> { diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 4fbf0d4..c78c346 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -58,7 +58,7 @@ impl_pubkey_getter!(RegisterAppNodeReq); impl_pubkey_getter!(AppNodeFilters); pub fn check_sig_from_req(req: Request) -> Result { - log::trace!("Checking signature from request: {:?}", req); + log::trace!("Checking signature from request: {req:?}"); let time = match req.metadata().get("timestamp") { Some(t) => t.clone(), None => return Err(Status::unauthenticated("Timestamp not found in metadata.")), @@ -73,8 +73,7 @@ pub fn check_sig_from_req(req: Request) -> let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); if !(-4..=4).contains(&seconds_elapsed) { return Err(Status::unauthenticated(format!( - "Date is not within 4 sec of the time of the server: CLI {} vs Server {}", - parsed_time, now + "Date is not within 4 sec of the time of the server: CLI {parsed_time} vs Server {now}", ))); } @@ -131,8 +130,7 @@ pub fn check_sig_from_parts(pubkey: &str, time: &str, msg: &str, sig: &str) -> R let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); if !(-4..=4).contains(&seconds_elapsed) { return Err(Status::unauthenticated(format!( - "Date is not within 4 sec of the time of the server: CLI {} vs Server {}", - parsed_time, now + "Date is not within 4 sec of the time of the server: CLI {parsed_time} vs Server {now}", ))); } diff --git a/src/grpc/types.rs b/src/grpc/types.rs index dfde396..ee7e716 100644 --- a/src/grpc/types.rs +++ b/src/grpc/types.rs @@ -263,15 +263,15 @@ 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 as u32, - disk_mb: value.disk_size_gb as u32, - vcpu: value.vcpus as u32, - ports: value.mapped_ports.iter().map(|(_, g)| *g as u32).collect(), + memory_mb: value.memory_mb, + disk_mb: value.disk_size_gb, + vcpu: value.vcpus, + ports: value.mapped_ports.iter().map(|(_, g)| *g).collect(), }), mapped_ports: value .mapped_ports .iter() - .map(|(h, g)| MappedPort { host_port: *h as u32, guest_port: *g as u32 }) + .map(|(h, g)| MappedPort { host_port: *h, guest_port: *g }) .collect(), created_at: value.created_at.to_rfc3339(), @@ -294,7 +294,7 @@ impl From for db::NewAppReq { .public_package_mr_enclave .unwrap_or_default() .iter() - .fold(String::new(), |acc, x| acc + &format!("{:02x?}", x)); + .fold(String::new(), |acc, x| acc + &format!("{x:02x?}")); Self { id: RecordId::from((NEW_APP_REQ, nanoid!(40, &ID_ALPHABET))), @@ -377,7 +377,7 @@ impl From for NewAppRes { let mapped_ports = val .mapped_ports .iter() - .map(|(h, g)| MappedPort { host_port: *h as u32, guest_port: *g as u32 }) + .map(|(h, g)| MappedPort { host_port: *h, guest_port: *g }) .collect(); Self { uuid: val.id.key().to_string(), diff --git a/surql/functions.sql b/surql/functions.sql index 689635c..062b62d 100644 --- a/surql/functions.sql +++ b/surql/functions.sql @@ -25,3 +25,13 @@ DEFINE FUNCTION OVERWRITE fn::delete_vm( DELETE $vm.id; }; +DEFINE FUNCTION OVERWRITE fn::app_price_per_minute( + $app_id: record +) { + LET $app = (select * from $app_id)[0]; + RETURN + (($app.vcpus * 5) + + ($app.memory_mb / 200) + + ($app.disk_size_gb / 10)) + * $app.price_per_unit; +}; \ No newline at end of file diff --git a/surql/tables.sql b/surql/tables.sql index 278c7a1..0071da4 100644 --- a/surql/tables.sql +++ b/surql/tables.sql @@ -129,10 +129,8 @@ 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 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 DEFAULT time::now();; 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; @@ -144,6 +142,7 @@ DEFINE TABLE kick TYPE RELATION FROM account TO account; DEFINE FIELD created_at ON TABLE kick TYPE datetime; DEFINE FIELD reason ON TABLE kick TYPE string; DEFINE FIELD contract ON TABLE kick TYPE record; +DEFINE FIELD node ON TABLE kick TYPE record; DEFINE TABLE report TYPE RELATION FROM account TO vm_node|app_node; DEFINE FIELD created_at ON TABLE report TYPE datetime; diff --git a/tests/common/prepare_test_env.rs b/tests/common/prepare_test_env.rs index 7a1c825..96a8587 100644 --- a/tests/common/prepare_test_env.rs +++ b/tests/common/prepare_test_env.rs @@ -6,6 +6,7 @@ use dotenv::dotenv; use hyper_util::rt::TokioIo; use std::net::SocketAddr; use std::sync::Arc; +use surreal_brain::constants::DB_SCHEMA_FILES; use surreal_brain::grpc::general::GeneralCliServer; use surreal_brain::grpc::vm::{VmCliServer, VmDaemonServer}; use surrealdb::engine::remote::ws::Client; @@ -34,7 +35,9 @@ pub async fn prepare_test_db() -> Result> { .map_err(|e| anyhow!(e.to_string()))?; db.query(format!("REMOVE DATABASE {db_name}")).await?; - db.query(std::fs::read_to_string("interim_tables.surql")?).await?; + for schema in DB_SCHEMA_FILES.map(std::fs::read_to_string) { + db.query(schema?).await?; + } surreal_brain::db::migration0(&db, &old_brain_data).await?; Ok::<(), anyhow::Error>(()) }) diff --git a/tests/common/test_utils.rs b/tests/common/test_utils.rs index af25ecd..5ae3fae 100644 --- a/tests/common/test_utils.rs +++ b/tests/common/test_utils.rs @@ -1,9 +1,24 @@ use anyhow::Result; use detee_shared::vm_proto as snp_proto; use ed25519_dalek::{Signer, SigningKey}; +use itertools::Itertools; +use std::sync::OnceLock; use tonic::metadata::AsciiMetadataValue; use tonic::Request; +pub static ADMIN_KEYS: OnceLock> = OnceLock::new(); + +pub fn admin_keys() -> Vec { + let admin_keys = ADMIN_KEYS.get_or_init(|| { + let admin_keys = vec![Key::new(), Key::new(), Key::new()]; + let admin_pub_keys = admin_keys.iter().map(|k| k.pubkey.clone()).join(", "); + std::env::set_var("ADMIN_PUB_KEYS", admin_pub_keys); + admin_keys.clone() + }); + + admin_keys.clone() +} + #[derive(Debug, Clone)] pub struct Key { pub sg_key: SigningKey, diff --git a/tests/common/vm_cli_utils.rs b/tests/common/vm_cli_utils.rs index 2d7c2c3..3a1bb40 100644 --- a/tests/common/vm_cli_utils.rs +++ b/tests/common/vm_cli_utils.rs @@ -1,4 +1,4 @@ -use super::test_utils::Key; +use super::test_utils::{admin_keys, Key}; use anyhow::{anyhow, Result}; use detee_shared::common_proto::Empty; use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient; @@ -11,12 +11,11 @@ use surrealdb::engine::remote::ws::Client; use surrealdb::Surreal; use tonic::transport::Channel; -async fn airdrop(brain_channel: &Channel, wallet: &str, amount: u64) -> Result<()> { +pub async fn airdrop(brain_channel: &Channel, wallet: &str, amount: u64) -> Result<()> { let mut client = BrainGeneralCliClient::new(brain_channel.clone()); let airdrop_req = AirdropReq { pubkey: wallet.to_string(), tokens: amount }; - let admin_key = Key::new(); - std::env::set_var("ADMIN_PUB_KEYS", &admin_key.pubkey); + let admin_key = admin_keys()[0].clone(); client.airdrop(admin_key.sign_request(airdrop_req.clone())?).await?; diff --git a/tests/common/vm_daemon_utils.rs b/tests/common/vm_daemon_utils.rs index 58a9446..be49c3e 100644 --- a/tests/common/vm_daemon_utils.rs +++ b/tests/common/vm_daemon_utils.rs @@ -37,7 +37,7 @@ pub async fn register_vm_node( client: &mut BrainVmDaemonClient, key: &Key, operator_wallet: &str, -) -> Result> { +) -> Result> { log::info!("Registering vm_node: {}", key.pubkey); let node_pubkey = key.pubkey.clone(); @@ -53,18 +53,18 @@ pub async fn register_vm_node( let mut grpc_stream = client.register_vm_node(key.sign_request(req)?).await?.into_inner(); - let mut vm_contracts = Vec::new(); + let mut deleted_vm_reqs = Vec::new(); while let Some(stream_update) = grpc_stream.next().await { match stream_update { - Ok(vm_c) => { - vm_contracts.push(vm_c); + Ok(del_vm_rq) => { + deleted_vm_reqs.push(del_vm_rq); } Err(e) => { - panic!("Received error instead of vm_contracts: {e:?}"); + panic!("Received error instead of deleted_vm_reqs: {e:?}"); } } } - Ok(vm_contracts) + Ok(deleted_vm_reqs) } pub async fn daemon_listener( diff --git a/tests/grpc_general_test.rs b/tests/grpc_general_test.rs index a5b9537..e57c35b 100644 --- a/tests/grpc_general_test.rs +++ b/tests/grpc_general_test.rs @@ -1,17 +1,15 @@ use common::prepare_test_env::{ prepare_test_db, run_service_for_stream, run_service_in_background, }; -use common::test_utils::Key; -use common::vm_cli_utils::{create_new_vm, report_node}; +use common::test_utils::{admin_keys, Key}; +use common::vm_cli_utils::{airdrop, create_new_vm, report_node}; use common::vm_daemon_utils::{mock_vm_daemon, register_vm_node}; use detee_shared::common_proto::{Empty, Pubkey}; use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient; use detee_shared::general_proto::AirdropReq; use detee_shared::vm_proto::brain_vm_daemon_client::BrainVmDaemonClient; use futures::StreamExt; -use itertools::Itertools; -use std::vec; -use surreal_brain::constants::VM_NODE; +use surreal_brain::constants::{TOKEN_DECIMAL, VM_NODE}; use surreal_brain::db::vm::VmNodeWithReports; mod common; @@ -41,15 +39,12 @@ async fn test_general_airdrop() { // env_logger::builder().filter_level(log::LevelFilter::Trace).init(); prepare_test_db().await.unwrap(); - const AIRDROP_MULTIPLE: u64 = 1_000_000_000; let airdrop_amount = 10; let addr = run_service_in_background().await.unwrap(); let mut client = BrainGeneralCliClient::connect(format!("http://{}", addr)).await.unwrap(); - let admin_keys = vec![Key::new(), Key::new(), Key::new()]; - let admin_pub_keys = admin_keys.iter().map(|k| k.pubkey.clone()).join(", "); - std::env::set_var("ADMIN_PUB_KEYS", admin_pub_keys); + let admin_keys = admin_keys(); let user_01_key = Key::new(); let user_01_pubkey = user_01_key.pubkey.clone(); @@ -72,7 +67,7 @@ async fn test_general_airdrop() { let bal_req = user_01_key.sign_request(bal_req_data.clone()).unwrap(); let acc_bal_user_01 = client.get_balance(bal_req).await.unwrap().into_inner(); - assert_eq!(acc_bal_user_01.balance, airdrop_amount * AIRDROP_MULTIPLE); + assert_eq!(acc_bal_user_01.balance, airdrop_amount * TOKEN_DECIMAL); assert_eq!(acc_bal_user_01.tmp_locked, 0); // second airdrop from same admin @@ -84,7 +79,7 @@ async fn test_general_airdrop() { .unwrap() .into_inner(); - assert_eq!(acc_bal_user_01.balance, 2 * airdrop_amount * AIRDROP_MULTIPLE); + assert_eq!(acc_bal_user_01.balance, 2 * airdrop_amount * TOKEN_DECIMAL); // third airdrop from another admin let _ = client.airdrop(admin_keys[1].sign_request(airdrop_req.clone()).unwrap()).await.unwrap(); @@ -95,7 +90,7 @@ async fn test_general_airdrop() { .unwrap() .into_inner(); - assert_eq!(acc_bal_user_01.balance, 3 * airdrop_amount * AIRDROP_MULTIPLE); + assert_eq!(acc_bal_user_01.balance, 3 * airdrop_amount * TOKEN_DECIMAL); // self airdrop let airdrop_req = AirdropReq { pubkey: admin_keys[2].pubkey.clone(), tokens: airdrop_amount }; @@ -109,7 +104,7 @@ async fn test_general_airdrop() { .unwrap() .into_inner(); - assert_eq!(acc_bal_admin_3.balance, airdrop_amount * AIRDROP_MULTIPLE); + assert_eq!(acc_bal_admin_3.balance, airdrop_amount * TOKEN_DECIMAL); } #[tokio::test] @@ -127,6 +122,8 @@ async fn test_report_node() { log::info!("Report error: {:?}", report_error); assert!(report_error.to_string().contains("No contract found by this ID.")); + airdrop(&brain_channel, &key.pubkey, 10).await.unwrap(); + let active_vm_id = create_new_vm(&db, &key, &daemon_key, &brain_channel).await.unwrap(); let reason = String::from("something went wrong on vm"); @@ -211,3 +208,39 @@ 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::Info) + .filter_module("tungstenite", log::LevelFilter::Debug) + .filter_module("tokio_tungstenite", log::LevelFilter::Debug) + .init(); + + let db_conn = prepare_test_db().await.unwrap(); + let contract_uuid = "e3d01f252b2a410b80e312f44e474334"; + let operator_wallet = "7V3rEuh6j8VuwMVB5PyGqWKLmjJ4fYSv6WtrTL51NZTB"; + let reason = "'; THROW 'Injected error'; --"; // sql injection query + + let kick_response = + surreal_brain::db::general::kick_contract(&db_conn, operator_wallet, contract_uuid, reason) + .await; + match kick_response { + Ok(refund_amount) => { + println!("Refund amount: {}", refund_amount); + } + Err(e) => { + println!("Error: {}", e); + } + } +} diff --git a/tests/grpc_vm_cli_test.rs b/tests/grpc_vm_cli_test.rs index 4b6069d..b6d4c71 100644 --- a/tests/grpc_vm_cli_test.rs +++ b/tests/grpc_vm_cli_test.rs @@ -1,12 +1,14 @@ use common::prepare_test_env::{prepare_test_db, run_service_for_stream}; use common::test_utils::Key; -use common::vm_cli_utils::create_new_vm; +use common::vm_cli_utils::{airdrop, create_new_vm}; use common::vm_daemon_utils::{mock_vm_daemon, register_vm_node}; use detee_shared::vm_proto::brain_vm_cli_client::BrainVmCliClient; use detee_shared::vm_proto::brain_vm_daemon_client::BrainVmDaemonClient; use detee_shared::vm_proto::{ListVmContractsReq, NewVmReq}; use futures::StreamExt; use std::vec; +use surreal_brain::constants::ACTIVE_VM; +use surreal_brain::db::vm::ActiveVm; mod common; @@ -26,7 +28,11 @@ async fn test_vm_creation() { let grpc_error_message = new_vm_resp.err().unwrap().to_string(); assert!(grpc_error_message.contains("Insufficient funds")); - // TODO: Airdrop the user and try creating the VM again + airdrop(&brain_channel, &key.pubkey, 10).await.unwrap(); + + let new_vm_id = create_new_vm(&db, &key, &daemon_key, &brain_channel).await.unwrap(); + let active_vm: Option = db.select((ACTIVE_VM, new_vm_id)).await.unwrap(); + assert!(active_vm.is_some()); } #[tokio::test] @@ -51,6 +57,8 @@ async fn test_timeout_vm_creation() { ..Default::default() }; + airdrop(&brain_channel, &key.pubkey, 10).await.unwrap(); + let mut client_vm_cli = BrainVmCliClient::new(brain_channel.clone()); let timeout_error = client_vm_cli.new_vm(key.sign_request(new_vm_req).unwrap()).await.err().unwrap(); diff --git a/tests/grpc_vm_daemon_test.rs b/tests/grpc_vm_daemon_test.rs index 89d7983..c68c5da 100644 --- a/tests/grpc_vm_daemon_test.rs +++ b/tests/grpc_vm_daemon_test.rs @@ -2,6 +2,7 @@ use common::prepare_test_env::{ prepare_test_db, run_service_for_stream, run_service_in_background, }; use common::test_utils::Key; +use common::vm_cli_utils::airdrop; use common::vm_daemon_utils::{mock_vm_daemon, register_vm_node}; use detee_shared::vm_proto; use detee_shared::vm_proto::brain_vm_cli_client::BrainVmCliClient; @@ -29,7 +30,7 @@ async fn test_brain_message() { let brain_channel = run_service_for_stream().await.unwrap(); let daemon_key = mock_vm_daemon(&brain_channel).await.unwrap(); - let mut cli_client = BrainVmCliClient::new(brain_channel); + let mut cli_client = BrainVmCliClient::new(brain_channel.clone()); let cli_key = Key::new(); @@ -41,6 +42,7 @@ async fn test_brain_message() { locked_nano: 0, ..Default::default() }; + airdrop(&brain_channel, &cli_key.pubkey, 10).await.unwrap(); let new_vm_resp = cli_client.new_vm(cli_key.sign_request(req).unwrap()).await.unwrap().into_inner();