From 89315cb5031bfc67707ca10c7ec03025353211e9 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Tue, 9 Jan 2024 16:04:41 +0000 Subject: [PATCH] move sui-oracle move package to public repo (#12922) ## Description Sui Oracle is a general purpose oracle that can be used to fetch data from external sources and feed it to the Sui Chain. It is designed to be modular and extensible, so that it can be easily extended to support new data sources and new data types. ## Test Plan Move test + manual integration tests ## TODOs We need to add more documentation to explain the design of this package and turn it into an example. --- If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process. ### Type of Change (Check all that apply) - [ ] protocol change - [ ] user-visible impact - [ ] breaking change for a client SDKs - [ ] breaking change for FNs (FN binary must upgrade) - [ ] breaking change for validators or node operators (must upgrade binaries) - [ ] breaking change for on-chain data layout - [ ] necessitate either a data wipe or data migration ### Release notes --- Cargo.lock | 9 +- crates/sui-oracle/Cargo.toml | 12 +- crates/sui-oracle/move/oracle/Move.toml | 10 + .../sui-oracle/move/oracle/sources/data.move | 52 ++ .../move/oracle/sources/decimal_value.move | 21 + .../move/oracle/sources/meta_oracle.move | 260 ++++++++ .../move/oracle/sources/simple_oracle.move | 123 ++++ crates/sui-oracle/tests/data/Test/Move.toml | 11 + .../tests/data/Test/sources/test_module.move | 85 +++ crates/sui-oracle/tests/integration_tests.rs | 586 ++++++++++++++++++ 10 files changed, 1165 insertions(+), 4 deletions(-) create mode 100644 crates/sui-oracle/move/oracle/Move.toml create mode 100644 crates/sui-oracle/move/oracle/sources/data.move create mode 100644 crates/sui-oracle/move/oracle/sources/decimal_value.move create mode 100644 crates/sui-oracle/move/oracle/sources/meta_oracle.move create mode 100644 crates/sui-oracle/move/oracle/sources/simple_oracle.move create mode 100644 crates/sui-oracle/tests/data/Test/Move.toml create mode 100644 crates/sui-oracle/tests/data/Test/sources/test_module.move create mode 100644 crates/sui-oracle/tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index bb222eb8cb0af..896662bcd76ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,9 +1563,9 @@ dependencies = [ [[package]] name = "bcs" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b06b4c1f053002b70e7084ac944c77d58d5d92b2110dbc5e852735e00ad3ccc" +checksum = "85b6598a2f5d564fb7855dc6b06fd1c38cff5a72bd8b863a4d021938497b440a" dependencies = [ "serde", "thiserror", @@ -12542,15 +12542,20 @@ dependencies = [ "bcs", "chrono", "clap", + "dirs 4.0.0", "jsonpath_lib", "mysten-metrics", "once_cell", "prometheus", + "rand 0.8.5", "reqwest", "serde", "serde_json", + "shared-crypto", "sui-config", "sui-json-rpc-types", + "sui-keys", + "sui-move-build", "sui-sdk", "sui-types", "tap", diff --git a/crates/sui-oracle/Cargo.toml b/crates/sui-oracle/Cargo.toml index 34e0f2e34a3ff..bfb48eb488e61 100644 --- a/crates/sui-oracle/Cargo.toml +++ b/crates/sui-oracle/Cargo.toml @@ -13,9 +13,9 @@ prometheus = "0.13.3" tokio = { workspace = true, features = ["full"] } tracing = "0.1.36" once_cell.workspace = true -reqwest = { version = "0.11.13", default_features= false, features = ["blocking", "json", "rustls-tls"] } +reqwest = { version = "0.11.13", default_features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0.144", features = ["derive", "rc"] } -serde_json = { version = "1.0.1"} +serde_json = { version = "1.0.1" } jsonpath_lib = "0.3.0" chrono.workspace = true tap.workspace = true @@ -28,3 +28,11 @@ sui-types = { path = "../sui-types" } mysten-metrics = { path = "../mysten-metrics" } telemetry-subscribers.workspace = true workspace-hack.workspace = true + +[dev-dependencies] +sui-keys = { path = "../sui-keys" } +sui-move-build = { path = "../sui-move-build" } +shared-crypto = { path = "../shared-crypto" } +bcs = "0.1.5" +rand = "0.8.5" +dirs = "4.0.0" diff --git a/crates/sui-oracle/move/oracle/Move.toml b/crates/sui-oracle/move/oracle/Move.toml new file mode 100644 index 0000000000000..05c38235009f4 --- /dev/null +++ b/crates/sui-oracle/move/oracle/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "sui-oracle" +version = "0.0.1" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "9d0fba4a490e1cf80101bbd4019c7bb1ccfce99b" } + +[addresses] +oracle = "0x0" +sui = "0x2" \ No newline at end of file diff --git a/crates/sui-oracle/move/oracle/sources/data.move b/crates/sui-oracle/move/oracle/sources/data.move new file mode 100644 index 0000000000000..75edce5b15a22 --- /dev/null +++ b/crates/sui-oracle/move/oracle/sources/data.move @@ -0,0 +1,52 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module oracle::data { + use std::string::String; + + struct Data has drop, copy { + value: T, + metadata: Metadata, + } + + struct Metadata has drop, copy { + ticker: String, + sequence_number: u64, + timestamp: u64, + oracle: address, + /// An identifier for the reading (for example real time of observation, or sequence number of observation on other chain). + identifier: String, + } + + public fun new( + value: T, + ticker: String, + sequence_number: u64, + timestamp: u64, + oracle: address, + identifier: String + ): Data { + Data { + value, + metadata: Metadata { + ticker, + sequence_number, + timestamp, + oracle, + identifier, + }, + } + } + + public fun value(data: &Data): &T { + &data.value + } + + public fun oracle_address(data: &Data): &address { + &data.metadata.oracle + } + + public fun timestamp(data: &Data): u64 { + data.metadata.timestamp + } +} diff --git a/crates/sui-oracle/move/oracle/sources/decimal_value.move b/crates/sui-oracle/move/oracle/sources/decimal_value.move new file mode 100644 index 0000000000000..5322953539756 --- /dev/null +++ b/crates/sui-oracle/move/oracle/sources/decimal_value.move @@ -0,0 +1,21 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module oracle::decimal_value { + struct DecimalValue has store, drop, copy { + value: u64, + decimal: u8, + } + + public fun new(value: u64, decimal: u8): DecimalValue { + DecimalValue { value, decimal } + } + + public fun value(self: &DecimalValue): u64 { + self.value + } + + public fun decimal(self: &DecimalValue): u8 { + self.decimal + } +} diff --git a/crates/sui-oracle/move/oracle/sources/meta_oracle.move b/crates/sui-oracle/move/oracle/sources/meta_oracle.move new file mode 100644 index 0000000000000..0d027c53549d7 --- /dev/null +++ b/crates/sui-oracle/move/oracle/sources/meta_oracle.move @@ -0,0 +1,260 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module oracle::meta_oracle { + use std::option::{Self, Option}; + use std::string::String; + use std::type_name; + use std::vector; + + use oracle::data::{Self, Data}; + use oracle::decimal_value::DecimalValue; + use oracle::simple_oracle::SimpleOracle; + use sui::bcs; + use sui::math; + + #[test_only] + use oracle::decimal_value; + + const EValidDataSizeLessThanThreshold: u64 = 0; + const EUnsupportedDataType: u64 = 1; + + struct MetaOracle { + oracle_data: vector>>, + threshold: u64, + time_window_ms: u64, + ticker: String, + max_timestamp: u64, + } + + public fun new(threshold: u64, time_window_ms: u64, ticker: String): MetaOracle { + MetaOracle { + oracle_data: vector::empty(), + threshold, + time_window_ms, + ticker, + max_timestamp: 0, + } + } + + public fun add_simple_oracle(meta_oracle: &mut MetaOracle, oracle: &SimpleOracle) { + let oracle_data = oracle::simple_oracle::get_latest_data(oracle, meta_oracle.ticker); + if (option::is_some(&oracle_data)) { + meta_oracle.max_timestamp = data::timestamp(option::borrow(&oracle_data)); + }; + vector::push_back(&mut meta_oracle.oracle_data, oracle_data); + } + + struct TrustedData has copy, drop { + value: T, + oracles: vector
, + } + + fun combine(meta_oracle: MetaOracle, ): (vector, vector
) { + let MetaOracle { oracle_data, threshold, time_window_ms, ticker: _, max_timestamp } = meta_oracle; + let min_timestamp = max_timestamp - time_window_ms; + let values = vector[]; + let oracles = vector
[]; + while (vector::length(&oracle_data) > 0) { + let oracle_data = vector::remove(&mut oracle_data, 0); + if (option::is_some(&oracle_data)) { + let oracle_data = option::destroy_some(oracle_data); + if (data::timestamp(&oracle_data) > min_timestamp) { + vector::push_back(&mut values, *data::value(&oracle_data)); + vector::push_back(&mut oracles, *data::oracle_address(&oracle_data)); + }; + }; + }; + assert!(vector::length(&values) >= threshold, EValidDataSizeLessThanThreshold); + (values, oracles) + } + + /// take the median value + public fun median(meta_oracle: MetaOracle): TrustedData { + let (values, oracles) = combine(meta_oracle); + let sortedData = quick_sort(values); + let i = vector::length(&sortedData) / 2; + let value = vector::remove(&mut sortedData, i); + TrustedData { value, oracles } + } + + fun cmp(a: &T, b: &T): u8 { + let type = type_name::get(); + let a = bcs::new(bcs::to_bytes(a)); + let b = bcs::new(bcs::to_bytes(b)); + + if (type == type_name::get()) { + let a = bcs::peel_u64(&mut a); + let b = bcs::peel_u64(&mut b); + if (a > b) { + return 1 + } else if (a == b) { + return 0 + } else { + return 2 + } + } else if (type == type_name::get()) { + let a = bcs::peel_u128(&mut a); + let b = bcs::peel_u128(&mut b); + if (a > b) { + return 1 + } else if (a == b) { + return 0 + } else { + return 2 + } + }else if (type == type_name::get()) { + let a = bcs::peel_u8(&mut a); + let b = bcs::peel_u8(&mut b); + if (a > b) { + return 1 + } else if (a == b) { + return 0 + } else { + return 2 + } + } else if (type == type_name::get()) { + let a_value = bcs::peel_u64(&mut a); + let a_decimal = bcs::peel_u8(&mut a); + let b_value = bcs::peel_u64(&mut b); + let b_decimal = bcs::peel_u8(&mut b); + + // Normalise the decimal values + let a = (a_value as u128) * (math::pow(10, b_decimal) as u128); + let b = (b_value as u128) * (math::pow(10, a_decimal) as u128); + + if (a > b) { + return 1 + } else if (a == b) { + return 0 + } else { + return 2 + } + }else { + assert!(false, EUnsupportedDataType) + }; + 0 + } + + public fun quick_sort(data: vector): vector { + if (vector::length(&data) <= 1) { + return data + }; + + let pivot = *vector::borrow(&data, 0); + let less = vector[]; + let equal = vector[]; + let greater = vector[]; + + while (vector::length(&data) > 0) { + let value = vector::remove(&mut data, 0); + let cmp = cmp(&value, &pivot); + if (cmp == 2) { + vector::push_back(&mut less, value); + } else if (cmp == 0) { + vector::push_back(&mut equal, value); + } else { + vector::push_back(&mut greater, value); + }; + }; + + let sortedData = vector[]; + vector::append(&mut sortedData, quick_sort(less)); + vector::append(&mut sortedData, equal); + vector::append(&mut sortedData, quick_sort(greater)); + sortedData + } + + public fun data(meta: &MetaOracle): &vector>> { + &meta.oracle_data + } + + public fun threshold(meta: &MetaOracle): u64 { + meta.threshold + } + + public fun time_window_ms(meta: &MetaOracle): u64 { + meta.time_window_ms + } + + public fun ticker(meta: &MetaOracle): String { + meta.ticker + } + + public fun max_timestamp(meta: &MetaOracle): u64 { + meta.max_timestamp + } + + public fun value(data: &TrustedData): &T { + &data.value + } + + public fun oracles(data: &TrustedData): vector
{ + data.oracles + } + + #[test] + fun test_quick_sort() { + let data = vector[1, 3, 2, 5, 4]; + let sortedData = quick_sort(data); + assert!(vector::length(&sortedData) == 5, 0); + assert!(*vector::borrow(&sortedData, 0) == 1, 0); + assert!(*vector::borrow(&sortedData, 1) == 2, 0); + assert!(*vector::borrow(&sortedData, 2) == 3, 0); + assert!(*vector::borrow(&sortedData, 3) == 4, 0); + assert!(*vector::borrow(&sortedData, 4) == 5, 0); + } + + #[test] + fun test_quick_sort_u128() { + let data = vector[1, 3, 2, 5, 4]; + let sortedData = quick_sort(data); + assert!(vector::length(&sortedData) == 5, 0); + assert!(*vector::borrow(&sortedData, 0) == 1, 0); + assert!(*vector::borrow(&sortedData, 1) == 2, 0); + assert!(*vector::borrow(&sortedData, 2) == 3, 0); + assert!(*vector::borrow(&sortedData, 3) == 4, 0); + assert!(*vector::borrow(&sortedData, 4) == 5, 0); + } + + #[test] + fun test_quick_sort_decimal_value() { + let data = vector[ + decimal_value::new(1000000, 6), + decimal_value::new(3000000, 6), + decimal_value::new(2000000, 6), + decimal_value::new(5000000, 6), + decimal_value::new(4000000, 6)]; + + let sortedData = quick_sort(data); + assert!(vector::length(&sortedData) == 5, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 0)) == 1000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 1)) == 2000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 2)) == 3000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 3)) == 4000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 4)) == 5000000, 0); + } + + #[test] + fun test_quick_sort_decimal_value_different_decimal() { + let data = vector[ + decimal_value::new(60000, 2), + decimal_value::new(70000, 2), + decimal_value::new(1000000, 6), + decimal_value::new(3000000, 6), + decimal_value::new(2000000, 6), + decimal_value::new(5000000, 6), + decimal_value::new(4000000, 6)]; + + let sortedData = quick_sort(data); + + assert!(vector::length(&sortedData) == 7, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 0)) == 1000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 1)) == 2000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 2)) == 3000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 3)) == 4000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 4)) == 5000000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 5)) == 60000, 0); + assert!(decimal_value::value(vector::borrow(&sortedData, 6)) == 70000, 0); + } +} diff --git a/crates/sui-oracle/move/oracle/sources/simple_oracle.move b/crates/sui-oracle/move/oracle/sources/simple_oracle.move new file mode 100644 index 0000000000000..f3c110eb263a8 --- /dev/null +++ b/crates/sui-oracle/move/oracle/sources/simple_oracle.move @@ -0,0 +1,123 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module oracle::simple_oracle { + use std::option::{Self, Option}; + use std::string; + use std::string::String; + + use oracle::data::{Self, Data}; + use sui::clock::{Self, Clock}; + use sui::dynamic_field as df; + use sui::object::{Self, UID}; + use sui::table; + use sui::table::Table; + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + + const ESenderNotOracle: u64 = 0; + const ETickerNotExists: u64 = 1; + + struct SimpleOracle has store, key { + id: UID, + /// The address of the oracle. + address: address, + /// The name of the oracle. + name: String, + /// The description of the oracle. + description: String, + /// The URL of the oracle. + url: String, + } + + struct StoredData has copy, store, drop { + value: T, + sequence_number: u64, + timestamp: u64, + /// An identifier for the reading (for example real time of observation, or sequence number of observation on other chain). + identifier: String, + } + + public fun get_historical_data( + oracle: &SimpleOracle, + ticker: String, + archival_key: K + ): Option> { + string::append(&mut string::utf8(b"[historical] "), ticker); + let historical_data: &Table> = df::borrow(&oracle.id, ticker); + let StoredData { value, sequence_number, timestamp, identifier } = *table::borrow( + historical_data, + archival_key + ); + option::some(data::new(value, ticker, sequence_number, timestamp, oracle.address, identifier)) + } + + public fun get_latest_data(oracle: &SimpleOracle, ticker: String): Option> { + if (!df::exists_(&oracle.id, ticker)) { + return option::none() + }; + let data: &StoredData = df::borrow(&oracle.id, ticker); + let StoredData { value, sequence_number, timestamp, identifier } = *data; + option::some(data::new(value, ticker, sequence_number, timestamp, oracle.address, identifier)) + } + + /// Create a new shared SimpleOracle object for publishing data. + public entry fun create(name: String, url: String, description: String, ctx: &mut TxContext) { + let oracle = SimpleOracle { id: object::new(ctx), address: tx_context::sender(ctx), name, description, url }; + transfer::share_object(oracle) + } + + public entry fun submit_data( + oracle: &mut SimpleOracle, + clock: &Clock, + ticker: String, + value: T, + identifier: String, + ctx: &mut TxContext + ) { + assert!(oracle.address == tx_context::sender(ctx), ESenderNotOracle); + + let old_data: Option> = df::remove_if_exists(&mut oracle.id, ticker); + + let sequence_number = if (option::is_some(&old_data)) { + let seq = option::borrow(&old_data).sequence_number + 1; + let _ = option::destroy_some(old_data); + seq + } else { + option::destroy_none(old_data); + 0 + }; + + let new_data = StoredData { + value, + sequence_number, + timestamp: clock::timestamp_ms(clock), + identifier, + }; + df::add(&mut oracle.id, ticker, new_data); + } + + public entry fun archive_data( + oracle: &mut SimpleOracle, + ticker: String, + archival_key: K, + ctx: &mut TxContext + ) { + assert!(oracle.address == tx_context::sender(ctx), ESenderNotOracle); + assert!(df::exists_(&oracle.id, ticker), ETickerNotExists); + + let latest_data: StoredData = *df::borrow_mut(&mut oracle.id, ticker); + + string::append(&mut string::utf8(b"[historical] "), ticker); + if (!df::exists_(&oracle.id, ticker)) { + let data_source = table::new>(ctx); + df::add(&mut oracle.id, ticker, data_source); + }; + let historical_data: &mut Table> = df::borrow_mut(&mut oracle.id, ticker); + // Replace the old data in historical data if any. + if (table::contains(historical_data, archival_key)) { + table::remove(historical_data, archival_key); + }; + table::add(historical_data, archival_key, latest_data); + } +} diff --git a/crates/sui-oracle/tests/data/Test/Move.toml b/crates/sui-oracle/tests/data/Test/Move.toml new file mode 100644 index 0000000000000..71c4c71ec0286 --- /dev/null +++ b/crates/sui-oracle/tests/data/Test/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "test" +version = "0.0.1" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "9d0fba4a490e1cf80101bbd4019c7bb1ccfce99b" } +sui-oracle = { local = "../../../move/oracle" } + +[addresses] +test = "0x0" +sui = "0x2" \ No newline at end of file diff --git a/crates/sui-oracle/tests/data/Test/sources/test_module.move b/crates/sui-oracle/tests/data/Test/sources/test_module.move new file mode 100644 index 0000000000000..6fe6b44198871 --- /dev/null +++ b/crates/sui-oracle/tests/data/Test/sources/test_module.move @@ -0,0 +1,85 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module test::test_module { + + use std::option; + use std::option::Option; + use std::string; + + use oracle::data; + use oracle::data::Data; + use oracle::decimal_value; + use oracle::decimal_value::DecimalValue; + use oracle::meta_oracle; + use oracle::simple_oracle; + use oracle::simple_oracle::SimpleOracle; + use sui::object; + use sui::object::UID; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct MockUSD has key, store { + id: UID, + amount: u64, + decimals: u8, + } + + public fun simple_fx_ptb(single_data: Option>, mist_amount: u64, ctx: &mut TxContext) { + let single_data = option::destroy_some(single_data); + let value = data::value(&single_data); + let decimals = decimal_value::decimal(value); + let value = decimal_value::value(value); + + let amount = mist_amount * value; + let usd = MockUSD { + id: object::new(ctx), + amount, + decimals, + }; + transfer::transfer(usd, tx_context::sender(ctx)); + } + + public fun simple_fx(oracle: &SimpleOracle, mist_amount: u64, ctx: &mut TxContext) { + let single_data = simple_oracle::get_latest_data(oracle, string::utf8(b"SUIUSD")); + let single_data = option::destroy_some(single_data); + let value = data::value(&single_data); + let decimals = decimal_value::decimal(value); + let value = decimal_value::value(value); + + let amount = mist_amount * value; + let usd = MockUSD { + id: object::new(ctx), + amount, + decimals, + }; + transfer::transfer(usd, tx_context::sender(ctx)); + } + + public fun trusted_fx( + oracle1: &SimpleOracle, + oracle2: &SimpleOracle, + oracle3: &SimpleOracle, + mist_amount: u64, + ctx: &mut TxContext + ) { + let meta_oracle = meta_oracle::new(3, 60000, string::utf8(b"SUIUSD")); + meta_oracle::add_simple_oracle(&mut meta_oracle, oracle1); + meta_oracle::add_simple_oracle(&mut meta_oracle, oracle2); + meta_oracle::add_simple_oracle(&mut meta_oracle, oracle3); + + let trusted_data = meta_oracle::median(meta_oracle); + let value = meta_oracle::value(&trusted_data); + let decimals = decimal_value::decimal(value); + let value = decimal_value::value(value); + + let amount = mist_amount * value; + let usd = MockUSD { + id: object::new(ctx), + amount, + decimals, + }; + transfer::transfer(usd, tx_context::sender(ctx)); + } +} diff --git a/crates/sui-oracle/tests/integration_tests.rs b/crates/sui-oracle/tests/integration_tests.rs new file mode 100644 index 0000000000000..5a2dc658007d6 --- /dev/null +++ b/crates/sui-oracle/tests/integration_tests.rs @@ -0,0 +1,586 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::PathBuf; +use std::str::FromStr; + +use shared_crypto::intent::Intent; +use sui_json_rpc_types::SuiTransactionBlockEffectsAPI; +use sui_json_rpc_types::{ObjectChange, SuiExecutionStatus}; +use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore}; +use sui_move_build::BuildConfig; +use sui_sdk::rpc_types::SuiTransactionBlockResponseOptions; +use sui_sdk::types::base_types::{ObjectID, SuiAddress}; +use sui_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use sui_sdk::types::quorum_driver_types::ExecuteTransactionRequestType; +use sui_sdk::types::transaction::{CallArg, ObjectArg, Transaction, TransactionData}; +use sui_sdk::types::Identifier; +use sui_sdk::{SuiClient, SuiClientBuilder}; +use sui_types::base_types::{ObjectRef, SequenceNumber}; +use sui_types::{parse_sui_type_tag, TypeTag}; + +// Integration tests for SUI Oracle, these test can be run manually on local or remote testnet. +#[ignore] +#[tokio::test] +async fn test_publish_primitive() { + let (client, keystore, sender) = init_test_client().await; + // publish package if not exists + let package = option_env!("package_id") + .map(|s| ObjectID::from_str(s).unwrap()) + .unwrap_or(publish_package(sender, &keystore, &client, PathBuf::from("move/oracle")).await); + let module = Identifier::from_str("simple_oracle").unwrap(); + + // create simple oracle if not exists + let (simple_oracle_id, version) = option_env!("oracle_id") + .and_then(|id| { + option_env!("oracle_version").map(|version| { + ( + ObjectID::from_str(id).unwrap(), + u64::from_str(version).unwrap().into(), + ) + }) + }) + .unwrap_or(create_oracle(sender, &keystore, &client, package, module.clone()).await); + + // publish oracle data + let submit_data = Identifier::from_str("submit_data").unwrap(); + let mut builder = ProgrammableTransactionBuilder::new(); + + for i in 1..200 { + let ticker = format!("SUI {}", i); + + let value = builder + .input(CallArg::Pure( + bcs::to_bytes(&rand::random::()).unwrap(), + )) + .unwrap(); + + let simple_oracle = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: simple_oracle_id, + initial_shared_version: version, + mutable: true, + })) + .unwrap(); + + let clock = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: ObjectID::from_str("0x6").unwrap(), + initial_shared_version: 1.into(), + mutable: false, + })) + .unwrap(); + + let ticker = builder + .input(CallArg::Pure(bcs::to_bytes(ticker.as_bytes()).unwrap())) + .unwrap(); + let identifier = builder + .input(CallArg::Pure( + bcs::to_bytes("identifier".as_bytes()).unwrap(), + )) + .unwrap(); + + builder.programmable_move_call( + package, + module.clone(), + submit_data.clone(), + vec![TypeTag::U64], + vec![simple_oracle, clock, ticker, value, identifier], + ); + } + + let pt = builder.finish(); + let (gas, gas_price) = get_gas(&client, sender).await; + let data = TransactionData::new_programmable(sender, vec![gas], pt, 1000000000, gas_price); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let tx = Transaction::from_data(data.clone(), vec![signature]); + + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new().with_effects(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + println!("{:#?}", result) +} + +#[ignore] +#[tokio::test] +async fn test_publish_complex_value() { + let (client, keystore, sender) = init_test_client().await; + // publish package if not exists + let package = option_env!("package_id") + .map(|s| ObjectID::from_str(s).unwrap()) + .unwrap_or(publish_package(sender, &keystore, &client, PathBuf::from("move/oracle")).await); + let module = Identifier::from_str("simple_oracle").unwrap(); + + // create simple oracle if not exists + let (simple_oracle_id, version) = option_env!("oracle_id") + .and_then(|id| { + option_env!("oracle_version").map(|version| { + ( + ObjectID::from_str(id).unwrap(), + u64::from_str(version).unwrap().into(), + ) + }) + }) + .unwrap_or(create_oracle(sender, &keystore, &client, package, module.clone()).await); + + // publish oracle data + let submit_data = Identifier::from_str("submit_data").unwrap(); + let data_types = Identifier::from_str("decimal_value").unwrap(); + let create_decimal = Identifier::from_str("new").unwrap(); + let mut builder = ProgrammableTransactionBuilder::new(); + + let decimal = builder + .input(CallArg::Pure(bcs::to_bytes(&6u8).unwrap())) + .unwrap(); + + for i in 1..200 { + let ticker = format!("SUI {}", i); + + let value = builder + .input(CallArg::Pure( + bcs::to_bytes(&rand::random::()).unwrap(), + )) + .unwrap(); + + let decimal_value = builder.programmable_move_call( + package, + data_types.clone(), + create_decimal.clone(), + vec![], + vec![value, decimal], + ); + + let simple_oracle = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: simple_oracle_id, + initial_shared_version: version, + mutable: true, + })) + .unwrap(); + + let clock = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: ObjectID::from_str("0x6").unwrap(), + initial_shared_version: 1.into(), + mutable: false, + })) + .unwrap(); + + let ticker = builder + .input(CallArg::Pure(bcs::to_bytes(ticker.as_bytes()).unwrap())) + .unwrap(); + let identifier = builder + .input(CallArg::Pure( + bcs::to_bytes("identifier".as_bytes()).unwrap(), + )) + .unwrap(); + + builder.programmable_move_call( + package, + module.clone(), + submit_data.clone(), + vec![parse_sui_type_tag(&format!("{package}::decimal_value::DecimalValue")).unwrap()], + vec![simple_oracle, clock, ticker, decimal_value, identifier], + ); + } + + let pt = builder.finish(); + let (gas, gas_price) = get_gas(&client, sender).await; + let data = TransactionData::new_programmable(sender, vec![gas], pt, 1000000000, gas_price); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let tx = Transaction::from_data(data.clone(), vec![signature]); + + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new().with_effects(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + println!("{:#?}", result) +} + +#[ignore] +#[tokio::test] +async fn test_consume_oracle_data() { + let (client, keystore, sender) = init_test_client().await; + // publish package if not exists + let Some(package) = option_env!("package_id").map(|s| ObjectID::from_str(s).unwrap()) else { + panic!("package_id not set"); + }; + + let module = Identifier::from_str("simple_oracle").unwrap(); + + // create simple oracle + let mut oracles = vec![]; + for _ in 0..3 { + let (simple_oracle_id, version) = + create_oracle(sender, &keystore, &client, package, module.clone()).await; + oracles.push((simple_oracle_id, version)); + + // publish oracle data + let submit_data = Identifier::from_str("submit_data").unwrap(); + let data_types = Identifier::from_str("decimal_value").unwrap(); + let create_decimal = Identifier::from_str("new").unwrap(); + let mut builder = ProgrammableTransactionBuilder::new(); + + // Create decimal value + let decimal = builder + .input(CallArg::Pure(bcs::to_bytes(&6u8).unwrap())) + .unwrap(); + + let value = builder + .input(CallArg::Pure(bcs::to_bytes(&10000000u64).unwrap())) + .unwrap(); + + let decimal_value = builder.programmable_move_call( + package, + data_types.clone(), + create_decimal.clone(), + vec![], + vec![value, decimal], + ); + // publish data + let simple_oracle = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: simple_oracle_id, + initial_shared_version: version, + mutable: true, + })) + .unwrap(); + + let clock = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: ObjectID::from_str("0x6").unwrap(), + initial_shared_version: 1.into(), + mutable: false, + })) + .unwrap(); + + let ticker = builder + .input(CallArg::Pure(bcs::to_bytes("SUIUSD".as_bytes()).unwrap())) + .unwrap(); + let identifier = builder + .input(CallArg::Pure( + bcs::to_bytes("identifier".as_bytes()).unwrap(), + )) + .unwrap(); + + builder.programmable_move_call( + package, + module.clone(), + submit_data.clone(), + vec![parse_sui_type_tag(&format!("{package}::decimal_value::DecimalValue")).unwrap()], + vec![simple_oracle, clock, ticker, decimal_value, identifier], + ); + + let pt = builder.finish(); + let (gas, gas_price) = get_gas(&client, sender).await; + let data = TransactionData::new_programmable(sender, vec![gas], pt, 1000000000, gas_price); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let tx = Transaction::from_data(data.clone(), vec![signature]); + + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new().with_effects(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + assert!(result.effects.unwrap().status().is_ok()); + } + + let (simple_oracle_id, version) = *oracles.first().unwrap(); + + // publish test package + let test_package = + publish_package(sender, &keystore, &client, PathBuf::from("tests/data/Test")).await; + // get data + let mut builder = ProgrammableTransactionBuilder::new(); + let simple_oracle = builder + .input(CallArg::Object(ObjectArg::SharedObject { + id: simple_oracle_id, + initial_shared_version: version, + mutable: false, + })) + .unwrap(); + let ticker = builder + .input(CallArg::Pure(bcs::to_bytes("SUIUSD".as_bytes()).unwrap())) + .unwrap(); + let data = builder.programmable_move_call( + package, + module, + Identifier::from_str("get_latest_data").unwrap(), + vec![parse_sui_type_tag(&format!("{package}::decimal_value::DecimalValue")).unwrap()], + vec![simple_oracle, ticker], + ); + + // call simple_fx_ptb + let test_module = Identifier::from_str("test_module").unwrap(); + let simple_fx_ptb = Identifier::from_str("simple_fx_ptb").unwrap(); + let mist_amount = builder + .input(CallArg::Pure(bcs::to_bytes(&10000000u64).unwrap())) + .unwrap(); + builder.programmable_move_call( + test_package, + test_module.clone(), + simple_fx_ptb, + vec![], + vec![data, mist_amount], + ); + + // call simple_fx + let simple_fx = Identifier::from_str("simple_fx").unwrap(); + let mist_amount = builder + .input(CallArg::Pure(bcs::to_bytes(&10000000u64).unwrap())) + .unwrap(); + + builder.programmable_move_call( + test_package, + test_module.clone(), + simple_fx, + vec![], + vec![simple_oracle, mist_amount], + ); + + // Call trusted_fx + let trusted_fx = Identifier::from_str("trusted_fx").unwrap(); + let oracles = oracles + .into_iter() + .map(|(id, version)| { + builder + .input(CallArg::Object(ObjectArg::SharedObject { + id, + initial_shared_version: version, + mutable: false, + })) + .unwrap() + }) + .collect::>(); + + builder.programmable_move_call( + test_package, + test_module, + trusted_fx, + vec![], + vec![oracles[0], oracles[1], oracles[2], mist_amount], + ); + + let pt = builder.finish(); + let (gas, gas_price) = get_gas(&client, sender).await; + let data = TransactionData::new_programmable(sender, vec![gas], pt, 1000000000, gas_price); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let tx = Transaction::from_data(data.clone(), vec![signature]); + + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new().with_effects(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + assert!(result.effects.unwrap().status().is_ok()); +} + +async fn get_gas(client: &SuiClient, sender: SuiAddress) -> (ObjectRef, u64) { + let gas = client + .coin_read_api() + .get_coins(sender, None, None, Some(1)) + .await + .unwrap(); + let gas = gas.data[0].object_ref(); + let gas_price = client + .governance_api() + .get_reference_gas_price() + .await + .unwrap(); + + (gas, gas_price) +} + +async fn init_test_client() -> (SuiClient, Keystore, SuiAddress) { + let client = SuiClientBuilder::default() + .build("https://rpc.devnet.sui.io:443") + .await + .unwrap(); + + let keystore = Keystore::File( + FileBasedKeystore::new( + &dirs::home_dir() + .unwrap() + .join(".sui/sui_config/sui.keystore"), + ) + .unwrap(), + ); + let sender: SuiAddress = keystore.addresses()[0]; + let gas = client + .coin_read_api() + .get_coins(sender, None, None, Some(1)) + .await + .unwrap(); + + assert!( + !gas.data.is_empty(), + "No gas coin found in account, please fund [{}]", + sender + ); + + (client, keystore, sender) +} + +async fn publish_package( + sender: SuiAddress, + keystore: &Keystore, + client: &SuiClient, + path: PathBuf, +) -> ObjectID { + let compiled_package = BuildConfig::new_for_testing().build(path).unwrap(); + let all_module_bytes = compiled_package.get_package_bytes(false); + let dependencies = compiled_package.get_dependency_original_package_ids(); + let gas = client + .coin_read_api() + .get_coins(sender, None, None, Some(1)) + .await + .unwrap(); + let gas = gas.data[0].object_ref(); + let data = TransactionData::new_module( + sender, + gas, + all_module_bytes, + dependencies, + 1000000000, + 1000, + ); + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let tx = Transaction::from_data(data.clone(), vec![signature]); + + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new() + .with_effects() + .with_object_changes(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + assert_eq!( + &SuiExecutionStatus::Success, + result.effects.unwrap().status() + ); + + let publish = result + .object_changes + .unwrap() + .iter() + .find(|change| matches!(change, ObjectChange::Published { .. })) + .unwrap() + .clone(); + + let ObjectChange::Published { package_id, .. } = publish else { + panic!("Expected published object change") + }; + package_id +} + +async fn create_oracle( + sender: SuiAddress, + keystore: &Keystore, + client: &SuiClient, + package: ObjectID, + module: Identifier, +) -> (ObjectID, SequenceNumber) { + let mut builder = ProgrammableTransactionBuilder::new(); + let create = Identifier::from_str("create").unwrap(); + builder + .move_call( + package, + module, + create, + vec![], + vec![ + CallArg::Pure(bcs::to_bytes("Teat Name".as_bytes()).unwrap()), + CallArg::Pure(bcs::to_bytes("Test URL".as_bytes()).unwrap()), + CallArg::Pure(bcs::to_bytes("Test description".as_bytes()).unwrap()), + ], + ) + .unwrap(); + let pt = builder.finish(); + let gas = client + .coin_read_api() + .get_coins(sender, None, None, Some(1)) + .await + .unwrap(); + let gas = gas.data[0].object_ref(); + let gas_price = client + .governance_api() + .get_reference_gas_price() + .await + .unwrap(); + let data = TransactionData::new_programmable(sender, vec![gas], pt, 1000000000, gas_price); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + let tx = Transaction::from_data(data.clone(), vec![signature]); + let result = client + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new() + .with_effects() + .with_object_changes(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + assert_eq!( + &SuiExecutionStatus::Success, + result.effects.unwrap().status() + ); + let simple_oracle = result.object_changes.unwrap().iter().find(|change| matches!(change, ObjectChange::Created {object_type,..} if object_type.name.as_str() == "SimpleOracle")).unwrap().clone(); + let ObjectChange::Created { + object_id: simple_oracle_id, + version, + .. + } = simple_oracle + else { + panic!("Expected created object change") + }; + + (simple_oracle_id, version) +}