From 382e6f49549a0e039042e0b0c2e1009292d1028f Mon Sep 17 00:00:00 2001 From: mario4tier Date: Wed, 21 Dec 2022 17:12:00 -0500 Subject: [PATCH] (Close #6) Move impl. in dtp-core + new Host/Localhost in DTP API. --- Cargo.lock | 18 ++ Cargo.toml | 27 ++- crates/dtp-core/Cargo.toml | 8 +- crates/dtp-core/src/lib.rs | 29 +-- crates/dtp-core/src/network/host_internal.rs | 49 +++++ .../src/network/localhost_internal.rs | 54 +++++ crates/dtp-core/src/network/mod.rs | 126 ++++++++++++ crates/dtp-dev-app/src/main.rs | 16 +- crates/dtp-sdk/Cargo.toml | 2 + crates/dtp-sdk/src/lib.rs | 186 ++++++++++++------ crates/dtp-sdk/tests/api_tests.rs | 24 ++- crates/dtp-sdk/tests/common/mod.rs | 18 ++ crates/dtp-test-helper/Cargo.toml | 17 ++ crates/dtp-test-helper/src/lib.rs | 124 ++++++++++++ 14 files changed, 595 insertions(+), 103 deletions(-) create mode 100644 crates/dtp-core/src/network/host_internal.rs create mode 100644 crates/dtp-core/src/network/localhost_internal.rs create mode 100644 crates/dtp-core/src/network/mod.rs create mode 100644 crates/dtp-test-helper/Cargo.toml create mode 100644 crates/dtp-test-helper/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2a5d782..1d13e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2301,8 +2301,13 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" name = "dtp-core" version = "0.1.0" dependencies = [ + "anyhow", "cargo-husky", + "derivative", + "serial_test", + "sui-json-rpc-types", "sui-sdk", + "tokio", ] [[package]] @@ -2319,6 +2324,19 @@ dependencies = [ [[package]] name = "dtp-sdk" version = "0.1.0" +dependencies = [ + "anyhow", + "cargo-husky", + "dtp-core", + "dtp-test-helper", + "serial_test", + "sui-sdk", + "tokio", +] + +[[package]] +name = "dtp-test-helper" +version = "0.1.0" dependencies = [ "anyhow", "cargo-husky", diff --git a/Cargo.toml b/Cargo.toml index 0387f36..df4c561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [workspace] -resolver="2" -members = ["crates/dtp-core", - "crates/dtp-sdk", - "crates/dtp-dev-app" +resolver = "2" +members = [ + "crates/dtp-core", + "crates/dtp-sdk", + "crates/dtp-dev-app", + "crates/dtp-test-helper", ] [workspace.package] @@ -24,13 +26,15 @@ documentation = "https://docs.dtp.dev" # about DTP directories structure. #sui-sdk = { path = "../dtp-dev/sui-devnet/crates/sui-sdk/" } +#sui-json-rpc-types = { path = "../dtp-dev/sui-devnet/crates/sui-json-rpc-types/" } -# Comment this sui-sdk "git" dependency if using the above "path" dependency. +# Comment these sui-XXXXX "git" dependencies if using the above "path" dependencies. sui-sdk = { git = "https://github.com/MystenLabs/sui", branch = "devnet" } +sui-json-rpc-types = { git = "https://github.com/MystenLabs/sui", branch = "devnet", package = "sui-json-rpc-types" } + tokio = { version = "1.22.0", features = ["full"] } anyhow = "1.0.64" - [workspace.dependencies.serial_test] # Intended to be used as [dev-dependencies] only. # @@ -40,7 +44,7 @@ anyhow = "1.0.64" # Imagine a RWLock, the attributes do the following: # [serial] : Equivalent to a writer lock on the "RWLock". # [parallel] : Equivalent to a reader lock on the "RWLock". -# No Attribute : Can run at anytime, ignore the RWLock. +# No Attribute : Can run at anytime, ignores the RWLock. version = "=0.9.0" # Force the version to keep things safe. [workspace.dependencies.cargo-husky] @@ -50,5 +54,10 @@ version = "=0.9.0" # Force the version to keep things safe. # https://github.com/rhysd/cargo-husky#readme version = "=1.5.0" # Force the version to keep things safe. default-features = false # Disable features which are enabled by default -features = ["precommit-hook", "run-for-all", "run-cargo-test", "run-cargo-clippy", "run-cargo-fmt"] - +features = [ + "precommit-hook", + "run-for-all", + "run-cargo-test", + "run-cargo-clippy", + "run-cargo-fmt", +] diff --git a/crates/dtp-core/Cargo.toml b/crates/dtp-core/Cargo.toml index 5040a4d..16f22d7 100644 --- a/crates/dtp-core/Cargo.toml +++ b/crates/dtp-core/Cargo.toml @@ -1,3 +1,4 @@ +# Cargo.toml documentation: https://doc.rust-lang.org/cargo/reference/manifest.html [package] name = "dtp-core" version.workspace = true @@ -7,7 +8,12 @@ license.workspace = true documentation.workspace = true [dependencies] +tokio.workspace = true +anyhow.workspace = true sui-sdk.workspace = true +sui-json-rpc-types.workspace = true +derivative = "2.2.0" [dev-dependencies] -cargo-husky.workspace = true \ No newline at end of file +serial_test.workspace = true +cargo-husky.workspace = true diff --git a/crates/dtp-core/src/lib.rs b/crates/dtp-core/src/lib.rs index acd9943..cba1f91 100644 --- a/crates/dtp-core/src/lib.rs +++ b/crates/dtp-core/src/lib.rs @@ -1,22 +1,9 @@ -// Most of the internal complexity related to DTP will move here (dtp-core) such -// that the dtp-sdk will remain a thin layer focusing on providing a simplified -// view to the user. +// dtp-core crate is "internal" to DTP. // -// That would also make possible one day to have an "alternative/advanced" API to -// co-exist. - -// Dummy unused code for now. -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +// See instead the dtp-sdk crate for the public API of DTP. +// +// dtp-core contains most of DTP implementation and complexity. +// +// dtp-sdk is a thin layer providing a simplified view to the user (facade pattern). +// +pub mod network; diff --git a/crates/dtp-core/src/network/host_internal.rs b/crates/dtp-core/src/network/host_internal.rs new file mode 100644 index 0000000..1e48589 --- /dev/null +++ b/crates/dtp-core/src/network/host_internal.rs @@ -0,0 +1,49 @@ +use anyhow::bail; +use sui_sdk::types::base_types::{ObjectID, SuiAddress}; +use sui_sdk::SuiClient; +/* +use sui_json_rpc_types::{ + EventPage, MoveCallParams, OwnedObjectRef, RPCTransactionRequestParams, + SuiCertifiedTransaction, SuiData, SuiEvent, SuiEventEnvelope, SuiExecutionStatus, + SuiGasCostSummary, SuiObject, SuiObjectInfo, SuiObjectRead, SuiObjectRef, SuiParsedData, + SuiPastObjectRead, SuiRawData, SuiRawMoveObject, SuiTransactionAuthSignersResponse, + SuiTransactionData, SuiTransactionEffects, SuiTransactionResponse, TransactionBytes, + TransactionsPage, TransferObjectParams, +};*/ + +#[derive(Debug)] +pub struct HostInternal { + sui_id: ObjectID, +} + +pub(crate) async fn get_host_by_address( + sui_client: &SuiClient, + host_address: SuiAddress, +) -> Result { + let sui_id = ObjectID::from(host_address); + + let net_resp = sui_client + .read_api() + .get_parsed_object(sui_id) + .await + .unwrap(); + + let object = net_resp.object()?; + + // Sanity test that the id is as expected. + if object.id() != sui_id { + bail!("Bad object id {} != {}", object.id(), sui_id); + } + + // TODO Validate the object type, get the services provided etc... + + // Success. Host Move object transformed into a convenient local Host handle. + let ret = HostInternal { sui_id }; + Ok(ret) +} + +impl HostInternal { + pub fn get_sui_id(&self) -> &ObjectID { + &self.sui_id + } +} diff --git a/crates/dtp-core/src/network/localhost_internal.rs b/crates/dtp-core/src/network/localhost_internal.rs new file mode 100644 index 0000000..27eefb5 --- /dev/null +++ b/crates/dtp-core/src/network/localhost_internal.rs @@ -0,0 +1,54 @@ +use super::host_internal::*; +use sui_sdk::types::base_types::SuiAddress; +use sui_sdk::SuiClient; +/* +use sui_json_rpc_types::{ + EventPage, MoveCallParams, OwnedObjectRef, RPCTransactionRequestParams, + SuiCertifiedTransaction, SuiData, SuiEvent, SuiEventEnvelope, SuiExecutionStatus, + SuiGasCostSummary, SuiObject, SuiObjectInfo, SuiObjectRead, SuiObjectRef, SuiParsedData, + SuiPastObjectRead, SuiRawData, SuiRawMoveObject, SuiTransactionAuthSignersResponse, + SuiTransactionData, SuiTransactionEffects, SuiTransactionResponse, TransactionBytes, + TransactionsPage, TransferObjectParams, +};*/ + +#[derive(Debug)] +pub struct LocalhostInternal { + admin_address: SuiAddress, + firewall_initialized: bool, +} + +pub(crate) async fn get_localhost_by_address( + sui_client: &SuiClient, + client_address: SuiAddress, + host_address: SuiAddress, +) -> Result<(HostInternal, LocalhostInternal), anyhow::Error> { + // Do the equivalent of get_host_by_address, but + // create a handle that will allow for administrator + // capabilities. + #[allow(clippy::needless_borrow)] + let host_internal = + super::host_internal::get_host_by_address(&sui_client, host_address).await?; + + let localhost_internal = LocalhostInternal { + admin_address: client_address, + firewall_initialized: false, + }; + + Ok((host_internal, localhost_internal)) +} + +impl LocalhostInternal { + pub fn get_admin_address(&self) -> &SuiAddress { + &self.admin_address + } + + pub(crate) async fn init_firewall( + &mut self, + _sui_client: &SuiClient, + ) -> Result<(), anyhow::Error> { + // Dummy mutable for now... just to test the software design "layering" + // with a mut. + self.firewall_initialized = true; + Ok(()) + } +} diff --git a/crates/dtp-core/src/network/mod.rs b/crates/dtp-core/src/network/mod.rs new file mode 100644 index 0000000..1038165 --- /dev/null +++ b/crates/dtp-core/src/network/mod.rs @@ -0,0 +1,126 @@ +use derivative::Derivative; +use sui_sdk::types::base_types::{ObjectID, SuiAddress}; +use sui_sdk::SuiClient; + +// Flatten many sub modules/files under the same dtp_core::network module. +// +// Allows to do: +// use dtp_core::network::{NetworkManager, HostInternal, LocalhostInternal} +// +// Instead of: +// use dtp_core::network::NetworkManager; +// use dtp_core::network::host_internal::HostInternal; +// use dtp_core::network::localhost_internal::LocalhostInternal; +pub use self::host_internal::*; +pub use self::localhost_internal::*; + +mod host_internal; +mod localhost_internal; + +// NetworkManager +// +// Perform network objects management associated to a single client address. +// Includes creation, deletion, indexing etc... +// +// A client address should be associated to only one NetworkManager instance (to +// prevent some equivocation scenarios). +// +// A Sui network object can have multiple local handles (say to represent the object +// at different point in time), and any handle can be used to interact with the +// latest version of the object on the network. +// +// For every handles in the API there is a one-to-one relationship with +// an 'Internal' version that encapsulate most of the implementation. +// +// Examples: +// An API dtp-sdk::Host --- owns a ----> dtp-core::HostInternal +// An API dtp-sdk::Locahost --- owns a ----> dtp-core::LocalhostInternal +// +#[derive(Derivative)] +#[derivative(Debug)] +pub struct NetworkManager { + package_id: ObjectID, + client_address: SuiAddress, + #[derivative(Debug = "ignore")] + sui_client: SuiClient, +} + +impl NetworkManager { + pub async fn new( + client_address: SuiAddress, + http_url: &str, + ws_url: Option<&str>, + ) -> Result { + let sui_client = SuiClient::new(http_url, ws_url, None).await?; + Ok(NetworkManager { + package_id: ObjectID::ZERO, // TODO Revisit this when mainnet. + client_address, + sui_client, + }) + } + + // Accessors + pub fn get_client_address(&self) -> &SuiAddress { + &self.client_address + } + pub fn get_package_id(&self) -> &ObjectID { + &self.package_id + } + pub fn get_sui_client(&self) -> &SuiClient { + &self.sui_client + } // Needed? + + // Mutators + pub fn set_package_id(&mut self, package_id: ObjectID) { + self.package_id = package_id; + } + + // Accessors that do a JSON-RPC call. + pub async fn get_host_by_address( + &self, + host_address: SuiAddress, + ) -> Result { + get_host_by_address(&self.sui_client, host_address).await + } + + pub async fn get_localhost_by_address( + &self, + host_address: SuiAddress, + ) -> Result<(HostInternal, LocalhostInternal), anyhow::Error> { + get_localhost_by_address(&self.sui_client, self.client_address, host_address).await + } + + // Mutators that do a JSON-RPC call and transaction. + pub async fn init_firewall( + &self, + localhost: &mut LocalhostInternal, + ) -> Result<(), anyhow::Error> { + // TODO Verify here client_address == localhost.admin_address + // Detect user error. + localhost.init_firewall(&self.sui_client).await + } +} + +/* +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instantiate_network_manager() -> Result<(), anyhow::Error> { + // TODO + Ok(()) + } + + #[test] + fn instantiate_hostinternal() -> Result<(), anyhow::Error> { + // TODO + Ok(()) + } + + #[test] + fn instantiate_localhostinternal() -> Result<(), anyhow::Error> { + // TODO + Ok(()) + } +}*/ diff --git a/crates/dtp-dev-app/src/main.rs b/crates/dtp-dev-app/src/main.rs index 889f27c..85cfba2 100644 --- a/crates/dtp-dev-app/src/main.rs +++ b/crates/dtp-dev-app/src/main.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use sui_sdk::types::base_types::SuiAddress; use sui_sdk::SuiClient; -use dtp_sdk::{ConnectionApi, Host, Localhost, DTP}; +use dtp_sdk::{Host, Localhost, DTP}; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -14,17 +14,17 @@ async fn main() -> Result<(), anyhow::Error> { let own_address = SuiAddress::from_str("0xcfed50a652b8fce7a7917a8a736a7c2b1d646ba2")?; - let dtp: DTP = DTP::new("http://0.0.0.0:9000", None).await?; - let con_api: &ConnectionApi = dtp.connection_api(); + let dtp: DTP = DTP::new(own_address, "http://0.0.0.0:9000", None).await?; let peer_address = SuiAddress::from_str("0xcfed50a652b8fce7a7917a8a736a7c2b1d646ba2")?; - let peer_node: Host = con_api.get_host_by_address(peer_address).await?; - let own_node: Localhost = con_api - .get_localhost_by_address(own_address, own_address) - .await?; + let peer_node: Host = dtp.get_host_by_address(peer_address).await?; + let own_node: Localhost = dtp.get_localhost_by_address(own_address).await?; - println!("Ping result is {:?}", con_api.ping(&own_node, &peer_node)); + println!( + "Ping result is {:?}", + dtp.ping(&own_node, &peer_node).await? + ); Ok(()) } diff --git a/crates/dtp-sdk/Cargo.toml b/crates/dtp-sdk/Cargo.toml index 652afcc..82c2036 100644 --- a/crates/dtp-sdk/Cargo.toml +++ b/crates/dtp-sdk/Cargo.toml @@ -11,7 +11,9 @@ documentation.workspace = true tokio.workspace = true anyhow.workspace = true sui-sdk.workspace = true +dtp-core = { path = "../dtp-core/" } [dev-dependencies] +dtp-test-helper = { path = "../dtp-test-helper/" } serial_test.workspace = true cargo-husky.workspace = true diff --git a/crates/dtp-sdk/src/lib.rs b/crates/dtp-sdk/src/lib.rs index 2344d6a..bed58a1 100644 --- a/crates/dtp-sdk/src/lib.rs +++ b/crates/dtp-sdk/src/lib.rs @@ -1,122 +1,186 @@ -// For now, just focusing on getting the SDKs dependencies right, code is meaningless. -use std::str::FromStr; -use sui_sdk::types::base_types::{ObjectID, SuiAddress}; -use sui_sdk::SuiClient; - -// High-Level Design (very rough for now, likely to evolve): +// DTP SDK API // -// - Create a DTPClient for every committe intended to be used. +// For most app, only one instance of DTP object will be needed. // -// - Use a DTPClient to access its various APIs e.g. dtp_client.connection_api() +// There is a one-to-one relationship between a Sui client address +// and a DTP instance. // -// - A Shared Object on the Sui network can have one or more local handles. +// A DTP object can create multiple child objects, e.g. Host, LocalHost... +// Operations on these childs are enforced to be done only in context of its +// parent. // -// Example: connection_api.get_peer_node_by_address() creates a local -// handle of an existing Sui Node Shared Object that belongs -// to someone else. +// Multiple DTP instance can co-exist in the same app. // -// When an handle is created it copies a subset of fields from the network object. +// Caller Responsibilities: +// - Do not instantiate two DTP object for the same client address. +// It may work, but it may also result in equivocation deadlock of +// the Sui network and the client might be unuseable until start +// of next epoch. // -// These fields can change only when the handle update() method is successfully -// called. +// - A DTP instance (and its children) must all be access within a +// single thread (or be Mutex protected by the caller). // -// Since the same network object can have multiple handles, it is possible -// to, as an example, identify change in a field using two handles. +// - Doing operations that mix children with the wrong DTP parent will +// be detected and result in a TBD error. +// +// TODO Define the error. + +use anyhow::bail; +use dtp_core::network::{HostInternal, LocalhostInternal, NetworkManager}; +use sui_sdk::types::base_types::{ObjectID, SuiAddress}; -#[derive(Debug)] #[allow(dead_code)] +#[derive(Debug)] pub struct Host { sui_id: ObjectID, + // Hidden implementation in dtp-core. + host_internal: HostInternal, } // Similar to Host, but with additional functionality available -// assuming you are the administrator of the Host. +// assuming the caller is the administrator of the Host. #[allow(dead_code)] pub struct Localhost { host: Host, + // Hidden implementation in dtp-core. + localhost_internal: LocalhostInternal, } -// Provides all the DTP SDK APIs. -#[allow(dead_code)] pub struct DTP { - connection_api: ConnectionApi, - sui_client: SuiClient, + // Implementation hidden in dtp-core. + netmgr: NetworkManager, } impl DTP { - pub async fn new(http_url: &str, ws_url: Option<&str>) -> Result { - let connection_api = ConnectionApi; - - // TODO Revisit if should be done here or caller should own it... - let sui_client = SuiClient::new(http_url, ws_url, None).await?; - + pub async fn new( + client_address: SuiAddress, + http_url: &str, + ws_url: Option<&str>, + ) -> Result { Ok(DTP { - connection_api, - sui_client, + #[allow(clippy::needless_borrow)] + netmgr: NetworkManager::new(client_address, &http_url, ws_url).await?, }) } - pub fn connection_api(&self) -> &ConnectionApi { - &self.connection_api + // Light Mutators + // JSON-RPC: No + // Gas Cost: No + pub fn set_package_id(&mut self, package_id: ObjectID) { + self.netmgr.set_package_id(package_id); } - pub fn sui_client(&self) -> &SuiClient { - &self.sui_client + // Light Accessors + // JSON-RPC: No + // Gas Cost: No + pub fn package_id(&self) -> &ObjectID { + self.netmgr.get_package_id() + } + pub fn client_address(&self) -> &SuiAddress { + self.netmgr.get_client_address() } -} -pub struct ConnectionApi; -impl ConnectionApi { + // get_host_by_address + // JSON-RPC: Yes + // Gas Cost: No + // // Get an handle of any DTP Host expected to be already on the Sui network. // - // The handle is used for doing operations such as ping the host and create connections. + // The handle is used for doing various operations such as pinging the host + // off-chain server and/or create a connection to it. pub async fn get_host_by_address( &self, - _host_address: SuiAddress, + host_address: SuiAddress, ) -> Result { - // TODO Mocking for now, but calling into Sui SDK for conversion. + let host_internal = self.netmgr.get_host_by_address(host_address).await?; Ok(Host { - sui_id: ObjectID::from_str("0x6205fc058b205227d7b7bd5b4e7802f0055157c6")?, + sui_id: *host_internal.get_sui_id(), + host_internal, }) } + // get_localhost_by_address + // JSON-RPC: Yes + // Gas Cost: No + // // Get an handle of a DTP Host that your application controls. // // It is expected that the host already exist on the network, if not, // then see create_localhost(). // + // TODO Add clear error if not own. pub async fn get_localhost_by_address( &self, localhost_address: SuiAddress, // Address of the targeted localhost. - _admin_address: SuiAddress, // Administrator address for the localhost. ) -> Result { - // TODO Mocking for now, but calling into Sui SDK for conversion. - let host = self.get_host_by_address(localhost_address).await?; - Ok(Localhost { host }) + let (host_internal, localhost_internal) = self + .netmgr + .get_localhost_by_address(localhost_address) + .await?; + let host = Host { + sui_id: *host_internal.get_sui_id(), + host_internal, + }; + Ok(Localhost { + host, + localhost_internal, + }) } // Create a new DTP Host on the Sui network. // - // The host object created on the network will be retreiveable - // as a DTP::Host handle for everyone (see get_host_XXXX). + // The shared object created on the network will be retreiveable + // as a read-only DTP::Host handle for everyone (see get_host_xxxx). // // For the administrator the same object can also be retreiveable - // as a DTP::Localhost handle (see get_localhost_xxxx). + // as a read/write DTP::Localhost handle (see get_localhost_xxxx). // - // The administrator is the only one allowed to configure the - // host object on the network using the Localhost handle. + /* pub async fn create_host_on_network(&self) -> Result { - // TODO Mocking for now, but calling into Sui SDK for conversion. - Ok(Localhost { - host: Host { - sui_id: ObjectID::from_str("0x6205fc058b205227d7b7bd5b4e7802f0055157c6")?, - }, - }) + Ok(()) + }*/ + + // Ping Service + // JSON-RPC: Yes + // Gas Cost: Yes + pub async fn ping( + &self, + _localhost: &Localhost, + _target_host: &Host, + ) -> Result<(), anyhow::Error> { + // Verify parameters are children of this NetworkManager. + // + // Particularly useful for the Localhost for an early detection + // of trying to access with an incorrect client_address + // (early failure --> no gas wasted). + /* + let &parent_id = self.netmgr.id(); + let &source_host = localhost.host; + if (source_host.get_parent_id() != parent_id) {} + + if (target_host.get_parent_id() != parent_id) {} + + if (source_host.get_object_id() == target_host.get_object_id()) {} + + self.netmgr.ping(localhost, target_host) + */ + Ok(()) } - pub fn ping(&self, _own_node: &Localhost, _peer_node: &Host) -> Result { - Ok(true) + // Initialize Firewall Service + // JSON-RPC: Yes + // Gas Cost: Yes + // + // The firewall will be configureable from this point, but not yet enabled. + + pub async fn init_firewall(&self, localhost: &mut Localhost) -> Result<(), anyhow::Error> { + // Detect API user mistakes. + if self.netmgr.get_client_address() != localhost.localhost_internal.get_admin_address() { + bail!("Localhost object unrelated to this DTP object") + } + + self.netmgr + .init_firewall(&mut localhost.localhost_internal) + .await } } - -// The API requires both localnet/devnet access. See dtp-sdk/tests for API integration tests. diff --git a/crates/dtp-sdk/tests/api_tests.rs b/crates/dtp-sdk/tests/api_tests.rs index eb21e9a..578e582 100644 --- a/crates/dtp-sdk/tests/api_tests.rs +++ b/crates/dtp-sdk/tests/api_tests.rs @@ -1,10 +1,28 @@ -use dtp_sdk::{ConnectionApi, DTP}; +use dtp_sdk::DTP; +//use sui_sdk::types::base_types::{ObjectID, SuiAddress}; +//use anyhow::anyhow; + +use dtp_test_helper::{Client, SuiNetworkForTest}; use serial_test::serial; +mod common; #[tokio::test] #[serial] async fn localhost_instantiation_localnet() -> Result<(), anyhow::Error> { - let dtp: DTP = DTP::new("http://0.0.0.0:9000", None).await?; - let _con_api: &ConnectionApi = dtp.connection_api(); + let network: SuiNetworkForTest = common::setup_localnet()?; + + let owner = network.get_client_address(Client::Test1).clone(); + let mut dtp: DTP = DTP::new(owner, "http://0.0.0.0:9000", None).await?; + + dtp.set_package_id(network.dtp_package_id); // This won't be needed for mainnet. + + // Test API to create a Localhost. + // + // Localhost is an handle on a Host shared object that can be + // administrated only by this sender. + //let localhost = dtp.create_host_on_network().await?; + + //assert!(network.object_exists(&localhost.get_object_id()).await?); + Ok(()) } diff --git a/crates/dtp-sdk/tests/common/mod.rs b/crates/dtp-sdk/tests/common/mod.rs index e69de29..2f1a830 100644 --- a/crates/dtp-sdk/tests/common/mod.rs +++ b/crates/dtp-sdk/tests/common/mod.rs @@ -0,0 +1,18 @@ +// Function to facilitate interacting with a localnet. +// +// Will verify: +// - Sui binary is installed. +// - sui process is running for localnet (devnet branch). +// - Latest Move file changes are compiled and publish on localnet. +// +// Can safely be called even if everything has already been setup. +// +// +use anyhow; +use dtp_test_helper::SuiNetworkForTest; +//use sui_sdk::types::base_types::{ObjectID, SuiAddress}; + +pub fn setup_localnet() -> Result { + let network = SuiNetworkForTest::new()?; + Ok(network) +} diff --git a/crates/dtp-test-helper/Cargo.toml b/crates/dtp-test-helper/Cargo.toml new file mode 100644 index 0000000..d614e2e --- /dev/null +++ b/crates/dtp-test-helper/Cargo.toml @@ -0,0 +1,17 @@ +# Cargo.toml documentation: https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "dtp-test-helper" +version.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +documentation.workspace = true + +[dependencies] +tokio.workspace = true +anyhow.workspace = true +sui-sdk.workspace = true + +[dev-dependencies] +serial_test.workspace = true +cargo-husky.workspace = true diff --git a/crates/dtp-test-helper/src/lib.rs b/crates/dtp-test-helper/src/lib.rs new file mode 100644 index 0000000..dcc8d3a --- /dev/null +++ b/crates/dtp-test-helper/src/lib.rs @@ -0,0 +1,124 @@ +// Access to the following information: +// - ObjectID of the last DTP Move Package that was published on localnet. +// - Various client_address with gas coins on that localnet. +// +// This module does not mutate the localnet (all read-only). +// +// Note: It is assumed that localnet is running and DTP modules were +// published to it. if not, then run "dtp/script/publish-localnet". +use std::fs::{self, File}; +use std::io::{self, BufRead}; +use std::path::Path; +use std::str::FromStr; + +use anyhow::Result; +use sui_sdk::types::base_types::{ObjectID, SuiAddress}; + +#[allow(dead_code)] +pub enum Client { + DevApp = 0, + Demo1 = 1, + Demo2 = 2, + Test1 = 3, + Test2 = 4, +} + +pub struct SuiNetworkForTest { + pub dtp_package_id: ObjectID, // Last package that was published. + + // localnet provides 5 owner addresses with funds. + // + // These addresses are "reserved" in a way that various process + // won't interfere with each other. + // + // 0: Reserved for dtp-dev-app. + // [1..2]: Reserved for demo with DTP daemon. + // [3..4]: Reserved for dtp-sdk integration tests. + // + + // Use get_client_address() for access. + client_addresses: Vec, +} + +impl SuiNetworkForTest { + fn read_lines

(filename: P) -> io::Result>> + where + P: AsRef, + { + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) + } + + fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } + } + + pub fn new() -> Result { + // Get the pre-funded clients from client_addresses.txt + let path = env!("CARGO_MANIFEST_DIR"); + let pathname = format!( + "{}{}", + path, "/../../../dtp-dev/publish_data/localnet/package_id.txt" + ); + + // Get the DTP package id from when it was last published. + let mut package_id_hex = fs::read_to_string(&pathname)?; + SuiNetworkForTest::trim_newline(&mut package_id_hex); + + let dtp_package_id = ObjectID::from_hex_literal(&package_id_hex)?; + // TODO .with_context(|| format!("Failed to parse package id in {}", pathname)?; + + let mut ret = SuiNetworkForTest { + dtp_package_id, + client_addresses: vec![], + }; + + // Get the client addresses. + let pathname = format!( + "{}{}", + path, "/../../../dtp-dev/publish_data/localnet/client_addresses.txt" + ); + if let Ok(lines) = SuiNetworkForTest::read_lines(pathname) { + for line in lines.flatten() { + ret.client_addresses + .push(SuiAddress::from_str(line.as_str())?); + } + } + dbg!(ret.client_addresses.len()); + assert!(ret.client_addresses.len() == 5); + Ok(ret) + } + + pub fn get_client_address(&self, client: Client) -> &SuiAddress { + // Sui localnet should always have 5 clients. + // + // If bailing out here (out of bound), then check if something + // is not right when client_addresses.txt was loaded. + match client { + Client::DevApp => &self.client_addresses[0], + Client::Demo1 => &self.client_addresses[1], + Client::Demo2 => &self.client_addresses[2], + Client::Test1 => &self.client_addresses[3], + Client::Test2 => &self.client_addresses[4], + } + } + + pub async fn object_exists( + self: &SuiNetworkForTest, + _id: &ObjectID, + ) -> Result { + Ok(true) + } + + pub async fn address_exists( + self: &SuiNetworkForTest, + _address: &SuiAddress, + ) -> Result { + Ok(true) + } +}