From 78884dc3dfa029772034bfe57615741cbea4fccf Mon Sep 17 00:00:00 2001 From: Omar Date: Sat, 13 Jan 2024 20:46:52 +0300 Subject: [PATCH] [Bootstrapper]: Add a bootstrap blueprint and code --- Cargo.toml | 5 +- Makefile.toml | 1 - packages/bootstrap/Cargo.toml | 23 ++ packages/bootstrap/src/lib.rs | 334 +++++++++++++++ tools/bootstrapper/Cargo.toml | 23 ++ tools/bootstrapper/src/cli.rs | 17 + tools/bootstrapper/src/constants.rs | 7 + tools/bootstrapper/src/error.rs | 21 + tools/bootstrapper/src/gateway/client.rs | 92 +++++ tools/bootstrapper/src/gateway/mod.rs | 5 + tools/bootstrapper/src/gateway/types.rs | 164 ++++++++ tools/bootstrapper/src/macros.rs | 61 +++ tools/bootstrapper/src/main.rs | 21 + tools/bootstrapper/src/subcommands/mod.rs | 1 + .../subcommands/publish_test_to_stokenet.rs | 385 ++++++++++++++++++ tools/bootstrapper/src/utils.rs | 6 + 16 files changed, 1164 insertions(+), 2 deletions(-) create mode 100644 packages/bootstrap/Cargo.toml create mode 100644 packages/bootstrap/src/lib.rs create mode 100644 tools/bootstrapper/Cargo.toml create mode 100644 tools/bootstrapper/src/cli.rs create mode 100644 tools/bootstrapper/src/constants.rs create mode 100644 tools/bootstrapper/src/error.rs create mode 100644 tools/bootstrapper/src/gateway/client.rs create mode 100644 tools/bootstrapper/src/gateway/mod.rs create mode 100644 tools/bootstrapper/src/gateway/types.rs create mode 100644 tools/bootstrapper/src/macros.rs create mode 100644 tools/bootstrapper/src/main.rs create mode 100644 tools/bootstrapper/src/subcommands/mod.rs create mode 100644 tools/bootstrapper/src/subcommands/publish_test_to_stokenet.rs create mode 100644 tools/bootstrapper/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 2fe760e1..39686302 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,16 @@ resolver = "2" members = [ # Packages "packages/ignition", + "packages/bootstrap", "packages/test-oracle", "packages/ociswap-adapter-v1", "packages/caviarnine-adapter-v1", # Libraries "libraries/package-loader", "libraries/scrypto-interface", - "libraries/adapters-interface" + "libraries/adapters-interface", + # Tools + "tools/bootstrapper" ] [workspace.package] diff --git a/Makefile.toml b/Makefile.toml index 03ff7cd5..ab61ea06 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -2,7 +2,6 @@ default_to_workspace = false [tasks.test] -install_crate = "nextest" command = "cargo" args = ["nextest", "run"] diff --git a/packages/bootstrap/Cargo.toml b/packages/bootstrap/Cargo.toml new file mode 100644 index 00000000..e6d07acb --- /dev/null +++ b/packages/bootstrap/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bootstrap" +version.workspace = true +edition.workspace = true +description = "Bootstraps Ignition either for testing or for production" + +[dependencies] +sbor = { workspace = true } +scrypto = { workspace = true } +radix-engine-derive = { workspace = true } + +ignition = { path = "../../packages/ignition" } +ociswap-adapter-v1 = { path = "../../packages/ociswap-adapter-v1" } +caviarnine-adapter-v1 = { path = "../../packages/caviarnine-adapter-v1" } + +humantime = { version = "2.1.0" } + +[features] +default = [] +test = [] + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/packages/bootstrap/src/lib.rs b/packages/bootstrap/src/lib.rs new file mode 100644 index 00000000..60db8b07 --- /dev/null +++ b/packages/bootstrap/src/lib.rs @@ -0,0 +1,334 @@ +use caviarnine_adapter_v1::*; +use ignition::ignition::*; +use ignition::{LiquidityReceipt, LockupPeriod, PoolBlueprintInformation}; +use scrypto::prelude::*; + +use caviarnine_adapter_v1::adapter::*; + +#[blueprint] +#[events(TestingBootstrapInformation, EncodedTestingBootstrapInformation)] +mod bootstrap { + struct Bootstrap; + + impl Bootstrap { + pub fn bootstrap_for_testing( + /* Core packages */ + ignition_package_address: PackageAddress, + oracle_package_address: PackageAddress, + /* Dexes */ + caviarnine_package_address: PackageAddress, + /* Adapters */ + _ociswap_adapter_v1_package_address: PackageAddress, + caviarnine_adapter_v1_package_address: PackageAddress, + ) -> (TestingBootstrapInformation, Vec) { + // A vector of buckets that must be returned by the end of this + // process. These are obtained as a result of the creation of pools, + // contribution to pools, or from other sources. + let mut buckets = vec![]; + + // Instantiating an oracle component. + let oracle = scrypto_decode::( + &ScryptoVmV1Api::blueprint_call( + oracle_package_address, + "TestOracle", + "instantiate", + scrypto_args!( + rule!(allow_all), + OwnerRole::None, + None:: + ), + ), + ) + .unwrap(); + + // Defining the protocol's resource. This is the resource that the + // protocol will be lending out to users. + let protocol_resource_information = ResourceInformation { + divisibility: 18, + name: "Fake XRD".into(), + symbol: "fakeXRD".into(), + icon_url: + "https://assets.radixdlt.com/icons/icon-xrd-32x32.png" + .into(), + }; + let protocol_resource = + protocol_resource_information.create_resource(); + + // Defining the information of the user resources. We're creating + // these resources for testing purposed only. + let user_resources_information = vec![ + ResourceInformation { + divisibility: 6, + name: "Fake Bitcoin".into(), + symbol: "fakeBTC".into(), + icon_url: + "https://assets.instabridge.io/tokens/icons/xwBTC.png" + .into(), + }, + ResourceInformation { + divisibility: 18, + name: "Fake Ethereum".into(), + symbol: "fakeETH".into(), + icon_url: + "https://assets.instabridge.io/tokens/icons/xETH.png" + .into(), + }, + ResourceInformation { + divisibility: 6, + name: "Fake USDC".into(), + symbol: "fakeUSDC".into(), + icon_url: + "https://assets.instabridge.io/tokens/icons/xUSDC.png" + .into(), + }, + ResourceInformation { + divisibility: 6, + name: "Fake USDT".into(), + symbol: "fakeUSDT".into(), + icon_url: + "https://assets.instabridge.io/tokens/icons/xUSDT.png" + .into(), + }, + ]; + let mut user_resources = user_resources_information + .into_iter() + .map(|information| { + let resource = information.create_resource(); + (information, resource) + }) + .collect::>(); + + // Creating the pools of all of the user resources. + let mut caviarnine_pools = indexmap! {}; + for (_, resource_manager) in user_resources.iter_mut() { + let mut pool = CaviarNinePoolInterfaceScryptoStub::new( + rule!(allow_all), + rule!(allow_all), + resource_manager.address(), + protocol_resource.address(), + 50, + None, + caviarnine_package_address, + ); + + let user_resource = resource_manager.mint(100_000_000); + let protocol_resource = protocol_resource.mint(100_000_000); + let (receipt, change_x, change_y) = pool.add_liquidity( + user_resource, + protocol_resource, + vec![(30_000, dec!(100_000_000), dec!(100_000_000))], + ); + buckets.push(receipt); + buckets.push(change_x); + buckets.push(change_y); + + caviarnine_pools.insert( + resource_manager.address(), + ComponentAddress::try_from(pool).unwrap(), + ); + } + + // Instantiating the adapters of the various exchanges. + let caviarnine_adapter = scrypto_decode::( + &ScryptoVmV1Api::blueprint_call( + caviarnine_adapter_v1_package_address, + "CaviarNineAdapter", + "instantiate", + scrypto_args!( + OwnerRole::None, + None:: + ), + ), + ) + .map(|address| { + Global::(CaviarNineAdapterObjectStub { + handle: ObjectStubHandle::Global(address.into()), + }) + }) + .unwrap(); + + // Creating the liquidity receipt resource of the various exchanges. + let caviar_nine_liquidity_receipt = + ResourceBuilder::new_ruid_non_fungible::(OwnerRole::None) + .mint_roles(mint_roles! { + minter => rule!(require(global_caller(ignition_package_address))); + minter_updater => rule!(require(global_caller(ignition_package_address))); + }) + .burn_roles(burn_roles! { + burner => rule!(require(global_caller(ignition_package_address))); + burner_updater => rule!(require(global_caller(ignition_package_address))); + }) + .metadata(metadata! { + init { + "name" => "CaviarNine Ignition Liquidity Receipt", locked; + "description" => "A receipt of liquidity provided to CaviarNine through project Ignition.", locked; + } + }) + .create_with_no_initial_supply(); + + // Creating the ignition component and initializing it to the + // expected state. + let ignition = { + let ignition = scrypto_decode::( + &ScryptoVmV1Api::blueprint_call( + ignition_package_address, + "Ignition", + "instantiate", + scrypto_args!( + OwnerRole::None, + rule!(allow_all), + rule!(allow_all), + protocol_resource, + oracle, + 300i64, + dec!(0.05), + None:: + ), + ), + ) + .map(|address| { + Global::(IgnitionObjectStub { + handle: ObjectStubHandle::Global(address.into()), + }) + }) + .unwrap(); + + // Allow the opening and closing of liquidity positions. + ignition.set_is_close_position_enabled(true); + ignition.set_is_open_position_enabled(true); + + // Fund ignition with the protocol resources that it needs to + // start lending out to users. + let protocol_resources_bucket = + protocol_resource.mint(dec!(100_000_000_000_000)); + ignition.deposit_resources(FungibleBucket( + protocol_resources_bucket, + )); + + // Add the reward rates that Ignition will use. + ignition + .add_reward_rate(LockupPeriod::from_seconds(0), dec!(0.10)); + ignition.add_reward_rate( + LockupPeriod::from_seconds(60), + dec!(0.20), + ); + + // Adding the pool information for CaviarSwap + ignition.insert_pool_information( + CaviarNinePoolInterfaceScryptoStub::blueprint_id( + caviarnine_package_address, + ), + PoolBlueprintInformation { + adapter: caviarnine_adapter.address().into(), + allowed_pools: caviarnine_pools + .values() + .copied() + .collect(), + liquidity_receipt: caviar_nine_liquidity_receipt, + }, + ); + + ignition + }; + + let bootstrap_information = TestingBootstrapInformation { + resources: user_resources + .into_iter() + .map(|(information, manager)| { + (manager.address(), information) + }) + .collect(), + protocol: ProtocolEntities { + ignition_package_address, + ignition: ignition.address(), + protocol_resource: protocol_resource.address(), + oracle_package_address, + oracle, + }, + caviarnine: DexEntities { + package: caviarnine_package_address, + pools: caviarnine_pools, + adapter_package: caviarnine_adapter_v1_package_address, + adapter: caviarnine_adapter.address(), + }, + }; + Runtime::emit_event(bootstrap_information.clone()); + Runtime::emit_event(EncodedTestingBootstrapInformation::from( + bootstrap_information.clone(), + )); + + (bootstrap_information, buckets) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor, ScryptoEvent)] +pub struct EncodedTestingBootstrapInformation(Vec); + +impl From for EncodedTestingBootstrapInformation { + fn from(value: TestingBootstrapInformation) -> Self { + Self(scrypto_encode(&value).unwrap()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor, ScryptoEvent)] +pub struct TestingBootstrapInformation { + pub resources: IndexMap, + pub protocol: ProtocolEntities, + pub caviarnine: DexEntities, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub struct ProtocolEntities { + /* Ignition */ + pub ignition_package_address: PackageAddress, + pub ignition: ComponentAddress, + pub protocol_resource: ResourceAddress, + /* Oracle */ + pub oracle_package_address: PackageAddress, + pub oracle: ComponentAddress, +} + +/// A struct that defines the entities that belong to a Decentralized Exchange. +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub struct DexEntities { + /* Packages */ + pub package: PackageAddress, + /* Pools */ + pub pools: IndexMap, + /* Adapter */ + pub adapter_package: PackageAddress, + pub adapter: ComponentAddress, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub struct ResourceInformation { + pub divisibility: u8, + pub name: String, + pub symbol: String, + pub icon_url: String, +} + +impl ResourceInformation { + pub fn create_resource(&self) -> ResourceManager { + ResourceBuilder::new_fungible(OwnerRole::None) + .divisibility(self.divisibility) + .mint_roles(mint_roles! { + minter => rule!(allow_all); + minter_updater => rule!(allow_all); + }) + .burn_roles(burn_roles! { + burner => rule!(allow_all); + burner_updater => rule!(allow_all); + }) + .metadata(metadata! { + init { + "name" => self.name.as_str(), locked; + "symbol" => self.symbol.as_str(), locked; + "description" => "This is a fake resource made just for testing, this has no value", locked; + "icon_url" => UncheckedUrl(self.icon_url.clone()), locked; + } + }) + .create_with_no_initial_supply() + } +} diff --git a/tools/bootstrapper/Cargo.toml b/tools/bootstrapper/Cargo.toml new file mode 100644 index 00000000..cba3d4cb --- /dev/null +++ b/tools/bootstrapper/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bootstrapper" +description = "A tool used to bootstrap and publish project Ignition onto various networks" +version.workspace = true +edition.workspace = true + +[dependencies] +sbor = { workspace = true } +transaction = { workspace = true } +radix-engine = { workspace = true } +radix-engine-interface = { workspace = true } + +bootstrap = { path = "../../packages/bootstrap" } +package-loader = { path = "../../libraries/package-loader" } +caviarnine-adapter-v1 = { path = "../../packages/caviarnine-adapter-v1", features = ["transaction"] } + +clap = { version = "4.4.16", features = ["derive"] } +reqwest = { version = "0.11.23", features = ["blocking", "json"] } +serde = { version = "1.0.195" } +serde_with = { version = "3.4.0" } +hex = "0.4.3" +rand = "0.8.5" +serde_json = "1.0.111" diff --git a/tools/bootstrapper/src/cli.rs b/tools/bootstrapper/src/cli.rs new file mode 100644 index 00000000..29ad0cf3 --- /dev/null +++ b/tools/bootstrapper/src/cli.rs @@ -0,0 +1,17 @@ +use crate::error::*; +use crate::subcommands::*; +use clap::Parser; + +#[derive(Parser, Debug)] +pub enum Cli { + /// Publishes project Ignition to stokenet with the test configuration. + PublishTestToStokenet(publish_test_to_stokenet::PublishTestToStokenet), +} + +impl Cli { + pub fn run(self, out: &mut O) -> Result<(), Error> { + match self { + Self::PublishTestToStokenet(cmd) => cmd.run(out), + } + } +} diff --git a/tools/bootstrapper/src/constants.rs b/tools/bootstrapper/src/constants.rs new file mode 100644 index 00000000..cec4c068 --- /dev/null +++ b/tools/bootstrapper/src/constants.rs @@ -0,0 +1,7 @@ +pub const STOKENET_GATEWAY_URL: &str = "https://stokenet.radixdlt.com/"; + +pub const IGNITION_PACKAGE_NAME: &str = "ignition"; +pub const OCISWAP_ADAPTER_V1_PACKAGE_NAME: &str = "ociswap-adapter-v1"; +pub const CAVIARNINE_ADAPTER_V1_PACKAGE_NAME: &str = "caviarnine-adapter-v1"; +pub const BOOTSTRAP_PACKAGE_NAME: &str = "bootstrap"; +pub const TEST_ORACLE_PACKAGE_NAME: &str = "test-oracle"; diff --git a/tools/bootstrapper/src/error.rs b/tools/bootstrapper/src/error.rs new file mode 100644 index 00000000..918e565c --- /dev/null +++ b/tools/bootstrapper/src/error.rs @@ -0,0 +1,21 @@ +#![allow(clippy::enum_variant_names)] + +#[derive(Debug)] +pub enum Error { + ReqwestError(reqwest::Error), + TransactionPollingTimeOut, + TransactionDidNotSucceed, + IoError(std::io::Error), +} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Self::ReqwestError(value) + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} diff --git a/tools/bootstrapper/src/gateway/client.rs b/tools/bootstrapper/src/gateway/client.rs new file mode 100644 index 00000000..8246c0e2 --- /dev/null +++ b/tools/bootstrapper/src/gateway/client.rs @@ -0,0 +1,92 @@ +use radix_engine_interface::prelude::*; +use transaction::prelude::*; + +use reqwest::blocking::*; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GatewayClient { + base_url: String, +} + +impl GatewayClient { + pub fn new(url: impl Into) -> Self { + Self { + base_url: url.into(), + } + } + + pub fn stokenet() -> Self { + Self::new(crate::constants::STOKENET_GATEWAY_URL) + } + + pub fn get_current_epoch(&self) -> reqwest::Result { + self.make_request::( + &super::types::status_gateway_status::Input, + ) + .map(|response| Epoch::of(response.ledger_state.epoch)) + } + + pub fn submit_transaction( + &self, + transaction: &NotarizedTransactionV1, + ) -> reqwest::Result { + let bytes = transaction.to_payload_bytes().expect("Can't happen!"); + self.make_request::( + &super::types::transaction_submit::Input { + notarized_transaction_hex: hex::encode(bytes), + }, + ) + .map(|response| response.duplicate) + } + + pub fn transaction_committed_details( + &self, + bech32m_intent_hash: String, + ) -> reqwest::Result + { + self.make_request::( + &super::types::transaction_committed_details::Input { + intent_hash: bech32m_intent_hash, + opt_ins: super::types::transaction_committed_details::OptIns { + raw_hex: true, + receipt_state_changes: true, + receipt_fee_summary: true, + receipt_fee_source: true, + receipt_fee_destination: true, + receipt_costing_parameters: true, + receipt_events: true, + affected_global_entities: true, + } + }, + ) + } + + fn make_request( + &self, + input: &R::Input, + ) -> Result { + Client::new() + .post(format!("{}/{}", self.base_url, R::ENDPOINT)) + .json(input) + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46") + .send() + .and_then(|response| response.json()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn can_get_current_epoch() { + // Arrange + let client = GatewayClient::stokenet(); + + // Act + let epoch = dbg!(client.get_current_epoch()); + + // Assert + assert!(epoch.is_ok()) + } +} diff --git a/tools/bootstrapper/src/gateway/mod.rs b/tools/bootstrapper/src/gateway/mod.rs new file mode 100644 index 00000000..ad21a411 --- /dev/null +++ b/tools/bootstrapper/src/gateway/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod types; + +pub use client::*; +pub(crate) use types::*; diff --git a/tools/bootstrapper/src/gateway/types.rs b/tools/bootstrapper/src/gateway/types.rs new file mode 100644 index 00000000..3bbc2e21 --- /dev/null +++ b/tools/bootstrapper/src/gateway/types.rs @@ -0,0 +1,164 @@ +use radix_engine_interface::prelude::*; +use serde::*; +use serde_with::*; + +pub(crate) trait Request { + const ENDPOINT: &'static str; + type Input: serde::Serialize; + type Output: serde::de::DeserializeOwned; +} + +pub(crate) mod status_gateway_status { + use super::*; + use common::*; + + pub const ENDPOINT: &str = "/status/gateway-status"; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Input; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Output { + pub ledger_state: LedgerState, + pub release_info: ReleaseInfo, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct ReleaseInfo { + pub release_version: String, + pub open_api_schema_version: String, + pub image_tag: String, + } + + pub struct Request; + impl super::Request for Request { + const ENDPOINT: &'static str = ENDPOINT; + type Input = Input; + type Output = Output; + } +} + +pub(crate) mod transaction_submit { + use super::*; + + pub const ENDPOINT: &str = "/transaction/submit"; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Input { + pub notarized_transaction_hex: String, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Output { + pub duplicate: bool, + } + + pub struct Request; + impl super::Request for Request { + const ENDPOINT: &'static str = ENDPOINT; + type Input = Input; + type Output = Output; + } +} + +pub(crate) mod transaction_committed_details { + use super::*; + + pub const ENDPOINT: &str = "/transaction/committed-details"; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Input { + pub intent_hash: String, + pub opt_ins: OptIns, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct OptIns { + pub raw_hex: bool, + pub receipt_state_changes: bool, + pub receipt_fee_summary: bool, + pub receipt_fee_source: bool, + pub receipt_fee_destination: bool, + pub receipt_costing_parameters: bool, + pub receipt_events: bool, + pub affected_global_entities: bool, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Output { + pub transaction: Transaction, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Transaction { + pub transaction_status: TransactionStatus, + pub receipt: Receipt, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub enum TransactionStatus { + Unknown, + CommittedSuccess, + CommittedFailure, + Pending, + Rejected, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Receipt { + pub state_updates: StateUpdates, + pub events: Vec, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Event { + pub name: String, + pub emitter: Emitter, + pub data: serde_json::Value, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + #[serde(tag = "type")] + pub enum Emitter { + Method { + entity: Entity, + object_module_id: String, + }, + Function { + blueprint_name: String, + package_address: String, + }, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct StateUpdates { + pub new_global_entities: Vec, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Entity { + pub is_global: bool, + pub entity_type: String, + pub entity_address: String, + } + + pub struct Request; + impl super::Request for Request { + const ENDPOINT: &'static str = ENDPOINT; + type Input = Input; + type Output = Output; + } +} + +pub(crate) mod common { + use super::*; + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct LedgerState { + pub network: String, + pub state_version: u64, + pub proposer_round_timestamp: String, + pub epoch: u64, + pub round: u64, + } +} diff --git a/tools/bootstrapper/src/macros.rs b/tools/bootstrapper/src/macros.rs new file mode 100644 index 00000000..33081dd3 --- /dev/null +++ b/tools/bootstrapper/src/macros.rs @@ -0,0 +1,61 @@ +#[macro_export] +macro_rules! global_address { + ( + $address: expr + ) => { + $crate::address!( + ::radix_engine_interface::prelude::GlobalAddress, + $address + ) + }; +} + +#[macro_export] +macro_rules! component_address { + ( + $address: expr + ) => { + $crate::address!( + ::radix_engine_interface::prelude::ComponentAddress, + $address + ) + }; +} + +#[macro_export] +macro_rules! package_address { + ( + $address: expr + ) => { + $crate::address!( + ::radix_engine_interface::prelude::PackageAddress, + $address + ) + }; +} + +#[macro_export] +macro_rules! resource_address { + ( + $address: expr + ) => { + $crate::address!( + ::radix_engine_interface::prelude::ResourceAddress, + $address + ) + }; +} + +#[macro_export] +macro_rules! address { + ( + $ty: ty, + $address: expr + ) => { + AddressBech32Decoder::validate_and_decode_ignore_hrp($address.as_ref()) + .ok() + .and_then(|(_, _, address)| address.try_into().ok()) + .map(<$ty>::new_or_panic) + .unwrap() + }; +} diff --git a/tools/bootstrapper/src/main.rs b/tools/bootstrapper/src/main.rs new file mode 100644 index 00000000..a4ab1de4 --- /dev/null +++ b/tools/bootstrapper/src/main.rs @@ -0,0 +1,21 @@ +//! A tool used to bootstrap and publish project Ignition on various networks +//! and with varying configurations. This is a CLI tool and is made to be run +//! like a script. + +#[macro_use] +mod macros; +mod cli; +mod constants; +mod error; +mod gateway; +mod subcommands; +mod utils; + +use clap::Parser; +use error::*; + +fn main() -> Result<(), Error> { + let mut out = std::io::stdout(); + let cli = cli::Cli::parse(); + cli.run(&mut out) +} diff --git a/tools/bootstrapper/src/subcommands/mod.rs b/tools/bootstrapper/src/subcommands/mod.rs new file mode 100644 index 00000000..958923f9 --- /dev/null +++ b/tools/bootstrapper/src/subcommands/mod.rs @@ -0,0 +1 @@ +pub mod publish_test_to_stokenet; diff --git a/tools/bootstrapper/src/subcommands/publish_test_to_stokenet.rs b/tools/bootstrapper/src/subcommands/publish_test_to_stokenet.rs new file mode 100644 index 00000000..329cc606 --- /dev/null +++ b/tools/bootstrapper/src/subcommands/publish_test_to_stokenet.rs @@ -0,0 +1,385 @@ +use crate::constants::*; +use crate::error::*; +use crate::gateway::transaction_committed_details::{Emitter, Event}; +use crate::gateway::*; +use crate::utils::*; + +use ::bootstrap::*; +use package_loader::*; + +use clap::Parser; +use serde::{Deserialize, Serialize}; + +use radix_engine_interface::blueprints::package::*; +use transaction::builder::*; +use transaction::prelude::*; + +/// Publishes project Ignition to stokenet with the test configuration. +#[derive(Parser, Debug)] +pub struct PublishTestToStokenet { + /// The path to write the addresses out to, this should be to a JSON file. + pub output_path: String, +} + +impl PublishTestToStokenet { + pub fn run(self, _: &mut O) -> Result<(), Error> { + let network_definition = NetworkDefinition::stokenet(); + let address_encoder = AddressBech32Encoder::new(&network_definition); + + let caviarnine_package = package_address!("package_tdx_2_1p57g523zj736u370z6g4ynrytn7t6r2hledvzkhl6tzpg3urn0707e"); + + let public_key = + "02e78ec7992207d7d814173ffd8d88d04a2153481477104b8008dc424a713ddb03"; + let public_key = Secp256k1PublicKey::from_str(public_key).unwrap(); + let account = + ComponentAddress::virtual_account_from_public_key(&public_key); + + // Publishing all of the packages and getting their addresses. + let ( + ignition_package, + ociswap_adapter_v1_package, + caviarnine_adapter_v1_package, + test_oracle_package, + bootstrap_package, + ) = { + let (ignition_code, ignition_package_definition) = + PackageLoader::get(IGNITION_PACKAGE_NAME); + let ( + ociswap_adapter_v1_code, + ociswap_adapter_v1_package_definition, + ) = PackageLoader::get(OCISWAP_ADAPTER_V1_PACKAGE_NAME); + let ( + caviarnine_adapter_v1_code, + caviarnine_adapter_v1_package_definition, + ) = PackageLoader::get(CAVIARNINE_ADAPTER_V1_PACKAGE_NAME); + let (test_oracle_code, test_oracle_package_definition) = + PackageLoader::get(TEST_ORACLE_PACKAGE_NAME); + let (bootstrap_code, bootstrap_package_definition) = + PackageLoader::get(BOOTSTRAP_PACKAGE_NAME); + + let ignition_package = + publish_package(ignition_code, ignition_package_definition); + let ociswap_adapter_v1_package = publish_package( + ociswap_adapter_v1_code, + ociswap_adapter_v1_package_definition, + ); + let caviarnine_adapter_v1_package = publish_package( + caviarnine_adapter_v1_code, + caviarnine_adapter_v1_package_definition, + ); + let test_oracle_package = publish_package( + test_oracle_code, + test_oracle_package_definition, + ); + let bootstrap_package = + publish_package(bootstrap_code, bootstrap_package_definition); + + ( + ignition_package, + ociswap_adapter_v1_package, + caviarnine_adapter_v1_package, + test_oracle_package, + bootstrap_package, + ) + }; + + let manifest = ManifestBuilder::new() + .lock_fee_from_faucet() + .call_function( + bootstrap_package, + "Bootstrap", + "bootstrap_for_testing", + manifest_args!( + ignition_package, + test_oracle_package, + caviarnine_package, + ociswap_adapter_v1_package, + caviarnine_adapter_v1_package, + ), + ) + .try_deposit_entire_worktop_or_abort(account, None) + .build(); + + let (_, events) = submit_manifest(manifest)?; + let testing_bootstrap_information = events + .into_iter() + .find_map(|event| { + if event.name == EncodedTestingBootstrapInformation::EVENT_NAME + { + match event.emitter { + Emitter::Function { + blueprint_name, + package_address, + } if blueprint_name == "Bootstrap" + && package_address + == address_encoder + .encode( + bootstrap_package + .as_node_id() + .as_bytes(), + ) + .unwrap() => + { + let hex = serde_json::from_value::( + event + .data + .get("fields") + .unwrap() + .get(0) + .unwrap() + .get("hex") + .unwrap() + .clone(), + ) + .unwrap(); + + let encoded_bootstrap_information = + hex::decode(hex).unwrap(); + scrypto_decode::( + &encoded_bootstrap_information, + ) + .ok() + } + _ => None, + } + } else { + None + } + }) + .expect("Should not fail"); + + let serializable_testing_bootstrap_information = + SerializableTestingBootstrapInformation::new( + testing_bootstrap_information, + &address_encoder, + ); + std::fs::write( + self.output_path, + serde_json::to_string(&serializable_testing_bootstrap_information) + .unwrap(), + )?; + + Ok(()) + } +} + +fn publish_package( + wasm: Vec, + package_definition: PackageDefinition, +) -> PackageAddress { + let manifest = ManifestBuilder::new() + .lock_fee_from_faucet() + .publish_package_advanced( + None, + wasm, + package_definition, + MetadataInit::default(), + OwnerRole::None, + ) + .build(); + (*submit_manifest(manifest).unwrap().0.first().unwrap()) + .try_into() + .unwrap() +} + +/// Constructs a transaction from a manifest, submits it through the gateway, +/// and then pools the gateway for the status of the transaction. This fails +/// if the pooling time exceeds 45 seconds. +fn submit_manifest( + manifest: TransactionManifestV1, +) -> Result<(Vec, Vec), crate::error::Error> { + let gateway_client = GatewayClient::stokenet(); + let network_definition = NetworkDefinition::stokenet(); + let transaction_hash_encoder = + TransactionHashBech32Encoder::new(&network_definition); + + let private_key = Secp256k1PrivateKey::from_u64(1).expect("must succeed!"); + let public_key = private_key.public_key(); + + let current_epoch = gateway_client.get_current_epoch()?; + let transaction = TransactionBuilder::new() + .header(TransactionHeaderV1 { + network_id: 0x02, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch.after(100).unwrap(), + nonce: random_nonce(), + notary_public_key: public_key.into(), + notary_is_signatory: false, + tip_percentage: 0, + }) + .manifest(manifest) + .notarize(&private_key) + .build(); + let intent_hash = transaction.prepare().unwrap().intent_hash(); + assert!(!(gateway_client.submit_transaction(&transaction)?)); + + let bech32m_intent_hash = + transaction_hash_encoder.encode(&intent_hash).unwrap(); + println!("Submitting transaction: {bech32m_intent_hash}"); + + // Check a total of 9 times at 5 second intervals = 45 seconds. + for _ in 0..9 { + use crate::gateway::transaction_committed_details::*; + match gateway_client + .transaction_committed_details(bech32m_intent_hash.clone()) + { + Err(..) + | Ok(Output { + transaction: + Transaction { + transaction_status: TransactionStatus::Unknown, + receipt: Receipt { .. }, + }, + }) => { + // Do nothing, just get out of the match statement and let the + // thread sleep. + } + Ok(Output { + transaction: + Transaction { + transaction_status, + receipt: + Receipt { + state_updates, + events, + }, + }, + }) => { + return if let TransactionStatus::CommittedSuccess = + transaction_status + { + Ok(( + state_updates + .new_global_entities + .into_iter() + .map(|item| global_address!(item.entity_address)) + .collect(), + events, + )) + } else { + Err(Error::TransactionDidNotSucceed) + } + } + } + std::thread::sleep(std::time::Duration::from_secs(5)); + } + Err(Error::TransactionPollingTimeOut) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableTestingBootstrapInformation { + pub resources: BTreeMap, + pub protocol: SerializableProtocolEntities, + pub caviarnine: SerializableDexEntities, +} + +impl SerializableTestingBootstrapInformation { + pub fn new( + value: TestingBootstrapInformation, + encoder: &AddressBech32Encoder, + ) -> Self { + Self { + resources: value + .resources + .into_iter() + .map(|(k, v)| { + ( + encode(k, encoder), + SerializableResourceInformation::from(v), + ) + }) + .collect(), + protocol: SerializableProtocolEntities::new( + value.protocol, + encoder, + ), + caviarnine: SerializableDexEntities::new(value.caviarnine, encoder), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableProtocolEntities { + /* Ignition */ + pub ignition_package_address: String, + pub ignition: String, + pub protocol_resource: String, + /* Oracle */ + pub oracle_package_address: String, + pub oracle: String, +} + +impl SerializableProtocolEntities { + pub fn new( + value: ProtocolEntities, + encoder: &AddressBech32Encoder, + ) -> Self { + Self { + ignition_package_address: encode( + value.ignition_package_address, + encoder, + ), + ignition: encode(value.ignition, encoder), + protocol_resource: encode(value.protocol_resource, encoder), + oracle_package_address: encode( + value.oracle_package_address, + encoder, + ), + oracle: encode(value.oracle, encoder), + } + } +} + +/// A struct that defines the entities that belong to a Decentralized Exchange. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableDexEntities { + /* Packages */ + pub package: String, + /* Pools */ + pub pools: BTreeMap, + /* Adapter */ + pub adapter_package: String, + pub adapter: String, +} + +impl SerializableDexEntities { + pub fn new(value: DexEntities, encoder: &AddressBech32Encoder) -> Self { + Self { + package: encode(value.package, encoder), + pools: value + .pools + .into_iter() + .map(|(k, v)| (encode(k, encoder), encode(v, encoder))) + .collect(), + adapter_package: encode(value.adapter_package, encoder), + adapter: encode(value.adapter, encoder), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableResourceInformation { + pub divisibility: u8, + pub name: String, + pub symbol: String, + pub icon_url: String, +} + +impl From for SerializableResourceInformation { + fn from(value: ResourceInformation) -> Self { + Self { + divisibility: value.divisibility, + name: value.name, + symbol: value.symbol, + icon_url: value.icon_url, + } + } +} + +fn encode(item: T, encoder: &AddressBech32Encoder) -> String +where + T: Into, +{ + let node_id = item.into(); + encoder.encode(node_id.as_bytes()).unwrap() +} diff --git a/tools/bootstrapper/src/utils.rs b/tools/bootstrapper/src/utils.rs new file mode 100644 index 00000000..c0b3d55e --- /dev/null +++ b/tools/bootstrapper/src/utils.rs @@ -0,0 +1,6 @@ +use rand::Rng; + +#[inline] +pub fn random_nonce() -> u32 { + rand::thread_rng().gen() +}