From 7d7478ab3b454dd5a309ef3d1df053c67f83d879 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 26 Feb 2024 15:18:03 +0300 Subject: [PATCH 01/47] [Ignition]: Use indexed buckets --- Cargo.toml | 1 + libraries/common/Cargo.toml | 2 +- .../common}/src/indexed_buckets.rs | 91 ++++++++++++++----- libraries/common/src/lib.rs | 1 + libraries/common/src/prelude.rs | 1 + libraries/ports-interface/src/pool.rs | 4 +- packages/caviarnine-v1-adapter-v1/src/lib.rs | 10 +- packages/ociswap-v1-adapter-v1/src/lib.rs | 11 +-- packages/ociswap-v2-adapter-v1/src/lib.rs | 12 +-- tests/src/lib.rs | 1 - tests/src/prelude.rs | 1 - tests/tests/caviarnine_v1.rs | 4 +- tests/tests/caviarnine_v1_simulation.rs | 2 + tests/tests/ociswap_v1.rs | 2 +- tests/tests/ociswap_v2.rs | 2 +- tests/tests/protocol.rs | 16 ++-- 16 files changed, 98 insertions(+), 63 deletions(-) rename {tests => libraries/common}/src/indexed_buckets.rs (55%) diff --git a/Cargo.toml b/Cargo.toml index 13d13884..c8c2ed29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ description = "The implementation of project Ignition in Scrypto for the Radix L sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } utils = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } +native-sdk = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } diff --git a/libraries/common/Cargo.toml b/libraries/common/Cargo.toml index 3140d6bf..fe6a0f7f 100644 --- a/libraries/common/Cargo.toml +++ b/libraries/common/Cargo.toml @@ -7,7 +7,7 @@ description = "A crate that defines types used by the other crates." [dependencies] sbor = { workspace = true } scrypto = { workspace = true } -transaction = { workspace = true, optional = true } +native-sdk = { workspace = true } radix-engine-common = { workspace = true } radix-engine-derive = { workspace = true } radix-engine-interface = { workspace = true } diff --git a/tests/src/indexed_buckets.rs b/libraries/common/src/indexed_buckets.rs similarity index 55% rename from tests/src/indexed_buckets.rs rename to libraries/common/src/indexed_buckets.rs index 1fcc90ff..b675e82f 100644 --- a/tests/src/indexed_buckets.rs +++ b/libraries/common/src/indexed_buckets.rs @@ -1,6 +1,7 @@ -use crate::prelude::*; +use scrypto::prelude::*; /// Buckets indexed and aggregated by the resource address. +#[derive(Debug, ScryptoSbor)] pub struct IndexedBuckets(IndexMap); impl IndexedBuckets { @@ -8,7 +9,33 @@ impl IndexedBuckets { Self(Default::default()) } - pub fn from_bucket( + pub fn from_bucket(bucket: impl Into) -> Self { + let mut this = Self::new(); + this.insert(bucket); + this + } + + pub fn from_buckets( + buckets: impl IntoIterator>, + ) -> Self { + let mut this = Self::new(); + for bucket in buckets.into_iter() { + this.insert(bucket); + } + this + } + + pub fn insert(&mut self, bucket: impl Into) { + let bucket = bucket.into(); + let resource_address = bucket.resource_address(); + if let Some(existing_bucket) = self.0.get_mut(&resource_address) { + existing_bucket.put(bucket) + } else { + self.0.insert(resource_address, bucket); + }; + } + + pub fn native_from_bucket( bucket: impl Into, api: &mut Y, ) -> Result @@ -17,11 +44,10 @@ impl IndexedBuckets { E: Debug + ScryptoCategorize + ScryptoDecode, { let mut this = Self::new(); - this.insert(bucket, api)?; + this.native_insert(bucket, api)?; Ok(this) } - - pub fn from_buckets( + pub fn native_from_buckets( buckets: impl IntoIterator>, api: &mut Y, ) -> Result @@ -30,26 +56,12 @@ impl IndexedBuckets { E: Debug + ScryptoCategorize + ScryptoDecode, { let mut this = Self::new(); - for bucket in buckets.into_iter() { - this.insert(bucket, api)?; + this.native_insert(bucket, api)?; } - Ok(this) } - - pub fn get(&self, resource_address: &ResourceAddress) -> Option<&Bucket> { - self.0.get(resource_address) - } - - pub fn get_mut( - &mut self, - resource_address: &ResourceAddress, - ) -> Option<&mut Bucket> { - self.0.get_mut(resource_address) - } - - pub fn insert( + pub fn native_insert( &mut self, bucket: impl Into, api: &mut Y, @@ -59,15 +71,31 @@ impl IndexedBuckets { E: Debug + ScryptoCategorize + ScryptoDecode, { let bucket = bucket.into(); - let resource_address = bucket.resource_address(api)?; - if let Some(existing_bucket) = self.0.get_mut(&resource_address) { - existing_bucket.put(bucket, api)?; + let resource_address = + native_sdk::resource::NativeBucket::resource_address(&bucket, api)?; + if let Some(existing_bucket) = self.0.get(&resource_address) { + native_sdk::resource::NativeBucket::put( + existing_bucket, + bucket, + api, + )?; } else { self.0.insert(resource_address, bucket); }; Ok(()) } + pub fn get(&self, resource_address: &ResourceAddress) -> Option<&Bucket> { + self.0.get(resource_address) + } + + pub fn get_mut( + &mut self, + resource_address: &ResourceAddress, + ) -> Option<&mut Bucket> { + self.0.get_mut(resource_address) + } + pub fn keys(&self) -> impl Iterator { self.0.keys() } @@ -76,6 +104,10 @@ impl IndexedBuckets { self.0.values() } + pub fn into_values(self) -> impl Iterator { + self.0.into_values() + } + pub fn values_mut(&mut self) -> impl Iterator { self.0.values_mut() } @@ -87,6 +119,17 @@ impl IndexedBuckets { pub fn len(&self) -> usize { self.0.len() } + + pub fn remove( + &mut self, + resource_address: &ResourceAddress, + ) -> Option { + self.0.remove(resource_address) + } + + pub fn into_inner(self) -> IndexMap { + self.0 + } } impl Default for IndexedBuckets { diff --git a/libraries/common/src/lib.rs b/libraries/common/src/lib.rs index 2965da90..136b0f8a 100644 --- a/libraries/common/src/lib.rs +++ b/libraries/common/src/lib.rs @@ -1,4 +1,5 @@ mod any_value; +mod indexed_buckets; mod liquidity_receipt; mod lockup_period; mod price; diff --git a/libraries/common/src/prelude.rs b/libraries/common/src/prelude.rs index d9383b20..93755500 100644 --- a/libraries/common/src/prelude.rs +++ b/libraries/common/src/prelude.rs @@ -1,4 +1,5 @@ pub use crate::any_value::*; +pub use crate::indexed_buckets::*; pub use crate::liquidity_receipt::*; pub use crate::lockup_period::*; pub use crate::price::*; diff --git a/libraries/ports-interface/src/pool.rs b/libraries/ports-interface/src/pool.rs index 5bb7dd90..418c0411 100644 --- a/libraries/ports-interface/src/pool.rs +++ b/libraries/ports-interface/src/pool.rs @@ -59,7 +59,7 @@ pub struct OpenLiquidityPositionOutput { /// The pool units obtained as part of the contribution to the pool. pub pool_units: Bucket, /// Any change the pool has returned back indexed by the resource address. - pub change: IndexMap, + pub change: IndexedBuckets, /// Any additional tokens that the pool has returned back. pub others: Vec, /// Any adapter specific information that the adapter wishes to pass back @@ -72,7 +72,7 @@ pub struct OpenLiquidityPositionOutput { pub struct CloseLiquidityPositionOutput { /// Resources obtained from closing the liquidity position, indexed by the /// resource address. - pub resources: IndexMap, + pub resources: IndexedBuckets, /// Any additional tokens that the pool has returned back. pub others: Vec, /// The amount of trading fees earned on the position. diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index 1d8bb52b..b90fd1fb 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -365,10 +365,7 @@ pub mod adapter { OpenLiquidityPositionOutput { pool_units: receipt, - change: indexmap! { - change_x.resource_address() => change_x, - change_y.resource_address() => change_y, - }, + change: IndexedBuckets::from_buckets([change_x, change_y]), others: vec![], adapter_specific_information: adapter_specific_information .into(), @@ -446,10 +443,7 @@ pub mod adapter { }; CloseLiquidityPositionOutput { - resources: indexmap! { - resource_x => bucket_x, - resource_y => bucket_y, - }, + resources: IndexedBuckets::from_buckets([bucket_x, bucket_y]), others: Default::default(), fees, } diff --git a/packages/ociswap-v1-adapter-v1/src/lib.rs b/packages/ociswap-v1-adapter-v1/src/lib.rs index b0c59102..35e6bbd5 100644 --- a/packages/ociswap-v1-adapter-v1/src/lib.rs +++ b/packages/ociswap-v1-adapter-v1/src/lib.rs @@ -138,11 +138,7 @@ pub mod adapter { OpenLiquidityPositionOutput { pool_units, change: change - .map(|bucket| { - indexmap! { - bucket.resource_address() => bucket - } - }) + .map(IndexedBuckets::from_bucket) .unwrap_or_default(), others: Default::default(), adapter_specific_information: @@ -265,10 +261,7 @@ pub mod adapter { }; CloseLiquidityPositionOutput { - resources: indexmap! { - bucket1.resource_address() => bucket1, - bucket2.resource_address() => bucket2, - }, + resources: IndexedBuckets::from_buckets([bucket1, bucket2]), others: Default::default(), fees, } diff --git a/packages/ociswap-v2-adapter-v1/src/lib.rs b/packages/ociswap-v2-adapter-v1/src/lib.rs index 6bf03e0f..afc3071e 100644 --- a/packages/ociswap-v2-adapter-v1/src/lib.rs +++ b/packages/ociswap-v2-adapter-v1/src/lib.rs @@ -170,10 +170,7 @@ pub mod adapter { OpenLiquidityPositionOutput { pool_units: receipt, - change: indexmap! { - change_x.resource_address() => change_x, - change_y.resource_address() => change_y, - }, + change: IndexedBuckets::from_buckets([change_x, change_y]), others: Default::default(), adapter_specific_information: AnyValue::from_typed(&()) .expect(UNEXPECTED_ERROR), @@ -201,10 +198,9 @@ pub mod adapter { pool.remove_liquidity(pool_units.as_non_fungible()); CloseLiquidityPositionOutput { - resources: indexmap! { - resource_x.resource_address() => resource_x, - resource_y.resource_address() => resource_y, - }, + resources: IndexedBuckets::from_buckets([ + resource_x, resource_y, + ]), others: vec![], fees: indexmap! { resource_address_x => fees_x, diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 17aae498..b3fb765b 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,6 +4,5 @@ mod environment; mod errors; mod extensions; -mod indexed_buckets; pub mod prelude; diff --git a/tests/src/prelude.rs b/tests/src/prelude.rs index ad9a7ab7..f0f29cce 100644 --- a/tests/src/prelude.rs +++ b/tests/src/prelude.rs @@ -3,7 +3,6 @@ pub use crate::environment::*; pub use crate::errors::*; pub use crate::extensions::*; -pub use crate::indexed_buckets::*; pub use radix_engine::system::system_db_reader::*; pub use radix_engine_common::prelude::*; diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index d11f1a27..c2c822f6 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1,3 +1,5 @@ +#![allow(clippy::arithmetic_side_effects)] + use tests::prelude::*; #[test] @@ -725,7 +727,7 @@ fn non_strict_testing_of_fees( env, )?; - let buckets = IndexedBuckets::from_buckets( + let buckets = IndexedBuckets::native_from_buckets( protocol.ignition.close_liquidity_position(receipt, env)?, env, )?; diff --git a/tests/tests/caviarnine_v1_simulation.rs b/tests/tests/caviarnine_v1_simulation.rs index 36accb9a..84f09e3c 100644 --- a/tests/tests/caviarnine_v1_simulation.rs +++ b/tests/tests/caviarnine_v1_simulation.rs @@ -10,6 +10,8 @@ //! found to test the C9 pools in a "real environment" and ensuring that what //! we have works with C9. +#![allow(clippy::arithmetic_side_effects)] + use gateway_client::apis::configuration::Configuration as GatewayConfig; use gateway_client::apis::transaction_api::*; use gateway_client::models::*; diff --git a/tests/tests/ociswap_v1.rs b/tests/tests/ociswap_v1.rs index 5923493b..17a44d12 100644 --- a/tests/tests/ociswap_v1.rs +++ b/tests/tests/ociswap_v1.rs @@ -553,7 +553,7 @@ fn non_strict_testing_of_fees( env, )?; - let buckets = IndexedBuckets::from_buckets( + let buckets = IndexedBuckets::native_from_buckets( protocol.ignition.close_liquidity_position(receipt, env)?, env, )?; diff --git a/tests/tests/ociswap_v2.rs b/tests/tests/ociswap_v2.rs index 5c515809..13c3e8c4 100644 --- a/tests/tests/ociswap_v2.rs +++ b/tests/tests/ociswap_v2.rs @@ -520,7 +520,7 @@ fn non_strict_testing_of_fees( env, )?; - let buckets = IndexedBuckets::from_buckets( + let buckets = IndexedBuckets::native_from_buckets( protocol.ignition.close_liquidity_position(receipt, env)?, env, )?; diff --git a/tests/tests/protocol.rs b/tests/tests/protocol.rs index c1fa8591..91a40998 100644 --- a/tests/tests/protocol.rs +++ b/tests/tests/protocol.rs @@ -1287,7 +1287,8 @@ fn user_gets_back_the_same_amount_they_put_in_when_user_resource_price_goes_down protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let indexed_buckets = IndexedBuckets::from_buckets(assets_back, env)?; + let indexed_buckets = + IndexedBuckets::native_from_buckets(assets_back, env)?; assert_eq!(indexed_buckets.len(), 2); assert_eq!( @@ -1359,7 +1360,8 @@ fn user_gets_enough_protocol_resource_to_purchase_back_user_assets_lost_due_to_i protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let indexed_buckets = IndexedBuckets::from_buckets(assets_back, env)?; + let indexed_buckets = + IndexedBuckets::native_from_buckets(assets_back, env)?; assert_eq!(indexed_buckets.len(), 2); assert_eq!( @@ -1430,7 +1432,8 @@ fn user_gets_enough_protocol_resource_to_purchase_back_user_assets_lost_due_to_i protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let indexed_buckets = IndexedBuckets::from_buckets(assets_back, env)?; + let indexed_buckets = + IndexedBuckets::native_from_buckets(assets_back, env)?; assert_eq!(indexed_buckets.len(), 2); assert_eq!( @@ -1501,7 +1504,8 @@ fn amount_of_protocol_resources_returned_to_user_has_an_upper_bound_of_the_amoun protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let indexed_buckets = IndexedBuckets::from_buckets(assets_back, env)?; + let indexed_buckets = + IndexedBuckets::native_from_buckets(assets_back, env)?; assert_eq!(indexed_buckets.len(), 2); assert_eq!( @@ -1770,7 +1774,7 @@ fn forcefully_liquidated_resources_can_be_claimed_when_closing_liquidity_positio let buckets = protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let buckets = IndexedBuckets::from_buckets(buckets, env)?; + let buckets = IndexedBuckets::native_from_buckets(buckets, env)?; let bitcoin_bucket = buckets.get(&resources.bitcoin).unwrap(); let amount = bitcoin_bucket.amount(env)?; assert_eq!(amount, dec!(99.99999999)); @@ -1825,7 +1829,7 @@ fn forcefully_liquidated_resources_can_be_claimed_when_closing_liquidity_positio let buckets = protocol.ignition.close_liquidity_position(receipt, env)?; // Assert - let buckets = IndexedBuckets::from_buckets(buckets, env)?; + let buckets = IndexedBuckets::native_from_buckets(buckets, env)?; let bitcoin_bucket = buckets.get(&resources.bitcoin).unwrap(); let amount = bitcoin_bucket.amount(env)?; assert_eq!(amount, dec!(99.99999999)); From 5b8596085db1bc8ecdc8a99371727cf393f77f7c Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 26 Feb 2024 16:34:06 +0300 Subject: [PATCH 02/47] [Ignition]: Allow for multiple pool units to support DefiPlaza. --- .github/workflows/test.yml | 7 +-- Makefile.toml | 3 +- libraries/ports-interface/src/pool.rs | 6 +- libraries/scrypto-interface/src/handlers.rs | 10 ++-- packages/caviarnine-v1-adapter-v1/src/lib.rs | 14 ++++- packages/ignition/src/blueprint.rs | 62 ++++++++++++++++---- packages/ociswap-v1-adapter-v1/src/lib.rs | 14 ++++- packages/ociswap-v2-adapter-v1/src/lib.rs | 13 +++- tests/example | 1 + tests/src/environment.rs | 2 + tests/tests/caviarnine_v1.rs | 9 ++- tests/tests/ociswap_v2.rs | 5 +- 12 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 tests/example diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16d0b27b..ab59ecb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,15 +17,10 @@ jobs: components: rustfmt, clippy - name: Add WASM target run: rustup target add wasm32-unknown-unknown - - name: Install cargo nextest - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-nextest - locked: true - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - name: Run tests - run: cargo nextest run + run: cargo test --features package-loader/build-time-blueprints env: # Enable sccache SCCACHE_GHA_ENABLED: "true" diff --git a/Makefile.toml b/Makefile.toml index 24d4c392..6f4f6a2c 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -4,8 +4,7 @@ default_to_workspace = false [tasks.test] command = "cargo" args = [ - "nextest", - "run", + "test", "--features", "package-loader/build-time-blueprints", "--no-fail-fast", diff --git a/libraries/ports-interface/src/pool.rs b/libraries/ports-interface/src/pool.rs index 418c0411..517f6d84 100644 --- a/libraries/ports-interface/src/pool.rs +++ b/libraries/ports-interface/src/pool.rs @@ -38,8 +38,8 @@ define_interface! { fn close_liquidity_position( &mut self, pool_address: ComponentAddress, - #[manifest_type = "ManifestBucket"] - pool_units: Bucket, + #[manifest_type = "Vec"] + pool_units: Vec, adapter_specific_information: AnyValue ) -> CloseLiquidityPositionOutput; @@ -57,7 +57,7 @@ define_interface! { #[derive(Debug, ScryptoSbor)] pub struct OpenLiquidityPositionOutput { /// The pool units obtained as part of the contribution to the pool. - pub pool_units: Bucket, + pub pool_units: IndexedBuckets, /// Any change the pool has returned back indexed by the resource address. pub change: IndexedBuckets, /// Any additional tokens that the pool has returned back. diff --git a/libraries/scrypto-interface/src/handlers.rs b/libraries/scrypto-interface/src/handlers.rs index cefee704..8b3455ce 100644 --- a/libraries/scrypto-interface/src/handlers.rs +++ b/libraries/scrypto-interface/src/handlers.rs @@ -532,7 +532,7 @@ fn generate_manifest_builder_stub( } #(#attributes)* - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, unused_mut)] const _: () = { impl #trait_ident for ::transaction::builder::ManifestBuilder { #(#implementations)* @@ -668,11 +668,11 @@ pub fn handle_blueprint_with_traits( #[::scrypto::prelude::blueprint] #module - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, unused_mut)] const _: () = { struct #blueprint_ident; - #[allow(unused_variables)] + #[allow(unused_variables, unused_mut)] #(#unreachable_trait_impls)* }; }) @@ -791,11 +791,11 @@ mod test { } } - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, unused_mut)] const _: () = { struct Blueprint; - #[allow (unused_variables)] + #[allow (unused_variables, unused_mut)] impl MyTrait for Blueprint { fn func1() { unreachable!() diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index b90fd1fb..111ddaf2 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -34,6 +34,7 @@ define_error! { NO_ACTIVE_AMOUNTS_ERROR => "Pool has no active amounts."; NO_PRICE_ERROR => "Pool has no price."; OVERFLOW_ERROR => "Overflow error."; + INVALID_NUMBER_OF_BUCKETS => "Invalid number of buckets."; } macro_rules! pool { @@ -364,7 +365,7 @@ pub mod adapter { }; OpenLiquidityPositionOutput { - pool_units: receipt, + pool_units: IndexedBuckets::from_bucket(receipt), change: IndexedBuckets::from_buckets([change_x, change_y]), others: vec![], adapter_specific_information: adapter_specific_information @@ -375,10 +376,19 @@ pub mod adapter { fn close_liquidity_position( &mut self, pool_address: ComponentAddress, - pool_units: Bucket, + mut pool_units: Vec, adapter_specific_information: AnyValue, ) -> CloseLiquidityPositionOutput { let mut pool = pool!(pool_address); + let pool_units = { + let pool_units_bucket = + pool_units.pop().expect(INVALID_NUMBER_OF_BUCKETS); + if !pool_units.is_empty() { + panic!("{}", INVALID_NUMBER_OF_BUCKETS) + } + pool_units_bucket + }; + let pool_information @ PoolInformation { bin_span, resources: diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 75aab1f8..621cb503 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -85,6 +85,7 @@ type OracleAdapter = OracleAdapterInterfaceScryptoStub; LockupPeriod, Volatility, StoredPoolBlueprintInformation, + IndexMap, )] mod ignition { enable_method_auth! { @@ -242,7 +243,22 @@ mod ignition { /// global id of the liquidity receipt non-fungible token minted by /// the protocol when liquidity is provided. Only the owner of the /// protocol is allowed to deposit or withdraw into these vaults. - pool_units: KeyValueStore, + /// + /// The value of the map is another map which maps the address of the + /// pool unit to a vault containing this pool unit. A map is used since + /// not all exchanges work one pool units per contributions. Some of + /// them require two or more. + /// + /// Note: it is understood that the use of [`IndexMap`] here can make + /// the application vulnerable to state explosion. However, we chose to + /// continue using it as the size of this will realistically always be + /// 1 in the case of most exchanges and 2 in the case of DefiPlaza. It + /// should not be any more than that. There is realistically no way for + /// this map to have more than two items. + pool_units: KeyValueStore< + NonFungibleGlobalId, + IndexMap, + >, /// A KeyValueStore that stores all of the tokens owed to users of the /// protocol whose liquidity claims have been forcefully liquidated. @@ -630,8 +646,16 @@ mod ignition { liquidity_receipt_resource.address(), liquidity_receipt.non_fungible_local_id(), ); - self.pool_units - .insert(global_id, Vault::with_bucket(pool_units)); + self.pool_units.insert( + global_id, + pool_units + .into_inner() + .into_iter() + .map(|(address, bucket)| { + (address, Vault::with_bucket(bucket)) + }) + .collect(), + ); liquidity_receipt }; @@ -867,7 +891,9 @@ mod ignition { .pool_units .get_mut(&liquidity_receipt_global_id) .expect(UNEXPECTED_ERROR) - .take_all(); + .values_mut() + .map(|vault| vault.take_all()) + .collect::>(); adapter.close_liquidity_position( liquidity_receipt_data.pool_address, pool_units, @@ -1405,13 +1431,26 @@ mod ignition { global_id: NonFungibleGlobalId, pool_units: Bucket, ) { + let pool_units_resource_address = pool_units.resource_address(); + let entry = self.pool_units.get_mut(&global_id); - if let Some(mut vault) = entry { - vault.put(pool_units); + if let Some(mut vaults) = entry { + if let Some(vault) = + vaults.get_mut(&pool_units_resource_address) + { + vault.put(pool_units) + } else { + vaults.insert( + pool_units_resource_address, + Vault::with_bucket(pool_units), + ); + } } else { drop(entry); self.pool_units - .insert(global_id, Vault::with_bucket(pool_units)) + .insert(global_id, indexmap! { + pool_units_resource_address => Vault::with_bucket(pool_units) + }) } } @@ -1434,15 +1473,18 @@ mod ignition { /// /// # Returns /// - /// * [`Bucket`] - A bucket of the withdrawn tokens. + /// * [`Vec`] - A vector of buckets of the pool units for the + /// specified liquidity receipt. pub fn withdraw_pool_units( &mut self, global_id: NonFungibleGlobalId, - ) -> Bucket { + ) -> Vec { self.pool_units .get_mut(&global_id) .expect(NO_ASSOCIATED_LIQUIDITY_RECEIPT_VAULT_ERROR) - .take_all() + .values_mut() + .map(|vault| vault.take_all()) + .collect() } /// Updates the value of the maximum allowed price staleness used by diff --git a/packages/ociswap-v1-adapter-v1/src/lib.rs b/packages/ociswap-v1-adapter-v1/src/lib.rs index 35e6bbd5..35de999e 100644 --- a/packages/ociswap-v1-adapter-v1/src/lib.rs +++ b/packages/ociswap-v1-adapter-v1/src/lib.rs @@ -30,6 +30,7 @@ define_error! { FAILED_TO_CALCULATE_K_VALUE_OF_POOL_ERROR => "Failed to calculate the K value of the pool."; OVERFLOW_ERROR => "Calculation overflowed."; + INVALID_NUMBER_OF_BUCKETS => "Invalid number of buckets."; } macro_rules! pool { @@ -136,7 +137,7 @@ pub mod adapter { .expect(FAILED_TO_CALCULATE_K_VALUE_OF_POOL_ERROR); OpenLiquidityPositionOutput { - pool_units, + pool_units: IndexedBuckets::from_bucket(pool_units), change: change .map(IndexedBuckets::from_bucket) .unwrap_or_default(), @@ -198,11 +199,20 @@ pub mod adapter { fn close_liquidity_position( &mut self, pool_address: ComponentAddress, - pool_units: Bucket, + mut pool_units: Vec, adapter_specific_information: AnyValue, ) -> CloseLiquidityPositionOutput { let mut pool = pool!(pool_address); + let pool_units = { + let pool_units_bucket = + pool_units.pop().expect(INVALID_NUMBER_OF_BUCKETS); + if !pool_units.is_empty() { + panic!("{}", INVALID_NUMBER_OF_BUCKETS) + } + pool_units_bucket + }; + let (bucket1, bucket2) = pool.remove_liquidity(pool_units); // Calculating the fees. diff --git a/packages/ociswap-v2-adapter-v1/src/lib.rs b/packages/ociswap-v2-adapter-v1/src/lib.rs index afc3071e..c27868bb 100644 --- a/packages/ociswap-v2-adapter-v1/src/lib.rs +++ b/packages/ociswap-v2-adapter-v1/src/lib.rs @@ -24,6 +24,7 @@ define_error! { => "One or more of the resources do not belong to pool."; OVERFLOW_ERROR => "Calculation overflowed."; UNEXPECTED_ERROR => "Unexpected error."; + INVALID_NUMBER_OF_BUCKETS => "Invalid number of buckets."; } macro_rules! pool { @@ -169,7 +170,7 @@ pub mod adapter { pool.add_liquidity(lower_tick, upper_tick, bucket_x, bucket_y); OpenLiquidityPositionOutput { - pool_units: receipt, + pool_units: IndexedBuckets::from_bucket(receipt), change: IndexedBuckets::from_buckets([change_x, change_y]), others: Default::default(), adapter_specific_information: AnyValue::from_typed(&()) @@ -180,10 +181,18 @@ pub mod adapter { fn close_liquidity_position( &mut self, pool_address: ComponentAddress, - pool_units: Bucket, + mut pool_units: Vec, _: AnyValue, ) -> CloseLiquidityPositionOutput { let mut pool = pool!(pool_address); + let pool_units = { + let pool_units_bucket = + pool_units.pop().expect(INVALID_NUMBER_OF_BUCKETS); + if !pool_units.is_empty() { + panic!("{}", INVALID_NUMBER_OF_BUCKETS) + } + pool_units_bucket + }; // Calculate how much fees were earned on the position while it was // opened. diff --git a/tests/example b/tests/example new file mode 100644 index 00000000..8b88f962 --- /dev/null +++ b/tests/example @@ -0,0 +1 @@ +Reading now! \ No newline at end of file diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 6debe76d..34400f06 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -1,3 +1,5 @@ +#![allow(clippy::arithmetic_side_effects)] + use crate::prelude::*; pub type ScryptoTestEnv = Environment; diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index c2c822f6..5980e7a9 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -497,7 +497,9 @@ pub fn contribution_amount_reported_in_receipt_nft_matches_caviarnine_state( let caviarnine_receipt = protocol .ignition - .withdraw_pool_units(ignition_receipt_global_id, env)?; + .withdraw_pool_units(ignition_receipt_global_id, env)? + .pop() + .unwrap(); let mut caviarnine_reported_contributions = caviarnine_v1.pools.bitcoin.get_redemption_bin_values( @@ -667,7 +669,10 @@ fn non_strict_testing_of_fees( ), env, )? - .pool_units; + .pool_units + .into_values() + .next() + .unwrap(); match price_of_user_asset { // User asset price goes down - i.e., we inject it into the pool. diff --git a/tests/tests/ociswap_v2.rs b/tests/tests/ociswap_v2.rs index 13c3e8c4..28931740 100644 --- a/tests/tests/ociswap_v2.rs +++ b/tests/tests/ociswap_v2.rs @@ -460,7 +460,10 @@ fn non_strict_testing_of_fees( ), env, )? - .pool_units; + .pool_units + .into_values() + .next() + .unwrap(); match price_of_user_asset { // User asset price goes down - i.e., we inject it into the pool. From fd64efecb3a489c1f51b92595c6cba3acf2ba486 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 26 Feb 2024 19:51:49 +0300 Subject: [PATCH 03/47] [Defiplaza v1 Adapter v1]: Initial implementation of adapter. --- Cargo.toml | 1 + packages/defiplaza-v2-adapter-v1/Cargo.toml | 27 ++ .../src/blueprint_interface.rs | 113 +++++ packages/defiplaza-v2-adapter-v1/src/lib.rs | 402 ++++++++++++++++++ tests/assets/defiplaza_v2.rpd | Bin 0 -> 6284 bytes tests/assets/defiplaza_v2.wasm | Bin 0 -> 353184 bytes 6 files changed, 543 insertions(+) create mode 100644 packages/defiplaza-v2-adapter-v1/Cargo.toml create mode 100644 packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs create mode 100644 packages/defiplaza-v2-adapter-v1/src/lib.rs create mode 100644 tests/assets/defiplaza_v2.rpd create mode 100755 tests/assets/defiplaza_v2.wasm diff --git a/Cargo.toml b/Cargo.toml index c8c2ed29..c0f8ebff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "packages/simple-oracle", "packages/ociswap-v1-adapter-v1", "packages/ociswap-v2-adapter-v1", + "packages/defiplaza-v2-adapter-v1", "packages/caviarnine-v1-adapter-v1", # Libraries "libraries/common", diff --git a/packages/defiplaza-v2-adapter-v1/Cargo.toml b/packages/defiplaza-v2-adapter-v1/Cargo.toml new file mode 100644 index 00000000..c239bfa0 --- /dev/null +++ b/packages/defiplaza-v2-adapter-v1/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "defiplaza-v2-adapter-v1" +version.workspace = true +edition.workspace = true +description = "Defines the adapter for DefiPlaza v2" + +[dependencies] +sbor = { workspace = true } +scrypto = { workspace = true } +radix-engine-interface = { workspace = true } +transaction = { workspace = true, optional = true } + +scrypto-interface = { path = "../../libraries/scrypto-interface" } +ports-interface = { path = "../../libraries/ports-interface" } +common = { path = "../../libraries/common" } + +[features] +default = [] +test = [] + +manifest-builder-stubs = ["dep:transaction"] + +[lib] +crate-type = ["cdylib", "lib"] + +[lints] +workspace = true \ No newline at end of file diff --git a/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs new file mode 100644 index 00000000..762a2a96 --- /dev/null +++ b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs @@ -0,0 +1,113 @@ +use scrypto::prelude::*; +use scrypto_interface::*; + +define_interface! { + PlazaDex as DefiPlazaV2Pool impl [ + ScryptoStub, + ScryptoTestStub, + #[cfg(feature = "manifest-builder-stubs")] + ManifestBuilderStub + ] { + fn instantiate_pair( + owner_role: OwnerRole, + base_address: ResourceAddress, + quote_address: ResourceAddress, + config: PairConfig, + initial_price: Decimal, + ) -> Self; + fn add_liquidity( + &mut self, + #[manifest_type = "ManifestBucket"] + input_bucket: Bucket, + #[manifest_type = "Option"] + co_liquidity_bucket: Option, + ) -> (Bucket, Option); + fn remove_liquidity( + &mut self, + #[manifest_type = "ManifestBucket"] + lp_bucket: Bucket, + is_quote: bool, + ) -> (Bucket, Bucket); + fn swap( + &mut self, + #[manifest_type = "ManifestBucket"] + input_bucket: Bucket, + ) -> (Bucket, Option); + fn quote( + &self, + input_amount: Decimal, + input_is_quote: bool, + ) -> (Decimal, Decimal, Decimal, TradeAllocation, PairState); + fn get_state(&self) -> PairState; + fn get_tokens(&self) -> (ResourceAddress, ResourceAddress); + fn get_pools( + &self, + ) -> (ComponentAddress, ComponentAddress); + } +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub struct PairConfig { + pub k_in: Decimal, + pub k_out: Decimal, + pub fee: Decimal, + pub decay_factor: Decimal, +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub struct TradeAllocation { + pub base_base: Decimal, + pub base_quote: Decimal, + pub quote_base: Decimal, + pub quote_quote: Decimal, +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub struct PairState { + pub p0: Decimal, + pub shortage: Shortage, + pub target_ratio: Decimal, + pub last_outgoing: i64, + pub last_out_spot: Decimal, +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub enum Shortage { + BaseShortage, + Equilibrium, + QuoteShortage, +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub enum ShortageState { + Equilibrium, + Shortage(Asset), +} + +#[derive( + ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub enum Asset { + Base, + Quote, +} + +impl From for ShortageState { + fn from(value: Shortage) -> Self { + match value { + Shortage::Equilibrium => ShortageState::Equilibrium, + Shortage::BaseShortage => ShortageState::Shortage(Asset::Base), + Shortage::QuoteShortage => ShortageState::Shortage(Asset::Quote), + } + } +} diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs new file mode 100644 index 00000000..cbe8243b --- /dev/null +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -0,0 +1,402 @@ +mod blueprint_interface; +pub use blueprint_interface::*; + +use common::prelude::*; +use ports_interface::prelude::*; +use scrypto::prelude::*; +use scrypto_interface::*; + +macro_rules! define_error { + ( + $( + $name: ident => $item: expr; + )* + ) => { + $( + pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v2]", " ", $item); + )* + }; +} + +define_error! { + RESOURCE_DOESNT_BELONG_TO_POOL => "Resources don't belong to pool"; + OVERFLOW_ERROR => "Calculation overflowed."; + UNEXPECTED_ERROR => "Unexpected Error."; + INVALID_NUMBER_OF_BUCKETS => "Invalid number of buckets."; +} + +macro_rules! pool { + ($address: expr) => { + $crate::blueprint_interface::DefiPlazaV2PoolInterfaceScryptoStub::from( + $address, + ) + }; +} + +#[blueprint_with_traits] +pub mod adapter { + struct DefiPlazaV2Adapter; + + impl DefiPlazaV2Adapter { + pub fn instantiate( + metadata_init: MetadataInit, + owner_role: OwnerRole, + address_reservation: Option, + ) -> Global { + let address_reservation = + address_reservation.unwrap_or_else(|| { + Runtime::allocate_component_address(BlueprintId { + package_address: Runtime::package_address(), + blueprint_name: Runtime::blueprint_name(), + }) + .0 + }); + + Self {} + .instantiate() + .prepare_to_globalize(owner_role) + .metadata(ModuleConfig { + init: metadata_init, + roles: Default::default(), + }) + .with_address(address_reservation) + .globalize() + } + + pub fn liquidity_receipt_data( + // Does not depend on state, this is kept in case this is required + // in the future for whatever reason. + &self, + global_id: NonFungibleGlobalId, + ) -> LiquidityReceipt { + // Read the non-fungible data. + let LiquidityReceipt { + name, + lockup_period, + pool_address, + user_resource_address, + user_contribution_amount, + user_resource_volatility_classification, + protocol_contribution_amount, + maturity_date, + adapter_specific_information, + } = ResourceManager::from_address(global_id.resource_address()) + .get_non_fungible_data::>( + global_id.local_id(), + ); + let adapter_specific_information = adapter_specific_information + .as_typed::() + .unwrap(); + + LiquidityReceipt { + name, + lockup_period, + pool_address, + user_resource_address, + user_contribution_amount, + user_resource_volatility_classification, + protocol_contribution_amount, + maturity_date, + adapter_specific_information, + } + } + } + + impl PoolAdapterInterfaceTrait for DefiPlazaV2Adapter { + fn open_liquidity_position( + &mut self, + pool_address: ComponentAddress, + buckets: (Bucket, Bucket), + ) -> OpenLiquidityPositionOutput { + // When opening a liquidity position we follow the algorithm that + // Jazzer described to us: + // + // 1) state = pair.get_state() + // 2) see which token is in shortage by inspecting state.shortage + // 3) store lp1_original_target = state.target_ratio * bucket1.amount() + // where bucket1 is the token in shortage. + // 4) (lp1, remainder_bucket) = pair.add_liquidity(bucket1, bucket2) + // and store the resulting lp1 tokens + // 5) store lp2_original_target = remainder_bucket.amount() + // 6) call (lp2, remainder2) = + // pair.add_liquidity(remainder_bucket.expect(), None) and store + // the resulting lp2 tokens (remainder2 will be None) + + let mut pool = pool!(pool_address); + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + + // Ensure that the passed buckets belong to the pool and sort them + // into base and quote buckets. + let (base_bucket, quote_bucket) = { + let bucket_address1 = buckets.0.resource_address(); + let bucket_address2 = buckets.1.resource_address(); + + if bucket_address1 == base_resource_address + && bucket_address2 == quote_resource_address + { + (buckets.0, buckets.1) + } else if bucket_address2 == base_resource_address + && bucket_address1 == quote_resource_address + { + (buckets.1, buckets.0) + } else { + panic!("{}", RESOURCE_DOESNT_BELONG_TO_POOL) + } + }; + + // Step 1: Get the pair's state + let pair_state = pool.get_state(); + + // Step 2: Determine which of the resources is in shortage. The one + // in shortage is the one that we will be contributing first to the + // pool. If the pool is in equilibrium then we can pick any of the + // two resources as the first (shortage) resource. In the code here + // "first" and "second" refer to which one will be contributed first + // and which will be contributed second. + let shortage = pair_state.shortage; + let shortage_state = ShortageState::from(shortage); + + let [(first_resource_address, first_bucket), (second_resource_address, second_bucket)] = + match shortage_state { + ShortageState::Equilibrium => [ + (base_resource_address, base_bucket), + (quote_resource_address, quote_bucket), + ], + ShortageState::Shortage(Asset::Base) => [ + (base_resource_address, base_bucket), + (quote_resource_address, quote_bucket), + ], + ShortageState::Shortage(Asset::Quote) => [ + (quote_resource_address, quote_bucket), + (base_resource_address, base_bucket), + ], + }; + + // Step 3: Calculate tate.target_ratio * bucket1.amount() where + // bucket1 is the bucket currently in shortage or the resource that + // will be contributed first. + let first_original_target = pair_state + .target_ratio + .checked_mul(first_bucket.amount()) + .expect(OVERFLOW_ERROR); + + // Step 4: Contribute to the pool. The first bucket to provide the + // pool is the bucket of the asset in shortage or the asset that we + // now refer to as "first" and then followed by the "second" bucket. + let (first_pool_units, second_change) = + pool.add_liquidity(first_bucket, Some(second_bucket)); + + // Step 5: Calculate and store the original target of the second + // liquidity position. This is calculated as the amount of assets + // that are in the remainder (change) bucket. + let second_bucket = second_change.expect(UNEXPECTED_ERROR); + let second_original_target = second_bucket.amount(); + + // Step 6: Add liquidity with the second resource & no co-liquidity. + let (second_pool_units, change) = + pool.add_liquidity(second_bucket, None); + + // TODO: Should we subtract the change from the second original + // target? Seems like we should if the price if not the same in + // some way? + + // A sanity check to make sure that everything is correct. The pool + // units obtained from the first contribution should be different + // from those obtained in the second contribution. + assert_ne!( + first_pool_units.resource_address(), + second_pool_units.resource_address(), + ); + + // The procedure for adding liquidity to the pool is now complete. + // We can now construct the output. + OpenLiquidityPositionOutput { + pool_units: IndexedBuckets::from_buckets([ + first_pool_units, + second_pool_units, + ]), + change: change + .map(IndexedBuckets::from_bucket) + .unwrap_or_default(), + others: vec![], + adapter_specific_information: + DefiPlazaV2AdapterSpecificInformation { + original_targets: indexmap! { + first_resource_address => first_original_target, + second_resource_address => second_original_target + }, + } + .into(), + } + } + + fn close_liquidity_position( + &mut self, + pool_address: ComponentAddress, + mut pool_units: Vec, + adapter_specific_information: AnyValue, + ) -> CloseLiquidityPositionOutput { + // When closing a position we follow the algorithm Jazzer described + // to us: + // + // 1) state = pair.get_state() + // 2) see which token is in shortage by inspecting state.shortage + // 3) calculate new base target and quote target. Suppose base token + // is in shortage: new_base_target = state.target_ratio * current + // base tokens represented by base_LP. The new_quote_target will + // just be equal to the current quote tokens represented by + // quote_LP. + // 4) base_fees = new_base_target - original_base_target + // 5) quote_fees = new_quote_target - original_quote_target + // 6) settle + + let pool = pool!(pool_address); + + let (pool_units1, pool_units2) = { + let pool_units_bucket1 = + pool_units.pop().expect(INVALID_NUMBER_OF_BUCKETS); + let pool_units_bucket2 = + pool_units.pop().expect(INVALID_NUMBER_OF_BUCKETS); + if !pool_units.is_empty() { + panic!("{}", INVALID_NUMBER_OF_BUCKETS) + } + (pool_units_bucket1, pool_units_bucket2) + }; + + // Getting the base and quote assets + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + + // Decoding the adapter specific information as the type we expect + // it to be. + let DefiPlazaV2AdapterSpecificInformation { original_targets } = + adapter_specific_information.as_typed().unwrap(); + let [old_base_target, old_quote_target] = + [base_resource_address, quote_resource_address].map( + |address| original_targets.get(&address).copied().unwrap(), + ); + + // Step 1: Get the pair's state + let pair_state = pool.get_state(); + + // Step 2 & 3: Determine which of the resources is in shortage and + // based on that determine what the new target should be. + let claimed_tokens = IndexedBuckets::from_buckets( + [pool_units1, pool_units2].into_iter().flat_map(|bucket| { + let resource_manager = bucket.resource_manager(); + let entry = ComponentAddress::try_from( + resource_manager + .get_metadata::<_, GlobalAddress>("pool") + .unwrap() + .unwrap(), + ) + .unwrap(); + let mut two_resource_pool = + Global::::from(entry); + let (bucket1, bucket2) = two_resource_pool.redeem(bucket); + [bucket1, bucket2] + }), + ); + let base_bucket = + claimed_tokens.get(&base_resource_address).unwrap(); + let quote_bucket = + claimed_tokens.get("e_resource_address).unwrap(); + + let base_bucket_amount = base_bucket.amount(); + let quote_bucket_amount = quote_bucket.amount(); + + let shortage = pair_state.shortage; + let shortage_state = ShortageState::from(shortage); + let (new_base_target, new_quote_target) = match shortage_state { + ShortageState::Equilibrium => { + (base_bucket_amount, quote_bucket_amount) + } + ShortageState::Shortage(Asset::Base) => ( + base_bucket_amount + .checked_mul(pair_state.target_ratio) + .expect(OVERFLOW_ERROR), + quote_bucket_amount, + ), + ShortageState::Shortage(Asset::Quote) => ( + base_bucket_amount, + quote_bucket_amount + .checked_mul(pair_state.target_ratio) + .expect(OVERFLOW_ERROR), + ), + }; + + // Steps 4 and 5 + let base_fees = std::cmp::min( + new_base_target + .checked_sub(old_base_target) + .expect(OVERFLOW_ERROR), + Decimal::ZERO, + ); + let quote_fees = std::cmp::min( + new_quote_target + .checked_sub(old_quote_target) + .expect(OVERFLOW_ERROR), + Decimal::ZERO, + ); + + CloseLiquidityPositionOutput { + resources: claimed_tokens, + others: vec![], + fees: indexmap! { + base_resource_address => base_fees, + quote_resource_address => quote_fees, + }, + } + } + + fn price(&mut self, pool_address: ComponentAddress) -> Price { + // TODO: Still not sure how to find the price of assets in DefiPlaza + // and I'm working with them on that. For now, I will just say that + // the price is one. WE MUST CHANGE THIS BEFORE GOING LIVE! + // + // More information: The price for selling and buying the asset is + // different in DefiPlaza just like an order book (they're not an + // order book though). So, there is no current price that you can + // buy and sell at but two prices depending on what resource the + // input is. + let pool = pool!(pool_address); + let (base_asset, quote_asset) = pool.get_tokens(); + Price { + base: base_asset, + quote: quote_asset, + price: dec!(1), + } + } + + fn resource_addresses( + &mut self, + pool_address: ComponentAddress, + ) -> (ResourceAddress, ResourceAddress) { + let pool = pool!(pool_address); + let two_resource_pool = + Global::::from(pool.get_pools().0); + + let mut resource_addresses = + two_resource_pool.get_vault_amounts().into_keys(); + + let resource_address1 = + resource_addresses.next().expect(UNEXPECTED_ERROR); + let resource_address2 = + resource_addresses.next().expect(UNEXPECTED_ERROR); + + (resource_address1, resource_address2) + } + } +} + +#[derive(ScryptoSbor, Debug, Clone)] +pub struct DefiPlazaV2AdapterSpecificInformation { + pub original_targets: IndexMap, +} + +impl From for AnyValue { + fn from(value: DefiPlazaV2AdapterSpecificInformation) -> Self { + AnyValue::from_typed(&value).unwrap() + } +} diff --git a/tests/assets/defiplaza_v2.rpd b/tests/assets/defiplaza_v2.rpd new file mode 100644 index 0000000000000000000000000000000000000000..b0ca976a1bb02dd4979cce63e0725fc06ddb9817 GIT binary patch literal 6284 zcmbVQJ9FDc5I%sQNQxjS$uIepkaUq57nvkKz%zF2$z;Y!9Lqfd5hocVke~rjvQxQn zmm)>}fK#MMmp+A?;Ba9LVfJ;01+IAogg_{e$hM3;@L@-menXul7g3IROQh!PK!Q%;X}48!-PML zV38N6yxg0oiwJe2EPb*{leBt9vSpN?EqLKW0>B>`{Yd3nlrG9DUQ}sZ@d&}bUMyFY za>Wk`PCC#hnCLK0jya(WzsmB%I8#^({EC{0W(7xFa@YY5`iF7Jqr=tggjansBF0;@ zd~uW>`^(SB!uA8$>z6d8df~FyN8p;2@vl6Fpqh z<{*CNNL;%uWp@VWsZMR1f+Jmk4ZXkZkG1WB&+}7muwCt!8=xs1J8K3_tnFDg%NQdImQ7Sw5gnQh7} zF4+q`iW_c7D81YF#2-(*7Q#c}S1Mo!{>D>wBb>JF;0m#QDd$8_EM1mMYatWMSLE`B zUP)+qY_b0Ku3*f`FMG_9qe~ za52^EI5btp#=zR#0Mv1Ty2`nMNqtZrIOVnhN}ZW>QA1tSP!7sYy^J=%hOUPL<>6%w zTL)@Wg_Uru7P+AX2Ytk=aHw%eZQxYZ29S{j0?E3gd@@^gP_{&|ZP?(h1qL472}jx^ zp=bJ*286vj!k$LZg}84OlkJ;LT_)qh&Mo8s6&}@ypFNo zRNNcDFIwPmf;y|r%cco1wBH2{=$9;XI#Tnpv$dggOhc@1aKqth(@keUT*g@K26h=> zEWk_5n0pf$Tf0S!iS!3iCkGjmS?EJQ>~%!{8ky8`rtM^L7UbbOa~n?7S#LZVN6HDS zvtr&RO)PF7=|vZgQ873e`tk^nE65M>ybdg>;%N;L09l-7X*kBoHV!~gRc$Fl)5OtQ zRFa`RWt!OG;%oSjUC>otp{^G4;prtTsxrBW@YQ9|A({hS&we0ghx~{JD1sXy5%N?{ z;6_yGf}sjgDcec4;X|Pt!6zOfz6XFbg&$Df@h(ogcL6oS6{mlV7 zrrd1<#p;%i^P-B6d052-Uce#-jx)$&=(T|d9p~xdSU*MOG8f%n_du#47LlQrU_9X+ zAa#0?F+qy@_kfIM>0yzs=2OOe+Qr~|Bo>w|&0Y}9?a^8OkeB(YnDGa&ZBzm~;h0_& zxD$u9E^)$XbKBd3sTO(mrh||NHb@sK@ZT(27U@j1;Y|$ky79ni4_x!UBt?SJp0k;q zWW2PU_hfYpYi~Uc^=_i(2(mljMK>)YN~Cp~M*&@D#GbPYaXe=GA}O7yt8EJ|I26Qi zliAUuB2M`2EX!w@Z^Wtb1_dJNG4n-VPex?^<7{78mHU8<5N#E00MnOlo*M!YF~Rn< z9MQmXP0&QRhKqOaP2#(^ljOc4@ApoDC8=B1(s2*TXe5Gz=ORWF*YgjFq;G}@G(>+M z!w)=)YFCeWu^Fy+0B>K%4PX4+B2`5c#lv{2SP7I)sAI^MJnuOh{5;$L`}3cE_z%8l z^v|tZH-Gu}=5O>Ej%ru*!ztBUiBxMny5J@hbk%ZgjGhotjgyIpm{%>2rbOKxo7P}O zu{e?X=?}vLb~S&ovZ}7_c0LV_SB=i7UsJE!URLd$uIF$X4WDkTvS=rOqSUOW@M2fvgAk`Iw;JN%Bbd1B z4QMu-fs<9#L^4y2CNRi1Dg_N_HYEWx#V+c;5rpif;q4ht*OTv^T4y>l-+RH>U!VT@ a>mRCx)c>p2+QgJGDEoUdAXauF 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 h6nLWyd{7p?C Date: Tue, 27 Feb 2024 15:14:14 +0300 Subject: [PATCH 04/47] [Tests]: Integrate defiplaza into the test environment. --- .vscode/settings.json | 1 + libraries/scrypto-interface/Cargo.toml | 1 + .../src/blueprint_interface.rs | 68 ++++- packages/ociswap-v1-adapter-v1/Cargo.toml | 2 +- tests/Cargo.toml | 4 + tests/src/environment.rs | 251 +++++++++++++++++- tests/src/prelude.rs | 2 + 7 files changed, 318 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e06caaf..4264e1f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "cSpell.words": [ "caviarnine", "coffieicnet", + "defi", "humantime", "ociswap", "OLYPS", diff --git a/libraries/scrypto-interface/Cargo.toml b/libraries/scrypto-interface/Cargo.toml index 1a4a20b7..850f17c0 100644 --- a/libraries/scrypto-interface/Cargo.toml +++ b/libraries/scrypto-interface/Cargo.toml @@ -11,6 +11,7 @@ quote = { version = "1.0.35" } syn = { version = "2.0.48", features = ["full", "extra-traits"] } [lib] +doctest = false proc-macro = true [lints] diff --git a/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs index 762a2a96..37993a83 100644 --- a/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs +++ b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs @@ -2,7 +2,7 @@ use scrypto::prelude::*; use scrypto_interface::*; define_interface! { - PlazaDex as DefiPlazaV2Pool impl [ + PlazaPair as DefiPlazaV2Pool impl [ ScryptoStub, ScryptoTestStub, #[cfg(feature = "manifest-builder-stubs")] @@ -47,7 +47,16 @@ define_interface! { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub struct PairConfig { pub k_in: Decimal, @@ -57,7 +66,16 @@ pub struct PairConfig { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub struct TradeAllocation { pub base_base: Decimal, @@ -67,7 +85,16 @@ pub struct TradeAllocation { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub struct PairState { pub p0: Decimal, @@ -78,7 +105,16 @@ pub struct PairState { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub enum Shortage { BaseShortage, @@ -87,7 +123,16 @@ pub enum Shortage { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub enum ShortageState { Equilibrium, @@ -95,7 +140,16 @@ pub enum ShortageState { } #[derive( - ScryptoSbor, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, )] pub enum Asset { Base, diff --git a/packages/ociswap-v1-adapter-v1/Cargo.toml b/packages/ociswap-v1-adapter-v1/Cargo.toml index 1c6ceaa9..db42f94b 100644 --- a/packages/ociswap-v1-adapter-v1/Cargo.toml +++ b/packages/ociswap-v1-adapter-v1/Cargo.toml @@ -2,7 +2,7 @@ name = "ociswap-v1-adapter-v1" version.workspace = true edition.workspace = true -description = "Defines the adapter for Ociswap" +description = "Defines the adapter for Ociswap v1 - this is effectively deprecated since we won't be supporting Ociswap v1." [dependencies] sbor = { workspace = true } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 024ebdc7..929462f2 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -26,6 +26,10 @@ ociswap-v2-adapter-v1 = { path = "../packages/ociswap-v2-adapter-v1", features = "test", "manifest-builder-stubs" ] } +defiplaza-v2-adapter-v1 = { path = "../packages/defiplaza-v2-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } caviarnine-v1-adapter-v1 = { path = "../packages/caviarnine-v1-adapter-v1", features = [ "test", "manifest-builder-stubs" diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 34400f06..792a6f0f 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -14,10 +14,12 @@ pub trait EnvironmentSpecifier { type SimpleOracle; type OciswapV1Pool; type OciswapV2Pool; + type DefiPlazaV2Pool; type CaviarnineV1Pool; type OciswapV1Adapter; type OciswapV2Adapter; + type DefiPlazaV2Adapter; type CaviarnineV1Adapter; // Badges @@ -35,10 +37,12 @@ impl EnvironmentSpecifier for ScryptoTestEnvironmentSpecifier { type SimpleOracle = SimpleOracle; type OciswapV1Pool = OciswapV1PoolInterfaceScryptoTestStub; type OciswapV2Pool = OciswapV2PoolInterfaceScryptoTestStub; + type DefiPlazaV2Pool = DefiPlazaV2PoolInterfaceScryptoTestStub; type CaviarnineV1Pool = CaviarnineV1PoolInterfaceScryptoTestStub; type OciswapV1Adapter = OciswapV1Adapter; type OciswapV2Adapter = OciswapV2Adapter; + type DefiPlazaV2Adapter = DefiPlazaV2Adapter; type CaviarnineV1Adapter = CaviarnineV1Adapter; // Badges @@ -56,10 +60,12 @@ impl EnvironmentSpecifier for ScryptoUnitEnvironmentSpecifier { type SimpleOracle = ComponentAddress; type OciswapV1Pool = ComponentAddress; type OciswapV2Pool = ComponentAddress; + type DefiPlazaV2Pool = ComponentAddress; type CaviarnineV1Pool = ComponentAddress; type OciswapV1Adapter = ComponentAddress; type OciswapV2Adapter = ComponentAddress; + type DefiPlazaV2Adapter = ComponentAddress; type CaviarnineV1Adapter = ComponentAddress; // Badges @@ -80,6 +86,12 @@ impl EnvironmentSpecifier for ScryptoUnitEnvironmentSpecifier { /// returned back to the caller. Additionally, the auth module will be disabled /// by default for the created test environment. If it needs to be enabled then /// that must happen after the creation of the environment. +// Not quite a todo but more of a thought for the future. We publish all of the +// packages and create pools for all of the dexes when we realistically will not +// be using more than a single dex in a test (and if we do then its weird.). It +// would have been better for this to only be initialized with the state of the +// dex that we want instead of all of them. That would make it much faster to +// run. pub struct Environment where S: EnvironmentSpecifier, @@ -92,6 +104,7 @@ where /* Supported Dexes */ pub ociswap_v1: DexEntities, pub ociswap_v2: DexEntities, + pub defiplaza_v2: DexEntities, pub caviarnine_v1: DexEntities, } @@ -144,6 +157,7 @@ impl ScryptoTestEnv { // environment. If somebody want it, they can enable it after they // instantiate the environment. env.disable_auth_module(); + env.disable_limits_module(); // Creating the badges and their access rules let protocol_manager_badge = @@ -188,7 +202,7 @@ impl ScryptoTestEnv { // Creating the liquidity receipt resource that each of the exchanges // will use. - let [ociswap_v1_liquidity_receipt_resource, ociswap_v2_liquidity_receipt_resource, caviarnine_v1_liquidity_receipt_resource] = + let [ociswap_v1_liquidity_receipt_resource, ociswap_v2_liquidity_receipt_resource, defiplaza_v2_liquidity_receipt_resource, caviarnine_v1_liquidity_receipt_resource] = std::array::from_fn(|_| { ResourceBuilder::new_ruid_non_fungible::< LiquidityReceipt, @@ -383,6 +397,89 @@ impl ScryptoTestEnv { ) }; + let ( + defiplaza_v2_package, + defiplaza_v2_adapter_v1_package, + defiplaza_v2_pools, + ) = { + let defiplaza_v2_pool_package = { + let defiplaza_v2_package_wasm = + include_bytes!("../assets/defiplaza_v2.wasm"); + let defiplaza_v2_package_rpd = + include_bytes!("../assets/defiplaza_v2.rpd"); + let defiplaza_v2_package_definition = + manifest_decode::( + defiplaza_v2_package_rpd, + ) + .unwrap(); + + env.call_function_typed::<_, PackagePublishWasmOutput>( + PACKAGE_PACKAGE, + PACKAGE_BLUEPRINT, + PACKAGE_PUBLISH_WASM_IDENT, + &PackagePublishWasmInput { + code: defiplaza_v2_package_wasm.to_vec(), + definition: defiplaza_v2_package_definition, + metadata: Default::default(), + }, + )? + .0 + }; + + let defiplaza_v2_adapter_v1_package = + Self::publish_package("defiplaza-v2-adapter-v1", &mut env)?; + + let defiplaza_v2_pools = resource_addresses.try_map(|resource_address| { + let (resource_x, resource_y) = if XRD < *resource_address { + (XRD, *resource_address) + } else { + (*resource_address, XRD) + }; + + let mut defiplaza_pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( + OwnerRole::None, + resource_x, + resource_y, + // This pair config is obtained from DefiPlaza's + // repo. + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: dec!("0"), + decay_factor: dec!("0.9512"), + }, + dec!(1), + defiplaza_v2_pool_package, + &mut env, + )?; + + let resource_x = + ResourceManager(resource_x).mint_fungible(dec!(100_000_000), &mut env)?; + let resource_y = + ResourceManager(resource_y).mint_fungible(dec!(100_000_000), &mut env)?; + + let (_, change1) = + defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; + let (_, change2) = defiplaza_pool.add_liquidity(resource_y, None, &mut env)?; + let change_amount1 = change1 + .map(|bucket| bucket.amount(&mut env).unwrap()) + .unwrap_or_default(); + assert_eq!(change_amount1, dec!(0)); + let change_amount2 = change2 + .map(|bucket| bucket.amount(&mut env).unwrap()) + .unwrap_or_default(); + assert_eq!(change_amount2, dec!(0)); + + Ok::<_, RuntimeError>(defiplaza_pool) + })?; + + ( + defiplaza_v2_pool_package, + defiplaza_v2_adapter_v1_package, + defiplaza_v2_pools, + ) + }; + // Instantiating the components. let mut simple_oracle = SimpleOracle::instantiate( protocol_manager_rule.clone(), @@ -420,6 +517,13 @@ impl ScryptoTestEnv { ociswap_v2_adapter_v1_package, &mut env, )?; + let defiplaza_v2_adapter_v1 = DefiPlazaV2Adapter::instantiate( + Default::default(), + OwnerRole::None, + None, + defiplaza_v2_adapter_v1_package, + &mut env, + )?; let caviarnine_v1_adapter_v1 = CaviarnineV1Adapter::instantiate( Default::default(), OwnerRole::None, @@ -524,6 +628,21 @@ impl ScryptoTestEnv { &mut env, )?; + ignition.insert_pool_information( + DefiPlazaV2PoolInterfaceScryptoTestStub::blueprint_id( + defiplaza_v2_package, + ), + PoolBlueprintInformation { + adapter: defiplaza_v2_adapter_v1.try_into().unwrap(), + allowed_pools: defiplaza_v2_pools + .iter() + .map(|pool| pool.try_into().unwrap()) + .collect(), + liquidity_receipt: defiplaza_v2_liquidity_receipt_resource, + }, + &mut env, + )?; + ignition.insert_pool_information( CaviarnineV1PoolInterfaceScryptoTestStub::blueprint_id( caviarnine_v1_package, @@ -565,6 +684,13 @@ impl ScryptoTestEnv { adapter: ociswap_v2_adapter_v1, liquidity_receipt: ociswap_v2_liquidity_receipt_resource, }, + defiplaza_v2: DexEntities { + package: defiplaza_v2_package, + pools: defiplaza_v2_pools, + adapter_package: defiplaza_v2_adapter_v1_package, + adapter: defiplaza_v2_adapter_v1, + liquidity_receipt: defiplaza_v2_liquidity_receipt_resource, + }, caviarnine_v1: DexEntities { package: caviarnine_v1_package, pools: caviarnine_v1_pools, @@ -670,7 +796,7 @@ impl ScryptoUnitEnv { ) }); - let [ociswap_v1_liquidity_receipt_resource, ociswap_v2_liquidity_receipt_resource, caviarnine_v1_liquidity_receipt_resource] = + let [ociswap_v1_liquidity_receipt_resource, ociswap_v2_liquidity_receipt_resource, defiplaza_v2_liquidity_receipt_resource, caviarnine_v1_liquidity_receipt_resource] = std::array::from_fn(|_| { test_runner .execute_manifest( @@ -937,6 +1063,110 @@ impl ScryptoUnitEnv { ) }; + let ( + defiplaza_v2_package, + defiplaza_v2_adapter_v1_package, + defiplaza_v2_pools, + ) = { + let defiplaza_v2_pool_package = { + let defiplaza_v2_package_wasm = + include_bytes!("../assets/defiplaza_v2.wasm"); + let defiplaza_v2_package_rpd = + include_bytes!("../assets/defiplaza_v2.rpd"); + let defiplaza_v2_package_definition = + manifest_decode::( + defiplaza_v2_package_rpd, + ) + .unwrap(); + + test_runner.publish_package( + ( + defiplaza_v2_package_wasm.to_vec(), + defiplaza_v2_package_definition, + ), + Default::default(), + Default::default(), + ) + }; + + let (code, definition) = + package_loader::PackageLoader::get("defiplaza-v2-adapter-v1"); + let defiplaza_v2_adapter_v1_package = test_runner.publish_package( + (code, definition), + Default::default(), + OwnerRole::None, + ); + + let defiplaza_v2_pools = + resource_addresses.map(|resource_address| { + let (resource_x, resource_y) = if XRD < *resource_address { + (XRD, *resource_address) + } else { + (*resource_address, XRD) + }; + + let manifest = ManifestBuilder::new() + .lock_fee_from_faucet() + .defi_plaza_v2_pool_instantiate_pair( + defiplaza_v2_pool_package, + OwnerRole::None, + resource_x, + resource_y, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: dec!("0"), + decay_factor: dec!("0.9512"), + }, + dec!(1), + ) + .build(); + let component_address = *test_runner + .execute_manifest(manifest, vec![]) + .expect_commit_success() + .new_component_addresses() + .first() + .unwrap(); + + let manifest = ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(XRD, dec!(100_000_000)) + .mint_fungible(*resource_address, dec!(100_000_000)) + .take_all_from_worktop(resource_x, "resource_x_bucket") + .take_all_from_worktop(resource_y, "resource_y_bucket") + .with_name_lookup(|builder, _| { + let resource_x_bucket = + builder.bucket("resource_x_bucket"); + let resource_y_bucket = + builder.bucket("resource_y_bucket"); + builder + .defi_plaza_v2_pool_add_liquidity( + component_address, + resource_x_bucket, + None, + ) + .defi_plaza_v2_pool_add_liquidity( + component_address, + resource_y_bucket, + None, + ) + }) + .try_deposit_entire_worktop_or_abort(account, None) + .build(); + test_runner + .execute_manifest_without_auth(manifest) + .expect_commit_success(); + + component_address + }); + + ( + defiplaza_v2_pool_package, + defiplaza_v2_adapter_v1_package, + defiplaza_v2_pools, + ) + }; + let simple_oracle = test_runner .execute_manifest( ManifestBuilder::new() @@ -1010,10 +1240,11 @@ impl ScryptoUnitEnv { .copied() .unwrap(); - let [ociswap_v1_adapter_v1, ociswap_v2_adapter_v1, caviarnine_v1_adapter_v1] = + let [ociswap_v1_adapter_v1, ociswap_v2_adapter_v1, defiplaza_v2_adapter_v1, caviarnine_v1_adapter_v1] = [ (ociswap_v1_adapter_v1_package, "OciswapV1Adapter"), (ociswap_v2_adapter_v1_package, "OciswapV2Adapter"), + (defiplaza_v2_adapter_v1_package, "DefiPlazaV2Adapter"), (caviarnine_v1_adapter_v1_package, "CaviarnineV1Adapter"), ] .map(|(package_address, blueprint_name)| { @@ -1142,6 +1373,13 @@ impl ScryptoUnitEnv { ociswap_v2_package, "LiquidityPool", ), + ( + defiplaza_v2_adapter_v1, + defiplaza_v2_pools, + defiplaza_v2_liquidity_receipt_resource, + defiplaza_v2_package, + "PlazaDex", + ), ( caviarnine_v1_adapter_v1, caviarnine_v1_pools, @@ -1217,6 +1455,13 @@ impl ScryptoUnitEnv { adapter: ociswap_v2_adapter_v1, liquidity_receipt: ociswap_v2_liquidity_receipt_resource, }, + defiplaza_v2: DexEntities { + package: defiplaza_v2_package, + pools: defiplaza_v2_pools, + adapter_package: defiplaza_v2_adapter_v1_package, + adapter: defiplaza_v2_adapter_v1, + liquidity_receipt: defiplaza_v2_liquidity_receipt_resource, + }, caviarnine_v1: DexEntities { package: caviarnine_v1_package, pools: caviarnine_v1_pools, diff --git a/tests/src/prelude.rs b/tests/src/prelude.rs index f0f29cce..e397db6d 100644 --- a/tests/src/prelude.rs +++ b/tests/src/prelude.rs @@ -12,6 +12,7 @@ pub use scrypto_test::prelude::*; pub use scrypto_unit::*; pub use ::caviarnine_v1_adapter_v1::test_bindings::*; +pub use ::defiplaza_v2_adapter_v1::test_bindings::*; pub use ::ignition::test_bindings::*; pub use ::ignition::*; pub use ::ociswap_v1_adapter_v1::test_bindings::*; @@ -19,6 +20,7 @@ pub use ::ociswap_v2_adapter_v1::test_bindings::*; pub use ::simple_oracle::test_bindings::*; pub use ::caviarnine_v1_adapter_v1::*; +pub use ::defiplaza_v2_adapter_v1::*; pub use ::ociswap_v1_adapter_v1::*; pub use ::ociswap_v2_adapter_v1::*; From 48cd4f466c9bbe005fabe03ed4de060ce585f1ea Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 27 Feb 2024 18:19:51 +0300 Subject: [PATCH 05/47] [Tests]: Initial tests for DefiPlaza. --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 14 +- tests/src/environment.rs | 2 +- tests/tests/defiplaza_v2.rs | 269 ++++++++++++++++++++ 3 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 tests/tests/defiplaza_v2.rs diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index cbe8243b..c3e6051f 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -184,8 +184,18 @@ pub mod adapter { // Step 4: Contribute to the pool. The first bucket to provide the // pool is the bucket of the asset in shortage or the asset that we // now refer to as "first" and then followed by the "second" bucket. - let (first_pool_units, second_change) = - pool.add_liquidity(first_bucket, Some(second_bucket)); + // + // In the case of equilibrium we do not contribute the second bucket + // and instead just the first bucket. + let (first_pool_units, second_change) = match shortage_state { + ShortageState::Equilibrium => ( + pool.add_liquidity(first_bucket, None).0, + Some(second_bucket), + ), + ShortageState::Shortage(_) => { + pool.add_liquidity(first_bucket, Some(second_bucket)) + } + }; // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 792a6f0f..8fcc9b87 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -1378,7 +1378,7 @@ impl ScryptoUnitEnv { defiplaza_v2_pools, defiplaza_v2_liquidity_receipt_resource, defiplaza_v2_package, - "PlazaDex", + "PlazaPair", ), ( caviarnine_v1_adapter_v1, diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs new file mode 100644 index 00000000..d34393fc --- /dev/null +++ b/tests/tests/defiplaza_v2.rs @@ -0,0 +1,269 @@ +#![allow(clippy::arithmetic_side_effects)] + +use tests::prelude::*; + +#[test] +pub fn can_open_a_simple_position_against_a_defiplaza_pool( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + protocol + .ignition + .set_maximum_allowed_price_difference_percentage(dec!(0.50), env)?; + + let bitcoin_bucket = + ResourceManager(resources.bitcoin).mint_fungible(dec!(100), env)?; + + // Act + let rtn = protocol.ignition.open_liquidity_position( + FungibleBucket(bitcoin_bucket), + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + LockupPeriod::from_months(6).unwrap(), + env, + ); + + // Assert + let _ = rtn.expect("Should succeed!"); + + Ok(()) +} + +#[test] +fn can_open_a_liquidity_position_in_defiplaza_that_fits_into_fee_limits() { + // Arrange + let ScryptoUnitEnv { + environment: mut test_runner, + resources, + protocol, + defiplaza_v2, + .. + } = ScryptoUnitEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.03), + ..Default::default() + }); + let (_, private_key, account_address, _) = protocol.protocol_owner_badge; + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(resources.bitcoin, dec!(100_000_000_000_000)) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + vec![], + ) + .expect_commit_success(); + + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .lock_fee_from_faucet() + .withdraw_from_account( + account_address, + resources.bitcoin, + dec!(100_000), + ) + .take_all_from_worktop(resources.bitcoin, "bitcoin") + .with_bucket("bitcoin", |builder, bucket| { + builder.call_method( + protocol.ignition, + "open_liquidity_position", + ( + bucket, + defiplaza_v2.pools.bitcoin, + LockupPeriod::from_months(6).unwrap(), + ), + ) + }) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + EnabledModules::for_test_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success(); + + // Act + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee_from_faucet() + .withdraw_from_account( + account_address, + resources.bitcoin, + dec!(100_000), + ) + .take_all_from_worktop(resources.bitcoin, "bitcoin") + .with_bucket("bitcoin", |builder, bucket| { + builder.call_method( + protocol.ignition, + "open_liquidity_position", + ( + bucket, + defiplaza_v2.pools.bitcoin, + LockupPeriod::from_months(6).unwrap(), + ), + ) + }) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + &private_key, + ); + + // Assert + receipt.expect_commit_success(); + let TransactionFeeSummary { + total_execution_cost_in_xrd, + .. + } = receipt.fee_summary; + println!( + "Execution cost to open a position: {} XRD", + total_execution_cost_in_xrd + ); + + assert!(total_execution_cost_in_xrd <= dec!(4.8)) +} + +#[test] +fn can_close_a_liquidity_position_in_defiplaza_that_fits_into_fee_limits() { + // Arrange + let ScryptoUnitEnv { + environment: mut test_runner, + resources, + protocol, + defiplaza_v2, + .. + } = ScryptoUnitEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.03), + ..Default::default() + }); + let (_, private_key, account_address, _) = protocol.protocol_owner_badge; + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(resources.bitcoin, dec!(100_000_000_000_000)) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + vec![], + ) + .expect_commit_success(); + + for _ in 0..2 { + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .lock_fee_from_faucet() + .withdraw_from_account( + account_address, + resources.bitcoin, + dec!(100_000), + ) + .take_all_from_worktop(resources.bitcoin, "bitcoin") + .with_bucket("bitcoin", |builder, bucket| { + builder.call_method( + protocol.ignition, + "open_liquidity_position", + ( + bucket, + defiplaza_v2.pools.bitcoin, + LockupPeriod::from_months(6).unwrap(), + ), + ) + }) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + EnabledModules::for_test_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success(); + } + + let current_time = test_runner.get_current_time(TimePrecisionV2::Minute); + let maturity_instant = current_time + .add_seconds(*LockupPeriod::from_months(6).unwrap().seconds() as i64) + .unwrap(); + { + let db = test_runner.substate_db_mut(); + let mut writer = SystemDatabaseWriter::new(db); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMilliTimestamp.field_index(), + ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( + ProposerMilliTimestampSubstate { epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000 } + ), + ) + .unwrap(); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMinuteTimestamp.field_index(), + ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( + ProposerMinuteTimestampSubstate { + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60).unwrap(), + } + ), + ) + .unwrap(); + } + + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.oracle, + "set_price", + (resources.bitcoin, XRD, dec!(1)), + ) + .build(), + ) + .expect_commit_success(); + + // Act + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee_from_faucet() + .withdraw_from_account( + account_address, + defiplaza_v2.liquidity_receipt, + dec!(1), + ) + .take_all_from_worktop(defiplaza_v2.liquidity_receipt, "receipt") + .with_bucket("receipt", |builder, bucket| { + builder.call_method( + protocol.ignition, + "close_liquidity_position", + (bucket,), + ) + }) + .try_deposit_entire_worktop_or_abort(account_address, None) + .build(), + &private_key, + ); + + // Assert + receipt.expect_commit_success(); + let TransactionFeeSummary { + total_execution_cost_in_xrd, + .. + } = receipt.fee_summary; + println!( + "Execution cost to close a position: {} XRD", + total_execution_cost_in_xrd + ); + + assert!(total_execution_cost_in_xrd <= dec!(4.8)) +} From 80f731da1b0bc55c80e382e6554a29766626effb Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 27 Feb 2024 20:09:07 +0300 Subject: [PATCH 06/47] [Ignition]: Standardize the adapter instantiation function. --- packages/caviarnine-v1-adapter-v1/src/lib.rs | 2 ++ packages/defiplaza-v2-adapter-v1/src/lib.rs | 2 ++ packages/ociswap-v1-adapter-v1/src/lib.rs | 2 ++ packages/ociswap-v2-adapter-v1/src/lib.rs | 2 ++ tests/src/environment.rs | 10 ++++++++++ 5 files changed, 18 insertions(+) diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index 111ddaf2..306672fd 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -78,6 +78,8 @@ pub mod adapter { impl CaviarnineV1Adapter { pub fn instantiate( + _: AccessRule, + _: AccessRule, metadata_init: MetadataInit, owner_role: OwnerRole, address_reservation: Option, diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index c3e6051f..df3614bc 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -39,6 +39,8 @@ pub mod adapter { impl DefiPlazaV2Adapter { pub fn instantiate( + _: AccessRule, + _: AccessRule, metadata_init: MetadataInit, owner_role: OwnerRole, address_reservation: Option, diff --git a/packages/ociswap-v1-adapter-v1/src/lib.rs b/packages/ociswap-v1-adapter-v1/src/lib.rs index 35de999e..3a481433 100644 --- a/packages/ociswap-v1-adapter-v1/src/lib.rs +++ b/packages/ociswap-v1-adapter-v1/src/lib.rs @@ -47,6 +47,8 @@ pub mod adapter { impl OciswapV1Adapter { pub fn instantiate( + _: AccessRule, + _: AccessRule, metadata_init: MetadataInit, owner_role: OwnerRole, address_reservation: Option, diff --git a/packages/ociswap-v2-adapter-v1/src/lib.rs b/packages/ociswap-v2-adapter-v1/src/lib.rs index c27868bb..3ad18d58 100644 --- a/packages/ociswap-v2-adapter-v1/src/lib.rs +++ b/packages/ociswap-v2-adapter-v1/src/lib.rs @@ -41,6 +41,8 @@ pub mod adapter { impl OciswapV2Adapter { pub fn instantiate( + _: AccessRule, + _: AccessRule, metadata_init: MetadataInit, owner_role: OwnerRole, address_reservation: Option, diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 8fcc9b87..968d074a 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -504,6 +504,8 @@ impl ScryptoTestEnv { &mut env, )?; let ociswap_v1_adapter_v1 = OciswapV1Adapter::instantiate( + rule!(allow_all), + rule!(allow_all), Default::default(), OwnerRole::None, None, @@ -511,6 +513,8 @@ impl ScryptoTestEnv { &mut env, )?; let ociswap_v2_adapter_v1 = OciswapV2Adapter::instantiate( + rule!(allow_all), + rule!(allow_all), Default::default(), OwnerRole::None, None, @@ -518,6 +522,8 @@ impl ScryptoTestEnv { &mut env, )?; let defiplaza_v2_adapter_v1 = DefiPlazaV2Adapter::instantiate( + rule!(allow_all), + rule!(allow_all), Default::default(), OwnerRole::None, None, @@ -525,6 +531,8 @@ impl ScryptoTestEnv { &mut env, )?; let caviarnine_v1_adapter_v1 = CaviarnineV1Adapter::instantiate( + rule!(allow_all), + rule!(allow_all), Default::default(), OwnerRole::None, None, @@ -1257,6 +1265,8 @@ impl ScryptoUnitEnv { blueprint_name, "instantiate", ( + rule!(allow_all), + rule!(allow_all), MetadataInit::default(), OwnerRole::None, None::, From c3e74e99c8c0db6b202dd9f1bb665e3622030316 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 28 Feb 2024 09:48:00 +0300 Subject: [PATCH 07/47] [Defiplaza v2 Adapter v1]: Add a method to add the `PairConfig`. --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 63 +++++++++++++++++---- tests/src/environment.rs | 49 +++++++++++++++- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index df3614bc..3834eb96 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -23,6 +23,7 @@ define_error! { OVERFLOW_ERROR => "Calculation overflowed."; UNEXPECTED_ERROR => "Unexpected Error."; INVALID_NUMBER_OF_BUCKETS => "Invalid number of buckets."; + NO_PAIR_CONFIG => "The pair config of the provided pool is not known."; } macro_rules! pool { @@ -34,13 +35,36 @@ macro_rules! pool { } #[blueprint_with_traits] +#[types(ComponentAddress, PairConfig)] pub mod adapter { - struct DefiPlazaV2Adapter; + enable_method_auth! { + roles { + protocol_owner => updatable_by: [protocol_owner]; + protocol_manager => updatable_by: [protocol_manager, protocol_owner]; + }, + methods { + add_pair_config => restrict_to: [protocol_manager, protocol_owner]; + /* User methods */ + price => PUBLIC; + resource_addresses => PUBLIC; + liquidity_receipt_data => PUBLIC; + open_liquidity_position => PUBLIC; + close_liquidity_position => PUBLIC; + } + } + + struct DefiPlazaV2Adapter { + /// The pair config of the various pools is constant but there is no + /// getter function that can be used to get it on ledger. As such, the + /// protocol owner or manager must submit this information to the + /// adapter for its operation. + pair_config: KeyValueStore, + } impl DefiPlazaV2Adapter { pub fn instantiate( - _: AccessRule, - _: AccessRule, + protocol_manager_rule: AccessRule, + protocol_owner_rule: AccessRule, metadata_init: MetadataInit, owner_role: OwnerRole, address_reservation: Option, @@ -54,15 +78,30 @@ pub mod adapter { .0 }); - Self {} - .instantiate() - .prepare_to_globalize(owner_role) - .metadata(ModuleConfig { - init: metadata_init, - roles: Default::default(), - }) - .with_address(address_reservation) - .globalize() + Self { + pair_config: KeyValueStore::new_with_registered_type(), + } + .instantiate() + .prepare_to_globalize(owner_role) + .metadata(ModuleConfig { + init: metadata_init, + roles: Default::default(), + }) + .roles(roles! { + protocol_manager => protocol_manager_rule; + protocol_owner => protocol_owner_rule; + }) + .with_address(address_reservation) + .globalize() + } + + pub fn add_pair_config( + &mut self, + pair_config: IndexMap, + ) { + for (address, config) in pair_config.into_iter() { + self.pair_config.insert(address, config); + } } pub fn liquidity_receipt_data( diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 968d074a..066c8be1 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -521,7 +521,7 @@ impl ScryptoTestEnv { ociswap_v2_adapter_v1_package, &mut env, )?; - let defiplaza_v2_adapter_v1 = DefiPlazaV2Adapter::instantiate( + let mut defiplaza_v2_adapter_v1 = DefiPlazaV2Adapter::instantiate( rule!(allow_all), rule!(allow_all), Default::default(), @@ -540,6 +540,26 @@ impl ScryptoTestEnv { &mut env, )?; + // Registering all of pair configs to the adapter. + defiplaza_v2_adapter_v1.add_pair_config( + defiplaza_v2_pools + .iter() + .map(|pool| ComponentAddress::try_from(pool).unwrap()) + .map(|address| { + ( + address, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: dec!("0"), + decay_factor: dec!("0.9512"), + }, + ) + }) + .collect::>(), + &mut env, + )?; + // Submitting some dummy prices to the oracle to get things going. resource_addresses.try_map(|resource_address| { simple_oracle.set_price(*resource_address, XRD, dec!(1), &mut env) @@ -1282,6 +1302,33 @@ impl ScryptoUnitEnv { .unwrap() }); + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + defiplaza_v2_adapter_v1, + "add_pair_config", + (defiplaza_v2_pools + .iter() + .map(|address| { + ( + address, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: dec!("0"), + decay_factor: dec!("0.9512"), + }, + ) + }) + .collect::>(),), + ) + .build(), + vec![], + ) + .expect_commit_success(); + // Cache the addresses of the various Caviarnine pools. test_runner .execute_manifest_ignoring_fee( From 4c757bc160ada4ff8e9e8a243c4f2711cee24a00 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 28 Feb 2024 15:43:15 +0300 Subject: [PATCH 08/47] [Defiplaza v1 Adapter v1]: Basic price calculation. --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 212 ++++++++++++++++++-- tests/tests/caviarnine_v1.rs | 8 +- tests/tests/defiplaza_v2.rs | 2 +- tests/tests/ociswap_v1.rs | 4 +- tests/tests/ociswap_v2.rs | 4 +- tests/tests/protocol.rs | 8 +- 6 files changed, 212 insertions(+), 26 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 3834eb96..93c9fd3e 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -6,6 +6,8 @@ use ports_interface::prelude::*; use scrypto::prelude::*; use scrypto_interface::*; +// TODO: Remove all logging. + macro_rules! define_error { ( $( @@ -402,21 +404,34 @@ pub mod adapter { } fn price(&mut self, pool_address: ComponentAddress) -> Price { - // TODO: Still not sure how to find the price of assets in DefiPlaza - // and I'm working with them on that. For now, I will just say that - // the price is one. WE MUST CHANGE THIS BEFORE GOING LIVE! - // - // More information: The price for selling and buying the asset is - // different in DefiPlaza just like an order book (they're not an - // order book though). So, there is no current price that you can - // buy and sell at but two prices depending on what resource the - // input is. + // In DefiPlaza there is no concept of a current pool price. Instead + // there is a bid and ask kind of like an order book but they're not + // one. The price is different depending on whether a given trade + // would improve or worsen IL. We say that the current pool price is + // the arithmetic mean of the bid and ask prices of the pool. let pool = pool!(pool_address); - let (base_asset, quote_asset) = pool.get_tokens(); + let (base_pool, quote_pool) = pool.get_pools(); + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + let bid_ask = price_math::calculate_pair_prices( + pool.get_state(), + *self.pair_config.get(&pool_address).expect(NO_PAIR_CONFIG), + Global::::from(base_pool), + Global::::from(quote_pool), + ); + info!("bid ask = {bid_ask:?}"); + + let average_price = bid_ask + .bid + .checked_add(bid_ask.ask) + .and_then(|value| value.checked_div(dec!(2))) + .expect(OVERFLOW_ERROR); + + info!("average_price = {average_price}"); Price { - base: base_asset, - quote: quote_asset, - price: dec!(1), + base: base_resource_address, + quote: quote_resource_address, + price: average_price, } } @@ -451,3 +466,174 @@ impl From for AnyValue { AnyValue::from_typed(&value).unwrap() } } + +// The following functions are copied from the DefiPlaza repository (link: +// https://github.com/OmegaSyndicate/RadixPlaza) and have been slightly modified +// so that they're pure functions that require no state. The commit hash that +// is used here is `574acb12fef95d8040c449dce4d01cfc4115bd35`. DefiPlaza's +// source code is licensed under the MIT license which allows us to do such +// copies and modification of code. +// +// This module exposes two main functions which are the entrypoints into this +// module's functionality which calculate the incoming and outgoing spot prices. +#[allow(clippy::arithmetic_side_effects)] +mod price_math { + use super::*; + + #[derive( + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + )] + pub struct PairPrices { + pub bid: Decimal, + pub ask: Decimal, + } + + pub fn calculate_pair_prices( + pair_state: PairState, + pair_config: PairConfig, + base_pool: Global, + quote_pool: Global, + ) -> PairPrices { + let input_is_quote = false; + + // Check which pool we're workings with and extract relevant values + let (pool, old_pref, _) = + select_pool(pair_state, input_is_quote, base_pool, quote_pool); + let (actual, surplus, shortfall) = + assess_pool(pool, pair_state.target_ratio); + + // Compute time since previous trade and resulting decay factor for the + // filter + let t = + Clock::current_time_rounded_to_minutes().seconds_since_unix_epoch; + let delta_t = (t - pair_state.last_outgoing).max(0); + let factor = + Decimal::checked_powi(&pair_config.decay_factor, delta_t / 60) + .unwrap(); + + // Calculate the filtered reference price + let p_ref_ss = match shortfall > Decimal::ZERO { + true => calc_p0_from_curve( + shortfall, + surplus, + pair_state.target_ratio, + pair_config.k_in, + ), + false => old_pref, + }; + let p_ref = factor * old_pref + (Decimal::ONE - factor) * p_ref_ss; + + let adjusted_target_ratio = match actual > Decimal::ZERO { + true => calc_target_ratio(p_ref, actual, surplus, pair_config.k_in), + false => Decimal::ZERO, + }; + let bid = calc_spot(p_ref, adjusted_target_ratio, pair_config.k_in); + + let last_outgoing_spot = match pool == base_pool { + true => pair_state.last_out_spot, + false => Decimal::ONE / pair_state.last_out_spot, + }; + + let incoming_spot = + calc_spot(p_ref_ss, pair_state.target_ratio, pair_config.k_in); + let outgoing_spot = factor * last_outgoing_spot + + (Decimal::ONE - factor) * incoming_spot; + + let ask = outgoing_spot; + + info!("Shortage = {:?}", pair_state.shortage); + + // TODO: What to do at equilibrium? + match pair_state.shortage { + Shortage::Equilibrium | Shortage::BaseShortage => { + PairPrices { bid, ask } + } + Shortage::QuoteShortage => PairPrices { + bid: 1 / ask, + ask: 1 / bid, + }, + } + } + + const MIN_K_IN: Decimal = dec!(0.001); + + fn select_pool( + state: PairState, + input_is_quote: bool, + base_pool: Global, + quote_pool: Global, + ) -> (Global, Decimal, bool) { + let p_ref = state.p0; + let p_ref_inv = Decimal::ONE / p_ref; + match (state.shortage, input_is_quote) { + (Shortage::BaseShortage, true) => (base_pool, p_ref, false), + (Shortage::BaseShortage, false) => (base_pool, p_ref, true), + (Shortage::Equilibrium, true) => (base_pool, p_ref, false), + (Shortage::Equilibrium, false) => (quote_pool, p_ref_inv, false), + (Shortage::QuoteShortage, true) => (quote_pool, p_ref_inv, true), + (Shortage::QuoteShortage, false) => (quote_pool, p_ref_inv, false), + } + } + + fn assess_pool( + pool: Global, + target_ratio: Decimal, + ) -> (Decimal, Decimal, Decimal) { + let reserves = pool.get_vault_amounts(); + let actual = + *reserves.get_index(0).map(|(_addr, amount)| amount).unwrap(); + let surplus = + *reserves.get_index(1).map(|(_addr, amount)| amount).unwrap(); + let shortfall = target_ratio * actual - actual; + (actual, surplus, shortfall) + } + + fn calc_p0_from_curve( + shortfall: Decimal, + surplus: Decimal, + target_ratio: Decimal, + k: Decimal, + ) -> Decimal { + assert!(shortfall > Decimal::ZERO, "Invalid shortfall"); + assert!(surplus > Decimal::ZERO, "Invalid surplus"); + assert!(target_ratio >= Decimal::ONE, "Invalid target ratio"); + assert!(k >= MIN_K_IN, "Invalid k"); + + // Calculate the price at equilibrium (p0) using the given formula + surplus / shortfall / (Decimal::ONE + k * (target_ratio - Decimal::ONE)) + } + + fn calc_spot(p0: Decimal, target_ratio: Decimal, k: Decimal) -> Decimal { + assert!(p0 > Decimal::ZERO, "Invalid p0"); + assert!(target_ratio >= Decimal::ONE, "Invalid target ratio"); + assert!(k >= MIN_K_IN, "Invalid k"); + + let ratio2 = target_ratio * target_ratio; + (Decimal::ONE + k * (ratio2 - Decimal::ONE)) * p0 + } + + fn calc_target_ratio( + p0: Decimal, + actual: Decimal, + surplus: Decimal, + k: Decimal, + ) -> Decimal { + assert!(p0 > Decimal::ZERO, "Invalid p0"); + assert!(actual > Decimal::ZERO, "Invalid actual reserves"); + assert!(surplus >= Decimal::ZERO, "Invalid surplus amount"); + assert!(k >= MIN_K_IN, "Invalid k"); + + let radicand = Decimal::ONE + dec!(4) * k * surplus / p0 / actual; + let num = dec!(2) * k - Decimal::ONE + radicand.checked_sqrt().unwrap(); + num / k / dec!(2) + } +} diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 5980e7a9..d14103e5 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -3,7 +3,7 @@ use tests::prelude::*; #[test] -pub fn can_open_a_simple_position_against_a_caviarnine_pool( +fn can_open_a_simple_position_against_a_caviarnine_pool( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -35,7 +35,7 @@ pub fn can_open_a_simple_position_against_a_caviarnine_pool( } #[test] -pub fn liquidity_receipt_information_can_be_read_through_adapter( +fn liquidity_receipt_information_can_be_read_through_adapter( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -448,7 +448,7 @@ fn liquidity_receipt_includes_the_amount_of_liquidity_positions_we_expect_to_see } #[test] -pub fn contribution_amount_reported_in_receipt_nft_matches_caviarnine_state( +fn contribution_amount_reported_in_receipt_nft_matches_caviarnine_state( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -800,7 +800,7 @@ fn approximately_equals(a: Decimal, b: Decimal) -> bool { } #[test] -pub fn price_and_active_tick_reported_by_adapter_must_match_whats_reported_by_pool( +fn price_and_active_tick_reported_by_adapter_must_match_whats_reported_by_pool( ) -> Result<(), RuntimeError> { // Arrange let Environment { diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index d34393fc..17eaf21d 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -3,7 +3,7 @@ use tests::prelude::*; #[test] -pub fn can_open_a_simple_position_against_a_defiplaza_pool( +fn can_open_a_simple_position_against_a_defiplaza_pool( ) -> Result<(), RuntimeError> { // Arrange let Environment { diff --git a/tests/tests/ociswap_v1.rs b/tests/tests/ociswap_v1.rs index 17a44d12..eea19bd0 100644 --- a/tests/tests/ociswap_v1.rs +++ b/tests/tests/ociswap_v1.rs @@ -1,7 +1,7 @@ use tests::prelude::*; #[test] -pub fn can_open_a_simple_position_against_an_ociswap_pool( +fn can_open_a_simple_position_against_an_ociswap_pool( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -30,7 +30,7 @@ pub fn can_open_a_simple_position_against_an_ociswap_pool( } #[test] -pub fn price_reported_by_pool_is_equal_to_price_reported_by_adapter( +fn price_reported_by_pool_is_equal_to_price_reported_by_adapter( ) -> Result<(), RuntimeError> { // Arrange let Environment { diff --git a/tests/tests/ociswap_v2.rs b/tests/tests/ociswap_v2.rs index 28931740..5fded02b 100644 --- a/tests/tests/ociswap_v2.rs +++ b/tests/tests/ociswap_v2.rs @@ -3,7 +3,7 @@ use tests::prelude::*; #[test] -pub fn can_open_a_simple_position_against_an_ociswap_pool( +fn can_open_a_simple_position_against_an_ociswap_pool( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -32,7 +32,7 @@ pub fn can_open_a_simple_position_against_an_ociswap_pool( } #[test] -pub fn price_reported_by_pool_is_equal_to_price_reported_by_adapter( +fn price_reported_by_pool_is_equal_to_price_reported_by_adapter( ) -> Result<(), RuntimeError> { // Arrange let Environment { diff --git a/tests/tests/protocol.rs b/tests/tests/protocol.rs index 91a40998..a39c5f07 100644 --- a/tests/tests/protocol.rs +++ b/tests/tests/protocol.rs @@ -196,7 +196,7 @@ fn cant_open_a_liquidity_position_by_providing_the_protocol_resource( } #[test] -pub fn can_open_a_liquidity_position_before_the_price_is_stale( +fn can_open_a_liquidity_position_before_the_price_is_stale( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -229,7 +229,7 @@ pub fn can_open_a_liquidity_position_before_the_price_is_stale( } #[test] -pub fn can_open_a_liquidity_position_right_before_price_goes_stale( +fn can_open_a_liquidity_position_right_before_price_goes_stale( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -265,7 +265,7 @@ pub fn can_open_a_liquidity_position_right_before_price_goes_stale( } #[test] -pub fn cant_open_a_liquidity_position_right_after_price_goes_stale( +fn cant_open_a_liquidity_position_right_after_price_goes_stale( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -301,7 +301,7 @@ pub fn cant_open_a_liquidity_position_right_after_price_goes_stale( } #[test] -pub fn can_open_liquidity_position_when_oracle_price_is_lower_than_pool_but_within_allowed_relative_difference( +fn can_open_liquidity_position_when_oracle_price_is_lower_than_pool_but_within_allowed_relative_difference( ) -> Result<(), RuntimeError> { // Arrange let Environment { From fae502a5d859e65ef7b2c3174ccb53a1e75d879d Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 28 Feb 2024 17:00:55 +0300 Subject: [PATCH 09/47] [Defiplaza v2 Adapter v1]: Add some basic fee calculation tests. --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 4 +- tests/tests/defiplaza_v2.rs | 133 ++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 93c9fd3e..bf7e1764 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -380,13 +380,13 @@ pub mod adapter { }; // Steps 4 and 5 - let base_fees = std::cmp::min( + let base_fees = std::cmp::max( new_base_target .checked_sub(old_base_target) .expect(OVERFLOW_ERROR), Decimal::ZERO, ); - let quote_fees = std::cmp::min( + let quote_fees = std::cmp::max( new_quote_target .checked_sub(old_quote_target) .expect(OVERFLOW_ERROR), diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 17eaf21d..d7718394 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -267,3 +267,136 @@ fn can_close_a_liquidity_position_in_defiplaza_that_fits_into_fee_limits() { assert!(total_execution_cost_in_xrd <= dec!(4.8)) } + +#[test] +fn fees_are_zero_when_no_swaps_take_place() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + + let [bitcoin_bucket, xrd_bucket] = [resources.bitcoin, XRD] + .map(ResourceManager) + .map(|mut resource_manager| { + resource_manager.mint_fungible(dec!(100), env).unwrap() + }); + + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + (bitcoin_bucket, xrd_bucket), + env, + )?; + + // Act + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert + assert!(fees.values().all(|value| *value == Decimal::ZERO)); + + Ok(()) +} + +#[test] +fn a_swap_with_xrd_input_produces_xrd_fees() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + + let [bitcoin_bucket, xrd_bucket] = [resources.bitcoin, XRD] + .map(ResourceManager) + .map(|mut resource_manager| { + resource_manager.mint_fungible(dec!(100), env).unwrap() + }); + + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + (bitcoin_bucket, xrd_bucket), + env, + )?; + + let _ = ResourceManager(XRD) + .mint_fungible(dec!(100_000), env) + .and_then(|bucket| defiplaza_v2.pools.bitcoin.swap(bucket, env))?; + + // Act + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert + assert_eq!(*fees.get(&resources.bitcoin).unwrap(), dec!(0)); + assert_ne!(*fees.get(&XRD).unwrap(), dec!(0)); + + Ok(()) +} + +#[test] +fn a_swap_with_btc_input_produces_btc_fees() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + + let [bitcoin_bucket, xrd_bucket] = [resources.bitcoin, XRD] + .map(ResourceManager) + .map(|mut resource_manager| { + resource_manager.mint_fungible(dec!(100), env).unwrap() + }); + + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + (bitcoin_bucket, xrd_bucket), + env, + )?; + + let _ = ResourceManager(resources.bitcoin) + .mint_fungible(dec!(100_000), env) + .and_then(|bucket| defiplaza_v2.pools.bitcoin.swap(bucket, env))?; + + // Act + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert + assert_ne!(*fees.get(&resources.bitcoin).unwrap(), dec!(0)); + assert_eq!(*fees.get(&XRD).unwrap(), dec!(0)); + + Ok(()) +} From 3ff8236dff4b78a9b7406d3528a1a9a399c8a889 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 29 Feb 2024 09:24:58 +0300 Subject: [PATCH 10/47] [Defiplaza v2 Adapter v1]: Add failing non strict price movement tests. --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 18 +- tests/tests/defiplaza_v2.rs | 293 ++++++++++++++++++++ 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index bf7e1764..38ca81fa 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -230,6 +230,11 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. + info!("Doing the first one"); + info!( + "Shortage before first contribution: {:?}", + pool.get_state().shortage + ); let (first_pool_units, second_change) = match shortage_state { ShortageState::Equilibrium => ( pool.add_liquidity(first_bucket, None).0, @@ -239,6 +244,10 @@ pub mod adapter { pool.add_liquidity(first_bucket, Some(second_bucket)) } }; + info!( + "Shortage after first contribution: {:?}", + pool.get_state().shortage + ); // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets @@ -247,8 +256,13 @@ pub mod adapter { let second_original_target = second_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. + info!("Doing the second one"); let (second_pool_units, change) = pool.add_liquidity(second_bucket, None); + info!( + "Shortage after second contribution: {:?}", + pool.get_state().shortage + ); // TODO: Should we subtract the change from the second original // target? Seems like we should if the price if not the same in @@ -536,7 +550,6 @@ mod price_math { true => calc_target_ratio(p_ref, actual, surplus, pair_config.k_in), false => Decimal::ZERO, }; - let bid = calc_spot(p_ref, adjusted_target_ratio, pair_config.k_in); let last_outgoing_spot = match pool == base_pool { true => pair_state.last_out_spot, @@ -544,10 +557,11 @@ mod price_math { }; let incoming_spot = - calc_spot(p_ref_ss, pair_state.target_ratio, pair_config.k_in); + calc_spot(p_ref, adjusted_target_ratio, pair_config.k_in); let outgoing_spot = factor * last_outgoing_spot + (Decimal::ONE - factor) * incoming_spot; + let bid = incoming_spot; let ask = outgoing_spot; info!("Shortage = {:?}", pair_state.shortage); diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index d7718394..933d395e 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -400,3 +400,296 @@ fn a_swap_with_btc_input_produces_btc_fees() -> Result<(), RuntimeError> { Ok(()) } + +#[test] +fn contributions_to_defiplaza_through_adapter_dont_fail_due_to_bucket_ordering( +) -> Result<(), RuntimeError> { + // Arrange + let mut results = Vec::::new(); + for order in [true, false] { + // Arrange + let Environment { + environment: ref mut env, + resources, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new()?; + + let xrd_bucket = ResourceManager(XRD).mint_fungible(dec!(1), env)?; + let bitcoin_bucket = + ResourceManager(resources.bitcoin).mint_fungible(dec!(1), env)?; + let buckets = if order { + (xrd_bucket, bitcoin_bucket) + } else { + (bitcoin_bucket, xrd_bucket) + }; + + // Act + let result = defiplaza_v2.adapter.open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + buckets, + env, + ); + results.push(result.is_ok()); + } + + // Assert + assert_eq!(results.len(), 2); + assert_eq!(results.iter().filter(|item| **item).count(), 2); + + Ok(()) +} + +#[test] +fn when_price_of_user_asset_stays_the_same_and_k_stays_the_same_the_output_is_the_same_amount_as_the_input( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Same, + Movement::Same, + CloseLiquidityResult::SameAmount, + ) +} + +#[test] +fn when_price_of_user_asset_stays_the_same_and_k_goes_down_the_output_is_the_same_amount_as_the_input( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Down, + Movement::Same, + CloseLiquidityResult::SameAmount, + ) +} + +#[test] +fn when_price_of_user_asset_stays_the_same_and_k_goes_up_the_output_is_the_same_amount_as_the_input( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Up, + Movement::Same, + CloseLiquidityResult::SameAmount, + ) +} + +#[test] +fn when_price_of_user_asset_goes_down_and_k_stays_the_same_the_user_gets_fees( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Same, + Movement::Down, + CloseLiquidityResult::GetFees, + ) +} + +#[test] +fn when_price_of_user_asset_goes_down_and_k_goes_down_the_user_gets_fees( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Down, + Movement::Down, + CloseLiquidityResult::GetFees, + ) +} + +#[test] +fn when_price_of_user_asset_goes_down_and_k_goes_up_the_user_gets_fees( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Up, + Movement::Down, + CloseLiquidityResult::GetFees, + ) +} + +#[test] +fn when_price_of_user_asset_goes_up_and_k_stays_the_same_the_user_gets_reimbursed( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Same, + Movement::Up, + CloseLiquidityResult::Reimbursement, + ) +} + +#[test] +fn when_price_of_user_asset_goes_up_and_k_goes_down_the_user_gets_reimbursed( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Down, + Movement::Up, + CloseLiquidityResult::Reimbursement, + ) +} + +#[test] +fn when_price_of_user_asset_goes_up_and_k_goes_up_the_user_gets_reimbursed( +) -> Result<(), RuntimeError> { + non_strict_testing_of_fees( + Movement::Up, + Movement::Up, + CloseLiquidityResult::Reimbursement, + ) +} + +fn non_strict_testing_of_fees( + protocol_coefficient: Movement, + price_of_user_asset: Movement, + result: CloseLiquidityResult, +) -> Result<(), RuntimeError> { + let Environment { + environment: ref mut env, + mut protocol, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(defiplaza_v2.pools.bitcoin.try_into().unwrap(), env)?; + protocol.oracle.set_price( + pool_reported_price.base, + pool_reported_price.quote, + pool_reported_price.price, + env, + )?; + + let bitcoin_amount_in = dec!(100); + + let bitcoin_bucket = ResourceManager(resources.bitcoin) + .mint_fungible(bitcoin_amount_in, env)?; + let (receipt, _, _) = protocol.ignition.open_liquidity_position( + FungibleBucket(bitcoin_bucket), + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + LockupPeriod::from_months(6).unwrap(), + env, + )?; + + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + ( + ResourceManager(resources.bitcoin) + .mint_fungible(dec!(100_000), env)?, + ResourceManager(XRD).mint_fungible(dec!(100_000), env)?, + ), + env, + )?; + + match price_of_user_asset { + // User asset price goes down - i.e., we inject it into the pool. + Movement::Down => { + let bitcoin_bucket = ResourceManager(resources.bitcoin) + .mint_fungible(dec!(450_000_000), env)?; + let _ = defiplaza_v2.pools.bitcoin.swap(bitcoin_bucket, env)?; + } + // The user asset price stays the same. We do not do anything. + Movement::Same => {} + // User asset price goes up - i.e., we reduce it in the pool. + Movement::Up => { + let xrd_bucket = + ResourceManager(XRD).mint_fungible(dec!(450_000_000), env)?; + let _ = defiplaza_v2.pools.bitcoin.swap(xrd_bucket, env)?; + } + } + + match protocol_coefficient { + // Somebody claimed some portion of the pool + Movement::Down => { + defiplaza_v2.adapter.close_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + } + // Nothing + Movement::Same => {} + // Somebody contributed to the pool some amount + Movement::Up => { + let _ = defiplaza_v2 + .adapter + .open_liquidity_position( + defiplaza_v2.pools.bitcoin.try_into().unwrap(), + ( + ResourceManager(resources.bitcoin) + .mint_fungible(dec!(100_000), env)?, + ResourceManager(XRD) + .mint_fungible(dec!(100_000), env)?, + ), + env, + )? + .pool_units; + } + } + + env.set_current_time(Instant::new( + *LockupPeriod::from_months(12).unwrap().seconds() as i64, + )); + let pool_reported_price = defiplaza_v2 + .adapter + .price(defiplaza_v2.pools.bitcoin.try_into().unwrap(), env)?; + protocol.oracle.set_price( + pool_reported_price.base, + pool_reported_price.quote, + pool_reported_price.price, + env, + )?; + + let buckets = IndexedBuckets::native_from_buckets( + protocol.ignition.close_liquidity_position(receipt, env)?, + env, + )?; + + let bitcoin_amount_out = buckets + .get(&resources.bitcoin) + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or_default() + .checked_round(5, RoundingMode::ToPositiveInfinity) + .unwrap(); + let xrd_amount_out = buckets + .get(&XRD) + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or_default() + .checked_round(5, RoundingMode::ToZero) + .unwrap(); + + match result { + CloseLiquidityResult::GetFees => { + // Bitcoin we get back must be strictly greater than what we put in. + assert!(bitcoin_amount_out > bitcoin_amount_in); + // When we get back fees we MUST not get back any XRD + assert_eq!(xrd_amount_out, Decimal::ZERO) + } + CloseLiquidityResult::SameAmount => { + // Bitcoin we get back must be strictly equal to what we put in. + assert_eq!(bitcoin_amount_out, bitcoin_amount_in); + // If we get back the same amount then we must NOT get back any XRD. + assert_eq!(xrd_amount_out, Decimal::ZERO) + } + CloseLiquidityResult::Reimbursement => { + // Bitcoin we get back must be less than what we put in. + assert!(bitcoin_amount_out < bitcoin_amount_in); + // We must get back SOME xrd. + assert_ne!(xrd_amount_out, Decimal::ZERO); + } + } + + Ok(()) +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Movement { + Down, + Same, + Up, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CloseLiquidityResult { + GetFees, + SameAmount, + Reimbursement, +} From ca5a5583b627f84603651635ca5e48dec5125b09 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 29 Feb 2024 10:58:30 +0300 Subject: [PATCH 11/47] [Defiplaza v2 Adapter v1]: Fix shortage issue. --- tests/tests/defiplaza_v2.rs | 130 +++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 933d395e..78711179 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -582,7 +582,7 @@ fn non_strict_testing_of_fees( // User asset price goes down - i.e., we inject it into the pool. Movement::Down => { let bitcoin_bucket = ResourceManager(resources.bitcoin) - .mint_fungible(dec!(450_000_000), env)?; + .mint_fungible(dec!(10_000_000), env)?; let _ = defiplaza_v2.pools.bitcoin.swap(bitcoin_bucket, env)?; } // The user asset price stays the same. We do not do anything. @@ -590,7 +590,7 @@ fn non_strict_testing_of_fees( // User asset price goes up - i.e., we reduce it in the pool. Movement::Up => { let xrd_bucket = - ResourceManager(XRD).mint_fungible(dec!(450_000_000), env)?; + ResourceManager(XRD).mint_fungible(dec!(10_000_000), env)?; let _ = defiplaza_v2.pools.bitcoin.swap(xrd_bucket, env)?; } } @@ -693,3 +693,129 @@ pub enum CloseLiquidityResult { SameAmount, Reimbursement, } + +#[test] +fn user_resources_are_contributed_in_full_when_oracle_price_is_same_as_pool_price( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new()?; + + let pool = ComponentAddress::try_from(defiplaza_v2.pools.bitcoin).unwrap(); + let user_resource = resources.bitcoin; + + let pool_price = defiplaza_v2.adapter.price(pool, env)?; + protocol.oracle.set_price( + pool_price.base, + pool_price.quote, + pool_price.price, + env, + )?; + + let user_resource_bucket = + ResourceManager(user_resource).mint_fungible(dec!(100), env)?; + + // Act + let (_, _, change) = protocol.ignition.open_liquidity_position( + FungibleBucket(user_resource_bucket), + pool, + LockupPeriod::from_months(6).unwrap(), + env, + )?; + + // Assert + assert_eq!(change.len(), 0); + + Ok(()) +} + +#[test] +fn user_resources_are_contributed_in_full_when_oracle_price_is_higher_than_pool_price( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = ComponentAddress::try_from(defiplaza_v2.pools.bitcoin).unwrap(); + let user_resource = resources.bitcoin; + + let pool_price = defiplaza_v2.adapter.price(pool, env)?; + protocol.oracle.set_price( + pool_price.base, + pool_price.quote, + pool_price.price * dec!(1.05), + env, + )?; + + let user_resource_bucket = + ResourceManager(user_resource).mint_fungible(dec!(100), env)?; + + // Act + let (_, _, change) = protocol.ignition.open_liquidity_position( + FungibleBucket(user_resource_bucket), + pool, + LockupPeriod::from_months(6).unwrap(), + env, + )?; + + // Assert + assert_eq!(change.len(), 0); + + Ok(()) +} + +#[test] +fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_price( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + mut defiplaza_v2, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = ComponentAddress::try_from(defiplaza_v2.pools.bitcoin).unwrap(); + let user_resource = resources.bitcoin; + + let pool_price = defiplaza_v2.adapter.price(pool, env)?; + protocol.oracle.set_price( + pool_price.base, + pool_price.quote, + pool_price.price * dec!(0.96), + env, + )?; + + let user_resource_bucket = + ResourceManager(user_resource).mint_fungible(dec!(100), env)?; + + // Act + let (_, _, change) = protocol.ignition.open_liquidity_position( + FungibleBucket(user_resource_bucket), + pool, + LockupPeriod::from_months(6).unwrap(), + env, + )?; + + // Assert + assert_eq!(change.len(), 0); + + Ok(()) +} From 9f2811cb468794eea6f96928de3e710a1a221e8c Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 1 Mar 2024 15:36:50 +0300 Subject: [PATCH 12/47] [Ignition]: Input sanitization. --- Cargo.lock | 158 ++++++++++++++++------------- packages/ignition/src/blueprint.rs | 6 ++ packages/ignition/src/errors.rs | 4 + tests/src/errors.rs | 5 + tests/tests/protocol.rs | 131 +++++++++++++++++++++++- 5 files changed, 231 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ba5e2e3..e0461d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -311,7 +311,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -345,7 +345,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -394,12 +394,12 @@ name = "common" version = "0.1.0" dependencies = [ "humantime", + "native-sdk", "radix-engine-common", "radix-engine-derive", "radix-engine-interface", "sbor", "scrypto", - "transaction", ] [[package]] @@ -443,9 +443,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15f3b597018782655a05d417f28bac009f6eb60f4b6703eb818998c1aaa16a" +checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" dependencies = [ "cc", "cxxbridge-flags", @@ -502,9 +502,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81699747d109bba60bd6f87e7cb24b626824b8427b32f199b95c7faa06ee3dc9" +checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" dependencies = [ "cc", "codespan-reporting", @@ -512,24 +512,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] name = "cxxbridge-flags" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7eb4c4fd18505f5a935f9c2ee77780350dcdb56da7cd037634e806141c5c43" +checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" [[package]] name = "cxxbridge-macro" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d914fcc6452d133236ee067a9538be25ba6a644a450e1a6c617da84bf029854" +checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -553,7 +553,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -564,7 +564,20 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.51", + "syn 2.0.52", +] + +[[package]] +name = "defiplaza-v2-adapter-v1" +version = "0.1.0" +dependencies = [ + "common", + "ports-interface", + "radix-engine-interface", + "sbor", + "scrypto", + "scrypto-interface", + "transaction", ] [[package]] @@ -680,7 +693,7 @@ checksum = "311a6d2f1f9d60bff73d2c78a0af97ed27f79672f15c238192a5bbb64db56d00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -859,7 +872,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.3", + "indexmap 2.2.5", "slab", "tokio", "tokio-util", @@ -892,9 +905,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1054,9 +1067,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1159,9 +1172,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -1346,9 +1359,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" @@ -1373,7 +1386,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -1415,7 +1428,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2053,7 +2066,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2182,7 +2195,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2227,7 +2240,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.3", + "indexmap 2.2.5", "serde", "serde_derive", "serde_json", @@ -2244,7 +2257,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2404,9 +2417,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.51" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -2448,9 +2461,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", @@ -2473,6 +2486,7 @@ version = "0.1.0" dependencies = [ "caviarnine-v1-adapter-v1", "common", + "defiplaza-v2-adapter-v1", "extend", "flate2", "gateway-client", @@ -2510,7 +2524,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] [[package]] @@ -2635,7 +2649,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", @@ -2865,7 +2879,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -2899,7 +2913,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3015,7 +3029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ "bitflags 2.4.2", - "indexmap 2.2.3", + "indexmap 2.2.5", "semver", ] @@ -3085,7 +3099,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -3103,7 +3117,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.4", ] [[package]] @@ -3123,17 +3137,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -3144,9 +3158,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -3156,9 +3170,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -3168,9 +3182,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -3180,9 +3194,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -3192,9 +3206,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -3204,9 +3218,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -3216,15 +3230,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] @@ -3262,5 +3276,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.51", + "syn 2.0.52", ] diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 96b3ec7d..6f1cb30f 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -1545,6 +1545,7 @@ mod ignition { /// * `value`: [`i64`] - The maximum allowed staleness period in /// seconds. pub fn set_maximum_allowed_price_staleness(&mut self, value: i64) { + assert!(value >= 0, "{}", INVALID_MAXIMUM_PRICE_STALENESS); self.maximum_allowed_price_staleness = value } @@ -1576,6 +1577,11 @@ mod ignition { lockup_period: LockupPeriod, percentage: Decimal, ) { + assert!( + percentage >= Decimal::ZERO, + "{}", + INVALID_UPFRONT_REWARD_PERCENTAGE + ); self.reward_rates.insert(lockup_period, percentage) } diff --git a/packages/ignition/src/errors.rs b/packages/ignition/src/errors.rs index 8d424313..a2cc439c 100644 --- a/packages/ignition/src/errors.rs +++ b/packages/ignition/src/errors.rs @@ -54,4 +54,8 @@ define_error! { BOTH_POOL_ASSETS_ARE_THE_PROTOCOL_RESOURCE => "The user resource can not be the protocol resource."; OVERFLOW_ERROR => "Overflow error"; + INVALID_MAXIMUM_PRICE_STALENESS + => "Price staleness must be a positive or zero integer"; + INVALID_UPFRONT_REWARD_PERCENTAGE + => "Upfront rewards must be positive or zero decimals"; } diff --git a/tests/src/errors.rs b/tests/src/errors.rs index 7ec32ccf..21b787be 100644 --- a/tests/src/errors.rs +++ b/tests/src/errors.rs @@ -49,6 +49,7 @@ define_error_checking_functions! { ignition => [ NO_ADAPTER_FOUND_FOR_POOL_ERROR, NEITHER_POOL_RESOURCE_IS_PROTOCOL_RESOURCE_ERROR, + NEITHER_POOL_RESOURCE_IS_USER_RESOURCE_ERROR, NO_ASSOCIATED_VAULT_ERROR, NO_ASSOCIATED_LIQUIDITY_RECEIPT_VAULT_ERROR, NOT_AN_IGNITION_ADDRESS_ERROR, @@ -66,6 +67,10 @@ define_error_checking_functions! { LIQUIDITY_POSITION_HAS_NOT_MATURED_ERROR, USER_MUST_NOT_PROVIDE_PROTOCOL_ASSET_ERROR, USER_RESOURCES_VOLATILITY_UNKNOWN_ERROR, + BOTH_POOL_ASSETS_ARE_THE_PROTOCOL_RESOURCE, + OVERFLOW_ERROR, + INVALID_MAXIMUM_PRICE_STALENESS, + INVALID_UPFRONT_REWARD_PERCENTAGE, ], ociswap_adapter => [ FAILED_TO_GET_RESOURCE_ADDRESSES_ERROR, diff --git a/tests/tests/protocol.rs b/tests/tests/protocol.rs index 0720f8d6..6ab80614 100644 --- a/tests/tests/protocol.rs +++ b/tests/tests/protocol.rs @@ -38,6 +38,135 @@ fn cant_open_a_liquidity_position_when_opening_is_disabled( Ok(()) } +#[test] +fn cant_add_a_negative_upfront_reward_percentage() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol.ignition.add_reward_rate( + LockupPeriod::from_seconds(1), + dec!(-1), + env, + ); + + // Assert + assert_is_ignition_invalid_upfront_reward_percentage(&rtn); + + Ok(()) +} + +#[test] +fn can_add_a_zero_upfront_reward_percentage() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol.ignition.add_reward_rate( + LockupPeriod::from_seconds(1), + dec!(0), + env, + ); + + // Assert + assert!(rtn.is_ok()); + + Ok(()) +} + +#[test] +fn can_add_a_positive_upfront_reward_percentage() -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol.ignition.add_reward_rate( + LockupPeriod::from_seconds(1), + dec!(1), + env, + ); + + // Assert + assert!(rtn.is_ok()); + + Ok(()) +} + +#[test] +fn cant_set_the_maximum_allowed_price_staleness_to_a_negative_number( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol + .ignition + .set_maximum_allowed_price_staleness(-1, env); + + // Assert + assert_is_ignition_invalid_maximum_price_staleness(&rtn); + + Ok(()) +} + +#[test] +fn can_set_the_maximum_allowed_price_staleness_to_zero( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol + .ignition + .set_maximum_allowed_price_staleness(0, env); + + // Assert + assert!(rtn.is_ok()); + + Ok(()) +} + +#[test] +fn can_set_the_maximum_allowed_price_staleness_to_a_positive_number( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut protocol, + .. + } = ScryptoTestEnv::new()?; + + // Act + let rtn = protocol + .ignition + .set_maximum_allowed_price_staleness(1, env); + + // Assert + assert!(rtn.is_ok()); + + Ok(()) +} + #[test] fn cant_open_a_liquidity_position_on_a_pool_that_has_no_adapter( ) -> Result<(), RuntimeError> { @@ -601,8 +730,8 @@ fn cant_add_an_allowed_pool_where_neither_of_the_resources_is_the_protocol_resou let fungible2 = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(100, env)?; let (pool, ..) = OciswapV2PoolInterfaceScryptoTestStub::instantiate( - fungible2.resource_address(env)?, fungible1.resource_address(env)?, + fungible2.resource_address(env)?, pdec!(1), dec!(0.03), dec!(0.03), From 49f0e3a9a1ade61cea56014ffd6d23cc2f416c92 Mon Sep 17 00:00:00 2001 From: Omar Date: Sat, 2 Mar 2024 17:27:45 +0300 Subject: [PATCH 13/47] [Tests]: Minor test fix --- tests/tests/protocol.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/protocol.rs b/tests/tests/protocol.rs index 0720f8d6..83bd5178 100644 --- a/tests/tests/protocol.rs +++ b/tests/tests/protocol.rs @@ -601,8 +601,8 @@ fn cant_add_an_allowed_pool_where_neither_of_the_resources_is_the_protocol_resou let fungible2 = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(100, env)?; let (pool, ..) = OciswapV2PoolInterfaceScryptoTestStub::instantiate( - fungible2.resource_address(env)?, fungible1.resource_address(env)?, + fungible2.resource_address(env)?, pdec!(1), dec!(0.03), dec!(0.03), From 39cc83134661debec35f80b92c0060d143e4737f Mon Sep 17 00:00:00 2001 From: Omar Date: Sun, 3 Mar 2024 00:05:52 +0300 Subject: [PATCH 14/47] [Publishing Tool]: Introduce the executors --- Cargo.lock | 771 +++++++++++++++++- Cargo.toml | 47 +- tests/example | 1 - tools/publishing-tool/Cargo.toml | 32 + tools/publishing-tool/src/database_overlay.rs | 656 +++++++++++++++ .../src/executor/gateway_executor.rs | 295 +++++++ .../executor/mainnet_simulator_executor.rs | 99 +++ tools/publishing-tool/src/executor/mod.rs | 7 + tools/publishing-tool/src/executor/traits.rs | 41 + tools/publishing-tool/src/main.rs | 6 + 10 files changed, 1910 insertions(+), 45 deletions(-) delete mode 100644 tests/example create mode 100644 tools/publishing-tool/Cargo.toml create mode 100644 tools/publishing-tool/src/database_overlay.rs create mode 100644 tools/publishing-tool/src/executor/gateway_executor.rs create mode 100644 tools/publishing-tool/src/executor/mainnet_simulator_executor.rs create mode 100644 tools/publishing-tool/src/executor/mod.rs create mode 100644 tools/publishing-tool/src/executor/traits.rs create mode 100644 tools/publishing-tool/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e0461d07..b62361ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "aliasable" version = "0.1.3" @@ -92,6 +101,17 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -125,6 +145,27 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.52", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -137,6 +178,15 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "blake2" version = "0.10.6" @@ -231,6 +281,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "camino" version = "1.1.6" @@ -295,6 +356,21 @@ dependencies = [ "libc", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -314,6 +390,17 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob 0.3.1", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.1" @@ -389,6 +476,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "common" version = "0.1.0" @@ -567,6 +664,19 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "defiplaza-v2-adapter-v1" version = "0.1.0" @@ -660,6 +770,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -748,6 +870,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -755,6 +892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -763,12 +901,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -787,8 +947,11 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1045,6 +1208,20 @@ dependencies = [ "scrypto", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1082,6 +1259,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + [[package]] name = "ipnet" version = "2.9.0" @@ -1097,12 +1280,41 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.68" @@ -1127,6 +1339,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128" version = "0.2.5" @@ -1139,12 +1357,49 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libloading" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" +dependencies = [ + "cfg-if", + "windows-targets 0.52.4", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob 0.3.1", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "link-cplusplus" version = "1.0.9" @@ -1176,6 +1431,22 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "memchr" version = "2.7.1" @@ -1198,6 +1469,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1209,9 +1486,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1243,7 +1520,7 @@ dependencies = [ [[package]] name = "native-sdk" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "radix-engine-common", "radix-engine-derive", @@ -1270,6 +1547,54 @@ dependencies = [ "tempfile", ] +[[package]] +name = "node-common" +version = "0.1.0" +source = "git+https://github.com/radixdlt/babylon-node?rev=63a8267196995fef0830e4fbf0271bea65c90ab1#63a8267196995fef0830e4fbf0271bea65c90ab1" +dependencies = [ + "bech32", + "blake2", + "jni", + "opentelemetry", + "opentelemetry-jaeger", + "parking_lot", + "prometheus", + "radix-engine", + "radix-engine-common", + "radix-engine-interface", + "radix-engine-queries", + "radix-engine-store-interface", + "radix-engine-stores", + "sbor", + "tokio", + "tokio-util", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "transaction", + "utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -1407,6 +1732,89 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e" +dependencies = [ + "opentelemetry_api", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e785d273968748578931e4dc3b4f5ec86b26e09d9e0d66b55adda7fce742f7a" +dependencies = [ + "async-trait", + "futures", + "futures-executor", + "once_cell", + "opentelemetry", + "opentelemetry-semantic-conventions", + "thiserror", + "thrift", + "tokio", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b02e0230abb0ab6636d18e2ba8fa02903ea63772281340ccac18e0af3ec9eeb" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "opentelemetry_api" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22" +dependencies = [ + "fnv", + "futures-channel", + "futures-util", + "indexmap 1.9.3", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113" +dependencies = [ + "async-trait", + "crossbeam-channel", + "dashmap", + "fnv", + "futures-channel", + "futures-executor", + "futures-util", + "once_cell", + "opentelemetry_api", + "percent-encoding", + "rand 0.8.5", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + [[package]] name = "ouroboros" version = "0.17.2" @@ -1431,6 +1839,12 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "package-loader" version = "0.1.0" @@ -1476,6 +1890,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1536,6 +1956,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.52", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1569,6 +1999,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "thiserror", +] + +[[package]] +name = "publishing-tool" +version = "0.1.0" +dependencies = [ + "caviarnine-v1-adapter-v1", + "common", + "gateway-client", + "hex", + "ignition", + "ociswap-v1-adapter-v1", + "package-loader", + "radix-engine", + "radix-engine-common", + "radix-engine-interface", + "radix-engine-store-interface", + "sbor", + "scrypto-unit", + "state-manager", + "transaction", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -1592,7 +2057,7 @@ dependencies = [ [[package]] name = "radix-engine" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bitflags 1.3.2", "colored", @@ -1622,7 +2087,7 @@ dependencies = [ [[package]] name = "radix-engine-common" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bech32", "blake2", @@ -1647,7 +2112,7 @@ dependencies = [ [[package]] name = "radix-engine-derive" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "proc-macro2", "quote", @@ -1658,7 +2123,7 @@ dependencies = [ [[package]] name = "radix-engine-interface" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bitflags 1.3.2", "const-sha1", @@ -1680,7 +2145,7 @@ dependencies = [ [[package]] name = "radix-engine-macros" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "paste", "proc-macro2", @@ -1692,7 +2157,7 @@ dependencies = [ [[package]] name = "radix-engine-profiling" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "fixedstr", ] @@ -1700,10 +2165,10 @@ dependencies = [ [[package]] name = "radix-engine-queries" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "hex", - "itertools", + "itertools 0.10.5", "paste", "radix-engine", "radix-engine-interface", @@ -1716,10 +2181,10 @@ dependencies = [ [[package]] name = "radix-engine-store-interface" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "hex", - "itertools", + "itertools 0.10.5", "radix-engine-common", "radix-engine-derive", "radix-engine-interface", @@ -1730,10 +2195,10 @@ dependencies = [ [[package]] name = "radix-engine-stores" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "hex", - "itertools", + "itertools 0.10.5", "radix-engine-common", "radix-engine-derive", "radix-engine-store-interface", @@ -1812,6 +2277,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1827,6 +2301,8 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ + "aho-corasick", + "memchr", "regex-automata", "regex-syntax", ] @@ -1837,6 +2313,8 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1890,7 +2368,7 @@ dependencies = [ [[package]] name = "resources-tracker-macro" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "proc-macro2", "quote", @@ -1898,12 +2376,28 @@ dependencies = [ "syn 1.0.93", ] +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1959,7 +2453,7 @@ dependencies = [ [[package]] name = "sbor" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "const-sha1", "hex", @@ -1973,7 +2467,7 @@ dependencies = [ [[package]] name = "sbor-derive" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "proc-macro2", "sbor-derive-common", @@ -1982,10 +2476,10 @@ dependencies = [ [[package]] name = "sbor-derive-common" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "const-sha1", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.93", @@ -2024,7 +2518,7 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "scrypto" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bech32", "const-sha1", @@ -2046,7 +2540,7 @@ dependencies = [ [[package]] name = "scrypto-derive" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "proc-macro2", "quote", @@ -2083,7 +2577,7 @@ dependencies = [ [[package]] name = "scrypto-schema" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bitflags 1.3.2", "radix-engine-common", @@ -2094,7 +2588,7 @@ dependencies = [ [[package]] name = "scrypto-test" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "native-sdk", "ouroboros", @@ -2112,7 +2606,7 @@ dependencies = [ [[package]] name = "scrypto-unit" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "radix-engine", "radix-engine-interface", @@ -2283,6 +2777,30 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "1.6.4" @@ -2302,6 +2820,16 @@ dependencies = [ "scrypto-interface", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "skeptic" version = "0.13.7" @@ -2326,6 +2854,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.13.1" @@ -2348,6 +2885,37 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "state-manager" +version = "0.1.0" +source = "git+https://github.com/radixdlt/babylon-node?rev=63a8267196995fef0830e4fbf0271bea65c90ab1#63a8267196995fef0830e4fbf0271bea65c90ab1" +dependencies = [ + "blake2", + "enum_dispatch", + "hex", + "im", + "itertools 0.11.0", + "jni", + "lru", + "node-common", + "prometheus", + "radix-engine", + "radix-engine-common", + "radix-engine-interface", + "radix-engine-queries", + "radix-engine-store-interface", + "radix-engine-stores", + "rand 0.8.5", + "rocksdb", + "sbor", + "slotmap", + "tokio", + "tracing", + "transaction", + "transaction-scenarios", + "utils", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2527,6 +3095,16 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -2536,6 +3114,19 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "thrift" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09678c4cdbb4eed72e18b7c2af1329c69825ed16fcbac62d083fc3e2b0590ff0" +dependencies = [ + "byteorder", + "integer-encoding", + "log", + "ordered-float", + "threadpool", +] + [[package]] name = "time" version = "0.3.34" @@ -2593,11 +3184,25 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -2608,6 +3213,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2669,9 +3285,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2679,12 +3307,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de" +dependencies = [ + "once_cell", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log 0.2.0", ] [[package]] name = "transaction" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "bech32", "hex", @@ -2696,6 +3375,24 @@ dependencies = [ "utils", ] +[[package]] +name = "transaction-scenarios" +version = "1.1.0-rc1" +source = "git+https://github.com/radixdlt/radixdlt-scrypto?tag=anemone-e212f2ea#e212f2ea33f05f7980ef6b4026edf60e162aaae3" +dependencies = [ + "hex", + "itertools 0.10.5", + "radix-engine", + "radix-engine-interface", + "radix-engine-store-interface", + "radix-engine-stores", + "sbor", + "scrypto", + "transaction", + "utils", + "walkdir", +] + [[package]] name = "triomphe" version = "0.1.11" @@ -2776,7 +3473,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utils" version = "1.1.0-dev" -source = "git+https://github.com/radixdlt/radixdlt-scrypto?rev=ef169b1e1348b8dbad977ba81d086ee1e80d6ff8#ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" dependencies = [ "indexmap 2.0.0-pre", "serde", @@ -2792,6 +3489,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2829,9 +3532,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3278,3 +3981,13 @@ dependencies = [ "quote", "syn 2.0.52", ] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index c0f8ebff..8a52db6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "libraries/scrypto-math", # Tools "tools/bootstrap", + "tools/publishing-tool", # Tests "tests" ] @@ -27,21 +28,21 @@ edition = "2021" description = "The implementation of project Ignition in Scrypto for the Radix Ledger" [workspace.dependencies] -sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -utils = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -native-sdk = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-stores = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-derive = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -radix-engine-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +utils = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +native-sdk = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-stores = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-derive = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } -scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "ef169b1e1348b8dbad977ba81d086ee1e80d6ff8" } +scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } [profile.release] opt-level = 'z' @@ -52,4 +53,20 @@ strip = true overflow-checks = true [workspace.lints.clippy] -arithmetic_side_effects = "warn" \ No newline at end of file +arithmetic_side_effects = "warn" + +[patch.'https://github.com/radixdlt/radixdlt-scrypto'] +sbor = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +utils = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +native-sdk = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +transaction = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-common = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-stores = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-derive = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-queries = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +radix-engine-store-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto-unit = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto-test = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } \ No newline at end of file diff --git a/tests/example b/tests/example deleted file mode 100644 index 8b88f962..00000000 --- a/tests/example +++ /dev/null @@ -1 +0,0 @@ -Reading now! \ No newline at end of file diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml new file mode 100644 index 00000000..b0e7fab4 --- /dev/null +++ b/tools/publishing-tool/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "publishing-tool" +description = "A configurable tool used to publish Ignition to various networks." +version.workspace = true +edition.workspace = true + +[dependencies] +sbor = { workspace = true } +transaction = { workspace = true } +scrypto-unit = { workspace = true } +radix-engine = { workspace = true } +radix-engine-common = { workspace = true } +radix-engine-interface = { workspace = true } +radix-engine-store-interface = { workspace = true } + +common = { path = "../../libraries/common" } +ignition = { path = "../../packages/ignition" } +package-loader = { path = "../../libraries/package-loader" } +gateway-client = { path = "../../libraries/gateway-client" } +ociswap-v1-adapter-v1 = { path = "../../packages/ociswap-v1-adapter-v1", features = [ + "manifest-builder-stubs", +] } +caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", features = [ + "manifest-builder-stubs", +] } + +state-manager = { git = "https://github.com/radixdlt/babylon-node", rev = "63a8267196995fef0830e4fbf0271bea65c90ab1" } + +hex = { version = "0.4.3" } + +[lints] +workspace = true diff --git a/tools/publishing-tool/src/database_overlay.rs b/tools/publishing-tool/src/database_overlay.rs new file mode 100644 index 00000000..ce4e5856 --- /dev/null +++ b/tools/publishing-tool/src/database_overlay.rs @@ -0,0 +1,656 @@ +// TODO: This implementation is copied from radixdlt-scrypto into this repo so +// that we don't have to upgrade the scrypto dependency of Ignition and the node +// to a new one (especially since a bunch of things have been moved). The commit +// hash that this was copied from is `30b0956f7ec394ac981a2580d9c927aedd6ffc25`. +// This is an exact copy with no changes. Once we update to a Scrypto dependency +// that includes this, we should remove this copied code. + +#![allow(dead_code, clippy::mem_replace_with_default)] + +use core::borrow::*; +use core::cmp::*; +use core::iter::*; +use radix_engine_common::prelude::*; +use radix_engine_store_interface::interface::*; + +pub type UnmergeableSubstateDatabaseOverlay<'a, S> = + SubstateDatabaseOverlay<&'a S, S>; +pub type MergeableSubstateDatabaseOverlay<'a, S> = + SubstateDatabaseOverlay<&'a mut S, S>; +pub type OwnedSubstateDatabaseOverlay = SubstateDatabaseOverlay; + +pub struct SubstateDatabaseOverlay { + /// The database overlay. All commits made to the database are written to + /// the overlay. This covers new values and deletions too. + overlay: StagingDatabaseUpdates, + + /// A mutable or immutable reference to the root database that this type + /// overlays. It only needs to be mutable if you wish to commit to the root + /// store. To be useful, `S` should implement at least `Borrow`. + root: S, + + /// The concrete type of the underlying substate database. + substate_database_type: PhantomData, +} + +impl<'a, D> UnmergeableSubstateDatabaseOverlay<'a, D> { + pub fn new_unmergeable(root_database: &'a D) -> Self { + Self::new(root_database) + } +} + +impl<'a, D> MergeableSubstateDatabaseOverlay<'a, D> { + pub fn new_mergeable(root_database: &'a mut D) -> Self { + Self::new(root_database) + } +} + +impl OwnedSubstateDatabaseOverlay { + pub fn new_owned(root_database: D) -> Self { + Self::new(root_database) + } +} + +impl SubstateDatabaseOverlay { + pub fn new(root_database: S) -> Self { + Self { + overlay: Default::default(), + root: root_database, + substate_database_type: PhantomData, + } + } + + pub fn deconstruct(self) -> (S, DatabaseUpdates) { + (self.root, self.overlay.into()) + } + + pub fn database_updates(&self) -> DatabaseUpdates { + self.overlay.clone().into() + } + + pub fn into_database_updates(self) -> DatabaseUpdates { + self.overlay.into() + } +} + +impl, D> SubstateDatabaseOverlay { + fn get_readable_root(&self) -> &D { + self.root.borrow() + } +} + +impl, D> SubstateDatabaseOverlay { + fn get_writable_root(&mut self) -> &mut D { + self.root.borrow_mut() + } +} + +impl, D: CommittableSubstateDatabase> + SubstateDatabaseOverlay +{ + pub fn commit_overlay_into_root_store(&mut self) { + let overlay = core::mem::replace( + &mut self.overlay, + StagingDatabaseUpdates::default(), + ); + self.get_writable_root().commit(&overlay.into()); + } +} + +impl, D: SubstateDatabase> SubstateDatabase + for SubstateDatabaseOverlay +{ + fn get_substate( + &self, + partition_key @ DbPartitionKey { + node_key, + partition_num, + }: &DbPartitionKey, + sort_key: &DbSortKey, + ) -> Option { + let overlay_lookup_result = + match self.overlay.node_updates.get(node_key) { + // This particular node key exists in the overlay and probably has + // some partitions written to the overlay. + Some(StagingNodeDatabaseUpdates { partition_updates }) => { + match partition_updates.get(partition_num) { + // This partition has some data written to the overlay + Some(StagingPartitionDatabaseUpdates::Delta { + substate_updates, + }) => { + match substate_updates.get(sort_key) { + // The substate value is written to the overlay. It + // is a database set so we return the new value. + Some(DatabaseUpdate::Set(substate_value)) => { + OverlayLookupResult::Found(Some( + substate_value, + )) + } + // The substate value is written to the overlay. It + // is a database delete so we return a `Found(None)`. + Some(DatabaseUpdate::Delete) => { + OverlayLookupResult::Found(None) + } + // This particular substate was not written to the + // overlay and should be read from the underlying + // database. + None => OverlayLookupResult::NotFound, + } + } + Some(StagingPartitionDatabaseUpdates::Reset { + new_substate_values, + }) => match new_substate_values.get(sort_key) { + // The substate value is written to the overlay. + Some(substate_value) => { + OverlayLookupResult::Found(Some(substate_value)) + } + // In a partition reset we delete all substates in a + // partition and can also write new substates there. If + // the substate that we're looking for can't be found in + // the new substate values of a partition delete then it + // is one of the deleted substates. Therefore, the + // following will report that it has found the substate + // value in the overlay and that the substate does not + // exist. + None => OverlayLookupResult::Found(None), + }, + // This particular partition for the specified node key does + // not exist in the overlay and should be read from the + // underlying database. + None => OverlayLookupResult::NotFound, + } + } + // This particular node key does not exist in the overlay. The + // substate must be read from the underlying database. + None => OverlayLookupResult::NotFound, + }; + + match overlay_lookup_result { + OverlayLookupResult::Found(substate_value) => { + substate_value.cloned() + } + OverlayLookupResult::NotFound => self + .get_readable_root() + .get_substate(partition_key, sort_key), + } + } + + fn list_entries_from( + &self, + partition_key @ DbPartitionKey { + node_key, + partition_num, + }: &DbPartitionKey, + from_sort_key: Option<&DbSortKey>, + ) -> Box + '_> { + // This function iterates over entries of the specified partition. + // Therefore, we don't need to think about other partitions + // here. We first check if there are any partition updates + // for the specified partition. If there is not, no overlaying is needed + // and we can just return the iterator of the root store. + let from_sort_key = from_sort_key.cloned(); + match self.overlay.node_updates.get(node_key) { + // There is a partition update in the overlay. + Some(StagingNodeDatabaseUpdates { partition_updates }) => { + match partition_updates.get(partition_num) { + // The partition was reset. None of the substates of this + // partition that exist in the root + // store "exist" anymore. We just need an iterator over the + // new substates in the reset action. + Some(StagingPartitionDatabaseUpdates::Reset { + new_substate_values, + }) => { + match from_sort_key { + // A `from_sort_key` is specified. Only return sort + // keys that are larger than or equal to the from + // sort key. We do this through BTreeMap's range + // function instead of doing filtering. We're able + // to do this since a `BTreeMap`'s keys are always + // sorted. + Some(from_sort_key) => Box::new( + new_substate_values.range(from_sort_key..).map( + |(sort_key, substate_value)| { + ( + sort_key.clone(), + substate_value.clone(), + ) + }, + ), + ), + // No `from_sort_key` is specified. Start iterating + // from the beginning. + None => Box::new(new_substate_values.iter().map( + |(sort_key, substate_value)| { + (sort_key.clone(), substate_value.clone()) + }, + )), + } + } + // There are some changes that need to be overlayed. + Some(StagingPartitionDatabaseUpdates::Delta { + substate_updates, + }) => { + let underlying = + self.get_readable_root().list_entries_from( + partition_key, + from_sort_key.as_ref(), + ); + + match from_sort_key { + // A `from_sort_key` is specified. Only return sort + // keys that are larger than or equal to the from + // sort key. We do this through BTreeMap's range + // function instead of doing filtering. We're able + // to do this since a `BTreeMap`'s keys are always + // sorted. + Some(from_sort_key) => { + let overlaying = substate_updates + .range(from_sort_key..) + .map(|(sort_key, database_update)| { + match database_update { + DatabaseUpdate::Set( + substate_value, + ) => ( + sort_key.clone(), + Some(substate_value.clone()), + ), + DatabaseUpdate::Delete => { + (sort_key.clone(), None) + } + } + }); + Box::new(OverlayingIterator::new( + underlying, overlaying, + )) + } + // No `from_sort_key` is specified. Start iterating + // from the beginning. + None => { + let overlaying = substate_updates.iter().map( + |(sort_key, database_update)| { + match database_update { + DatabaseUpdate::Set( + substate_value, + ) => ( + sort_key.clone(), + Some(substate_value.clone()), + ), + DatabaseUpdate::Delete => { + (sort_key.clone(), None) + } + } + }, + ); + Box::new(OverlayingIterator::new( + underlying, overlaying, + )) + } + } + } + // Overlay doesn't contain anything for the provided + // partition number. Return an iterator over the data in the + // root store. + None => self.get_readable_root().list_entries_from( + partition_key, + from_sort_key.as_ref(), + ), + } + } + // Overlay doesn't contain anything for the provided node key. + // Return an iterator over the data in the root store. + None => self + .get_readable_root() + .list_entries_from(partition_key, from_sort_key.as_ref()), + } + } +} + +impl CommittableSubstateDatabase for SubstateDatabaseOverlay { + fn commit(&mut self, database_updates: &DatabaseUpdates) { + merge_database_updates(&mut self.overlay, database_updates.clone()) + } +} + +impl, D: ListableSubstateDatabase> ListableSubstateDatabase + for SubstateDatabaseOverlay +{ + fn list_partition_keys( + &self, + ) -> Box + '_> { + let overlying = self + .overlay + .node_updates + .iter() + .flat_map( + |( + node_key, + StagingNodeDatabaseUpdates { partition_updates }, + )| { + partition_updates.keys().map(|partition_num| { + DbPartitionKey { + node_key: node_key.clone(), + partition_num: *partition_num, + } + }) + }, + ) + .map(|partition_key| (partition_key, Some(()))); + let underlying = self + .get_readable_root() + .list_partition_keys() + .map(|partition_key| (partition_key, ())); + + Box::new( + OverlayingIterator::new(underlying, overlying) + .map(|(value, _)| value), + ) + } +} + +pub enum OverlayLookupResult { + Found(T), + NotFound, +} + +fn merge_database_updates( + this: &mut StagingDatabaseUpdates, + other: DatabaseUpdates, +) { + for ( + other_node_key, + NodeDatabaseUpdates { + partition_updates: other_partition_updates, + }, + ) in other.node_updates.into_iter() + { + // Check if the other node key exists in `this` database updates. + match this.node_updates.get_mut(&other_node_key) { + // The node key exists in `this` database updates. + Some(StagingNodeDatabaseUpdates { + partition_updates: this_partition_updates, + }) => { + for (other_partition_num, other_partition_database_updates) in + other_partition_updates.into_iter() + { + // Check if the partition num exists in `this` database + // updates + match this_partition_updates.get_mut(&other_partition_num) { + // The partition exists in both `this` and `other` and + // now we must combine both the partition database + // updates together + Some(this_partition_database_updates) => { + match ( + this_partition_database_updates, + other_partition_database_updates, + ) { + // This and other are both `Delta`. We insert + // all entries in the other state updates into + // this substate updates. This will also + // override anything in `this` with anything in + // `other`. + ( + StagingPartitionDatabaseUpdates::Delta { + substate_updates: this_substate_updates, + }, + PartitionDatabaseUpdates::Delta { + substate_updates: other_substate_updates, + }, + ) => this_substate_updates.extend(other_substate_updates), + // We need to apply the delta on the reset. + ( + StagingPartitionDatabaseUpdates::Reset { + new_substate_values: this_new_substate_values, + }, + PartitionDatabaseUpdates::Delta { + substate_updates: other_substate_updates, + }, + ) => { + for (other_sort_key, other_database_update) in + other_substate_updates.into_iter() + { + match other_database_update { + DatabaseUpdate::Set(other_substate_value) => { + this_new_substate_values + .insert(other_sort_key, other_substate_value); + } + DatabaseUpdate::Delete => { + this_new_substate_values.remove(&other_sort_key); + } + } + } + } + // Whatever the current state is, if the other + // database update is a partition reset then it + // takes precedence. + ( + this_partition_database_updates, + other_partition_database_updates @ PartitionDatabaseUpdates::Reset { .. }, + ) => { + *this_partition_database_updates = other_partition_database_updates.into(); + } + } + } + // The partition num does not exist in `this` database + // updates. This merge is simple, just insert it. + None => { + this_partition_updates.insert( + other_partition_num, + other_partition_database_updates.into(), + ); + } + } + } + } + // The node key does not exist in `this` database updates. This + // merge is simple, just insert it. + None => { + this.node_updates.insert( + other_node_key, + NodeDatabaseUpdates { + partition_updates: other_partition_updates, + } + .into(), + ); + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Sbor, Default)] +struct StagingDatabaseUpdates { + node_updates: BTreeMap, +} + +impl From for DatabaseUpdates { + fn from(value: StagingDatabaseUpdates) -> Self { + Self { + node_updates: value + .node_updates + .into_iter() + .map(|(key, value)| (key, NodeDatabaseUpdates::from(value))) + .collect(), + } + } +} + +impl From for StagingDatabaseUpdates { + fn from(value: DatabaseUpdates) -> Self { + Self { + node_updates: value + .node_updates + .into_iter() + .map(|(key, value)| { + (key, StagingNodeDatabaseUpdates::from(value)) + }) + .collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Sbor, Default)] +struct StagingNodeDatabaseUpdates { + partition_updates: + BTreeMap, +} + +impl From for NodeDatabaseUpdates { + fn from(value: StagingNodeDatabaseUpdates) -> Self { + Self { + partition_updates: value + .partition_updates + .into_iter() + .map(|(key, value)| { + (key, PartitionDatabaseUpdates::from(value)) + }) + .collect(), + } + } +} + +impl From for StagingNodeDatabaseUpdates { + fn from(value: NodeDatabaseUpdates) -> Self { + Self { + partition_updates: value + .partition_updates + .into_iter() + .map(|(key, value)| { + (key, StagingPartitionDatabaseUpdates::from(value)) + }) + .collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Sbor)] +enum StagingPartitionDatabaseUpdates { + Delta { + substate_updates: BTreeMap, + }, + + Reset { + new_substate_values: BTreeMap, + }, +} + +impl From for PartitionDatabaseUpdates { + fn from(value: StagingPartitionDatabaseUpdates) -> Self { + match value { + StagingPartitionDatabaseUpdates::Delta { substate_updates } => { + Self::Delta { + substate_updates: substate_updates.into_iter().collect(), + } + } + StagingPartitionDatabaseUpdates::Reset { + new_substate_values, + } => Self::Reset { + new_substate_values: new_substate_values.into_iter().collect(), + }, + } + } +} + +impl From for StagingPartitionDatabaseUpdates { + fn from(value: PartitionDatabaseUpdates) -> Self { + match value { + PartitionDatabaseUpdates::Delta { substate_updates } => { + Self::Delta { + substate_updates: substate_updates.into_iter().collect(), + } + } + PartitionDatabaseUpdates::Reset { + new_substate_values, + } => Self::Reset { + new_substate_values: new_substate_values.into_iter().collect(), + }, + } + } +} + +/// An iterator overlaying a "change on a value" (coming from the [`overlaying`] +/// iterator) over a "base value" (coming from the [`underlying`] iterator). +/// The one is matched to another by a `K` part (of the iterated tuple `(K, +/// V)`), which both iterators are assumed to be ordered by. +pub struct OverlayingIterator +where + U: Iterator, + O: Iterator, +{ + underlying: Peekable, + overlaying: Peekable, +} + +impl OverlayingIterator +where + K: Ord, + U: Iterator, + O: Iterator)>, +{ + /// Creates an overlaying iterator. + /// The [`underlying`] iterator provides the "base values". + /// The [`overlaying`] one provides the "changes" to those values, + /// represented as `Option`: + /// - A [`Some`] is an upsert, i.e. it may override an existing base value, + /// or "insert" a completely new one to the iterated results. + /// - A [`None`] is a delete, which causes the base value to be omitted in + /// the iterated results. + #[allow(clippy::new_ret_no_self)] + pub fn new(underlying: U, overlaying: O) -> impl Iterator { + Self { + underlying: underlying.peekable(), + overlaying: overlaying.peekable(), + } + } +} + +impl Iterator for OverlayingIterator +where + K: Ord, + U: Iterator, + O: Iterator)>, +{ + type Item = (K, V); + + fn next(&mut self) -> Option { + loop { + if let Some(overlaying_key) = self.overlaying.peek_key() { + if let Some(underlying_key) = self.underlying.peek_key() { + match underlying_key.cmp(overlaying_key) { + Ordering::Less => { + return self.underlying.next(); // return and move it + // forward + } + Ordering::Equal => { + self.underlying.next(); // only move it forward + } + Ordering::Greater => { + // leave it as-is + } + }; + } + let (overlaying_key, overlaying_change) = + self.overlaying.next().unwrap(); + match overlaying_change { + Some(value) => return Some((overlaying_key, value)), + None => continue, /* we may need to skip over an + * unbounded number of deletes */ + } + } else { + return self.underlying.next(); + } + } + } +} + +pub trait PeekableKeyExt<'a, K> { + /// Peeks at the next entry's key. + fn peek_key(&'a mut self) -> Option<&'a K>; +} + +impl<'a, K, V: 'a, I> PeekableKeyExt<'a, K> for Peekable +where + I: Iterator, +{ + fn peek_key(&'a mut self) -> Option<&'a K> { + self.peek().map(|(key, _value)| key) + } +} diff --git a/tools/publishing-tool/src/executor/gateway_executor.rs b/tools/publishing-tool/src/executor/gateway_executor.rs new file mode 100644 index 00000000..5c35a065 --- /dev/null +++ b/tools/publishing-tool/src/executor/gateway_executor.rs @@ -0,0 +1,295 @@ +use super::*; +use gateway_client::apis::configuration::*; +use gateway_client::apis::status_api::gateway_status; +use gateway_client::apis::status_api::GatewayStatusError; +use gateway_client::apis::transaction_api::*; +use gateway_client::apis::Error as GatewayClientError; +use gateway_client::models::*; +use radix_engine::transaction::*; +use transaction::manifest::*; +use transaction::prelude::*; + +pub struct GatewayExecutor { + /// The configuration to use when making gateway HTTP requests. + pub configuration: Configuration, + /// The network definition of the network that the gateway talks to. + pub network_definition: NetworkDefinition, + /// The configuration to use when polling for the transaction status. + pub polling_configuration: PollingConfiguration, +} + +impl GatewayExecutor { + pub fn new( + base_url: impl ToOwned, + network_definition: NetworkDefinition, + polling_configuration: PollingConfiguration, + ) -> Self { + Self { + configuration: Configuration { + base_path: base_url.to_owned(), + ..Default::default() + }, + network_definition, + polling_configuration, + } + } +} + +impl Executor for GatewayExecutor { + type Error = GatewayExecutorError; + + fn execute_transaction( + &mut self, + notarized_transaction: &NotarizedTransactionV1, + ) -> Result { + let notarized_transaction_payload_bytes = notarized_transaction + .to_payload_bytes() + .map_err(GatewayExecutorError::NotarizedTransactionEncodeError)?; + + transaction_submit( + &self.configuration, + TransactionSubmitRequest { + notarized_transaction: notarized_transaction_payload_bytes, + }, + ) + .map_err(GatewayExecutorError::TransactionSubmissionError)?; + + let intent_hash_string = { + let intent_hash = notarized_transaction + .prepare() + .map_err( + GatewayExecutorError::NotarizedTransactionPrepareError, + )? + .intent_hash(); + let transaction_hash_encoder = + TransactionHashBech32Encoder::new(&self.network_definition); + transaction_hash_encoder.encode(&intent_hash).map_err( + GatewayExecutorError::TransactionHashBech32mEncoderError, + )? + }; + + for _ in 0..self.polling_configuration.retries { + let transaction_status_response = transaction_status( + &self.configuration, + TransactionStatusRequest { + intent_hash: intent_hash_string.clone(), + }, + ) + .map_err(GatewayExecutorError::TransactionStatusError)?; + + match transaction_status_response.intent_status { + // Do nothing and keep on polling. + TransactionIntentStatus::Unknown + | TransactionIntentStatus::CommitPendingOutcomeUnknown + | TransactionIntentStatus::Pending => {} + TransactionIntentStatus::CommittedSuccess => { + let transaction_committed_result_response = transaction_committed_details( + &self.configuration, + TransactionCommittedDetailsRequest { + intent_hash: intent_hash_string.clone(), + opt_ins: Some(Box::new(TransactionDetailsOptIns { + raw_hex: Some(true), + receipt_state_changes: Some(true), + receipt_fee_summary: Some(true), + receipt_fee_source: Some(true), + receipt_fee_destination: Some(true), + receipt_costing_parameters: Some(true), + receipt_events: Some(true), + receipt_output: Some(true), + affected_global_entities: Some(true), + balance_changes: Some(true), + })), + at_ledger_state: None, + }, + ) + .map_err(GatewayExecutorError::TransactionCommittedDetailsError)?; + + let new_entities = { + let mut new_entities = NewEntities::default(); + + let bech32m_address_decoder = + AddressBech32Decoder::new(&self.network_definition); + let new_global_entities = transaction_committed_result_response + .transaction + .receipt + .expect("We have opted into this") + .state_updates + .expect("We have opted into this") + .new_global_entities + .into_iter() + .map(|Entity { entity_address, .. }| { + bech32m_address_decoder + .validate_and_decode(&entity_address) + .map_err(|_| GatewayExecutorError::AddressBech32mDecodeError) + .and_then(|(_, node_id)| { + node_id.try_into().map(NodeId).map_err(|_| { + GatewayExecutorError::AddressBech32mDecodeError + }) + }) + }); + + for node_id in new_global_entities { + let node_id = node_id?; + if let Ok(package_address) = + PackageAddress::try_from(node_id) + { + new_entities + .new_package_addresses + .insert(package_address); + } else if let Ok(resource_address) = + ResourceAddress::try_from(node_id) + { + new_entities + .new_resource_addresses + .insert(resource_address); + } else if let Ok(component_address) = + ComponentAddress::try_from(node_id) + { + new_entities + .new_component_addresses + .insert(component_address); + } + } + + new_entities + }; + + return Ok(ExecutionReceipt::CommitSuccess { + new_entities, + }); + } + TransactionIntentStatus::CommittedFailure => { + return Ok(ExecutionReceipt::CommitFailure { + reason: transaction_status_response + .intent_status_description, + }) + } + TransactionIntentStatus::PermanentlyRejected + | TransactionIntentStatus::LikelyButNotCertainRejection => { + return Ok(ExecutionReceipt::Rejection { + reason: transaction_status_response + .intent_status_description, + }) + } + } + + std::thread::sleep(std::time::Duration::from_secs( + self.polling_configuration.interval_in_seconds, + )) + } + + Err(GatewayExecutorError::Timeout) + } + + fn preview_transaction( + &mut self, + preview_intent: PreviewIntentV1, + ) -> Result { + let string_manifest = decompile( + &preview_intent.intent.instructions.0, + &self.network_definition, + ) + .map_err(GatewayExecutorError::ManifestDecompileError)?; + + let blob_hex = preview_intent + .intent + .blobs + .blobs + .iter() + .map(|blob| hex::encode(&blob.0)) + .collect::>(); + + let request = TransactionPreviewRequest { + manifest: string_manifest, + blobs_hex: Some(blob_hex), + start_epoch_inclusive: preview_intent + .intent + .header + .start_epoch_inclusive + .number() as i64, + end_epoch_exclusive: preview_intent + .intent + .header + .end_epoch_exclusive + .number() as i64, + notary_public_key: Some(Box::new( + native_public_key_to_gateway_public_key( + &preview_intent.intent.header.notary_public_key, + ), + )), + notary_is_signatory: Some( + preview_intent.intent.header.notary_is_signatory, + ), + tip_percentage: preview_intent.intent.header.tip_percentage as i32, + nonce: preview_intent.intent.header.nonce as i64, + signer_public_keys: preview_intent + .signer_public_keys + .iter() + .map(native_public_key_to_gateway_public_key) + .collect(), + flags: Box::new(TransactionPreviewRequestFlags { + assume_all_signature_proofs: preview_intent + .flags + .assume_all_signature_proofs, + use_free_credit: preview_intent.flags.use_free_credit, + skip_epoch_check: preview_intent.flags.skip_epoch_check, + }), + }; + let response = transaction_preview(&self.configuration, request) + .map_err(GatewayExecutorError::TransactionPreviewError)?; + + scrypto_decode::(&response.encoded_receipt) + .map_err(GatewayExecutorError::TransactionReceiptDecodeError) + .map(|receipt| receipt.into_latest()) + } + + fn get_current_epoch(&mut self) -> Result { + Ok(Epoch::of( + gateway_status(&self.configuration) + .map_err(GatewayExecutorError::GatewayStatusError)? + .ledger_state + .epoch as u64, + )) + } +} + +fn native_public_key_to_gateway_public_key( + native_public_key: &radix_engine_common::prelude::PublicKey, +) -> gateway_client::models::PublicKey { + match native_public_key { + radix_engine::types::PublicKey::Secp256k1(public_key) => { + gateway_client::models::PublicKey::EcdsaSecp256k1 { + key: public_key.0, + } + } + radix_engine::types::PublicKey::Ed25519(public_key) => { + gateway_client::models::PublicKey::EddsaEd25519 { + key: public_key.0, + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PollingConfiguration { + pub interval_in_seconds: u64, + pub retries: u64, +} + +#[derive(Debug)] +pub enum GatewayExecutorError { + ManifestDecompileError(DecompileError), + TransactionReceiptDecodeError(DecodeError), + NotarizedTransactionEncodeError(EncodeError), + NotarizedTransactionPrepareError(PrepareError), + TransactionHashBech32mEncoderError(TransactionHashBech32EncodeError), + GatewayStatusError(GatewayClientError), + TransactionStatusError(GatewayClientError), + TransactionPreviewError(GatewayClientError), + TransactionCommittedDetailsError( + GatewayClientError, + ), + TransactionSubmissionError(GatewayClientError), + AddressBech32mDecodeError, + Timeout, +} diff --git a/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs b/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs new file mode 100644 index 00000000..a21f15b6 --- /dev/null +++ b/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs @@ -0,0 +1,99 @@ +use super::*; +use crate::database_overlay::*; +use radix_engine::transaction::*; +use radix_engine::vm::*; +use scrypto_unit::*; +use state_manager::store::*; +use transaction::prelude::*; + +/// An [`Executor`] that simulates the transaction execution on mainnet state. +/// This requires having a mainnet database that the executor can read from. All +/// of the database changes from the transaction execution is written to an +/// overlay which means that the mainnet database's state remains unchanged. +pub struct MainnetSimulatorExecutor<'s>( + TestRunner< + NoExtension, + UnmergeableSubstateDatabaseOverlay<'s, RocksDBStore>, + >, +); + +impl<'s> MainnetSimulatorExecutor<'s> { + pub fn new(database: &'s RocksDBStore) -> Self { + let database = UnmergeableSubstateDatabaseOverlay::new(database); + let test_runner = TestRunnerBuilder::new() + .with_custom_database(database) + .without_trace() + .build(); + Self(test_runner) + } +} + +impl<'s> Executor for MainnetSimulatorExecutor<'s> { + type Error = MainnetSimulatorError; + + fn execute_transaction( + &mut self, + notarized_transaction: &NotarizedTransactionV1, + ) -> Result { + let network_definition = NetworkDefinition::mainnet(); + let raw_transaction = notarized_transaction.to_raw().map_err( + MainnetSimulatorError::NotarizedTransactionRawFormatError, + )?; + + let transaction_receipt = self + .0 + .execute_raw_transaction(&network_definition, &raw_transaction); + + let execution_receipt = match transaction_receipt.result { + TransactionResult::Commit(CommitResult { + outcome: TransactionOutcome::Success(..), + state_update_summary, + .. + }) => ExecutionReceipt::CommitSuccess { + new_entities: NewEntities { + new_component_addresses: state_update_summary + .new_components, + new_resource_addresses: state_update_summary.new_resources, + new_package_addresses: state_update_summary.new_packages, + }, + }, + TransactionResult::Commit(CommitResult { + outcome: TransactionOutcome::Failure(reason), + .. + }) => ExecutionReceipt::CommitFailure { + reason: format!("{:?}", reason), + }, + TransactionResult::Reject(RejectResult { reason }) => { + ExecutionReceipt::Rejection { + reason: format!("{:?}", reason), + } + } + TransactionResult::Abort(AbortResult { reason }) => { + ExecutionReceipt::Abort { + reason: format!("{:?}", reason), + } + } + }; + Ok(execution_receipt) + } + + fn preview_transaction( + &mut self, + preview_intent: PreviewIntentV1, + ) -> Result { + let network_definition = NetworkDefinition::mainnet(); + self.0 + .preview(preview_intent, &network_definition) + .map_err(MainnetSimulatorError::PreviewError) + } + + fn get_current_epoch(&mut self) -> Result { + Ok(self.0.get_current_epoch()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MainnetSimulatorError { + NotarizedTransactionRawFormatError(EncodeError), + PreviewError(PreviewError), +} diff --git a/tools/publishing-tool/src/executor/mod.rs b/tools/publishing-tool/src/executor/mod.rs new file mode 100644 index 00000000..8a6921a6 --- /dev/null +++ b/tools/publishing-tool/src/executor/mod.rs @@ -0,0 +1,7 @@ +mod gateway_executor; +mod mainnet_simulator_executor; +mod traits; + +pub use gateway_executor::*; +pub use mainnet_simulator_executor::*; +pub use traits::*; diff --git a/tools/publishing-tool/src/executor/traits.rs b/tools/publishing-tool/src/executor/traits.rs new file mode 100644 index 00000000..a3c80717 --- /dev/null +++ b/tools/publishing-tool/src/executor/traits.rs @@ -0,0 +1,41 @@ +use radix_engine::transaction::TransactionReceiptV1; +use transaction::prelude::*; + +/// A trait that can be implemented by various structs to execute transactions +/// and produce execution receipts. The executor could be object doing the +/// execution itself in case of a node or could delegate the execution to +/// another object like in the case of the gateway. This detail does not matter +/// for the executor. +pub trait Executor { + type Error: Debug; + + fn execute_transaction( + &mut self, + notarized_transaction: &NotarizedTransactionV1, + ) -> Result; + + fn preview_transaction( + &mut self, + preview_intent: PreviewIntentV1, + ) -> Result; + + fn get_current_epoch(&mut self) -> Result; +} + +/// A simplified transaction receipt containing the key pieces of information +/// that must be included in an execution receipt. This is limited by the data +/// that the node can give us. +#[derive(Clone, Debug, PartialEq, Eq, ScryptoSbor)] +pub enum ExecutionReceipt { + CommitSuccess { new_entities: NewEntities }, + CommitFailure { reason: String }, + Rejection { reason: String }, + Abort { reason: String }, +} + +#[derive(Clone, Default, Debug, PartialEq, Eq, ScryptoSbor)] +pub struct NewEntities { + pub new_component_addresses: IndexSet, + pub new_resource_addresses: IndexSet, + pub new_package_addresses: IndexSet, +} diff --git a/tools/publishing-tool/src/main.rs b/tools/publishing-tool/src/main.rs new file mode 100644 index 00000000..3c495211 --- /dev/null +++ b/tools/publishing-tool/src/main.rs @@ -0,0 +1,6 @@ +mod database_overlay; +mod executor; + +fn main() { + println!("Hello, world!"); +} From 8ea84bc06115d45f22b390fb24bccc6627af0216 Mon Sep 17 00:00:00 2001 From: Omar Date: Sun, 3 Mar 2024 00:44:25 +0300 Subject: [PATCH 15/47] [Publishing Tool]: Create the execution service. --- Cargo.lock | 1 + tools/publishing-tool/Cargo.toml | 1 + .../src/executor/execution_service.rs | 154 ++++++++++++++++++ .../src/executor/gateway_executor.rs | 6 + .../executor/mainnet_simulator_executor.rs | 6 + tools/publishing-tool/src/executor/mod.rs | 2 + tools/publishing-tool/src/executor/traits.rs | 4 + 7 files changed, 174 insertions(+) create mode 100644 tools/publishing-tool/src/executor/execution_service.rs diff --git a/Cargo.lock b/Cargo.lock index b62361ea..e73840ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2028,6 +2028,7 @@ dependencies = [ "radix-engine-common", "radix-engine-interface", "radix-engine-store-interface", + "rand 0.8.5", "sbor", "scrypto-unit", "state-manager", diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml index b0e7fab4..d48427a2 100644 --- a/tools/publishing-tool/Cargo.toml +++ b/tools/publishing-tool/Cargo.toml @@ -27,6 +27,7 @@ caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", f state-manager = { git = "https://github.com/radixdlt/babylon-node", rev = "63a8267196995fef0830e4fbf0271bea65c90ab1" } hex = { version = "0.4.3" } +rand = "0.8.5" [lints] workspace = true diff --git a/tools/publishing-tool/src/executor/execution_service.rs b/tools/publishing-tool/src/executor/execution_service.rs new file mode 100644 index 00000000..862853a0 --- /dev/null +++ b/tools/publishing-tool/src/executor/execution_service.rs @@ -0,0 +1,154 @@ +use radix_engine::transaction::*; +use radix_engine_common::prelude::*; +use radix_engine_interface::blueprints::account::*; +use transaction::model::*; +use transaction::prelude::*; + +use super::*; + +/// A simple execution service whose main responsibilities is to construct, +/// submit, and return the result of transactions. +pub struct ExecutionService<'e, E: Executor> { + /// The executor that the service will use to execute transactions. + executor: &'e mut E, + /// The account to use for the payment of fees. + fee_payer_account_address: ComponentAddress, + /// The notary of the transaction + notary_private_key: &'e PrivateKey, + /// The set of private keys that should sign the transaction. + signers_private_keys: &'e [PrivateKey], +} + +impl<'e, E: Executor> ExecutionService<'e, E> { + pub fn new( + executor: &'e mut E, + fee_payer_account_address: ComponentAddress, + notary_private_key: &'e PrivateKey, + additional_signatures: &'e [PrivateKey], + ) -> Self { + Self { + executor, + fee_payer_account_address, + notary_private_key, + signers_private_keys: additional_signatures, + } + } + + pub fn execute_manifest( + &mut self, + mut manifest: TransactionManifestV1, + ) -> Result> { + // The signers for the transaction + let notary_is_signatory = + self.signers_private_keys.iter().any(|private_key| { + private_key.public_key() == self.notary_private_key.public_key() + }); + let signer_private_keys = + self.signers_private_keys.iter().filter(|private_key| { + private_key.public_key() != self.notary_private_key.public_key() + }); + + // Getting the current network definition + let network_definition = self + .executor + .get_network_definition() + .map_err(ExecutionServiceError::ExecutorError)?; + + // Constructing the header + let current_epoch = self + .executor + .get_current_epoch() + .map_err(ExecutionServiceError::ExecutorError)?; + let header = TransactionHeaderV1 { + network_id: network_definition.id, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch + .after(10) + .expect("Not currently an issue"), + nonce: rand::random(), + notary_public_key: self.notary_private_key.public_key(), + notary_is_signatory, + tip_percentage: 0, + }; + + // Getting a preview of the transaction to determine the fees. + let preview_receipt = self + .executor + .preview_transaction(PreviewIntentV1 { + intent: IntentV1 { + header: header.clone(), + instructions: InstructionsV1(manifest.instructions.clone()), + blobs: BlobsV1 { + blobs: manifest + .blobs + .clone() + .into_values() + .map(BlobV1) + .collect(), + }, + message: MessageV1::None, + }, + signer_public_keys: signer_private_keys + .clone() + .map(|private_key| private_key.public_key()) + .collect(), + flags: PreviewFlags { + use_free_credit: false, + assume_all_signature_proofs: false, + skip_epoch_check: false, + }, + }) + .map_err(ExecutionServiceError::ExecutorError)?; + + if !preview_receipt.is_commit_success() { + return Err( + ExecutionServiceError::TransactionPreviewWasNotSuccessful( + manifest.clone(), + preview_receipt, + ), + ); + } + let total_fees = preview_receipt.fee_summary.total_cost(); + let total_fees_plus_padding = total_fees * dec!(1.20); + + // Adding a lock fee instruction to the manifest. + manifest.instructions.insert( + 0, + InstructionV1::CallMethod { + address: self.fee_payer_account_address.into(), + method_name: ACCOUNT_LOCK_FEE_IDENT.to_string(), + args: to_manifest_value(&AccountLockFeeInput { + amount: total_fees_plus_padding, + }) + .expect("Can't fail!"), + }, + ); + + // Constructing the transaction. + let mut transaction_builder = + TransactionBuilder::new().header(header).manifest(manifest); + for signer_private_key in signer_private_keys { + transaction_builder = transaction_builder.sign(signer_private_key) + } + let transaction = transaction_builder + .notarize(self.notary_private_key) + .build(); + + // Submitting the transaction + let receipt = self + .executor + .execute_transaction(&transaction) + .map_err(ExecutionServiceError::ExecutorError)?; + + Ok(receipt) + } +} + +#[derive(Debug)] +pub enum ExecutionServiceError { + ExecutorError(::Error), + TransactionPreviewWasNotSuccessful( + TransactionManifestV1, + TransactionReceipt, + ), +} diff --git a/tools/publishing-tool/src/executor/gateway_executor.rs b/tools/publishing-tool/src/executor/gateway_executor.rs index 5c35a065..f3555b26 100644 --- a/tools/publishing-tool/src/executor/gateway_executor.rs +++ b/tools/publishing-tool/src/executor/gateway_executor.rs @@ -251,6 +251,12 @@ impl Executor for GatewayExecutor { .epoch as u64, )) } + + fn get_network_definition( + &mut self, + ) -> Result { + Ok(self.network_definition.clone()) + } } fn native_public_key_to_gateway_public_key( diff --git a/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs b/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs index a21f15b6..337771d5 100644 --- a/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs +++ b/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs @@ -90,6 +90,12 @@ impl<'s> Executor for MainnetSimulatorExecutor<'s> { fn get_current_epoch(&mut self) -> Result { Ok(self.0.get_current_epoch()) } + + fn get_network_definition( + &mut self, + ) -> Result { + Ok(NetworkDefinition::mainnet()) + } } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/tools/publishing-tool/src/executor/mod.rs b/tools/publishing-tool/src/executor/mod.rs index 8a6921a6..0f330d14 100644 --- a/tools/publishing-tool/src/executor/mod.rs +++ b/tools/publishing-tool/src/executor/mod.rs @@ -1,7 +1,9 @@ +mod execution_service; mod gateway_executor; mod mainnet_simulator_executor; mod traits; +pub use execution_service::*; pub use gateway_executor::*; pub use mainnet_simulator_executor::*; pub use traits::*; diff --git a/tools/publishing-tool/src/executor/traits.rs b/tools/publishing-tool/src/executor/traits.rs index a3c80717..6a674fa3 100644 --- a/tools/publishing-tool/src/executor/traits.rs +++ b/tools/publishing-tool/src/executor/traits.rs @@ -20,6 +20,10 @@ pub trait Executor { ) -> Result; fn get_current_epoch(&mut self) -> Result; + + fn get_network_definition( + &mut self, + ) -> Result; } /// A simplified transaction receipt containing the key pieces of information From a36666d3cc4fccce35ade09675814761d5455a89 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 00:47:48 +0300 Subject: [PATCH 16/47] [Publishing Tool]: Add a publishing tool --- Cargo.lock | 143 +- Cargo.toml | 57 +- ..._collection_item_vault_aggregated_vault.rs | 15 +- .../state_entity_details_response_item.rs | 3 +- ...entity_details_response_package_details.rs | 11 +- ..._fungible_resource_vaults_page_response.rs | 14 +- libraries/scrypto-interface/src/handlers.rs | 39 +- .../src/blueprint_interface.rs | 23 + packages/ignition/src/blueprint.rs | 10 +- tests/src/environment.rs | 3 +- tests/tests/caviarnine_v1.rs | 15 +- tests/tests/caviarnine_v1_simulation.rs | 6 +- tests/tests/defiplaza_v2.rs | 9 +- tests/tests/ociswap_v1.rs | 9 +- tests/tests/ociswap_v2.rs | 15 +- tools/bootstrap/Cargo.toml | 34 - tools/bootstrap/src/cli.rs | 20 - tools/bootstrap/src/error.rs | 65 - tools/bootstrap/src/main.rs | 16 - tools/bootstrap/src/mainnet_testing.rs | 666 -------- tools/bootstrap/src/stokenet_production.rs | 662 -------- tools/bootstrap/src/transaction_service.rs | 386 ----- tools/bootstrap/src/types/mod.rs | 5 - .../src/types/name_indexed_dex_information.rs | 30 - .../name_indexed_resource_information.rs | 45 - tools/publishing-tool/Cargo.toml | 17 +- .../default_configurations/mainnet_testing.rs | 304 ++++ .../src/cli/default_configurations/mod.rs | 33 + tools/publishing-tool/src/cli/mod.rs | 18 + tools/publishing-tool/src/cli/publish.rs | 70 + tools/publishing-tool/src/error.rs | 20 + .../executor/mainnet_simulator_executor.rs | 105 -- tools/publishing-tool/src/executor/mod.rs | 9 - .../src/macros.rs | 12 +- tools/publishing-tool/src/main.rs | 23 +- .../execution_service.rs | 91 +- .../gateway_connector.rs} | 69 +- .../mainnet_simulator_connector.rs | 140 ++ .../src/network_connection_provider/mod.rs | 9 + .../traits.rs | 26 +- .../src/publishing/configuration.rs | 254 +++ tools/publishing-tool/src/publishing/error.rs | 4 + .../publishing-tool/src/publishing/handler.rs | 1412 +++++++++++++++++ .../publishing-tool/src/publishing/macros.rs | 134 ++ tools/publishing-tool/src/publishing/mod.rs | 9 + tools/publishing-tool/src/utils.rs | 39 + 46 files changed, 2879 insertions(+), 2220 deletions(-) delete mode 100644 tools/bootstrap/Cargo.toml delete mode 100644 tools/bootstrap/src/cli.rs delete mode 100644 tools/bootstrap/src/error.rs delete mode 100644 tools/bootstrap/src/main.rs delete mode 100644 tools/bootstrap/src/mainnet_testing.rs delete mode 100644 tools/bootstrap/src/stokenet_production.rs delete mode 100644 tools/bootstrap/src/transaction_service.rs delete mode 100644 tools/bootstrap/src/types/mod.rs delete mode 100644 tools/bootstrap/src/types/name_indexed_dex_information.rs delete mode 100644 tools/bootstrap/src/types/name_indexed_resource_information.rs create mode 100644 tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs create mode 100644 tools/publishing-tool/src/cli/default_configurations/mod.rs create mode 100644 tools/publishing-tool/src/cli/mod.rs create mode 100644 tools/publishing-tool/src/cli/publish.rs create mode 100644 tools/publishing-tool/src/error.rs delete mode 100644 tools/publishing-tool/src/executor/mainnet_simulator_executor.rs delete mode 100644 tools/publishing-tool/src/executor/mod.rs rename tools/{bootstrap => publishing-tool}/src/macros.rs (87%) rename tools/publishing-tool/src/{executor => network_connection_provider}/execution_service.rs (62%) rename tools/publishing-tool/src/{executor/gateway_executor.rs => network_connection_provider/gateway_connector.rs} (82%) create mode 100644 tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs create mode 100644 tools/publishing-tool/src/network_connection_provider/mod.rs rename tools/publishing-tool/src/{executor => network_connection_provider}/traits.rs (57%) create mode 100644 tools/publishing-tool/src/publishing/configuration.rs create mode 100644 tools/publishing-tool/src/publishing/error.rs create mode 100644 tools/publishing-tool/src/publishing/handler.rs create mode 100644 tools/publishing-tool/src/publishing/macros.rs create mode 100644 tools/publishing-tool/src/publishing/mod.rs create mode 100644 tools/publishing-tool/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index e73840ed..617daa46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,27 +236,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "bootstrap" -version = "0.1.0" -dependencies = [ - "caviarnine-v1-adapter-v1", - "clap", - "common", - "gateway-client", - "hex", - "ignition", - "ociswap-v1-adapter-v1", - "package-loader", - "radix-engine", - "radix-engine-interface", - "rand 0.8.5", - "sbor", - "serde", - "serde_json", - "transaction", -] - [[package]] name = "bumpalo" version = "3.15.3" @@ -782,6 +761,29 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1081,6 +1083,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "http" version = "0.2.11" @@ -1289,6 +1297,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1447,6 +1464,22 @@ dependencies = [ "libc", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" + [[package]] name = "memchr" version = "2.7.1" @@ -1520,7 +1553,7 @@ dependencies = [ [[package]] name = "native-sdk" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "radix-engine-common", "radix-engine-derive", @@ -2018,11 +2051,19 @@ name = "publishing-tool" version = "0.1.0" dependencies = [ "caviarnine-v1-adapter-v1", + "clap", "common", + "defiplaza-v2-adapter-v1", + "env_logger", "gateway-client", "hex", + "hex-literal", "ignition", + "itertools 0.12.1", + "log", + "macro_rules_attribute", "ociswap-v1-adapter-v1", + "ociswap-v2-adapter-v1", "package-loader", "radix-engine", "radix-engine-common", @@ -2030,7 +2071,9 @@ dependencies = [ "radix-engine-store-interface", "rand 0.8.5", "sbor", + "sbor-json", "scrypto-unit", + "serde_json", "state-manager", "transaction", ] @@ -2058,7 +2101,7 @@ dependencies = [ [[package]] name = "radix-engine" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bitflags 1.3.2", "colored", @@ -2088,7 +2131,7 @@ dependencies = [ [[package]] name = "radix-engine-common" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bech32", "blake2", @@ -2113,7 +2156,7 @@ dependencies = [ [[package]] name = "radix-engine-derive" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "proc-macro2", "quote", @@ -2124,7 +2167,7 @@ dependencies = [ [[package]] name = "radix-engine-interface" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bitflags 1.3.2", "const-sha1", @@ -2146,7 +2189,7 @@ dependencies = [ [[package]] name = "radix-engine-macros" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "paste", "proc-macro2", @@ -2158,7 +2201,7 @@ dependencies = [ [[package]] name = "radix-engine-profiling" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "fixedstr", ] @@ -2166,7 +2209,7 @@ dependencies = [ [[package]] name = "radix-engine-queries" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "hex", "itertools 0.10.5", @@ -2182,7 +2225,7 @@ dependencies = [ [[package]] name = "radix-engine-store-interface" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "hex", "itertools 0.10.5", @@ -2196,7 +2239,7 @@ dependencies = [ [[package]] name = "radix-engine-stores" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "hex", "itertools 0.10.5", @@ -2369,7 +2412,7 @@ dependencies = [ [[package]] name = "resources-tracker-macro" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "proc-macro2", "quote", @@ -2454,7 +2497,7 @@ dependencies = [ [[package]] name = "sbor" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "const-sha1", "hex", @@ -2468,7 +2511,7 @@ dependencies = [ [[package]] name = "sbor-derive" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "proc-macro2", "sbor-derive-common", @@ -2477,7 +2520,7 @@ dependencies = [ [[package]] name = "sbor-derive-common" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "const-sha1", "itertools 0.10.5", @@ -2486,6 +2529,21 @@ dependencies = [ "syn 1.0.93", ] +[[package]] +name = "sbor-json" +version = "1.0.10" +source = "git+https://github.com/radixdlt/radix-engine-toolkit?rev=1cfe879c7370cfa497857ada7a8973f8a3388abc#1cfe879c7370cfa497857ada7a8973f8a3388abc" +dependencies = [ + "bech32", + "radix-engine-common", + "radix-engine-interface", + "regex", + "sbor", + "serde", + "serde_json", + "serde_with", +] + [[package]] name = "schannel" version = "0.1.23" @@ -2519,7 +2577,7 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "scrypto" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bech32", "const-sha1", @@ -2541,7 +2599,7 @@ dependencies = [ [[package]] name = "scrypto-derive" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "proc-macro2", "quote", @@ -2578,7 +2636,7 @@ dependencies = [ [[package]] name = "scrypto-schema" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bitflags 1.3.2", "radix-engine-common", @@ -2589,7 +2647,7 @@ dependencies = [ [[package]] name = "scrypto-test" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "native-sdk", "ouroboros", @@ -2607,7 +2665,7 @@ dependencies = [ [[package]] name = "scrypto-unit" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "radix-engine", "radix-engine-interface", @@ -2699,6 +2757,7 @@ version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ + "indexmap 2.2.5", "itoa", "ryu", "serde", @@ -3364,7 +3423,7 @@ dependencies = [ [[package]] name = "transaction" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "bech32", "hex", @@ -3474,7 +3533,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utils" version = "1.1.0-dev" -source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=feff02d54b6b7b348b6a9cc465971004a41a9b2f#feff02d54b6b7b348b6a9cc465971004a41a9b2f" +source = "git+https://www.github.com/radixdlt/radixdlt-scrypto.git?rev=4887c5e4be2603433592ed290b70b1a0c03cced3#4887c5e4be2603433592ed290b70b1a0c03cced3" dependencies = [ "indexmap 2.0.0-pre", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8a52db6b..1b59d8db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "libraries/ports-interface", "libraries/scrypto-math", # Tools - "tools/bootstrap", "tools/publishing-tool", # Tests "tests" @@ -28,21 +27,21 @@ edition = "2021" description = "The implementation of project Ignition in Scrypto for the Radix Ledger" [workspace.dependencies] -sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -utils = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -native-sdk = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-stores = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-derive = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +utils = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +native-sdk = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-stores = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-derive = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-queries = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-store-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } -scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } +scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } [profile.release] opt-level = 'z' @@ -56,17 +55,17 @@ overflow-checks = true arithmetic_side_effects = "warn" [patch.'https://github.com/radixdlt/radixdlt-scrypto'] -sbor = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -utils = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -native-sdk = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -transaction = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-common = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-stores = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-derive = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-queries = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -radix-engine-store-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto-unit = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } -scrypto-test = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "feff02d54b6b7b348b6a9cc465971004a41a9b2f" } \ No newline at end of file +sbor = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +utils = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +scrypto = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +native-sdk = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +transaction = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-common = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-stores = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-derive = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-queries = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +radix-engine-store-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +scrypto-unit = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +scrypto-test = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } \ No newline at end of file diff --git a/libraries/gateway-client/src/models/non_fungible_resources_collection_item_vault_aggregated_vault.rs b/libraries/gateway-client/src/models/non_fungible_resources_collection_item_vault_aggregated_vault.rs index 913b84f1..eb72fb67 100644 --- a/libraries/gateway-client/src/models/non_fungible_resources_collection_item_vault_aggregated_vault.rs +++ b/libraries/gateway-client/src/models/non_fungible_resources_collection_item_vault_aggregated_vault.rs @@ -1,10 +1,19 @@ #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct NonFungibleResourcesCollectionItemVaultAggregatedVault { - - #[serde(rename = "total_count", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "total_count", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] pub total_count: Option>, - #[serde(rename = "next_cursor", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "next_cursor", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option>, #[serde(rename = "items")] pub items: Vec, diff --git a/libraries/gateway-client/src/models/state_entity_details_response_item.rs b/libraries/gateway-client/src/models/state_entity_details_response_item.rs index c74fb398..ab048f5b 100644 --- a/libraries/gateway-client/src/models/state_entity_details_response_item.rs +++ b/libraries/gateway-client/src/models/state_entity_details_response_item.rs @@ -29,8 +29,7 @@ pub struct StateEntityDetailsResponseItem { )] pub explicit_metadata: Option>, #[serde(rename = "details", skip_serializing_if = "Option::is_none")] - pub details: - Option>, + pub details: Option, } impl StateEntityDetailsResponseItem { diff --git a/libraries/gateway-client/src/models/state_entity_details_response_package_details.rs b/libraries/gateway-client/src/models/state_entity_details_response_package_details.rs index 28de3148..1d629e9a 100644 --- a/libraries/gateway-client/src/models/state_entity_details_response_package_details.rs +++ b/libraries/gateway-client/src/models/state_entity_details_response_package_details.rs @@ -11,12 +11,17 @@ pub struct StateEntityDetailsResponsePackageDetails { #[serde(rename = "code_hex")] pub code_hex: String, - #[serde(rename = "royalty_vault_balance", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "royalty_vault_balance", + skip_serializing_if = "Option::is_none" + )] pub royalty_vault_balance: Option, #[serde(rename = "blueprints", skip_serializing_if = "Option::is_none")] - pub blueprints: Option>, + pub blueprints: + Option>, #[serde(rename = "schemas", skip_serializing_if = "Option::is_none")] - pub schemas: Option>, + pub schemas: + Option>, } impl StateEntityDetailsResponsePackageDetails { diff --git a/libraries/gateway-client/src/models/state_entity_non_fungible_resource_vaults_page_response.rs b/libraries/gateway-client/src/models/state_entity_non_fungible_resource_vaults_page_response.rs index 5587a241..529b86cf 100644 --- a/libraries/gateway-client/src/models/state_entity_non_fungible_resource_vaults_page_response.rs +++ b/libraries/gateway-client/src/models/state_entity_non_fungible_resource_vaults_page_response.rs @@ -3,10 +3,20 @@ pub struct StateEntityNonFungibleResourceVaultsPageResponse { #[serde(rename = "ledger_state")] pub ledger_state: Box, - #[serde(rename = "total_count", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "total_count", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] pub total_count: Option>, - #[serde(rename = "next_cursor", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "next_cursor", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option>, #[serde(rename = "items")] pub items: Vec, diff --git a/libraries/scrypto-interface/src/handlers.rs b/libraries/scrypto-interface/src/handlers.rs index 8b3455ce..d79da5e2 100644 --- a/libraries/scrypto-interface/src/handlers.rs +++ b/libraries/scrypto-interface/src/handlers.rs @@ -149,8 +149,8 @@ fn generate_scrypto_stub( let mut arguments = arguments.clone(); if arguments.is_function() { arguments.add_argument_to_end( - Ident::new("blueprint_package_address", ident.span()), - parse_quote!(::radix_engine_interface::prelude::PackageAddress) + Ident::new("blueprint_package_address", ident.span()), + parse_quote!(::radix_engine_interface::prelude::PackageAddress), ); } @@ -296,14 +296,12 @@ fn generate_scrypto_test_stub( let mut arguments = arguments.clone(); if arguments.is_function() { arguments.add_argument_to_end( - Ident::new("blueprint_package_address", ident.span()), - parse_quote!(::radix_engine_interface::prelude::PackageAddress) + Ident::new("blueprint_package_address", ident.span()), + parse_quote!(::radix_engine_interface::prelude::PackageAddress), ); } - arguments.add_argument_to_end( - Ident::new("env", ident.span()), - parse_quote!(&mut Y) - ); + arguments + .add_argument_to_end(Ident::new("env", ident.span()), parse_quote!(&mut Y)); let inner = if arguments.is_function() { quote! { @@ -421,29 +419,24 @@ fn generate_manifest_builder_stub( if arguments.is_function() { arguments.add_argument_to_beginning( Ident::new("blueprint_package_address", ident.span()), - parse_quote!( - ::radix_engine_interface::prelude::PackageAddress - ), + parse_quote!(::radix_engine_interface::prelude::PackageAddress), ); } else { arguments.add_argument_to_beginning( Ident::new("component_address", ident.span()), - parse_quote!( - impl ::transaction::builder::ResolvableGlobalAddress - ), + parse_quote!(impl ::transaction::builder::ResolvableGlobalAddress), ); } - let fn_ident = format_ident!( - "{}_{}", - struct_ident.to_string().to_snake_case(), - ident - ); + let fn_ident = + format_ident!("{}_{}", struct_ident.to_string().to_snake_case(), ident); - arguments.manifest_arguments().map(|arguments| quote! { - #(#attrs)* - #[allow(clippy::too_many_arguments)] - #token_fn #fn_ident ( self, #arguments ) -> Self #semi_colon + arguments.manifest_arguments().map(|arguments| { + quote! { + #(#attrs)* + #[allow(clippy::too_many_arguments)] + #token_fn #fn_ident ( self, #arguments ) -> Self #semi_colon + } }) }, ) diff --git a/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs index 37993a83..1e53a5fb 100644 --- a/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs +++ b/packages/defiplaza-v2-adapter-v1/src/blueprint_interface.rs @@ -165,3 +165,26 @@ impl From for ShortageState { } } } + +#[derive( + ScryptoSbor, + ManifestSbor, + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] +pub struct PlazaPair { + pub config: PairConfig, + pub state: PairState, + pub base_address: ResourceAddress, + pub quote_address: ResourceAddress, + pub base_divisibility: u8, + pub quote_divisibility: u8, + pub base_pool: ComponentAddress, + pub quote_pool: ComponentAddress, +} diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 6f1cb30f..47845e4a 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -579,7 +579,7 @@ mod ignition { value .checked_round(17, RoundingMode::ToPositiveInfinity) }) - .expect(OVERFLOW_ERROR); + .unwrap_or(Decimal::MAX); assert!( pool_reported_value_of_user_resource_in_protocol_resource <= maximum_amount, @@ -1483,10 +1483,12 @@ mod ignition { } } else { drop(entry); - self.pool_units - .insert(global_id, indexmap! { + self.pool_units.insert( + global_id, + indexmap! { pool_units_resource_address => Vault::with_bucket(pool_units) - }) + }, + ) } } diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 066c8be1..9b77ab26 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -458,8 +458,7 @@ impl ScryptoTestEnv { let resource_y = ResourceManager(resource_y).mint_fungible(dec!(100_000_000), &mut env)?; - let (_, change1) = - defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; + let (_, change1) = defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; let (_, change2) = defiplaza_pool.add_liquidity(resource_y, None, &mut env)?; let change_amount1 = change1 .map(|bucket| bucket.amount(&mut env).unwrap()) diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 5d9f2a4c..6a8ca582 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -248,7 +248,9 @@ fn can_close_a_liquidity_position_in_caviarnine_that_fits_into_fee_limits() { ModuleId::Main, ConsensusManagerField::ProposerMilliTimestamp.field_index(), ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( - ProposerMilliTimestampSubstate { epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000 } + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, ), ) .unwrap(); @@ -260,8 +262,9 @@ fn can_close_a_liquidity_position_in_caviarnine_that_fits_into_fee_limits() { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60).unwrap(), - } + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), + }, ), ) .unwrap(); @@ -1097,10 +1100,8 @@ fn test_effect_of_price_action_on_fees(multiplier: i32, bin_span: u32) { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from( - maturity_instant.seconds_since_unix_epoch / 60, - ) - .unwrap(), + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), }, ), ) diff --git a/tests/tests/caviarnine_v1_simulation.rs b/tests/tests/caviarnine_v1_simulation.rs index d9bae7e7..394585d0 100644 --- a/tests/tests/caviarnine_v1_simulation.rs +++ b/tests/tests/caviarnine_v1_simulation.rs @@ -1042,10 +1042,8 @@ fn test_effect_of_price_action_on_fees( ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from( - maturity_instant.seconds_since_unix_epoch / 60, - ) - .unwrap(), + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), }, ), ) diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 78711179..323bc054 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -200,7 +200,9 @@ fn can_close_a_liquidity_position_in_defiplaza_that_fits_into_fee_limits() { ModuleId::Main, ConsensusManagerField::ProposerMilliTimestamp.field_index(), ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( - ProposerMilliTimestampSubstate { epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000 } + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, ), ) .unwrap(); @@ -212,8 +214,9 @@ fn can_close_a_liquidity_position_in_defiplaza_that_fits_into_fee_limits() { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60).unwrap(), - } + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), + }, ), ) .unwrap(); diff --git a/tests/tests/ociswap_v1.rs b/tests/tests/ociswap_v1.rs index f03f9c45..cf2bc2f4 100644 --- a/tests/tests/ociswap_v1.rs +++ b/tests/tests/ociswap_v1.rs @@ -210,7 +210,9 @@ fn can_close_a_liquidity_position_in_ociswap_that_fits_into_fee_limits() { ModuleId::Main, ConsensusManagerField::ProposerMilliTimestamp.field_index(), ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( - ProposerMilliTimestampSubstate { epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000 } + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, ), ) .unwrap(); @@ -222,8 +224,9 @@ fn can_close_a_liquidity_position_in_ociswap_that_fits_into_fee_limits() { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60).unwrap(), - } + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), + }, ), ) .unwrap(); diff --git a/tests/tests/ociswap_v2.rs b/tests/tests/ociswap_v2.rs index 13aae66b..44224f4d 100644 --- a/tests/tests/ociswap_v2.rs +++ b/tests/tests/ociswap_v2.rs @@ -209,7 +209,9 @@ fn can_close_a_liquidity_position_in_ociswap_that_fits_into_fee_limits() { ModuleId::Main, ConsensusManagerField::ProposerMilliTimestamp.field_index(), ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( - ProposerMilliTimestampSubstate { epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000 } + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, ), ) .unwrap(); @@ -221,8 +223,9 @@ fn can_close_a_liquidity_position_in_ociswap_that_fits_into_fee_limits() { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60).unwrap(), - } + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), + }, ), ) .unwrap(); @@ -801,10 +804,8 @@ fn test_effect_of_price_action_on_fees(multiplier: i32) { ConsensusManagerField::ProposerMinuteTimestamp.field_index(), ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( ProposerMinuteTimestampSubstate { - epoch_minute: i32::try_from( - maturity_instant.seconds_since_unix_epoch / 60, - ) - .unwrap(), + epoch_minute: i32::try_from(maturity_instant.seconds_since_unix_epoch / 60) + .unwrap(), }, ), ) diff --git a/tools/bootstrap/Cargo.toml b/tools/bootstrap/Cargo.toml deleted file mode 100644 index 084cc679..00000000 --- a/tools/bootstrap/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "bootstrap" -description = "A tool used to bootstrap project Ignition on various networks." -version.workspace = true -edition.workspace = true - -[dependencies] -sbor = { workspace = true } -transaction = { workspace = true } -radix-engine = { workspace = true } -radix-engine-interface = { workspace = true } - -common = { path = "../../libraries/common" } -ignition = { path = "../../packages/ignition" } -gateway-client = { path = "../../libraries/gateway-client" } -package-loader = { path = "../../libraries/package-loader", features = [ - "build-time-blueprints", -] } -ociswap-v1-adapter-v1 = { path = "../../packages/ociswap-v1-adapter-v1", features = [ - "manifest-builder-stubs", -] } -caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", features = [ - "manifest-builder-stubs", -] } - -serde = { version = "1.0.196", features = ["derive"] } -serde_json = { version = "1.0.112" } - -hex = { version = "0.4.3", features = ["serde"] } -clap = { version = "4.4.18", features = ["derive"] } -rand = { version = "0.8.5" } - -[lints] -workspace = true \ No newline at end of file diff --git a/tools/bootstrap/src/cli.rs b/tools/bootstrap/src/cli.rs deleted file mode 100644 index 839ab556..00000000 --- a/tools/bootstrap/src/cli.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::error::*; -use crate::mainnet_testing; -use crate::stokenet_production; - -use clap::Parser; - -#[derive(Parser, Debug)] -pub enum Cli { - StokenetProduction(stokenet_production::StokenetProduction), - MainnetTesting(mainnet_testing::MainnetTesting), -} - -impl Cli { - pub fn run(self, out: &mut O) -> Result<(), Error> { - match self { - Self::StokenetProduction(cmd) => cmd.run(out), - Self::MainnetTesting(cmd) => cmd.run(out), - } - } -} diff --git a/tools/bootstrap/src/error.rs b/tools/bootstrap/src/error.rs deleted file mode 100644 index 4a80a1d1..00000000 --- a/tools/bootstrap/src/error.rs +++ /dev/null @@ -1,65 +0,0 @@ -use radix_engine::transaction::*; -use transaction::manifest::*; - -type TransactionPreviewError = gateway_client::apis::Error< - gateway_client::apis::transaction_api::TransactionPreviewError, ->; -type TransactionCommittedDetailsError = gateway_client::apis::Error< - gateway_client::apis::transaction_api::TransactionCommittedDetailsError, ->; -type TransactionSubmitError = gateway_client::apis::Error< - gateway_client::apis::transaction_api::TransactionSubmitError, ->; -type GatewayStatusError = gateway_client::apis::Error< - gateway_client::apis::status_api::GatewayStatusError, ->; - -#[derive(Debug)] -pub enum Error { - ManifestDecompilation(DecompileError), - TransactionPreviewError(TransactionPreviewError), - TransactionSubmitError(TransactionSubmitError), - TransactionCommittedDetailsError(TransactionCommittedDetailsError), - GatewayStatusError(GatewayStatusError), - PreviewFailed { - manifest: String, - receipt: TransactionReceiptV1, - }, - TransactionPollingYieldedNothing { - intent_hash: String, - }, - TransactionWasNotSuccessful { - intent_hash: String, - }, - FailedToLoadPrivateKey, -} - -impl From for Error { - fn from(value: DecompileError) -> Self { - Self::ManifestDecompilation(value) - } -} - -impl From for Error { - fn from(value: TransactionPreviewError) -> Self { - Self::TransactionPreviewError(value) - } -} - -impl From for Error { - fn from(value: TransactionSubmitError) -> Self { - Self::TransactionSubmitError(value) - } -} - -impl From for Error { - fn from(value: TransactionCommittedDetailsError) -> Self { - Self::TransactionCommittedDetailsError(value) - } -} - -impl From for Error { - fn from(value: GatewayStatusError) -> Self { - Self::GatewayStatusError(value) - } -} diff --git a/tools/bootstrap/src/main.rs b/tools/bootstrap/src/main.rs deleted file mode 100644 index f4da8342..00000000 --- a/tools/bootstrap/src/main.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![allow(dead_code, clippy::enum_variant_names)] - -#[macro_use] -mod macros; -mod cli; -mod error; -mod mainnet_testing; -mod stokenet_production; -mod transaction_service; -mod types; - -fn main() -> Result<(), error::Error> { - let mut out = std::io::stdout(); - let cli = ::parse(); - cli.run(&mut out) -} diff --git a/tools/bootstrap/src/mainnet_testing.rs b/tools/bootstrap/src/mainnet_testing.rs deleted file mode 100644 index 50d50445..00000000 --- a/tools/bootstrap/src/mainnet_testing.rs +++ /dev/null @@ -1,666 +0,0 @@ -use crate::error::*; -use crate::transaction_service::*; -use crate::types::*; -use crate::*; -use clap::Parser; -use common::prelude::*; -use ignition::{InitializationParametersManifest, PoolBlueprintInformation}; -use package_loader::PackageLoader; -use radix_engine_interface::api::node_modules::auth::*; -use radix_engine_interface::api::node_modules::*; -use radix_engine_interface::blueprints::account::*; -use radix_engine_interface::prelude::*; -use transaction::prelude::*; - -const PRIVATE_KEY_ENVIRONMENT_VARIABLE: &str = "PRIVATE_KEY"; - -#[derive(Parser, Debug)] -pub struct MainnetTesting {} - -impl MainnetTesting { - pub fn run(self, _: &mut O) -> Result<(), Error> { - // Loading the private key that will notarize and pay the fees of the - // transaction. - let notary_private_key = { - std::env::var(PRIVATE_KEY_ENVIRONMENT_VARIABLE) - .map_err(|_| Error::FailedToLoadPrivateKey) - .and_then(|hex| { - hex::decode(hex).map_err(|_| Error::FailedToLoadPrivateKey) - }) - .and_then(|bytes| { - Ed25519PrivateKey::from_bytes(&bytes) - .map_err(|_| Error::FailedToLoadPrivateKey) - }) - .map(PrivateKey::Ed25519) - }?; - let notary_account = ComponentAddress::virtual_account_from_public_key( - ¬ary_private_key.public_key(), - ); - let fee_handling = FeeHandling::EstimateAndLock { - fee_payer_account: notary_account, - fee_payer_private_key: ¬ary_private_key, - }; - - // Initializing all of the data that this command will use. These are - // pretty much constants but we can't make them constants because most - // of the functions are not `const`. There is also not really a point - // in making them a lazy static, let's keep things simple. - - /* cSpell:disable - Sorry for this, I dislike it too. */ - const GATEWAY_API_BASE_URL: &str = "https://mainnet.radixdlt.com/"; - let network_definition = NetworkDefinition::mainnet(); - let bech32m_coders = - Bech32mCoders::from_network_definition(&network_definition); - - // TODO: What do we want these values to be? - const MAXIMUM_ALLOWED_PRICE_STALENESS_IN_SECONDS: i64 = 60; // 60 seconds - const MAXIMUM_ALLOWED_PRICE_DIFFERENCE_PERCENTAGE: Decimal = - Decimal::MAX; // TODO: No oracle is deployed on mainnet for testing yet. - - let protocol_resource = resource_address!("resource_rdx1t4dekrf58h0r28s3c93z92w3jt5ngx87jzd63mgc597zmf3534rxfv"); - let resources = NameIndexedResourceInformation { - bitcoin: resource_address!("resource_rdx1t58dla7ykxzxe5es89wlhgzatqla0gceukg0eeduzvtj4cxd55etn8"), - ethereum: resource_address!("resource_rdx1tkscrlztcyn82ej5z3n232f0qqp0qur69arjf279ppmg5usa3xhnsm"), - usdc: resource_address!("resource_rdx1th7nx2hy0cf6aea6mz7zhkdmy4p45s488xutltnp7296zxj8hwchpf"), - usdt: resource_address!("resource_rdx1tkafx32lu72mcxr85gjx0rh3rx9q89zqffg4phmv5rxdqg5fnd0w7s"), - }; - let exchanges = NameIndexedDexInformation { - caviarnine_v1: DexInformation { - package: package_address!("package_rdx1p4r9rkp0cq67wmlve544zgy0l45mswn6h798qdqm47x4762h383wa3"), - pools: NameIndexedResourceInformation { - bitcoin: component_address!("component_rdx1crzl2c39m83lpe6fv62epgp3phqunxhc264ys23qz8xeemjcu8lln3"), - ethereum: component_address!("component_rdx1cqk2ufmdq6pkcu7ed7r6u9hmdsht9gyd8y8wwtd7w5znefz9k54a7d"), - usdc: component_address!("component_rdx1cq9q8umlpmngff6y4e534htz0n37te4m7vsj50u9zc58ys65zl6jv9"), - usdt: component_address!("component_rdx1cpl0v3lndt9d7g7uuepztxs9m7m24ly0yfhvcum2y7tm0vlzst0l5y") - } - }, - // TODO: Ths following is INCORRECT INFORMATION! There is no Ociswap - // package on mainnet. - ociswap_v1: DexInformation { - package: package_address!("package_rdx1p5l6dp3slnh9ycd7gk700czwlck9tujn0zpdnd0efw09n2zdnn0lzx"), - pools: NameIndexedResourceInformation { - bitcoin: component_address!("component_rdx1cr5uxxjq4a0r3gfn6yd62lk96fqca34tnmyqdxkwefhckcjea4t3am"), - ethereum: component_address!("component_rdx1cqylpcl8p45l2h5ew0qrkwyz23dky3e6ucs7kkhrtm90k9z3kzeztn"), - usdc: component_address!("component_rdx1cq96chge0q6kkk962heg0mgfl82gjw7x25dp9jv80gkx90mc3hk2ua"), - usdt: component_address!("component_rdx1cz3fa8qtfgfwjt3fzrtm544a89p5laerww7590g2tfcradqwdv3laq") - } - }, - }; - // TODO: Numbers here are not real and I have added from just to get - // things going. MUST modify before launch. - let reward_information = indexmap! { - LockupPeriod::from_minutes(0).unwrap() => dec!(0.125), // 12.5% - LockupPeriod::from_minutes(1).unwrap() => dec!(0.15), // 15.0% - }; - - // TODO: MUST determine what those accounts are prior to launch! - // For now, for the TEST deployments these are accounts that I CONTROL! - let protocol_manager_account = component_address!("account_rdx12xvk6x3usuzu7hdc5clc7lpu8e4czze6xa7vrw7vlek0h84j9299na"); - let protocol_owner_account = component_address!("account_rdx12xvk6x3usuzu7hdc5clc7lpu8e4czze6xa7vrw7vlek0h84j9299na"); - - /* cSpell:enable */ - - // An ephemeral private key that we will use the bootstrapping process. - // This key will initially control the dApp definition to allow us to - // easily update the metadata and will later on change the owner role - // of the dApp definition to the protocol owner. - - let ephemeral_private_key = - Ed25519PrivateKey::from_u64(rand::random()).unwrap(); - println!( - "Ephemeral Private Key: {:?}", - ephemeral_private_key.to_bytes() - ); - - let ephemeral_private_key = PrivateKey::Ed25519( - Ed25519PrivateKey::from_u64(rand::random()).unwrap(), - ); - let ephemeral_virtual_signature_badge = - NonFungibleGlobalId::from_public_key( - &ephemeral_private_key.public_key(), - ); - - // This is the transaction service that the submission will happen - // through. It does most of the heavy lifting associated with the - // transaction submission. - let transaction_service = - TransactionService::new(&bech32m_coders, GATEWAY_API_BASE_URL); - - // Creating the dApp definition account. When this account starts it - // its owner will be a virtual signature badge which will change once - // add all of the metadata fields that we want to add. The the manifest - // that involves the dApp definition will set the metadata on it and - // will also change its owner to be the protocol Owner badge. - let dapp_definition_account = { - let manifest = ManifestBuilder::new() - .call_function( - ACCOUNT_PACKAGE, - ACCOUNT_BLUEPRINT, - ACCOUNT_CREATE_ADVANCED_IDENT, - AccountCreateAdvancedManifestInput { - owner_role: OwnerRole::Updatable(rule!(require( - ephemeral_virtual_signature_badge - ))), - address_reservation: None, - }, - ) - .build(); - std::thread::sleep(std::time::Duration::from_secs(5)); - *transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_component_addresses - .first() - .expect("Must succeed!") - }; - - // Creating the protocol owner and the protocol manager badges and - // sending them off to the accounts specified up above. - let (protocol_manager_badge, protocol_owner_badge) = { - let manifest = ManifestBuilder::new() - // The protocol manager badge - .create_fungible_resource( - OwnerRole::None, - true, - 0, - Default::default(), - // TODO: What do we want those to be? Any preference? - metadata! { - init { - "name" => "Ignition Protocol Manager", updatable; - "symbol" => "IGNPM", updatable; - "description" => "A badge that gives the authority to manage the Ignition protocol.", updatable; - "badge" => vec!["badge"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - } - }, - Some(dec!(1)), - ) - .try_deposit_entire_worktop_or_abort(protocol_manager_account, None) - // The protocol owner badge - .create_fungible_resource( - OwnerRole::None, - true, - 0, - Default::default(), - metadata! { - init { - "name" => "Ignition Protocol Owner", updatable; - "symbol" => "IGNPO", updatable; - "description" => "A badge that of the owner of the ignition protocol.", updatable; - "badge" => vec!["badge"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - } - }, - Some(dec!(1)), - ) - .try_deposit_entire_worktop_or_abort(protocol_owner_account, None) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let resource_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_resource_addresses; - ( - *resource_addresses.first().unwrap(), - *resource_addresses.get(1).unwrap(), - ) - }; - - let protocol_manager_rule = rule!(require(protocol_manager_badge)); - let protocol_owner_rule = rule!(require(protocol_owner_badge)); - let owner_role = OwnerRole::Fixed(protocol_owner_rule.clone()); - - // Publishing the packages. - let ( - ignition_package_address, - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) = { - let (ignition_code, ignition_package_definition) = - PackageLoader::get("ignition"); - let (simple_oracle_code, simple_oracle_package_definition) = - PackageLoader::get("simple-oracle"); - let ( - ociswap_v1_adapter_v1_code, - ociswap_v1_adapter_v1_package_definition, - ) = PackageLoader::get("ociswap-v1-adapter-v1"); - let ( - caviarnine_v1_adapter_v1_code, - caviarnine_v1_adapter_v1_package_definition, - ) = PackageLoader::get("caviarnine-v1-adapter-v1"); - - // We can publish the simple oracle, ociswap adapter v1, and - // caviarnine adapter v1 all in a single transaction since they - // are below the size limit. - let manifest = ManifestBuilder::new() - .publish_package_advanced( - None, - simple_oracle_code, - simple_oracle_package_definition, - metadata_init! { - "name" => "Simple Oracle Package", updatable; - "description" => "The implementation of the Oracle used by the Ignition protocol.", updatable; - "tags" => vec!["oracle"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .publish_package_advanced( - None, - ociswap_v1_adapter_v1_code, - ociswap_v1_adapter_v1_package_definition, - metadata_init! { - "name" => "Ociswap Adapter v1 Package", updatable; - "description" => "The implementation of an adapter for Ociswap for the Ignition protocol.", updatable; - "tags" => vec!["adapter"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .publish_package_advanced( - None, - caviarnine_v1_adapter_v1_code, - caviarnine_v1_adapter_v1_package_definition, - metadata_init! { - "name" => "Caviarnine Adapter v1 Package", updatable; - "description" => "The implementation of an adapter for Caviarnine for the Ignition protocol.", updatable; - "tags" => vec!["adapter"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ).build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let package_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_package_addresses; - - let ( - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) = ( - *package_addresses.first().unwrap(), - *package_addresses.get(1).unwrap(), - *package_addresses.get(2).unwrap(), - ); - - // Publishing the Ignition package - let manifest = ManifestBuilder::new() - .publish_package_advanced( - None, - ignition_code, - ignition_package_definition, - metadata_init! { - "name" => "Ignition Package", updatable; - "description" => "The implementation of the Ignition protocol.", updatable; - "tags" => Vec::::new(), updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .build(); - std::thread::sleep(std::time::Duration::from_secs(5)); - let ignition_package_address = *transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_package_addresses - .first() - .unwrap(); - - ( - ignition_package_address, - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) - }; - - // Creating the different liquidity receipt resources that the different - // exchanges will use. They will be mintable and burnable through the - // Ignition package caller badge. - let ignition_package_global_caller_rule = - rule!(require(package_of_direct_caller(ignition_package_address))); - let ( - ociswap_v1_liquidity_receipt_resource, - caviarnine_v1_liquidity_receipt_resource, - ) = { - let roles = NonFungibleResourceRoles { - // Mintable and burnable by the Ignition package and - // the protocol owner can update who can do that. - mint_roles: mint_roles! { - minter => ignition_package_global_caller_rule.clone(); - minter_updater => protocol_owner_rule.clone(); - }, - burn_roles: burn_roles! { - burner => ignition_package_global_caller_rule.clone(); - burner_updater => protocol_owner_rule.clone(); - }, - // We reserve the right to change the data of the - // liquidity receipts when we want. - non_fungible_data_update_roles: non_fungible_data_update_roles! { - non_fungible_data_updater => rule!(deny_all); - non_fungible_data_updater_updater => protocol_owner_rule.clone(); - }, - // Everything else is deny all and can't be changed. - recall_roles: recall_roles! { - recaller => rule!(deny_all); - recaller_updater => rule!(deny_all); - }, - freeze_roles: freeze_roles! { - freezer => rule!(deny_all); - freezer_updater => rule!(deny_all); - }, - deposit_roles: deposit_roles! { - depositor => rule!(allow_all); - depositor_updater => rule!(deny_all); - }, - withdraw_roles: withdraw_roles! { - withdrawer => rule!(allow_all); - withdrawer_updater => rule!(deny_all); - }, - }; - - let manifest = ManifestBuilder::new() - // Ociswap liquidity receipt - .call_function( - RESOURCE_PACKAGE, - NON_FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT, - NON_FUNGIBLE_RESOURCE_MANAGER_CREATE_RUID_WITH_INITIAL_SUPPLY_IDENT, - NonFungibleResourceManagerCreateRuidWithInitialSupplyManifestInput { - owner_role: owner_role.clone(), - track_total_supply: true, - non_fungible_schema: NonFungibleDataSchema::new_local_without_self_package_replacement::>(), - entries: Vec::new(), - resource_roles: roles.clone(), - metadata: metadata! { - roles { - metadata_setter => protocol_owner_rule.clone(); - metadata_setter_updater => protocol_owner_rule.clone(); - metadata_locker => protocol_owner_rule.clone(); - metadata_locker_updater => protocol_owner_rule.clone(); - }, - init { - // TODO: Confirm with the exchanges what they - // want their name to be. - "name" => "Ignition LP: Ociswap", updatable; - "description" => "Represents a particular contribution of liquidity to Ociswap through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; - "tags" => vec!["lp token"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - // TODO: Must get this from our design team - "icon_url" => UncheckedUrl::of("https://www.google.com"), updatable; - "DEX" => "Ociswap", updatable; - // TODO: Must get this from Ociswap! - "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; - } - }, - address_reservation: None - } - ) - // Caviarnine liquidity receipt - .call_function( - RESOURCE_PACKAGE, - NON_FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT, - NON_FUNGIBLE_RESOURCE_MANAGER_CREATE_RUID_WITH_INITIAL_SUPPLY_IDENT, - NonFungibleResourceManagerCreateRuidWithInitialSupplyManifestInput { - owner_role: owner_role.clone(), - track_total_supply: true, - non_fungible_schema: NonFungibleDataSchema::new_local_without_self_package_replacement::>(), - entries: Vec::new(), - resource_roles: roles.clone(), - metadata: metadata! { - roles { - metadata_setter => protocol_owner_rule.clone(); - metadata_setter_updater => protocol_owner_rule.clone(); - metadata_locker => protocol_owner_rule.clone(); - metadata_locker_updater => protocol_owner_rule.clone(); - }, - init { - // TODO: Confirm with the exchanges what they want - // their name to be. - "name" => "Ignition LP: Caviarnine", updatable; - "description" => "Represents a particular contribution of liquidity to Caviarnine through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; - "tags" => vec!["lp token"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - // TODO: Must get this from our design team - "icon_url" => UncheckedUrl::of("https://www.google.com"), updatable; - "DEX" => "Caviarnine", updatable; - // TODO: Must get this from Caviarnine! - "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; - } - }, - address_reservation: None - } - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let resource_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_resource_addresses; - ( - *resource_addresses.first().unwrap(), - *resource_addresses.get(1).unwrap(), - ) - }; - - // Creating the oracle and adapters. - let ( - ignition_component, - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) = { - let manifest = ManifestBuilder::new() - // Creating the oracle component - .call_function( - simple_oracle_package_address, - "SimpleOracle", - "instantiate", - ( - protocol_manager_rule.clone(), - metadata_init! { - "name" => "Ignition Oracle", updatable; - "description" => "The oracle used by the Ignition protocol.", updatable; - "tags" => vec!["oracle"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - // Creating the ociswap adapter v1 component - .call_function( - ociswap_v1_adapter_v1_package_address, - "OciswapV1Adapter", - "instantiate", - ( - metadata_init! { - "name" => "Ignition Ociswap Adapter", updatable; - "description" => "The adapter used by the Ignition protocol to communicate with Ociswap pools.", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - // Creating the ociswap adapter v1 component - .call_function( - caviarnine_v1_adapter_v1_package_address, - "CaviarnineV1Adapter", - "instantiate", - ( - metadata_init! { - "name" => "Ignition Caviarnine Adapter", updatable; - "description" => "The adapter used by the Ignition protocol to communicate with Caviarnine pools.", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let component_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_component_addresses; - - let ( - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) = ( - *component_addresses.first().unwrap(), - *component_addresses.get(1).unwrap(), - *component_addresses.get(2).unwrap(), - ); - - // Instantiating the Ignition component - let manifest = ManifestBuilder::new() - // Instantiate Ignition. - .call_function( - ignition_package_address, - "Ignition", - "instantiate", - manifest_args!( - metadata_init! { - "name" => "Ignition", updatable; - "description" => "The Ignition protocol component", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - protocol_owner_rule.clone(), - protocol_manager_rule.clone(), - protocol_resource, - simple_oracle_component, - MAXIMUM_ALLOWED_PRICE_STALENESS_IN_SECONDS, - MAXIMUM_ALLOWED_PRICE_DIFFERENCE_PERCENTAGE, - InitializationParametersManifest { - initial_pool_information: Some(indexmap! { - BlueprintId { - package_address: exchanges.caviarnine_v1.package, - blueprint_name: "QuantaSwap".to_owned() - } => PoolBlueprintInformation { - adapter: caviarnine_v1_adapter_v1_component, - allowed_pools: exchanges.caviarnine_v1.pools.into_iter().collect(), - liquidity_receipt: caviarnine_v1_liquidity_receipt_resource - }, - BlueprintId { - package_address: exchanges.ociswap_v1.package, - blueprint_name: "BasicPool".to_owned() - } => PoolBlueprintInformation { - adapter: ociswap_v1_adapter_v1_component, - allowed_pools: exchanges.ociswap_v1.pools.into_iter().collect(), - liquidity_receipt: ociswap_v1_liquidity_receipt_resource - } - }), - initial_user_resource_volatility: Some( - indexmap! { - resources.bitcoin => Volatility::Volatile, - resources.ethereum => Volatility::Volatile, - resources.usdc => Volatility::NonVolatile, - resources.usdt => Volatility::NonVolatile, - } - ), - initial_reward_rates: Some(reward_information), - initial_volatile_protocol_resources: None, - initial_non_volatile_protocol_resources: None, - initial_is_open_position_enabled: Some(true), - initial_is_close_position_enabled: Some(true), - }, - None:: - ) - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let component_addresses = transaction_service - .submit_manifest( - manifest, - &ephemeral_private_key, - &fee_handling, - )? - .new_component_addresses; - - let ignition_component_address = - *component_addresses.first().unwrap(); - - ( - ignition_component_address, - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) - }; - - // Updating the dapp definition account with the metadata that it - // should have. - { - let manifest = ManifestBuilder::new() - .set_metadata( - dapp_definition_account, - "account_type", - "dapp definition", - ) - .set_metadata( - dapp_definition_account, - "claimed_websites", - Vec::::new(), - ) - .set_metadata( - dapp_definition_account, - "dapp_definitions", - Vec::::new(), - ) - .set_metadata( - dapp_definition_account, - "claimed_entities", - vec![ - GlobalAddress::from(protocol_manager_badge), - GlobalAddress::from(protocol_owner_badge), - GlobalAddress::from(ignition_package_address), - GlobalAddress::from(simple_oracle_package_address), - GlobalAddress::from( - ociswap_v1_adapter_v1_package_address, - ), - GlobalAddress::from( - caviarnine_v1_adapter_v1_package_address, - ), - GlobalAddress::from( - ociswap_v1_liquidity_receipt_resource, - ), - GlobalAddress::from( - caviarnine_v1_liquidity_receipt_resource, - ), - GlobalAddress::from(ignition_component), - GlobalAddress::from(simple_oracle_component), - GlobalAddress::from(ociswap_v1_adapter_v1_component), - GlobalAddress::from(caviarnine_v1_adapter_v1_component), - ], - ) - .call_role_assignment_method( - dapp_definition_account, - ROLE_ASSIGNMENT_SET_OWNER_IDENT, - RoleAssignmentSetOwnerInput { - rule: protocol_owner_rule, - }, - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - transaction_service.submit_manifest( - manifest, - &ephemeral_private_key, - &fee_handling, - )?; - } - - Ok(()) - } -} - -pub struct DexInformation { - pub pools: NameIndexedResourceInformation, - pub package: PackageAddress, -} diff --git a/tools/bootstrap/src/stokenet_production.rs b/tools/bootstrap/src/stokenet_production.rs deleted file mode 100644 index 03765762..00000000 --- a/tools/bootstrap/src/stokenet_production.rs +++ /dev/null @@ -1,662 +0,0 @@ -use crate::error::*; -use crate::transaction_service::*; -use crate::types::*; -use crate::*; -use clap::Parser; -use common::prelude::*; -use ignition::{InitializationParametersManifest, PoolBlueprintInformation}; -use package_loader::PackageLoader; -use radix_engine_interface::api::node_modules::auth::*; -use radix_engine_interface::api::node_modules::*; -use radix_engine_interface::blueprints::account::*; -use radix_engine_interface::prelude::*; -use transaction::prelude::*; - -const PRIVATE_KEY_ENVIRONMENT_VARIABLE: &str = "PRIVATE_KEY"; - -#[derive(Parser, Debug)] -pub struct StokenetProduction {} - -impl StokenetProduction { - pub fn run(self, _: &mut O) -> Result<(), Error> { - // Loading the private key that will notarize and pay the fees of the - // transaction. - let notary_private_key = { - std::env::var(PRIVATE_KEY_ENVIRONMENT_VARIABLE) - .map_err(|_| Error::FailedToLoadPrivateKey) - .and_then(|hex| { - hex::decode(hex).map_err(|_| Error::FailedToLoadPrivateKey) - }) - .and_then(|bytes| { - Ed25519PrivateKey::from_bytes(&bytes) - .map_err(|_| Error::FailedToLoadPrivateKey) - }) - .map(PrivateKey::Ed25519) - }?; - let notary_account = ComponentAddress::virtual_account_from_public_key( - ¬ary_private_key.public_key(), - ); - let fee_handling = FeeHandling::EstimateAndLock { - fee_payer_account: notary_account, - fee_payer_private_key: ¬ary_private_key, - }; - - // Initializing all of the data that this command will use. These are - // pretty much constants but we can't make them constants because most - // of the functions are not `const`. There is also not really a point - // in making them a lazy static, let's keep things simple. - - /* cSpell:disable - Sorry for this, I dislike it too. */ - const GATEWAY_API_BASE_URL: &str = "https://stokenet.radixdlt.com/"; - let network_definition = NetworkDefinition::stokenet(); - let bech32m_coders = - Bech32mCoders::from_network_definition(&network_definition); - - // TODO: What do we want these values to be? - const MAXIMUM_ALLOWED_PRICE_STALENESS_IN_SECONDS: i64 = 60; // 60 seconds - const MAXIMUM_ALLOWED_PRICE_DIFFERENCE_PERCENTAGE: Decimal = dec!(0.05); // 5 % - - let protocol_resource = resource_address!("resource_tdx_2_1thwmtk9qet08y3wpujd8nddmvjuqyptg5nt0mw0zcdgcrahu5k36qx"); - let resources = NameIndexedResourceInformation { - bitcoin: resource_address!("resource_tdx_2_1thltk578jr4v7axqpu5ceznhlha6ca2qtzcflqdmytgtf37xncu7l9"), - ethereum: resource_address!("resource_tdx_2_1t59gx963vzd6u6fz63h5de2zh9nmgwxc8y832edmr6pxvz98wg6zu3"), - usdc: resource_address!("resource_tdx_2_1thfv477eqwlh8x4wt6xsc62myt4z0zxmdpr4ea74fa8jnxh243y60r"), - usdt: resource_address!("resource_tdx_2_1t4p3ytx933n576pdps4ua7jkjh36zrh36a543u0tfcsu2vthavlqg8"), - }; - let exchanges = NameIndexedDexInformation { - caviarnine_v1: DexInformation { - package: package_address!("package_tdx_2_1p57g523zj736u370z6g4ynrytn7t6r2hledvzkhl6tzpg3urn0707e"), - pools: NameIndexedResourceInformation { - bitcoin: component_address!("component_tdx_2_1czt59vxdqg7q4l0gzphmt5ev6lagl2cu6sm2hsaz9y8ypcf0aukf8r"), - ethereum: component_address!("component_tdx_2_1crqpgnpf3smh7kg8d4sz4h3502l65s4tslwhg46ru07ra6l30pcsj4"), - usdc: component_address!("component_tdx_2_1cpwkf9uhel3ut4ydm58g0uyaw7sxckmp2pz7sdv79vzt9y3p7ad4fu"), - usdt: component_address!("component_tdx_2_1czmdhtq0u8f40khky4c6j74msskuz60yq3y0zewu85phrdj0ryz2hl") - } - }, - // TODO: Ths following is INCORRECT INFORMATION! There is no Ociswap - // package on Stokenet. - ociswap_v1: DexInformation { - package: package_address!("package_tdx_2_1p40dekel26tp2a2srma4sc3lj2ukr6y8k4amr7x8yav86lyyeg7ta7"), - pools: NameIndexedResourceInformation { - bitcoin: component_address!("component_tdx_2_1czt59vxdqg7q4l0gzphmt5ev6lagl2cu6sm2hsaz9y8ypcf0aukf8r"), - ethereum: component_address!("component_tdx_2_1crqpgnpf3smh7kg8d4sz4h3502l65s4tslwhg46ru07ra6l30pcsj4"), - usdc: component_address!("component_tdx_2_1cpwkf9uhel3ut4ydm58g0uyaw7sxckmp2pz7sdv79vzt9y3p7ad4fu"), - usdt: component_address!("component_tdx_2_1czmdhtq0u8f40khky4c6j74msskuz60yq3y0zewu85phrdj0ryz2hl") - } - }, - }; - // TODO: Numbers here are not real and I have added from just to get - // things going. MUST modify before launch. - let reward_information = indexmap! { - LockupPeriod::from_months(9).unwrap() => dec!(0.125), // 12.5% - LockupPeriod::from_months(10).unwrap() => dec!(0.15), // 15.0% - LockupPeriod::from_months(11).unwrap() => dec!(0.175), // 17.5% - LockupPeriod::from_months(12).unwrap() => dec!(0.20), // 20.0% - }; - - // TODO: MUST determine what those accounts are prior to launch! - // For now they are MY stokenet accounts! - let protocol_manager_account = component_address!("account_tdx_2_12xxuglkrdgcphpqk34fv59ewq3gu5uwlzs42hpy0grsrefvgwgxrev"); - let protocol_owner_account = component_address!("account_tdx_2_12xxuglkrdgcphpqk34fv59ewq3gu5uwlzs42hpy0grsrefvgwgxrev"); - - /* cSpell:enable */ - - // An ephemeral private key that we will use the bootstrapping process. - // This key will initially control the dApp definition to allow us to - // easily update the metadata and will later on change the owner role - // of the dApp definition to the protocol owner. - let ephemeral_private_key = PrivateKey::Ed25519( - Ed25519PrivateKey::from_u64(rand::random()).unwrap(), - ); - let ephemeral_virtual_signature_badge = - NonFungibleGlobalId::from_public_key( - &ephemeral_private_key.public_key(), - ); - - // This is the transaction service that the submission will happen - // through. It does most of the heavy lifting associated with the - // transaction submission. - let transaction_service = - TransactionService::new(&bech32m_coders, GATEWAY_API_BASE_URL); - - // Creating the dApp definition account. When this account starts it - // its owner will be a virtual signature badge which will change once - // add all of the metadata fields that we want to add. The the manifest - // that involves the dApp definition will set the metadata on it and - // will also change its owner to be the protocol Owner badge. - let dapp_definition_account = { - let manifest = ManifestBuilder::new() - .call_function( - ACCOUNT_PACKAGE, - ACCOUNT_BLUEPRINT, - ACCOUNT_CREATE_ADVANCED_IDENT, - AccountCreateAdvancedManifestInput { - owner_role: OwnerRole::Updatable(rule!(require( - ephemeral_virtual_signature_badge - ))), - address_reservation: None, - }, - ) - .build(); - std::thread::sleep(std::time::Duration::from_secs(5)); - *transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_component_addresses - .first() - .expect("Must succeed!") - }; - - // Creating the protocol owner and the protocol manager badges and - // sending them off to the accounts specified up above. - let (protocol_manager_badge, protocol_owner_badge) = { - let manifest = ManifestBuilder::new() - // The protocol manager badge - .create_fungible_resource( - OwnerRole::None, - true, - 0, - Default::default(), - // TODO: What do we want those to be? Any preference? - metadata! { - init { - "name" => "Ignition Protocol Manager", updatable; - "symbol" => "IGNPM", updatable; - "description" => "A badge that gives the authority to manage the Ignition protocol.", updatable; - "badge" => vec!["badge"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - } - }, - Some(dec!(1)), - ) - .try_deposit_entire_worktop_or_abort(protocol_manager_account, None) - // The protocol owner badge - .create_fungible_resource( - OwnerRole::None, - true, - 0, - Default::default(), - metadata! { - init { - "name" => "Ignition Protocol Owner", updatable; - "symbol" => "IGNPO", updatable; - "description" => "A badge that of the owner of the ignition protocol.", updatable; - "badge" => vec!["badge"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - } - }, - Some(dec!(1)), - ) - .try_deposit_entire_worktop_or_abort(protocol_owner_account, None) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let resource_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_resource_addresses; - ( - *resource_addresses.first().unwrap(), - *resource_addresses.get(1).unwrap(), - ) - }; - - let protocol_manager_rule = rule!(require(protocol_manager_badge)); - let protocol_owner_rule = rule!(require(protocol_owner_badge)); - let owner_role = OwnerRole::Fixed(protocol_owner_rule.clone()); - - // Publishing the packages. - let ( - ignition_package_address, - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) = { - let (ignition_code, ignition_package_definition) = - PackageLoader::get("ignition"); - let (simple_oracle_code, simple_oracle_package_definition) = - PackageLoader::get("simple-oracle"); - let ( - ociswap_v1_adapter_v1_code, - ociswap_v1_adapter_v1_package_definition, - ) = PackageLoader::get("ociswap-v1-adapter-v1"); - let ( - caviarnine_v1_adapter_v1_code, - caviarnine_v1_adapter_v1_package_definition, - ) = PackageLoader::get("caviarnine-v1-adapter-v1"); - - // We can publish the simple oracle, ociswap adapter v1, and - // caviarnine adapter v1 all in a single transaction since they - // are below the size limit. - let manifest = ManifestBuilder::new() - .publish_package_advanced( - None, - simple_oracle_code, - simple_oracle_package_definition, - metadata_init! { - "name" => "Simple Oracle Package", updatable; - "description" => "The implementation of the Oracle used by the Ignition protocol.", updatable; - "tags" => vec!["oracle"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .publish_package_advanced( - None, - ociswap_v1_adapter_v1_code, - ociswap_v1_adapter_v1_package_definition, - metadata_init! { - "name" => "Ociswap Adapter v1 Package", updatable; - "description" => "The implementation of an adapter for Ociswap for the Ignition protocol.", updatable; - "tags" => vec!["adapter"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .publish_package_advanced( - None, - caviarnine_v1_adapter_v1_code, - caviarnine_v1_adapter_v1_package_definition, - metadata_init! { - "name" => "Caviarnine Adapter v1 Package", updatable; - "description" => "The implementation of an adapter for Caviarnine for the Ignition protocol.", updatable; - "tags" => vec!["adapter"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ).build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let package_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_package_addresses; - - let ( - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) = ( - *package_addresses.first().unwrap(), - *package_addresses.get(1).unwrap(), - *package_addresses.get(2).unwrap(), - ); - - // Publishing the Ignition package - let manifest = ManifestBuilder::new() - .publish_package_advanced( - None, - ignition_code, - ignition_package_definition, - metadata_init! { - "name" => "Ignition Package", updatable; - "description" => "The implementation of the Ignition protocol.", updatable; - "tags" => Vec::::new(), updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - ) - .build(); - std::thread::sleep(std::time::Duration::from_secs(5)); - let ignition_package_address = *transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_package_addresses - .first() - .unwrap(); - - ( - ignition_package_address, - simple_oracle_package_address, - ociswap_v1_adapter_v1_package_address, - caviarnine_v1_adapter_v1_package_address, - ) - }; - - // Creating the different liquidity receipt resources that the different - // exchanges will use. They will be mintable and burnable through the - // Ignition package caller badge. - let ignition_package_global_caller_rule = - rule!(require(package_of_direct_caller(ignition_package_address))); - let ( - ociswap_v1_liquidity_receipt_resource, - caviarnine_v1_liquidity_receipt_resource, - ) = { - let roles = NonFungibleResourceRoles { - // Mintable and burnable by the Ignition package and - // the protocol owner can update who can do that. - mint_roles: mint_roles! { - minter => ignition_package_global_caller_rule.clone(); - minter_updater => protocol_owner_rule.clone(); - }, - burn_roles: burn_roles! { - burner => ignition_package_global_caller_rule.clone(); - burner_updater => protocol_owner_rule.clone(); - }, - // We reserve the right to change the data of the - // liquidity receipts when we want. - non_fungible_data_update_roles: non_fungible_data_update_roles! { - non_fungible_data_updater => rule!(deny_all); - non_fungible_data_updater_updater => protocol_owner_rule.clone(); - }, - // Everything else is deny all and can't be changed. - recall_roles: recall_roles! { - recaller => rule!(deny_all); - recaller_updater => rule!(deny_all); - }, - freeze_roles: freeze_roles! { - freezer => rule!(deny_all); - freezer_updater => rule!(deny_all); - }, - deposit_roles: deposit_roles! { - depositor => rule!(allow_all); - depositor_updater => rule!(deny_all); - }, - withdraw_roles: withdraw_roles! { - withdrawer => rule!(allow_all); - withdrawer_updater => rule!(deny_all); - }, - }; - - let manifest = ManifestBuilder::new() - // Ociswap liquidity receipt - .call_function( - RESOURCE_PACKAGE, - NON_FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT, - NON_FUNGIBLE_RESOURCE_MANAGER_CREATE_RUID_WITH_INITIAL_SUPPLY_IDENT, - NonFungibleResourceManagerCreateRuidWithInitialSupplyManifestInput { - owner_role: owner_role.clone(), - track_total_supply: true, - non_fungible_schema: NonFungibleDataSchema::new_local_without_self_package_replacement::>(), - entries: Vec::new(), - resource_roles: roles.clone(), - metadata: metadata! { - roles { - metadata_setter => protocol_owner_rule.clone(); - metadata_setter_updater => protocol_owner_rule.clone(); - metadata_locker => protocol_owner_rule.clone(); - metadata_locker_updater => protocol_owner_rule.clone(); - }, - init { - // TODO: Confirm with the exchanges what they want - // their name to be. - "name" => "Ignition LP: Ociswap", updatable; - "description" => "Represents a particular contribution of liquidity to Ociswap through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; - "tags" => vec!["lp token"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - // TODO: Must get this from our design team - "icon_url" => UncheckedUrl::of("https://www.google.com"), updatable; - "DEX" => "Ociswap", updatable; - // TODO: Must get this from Ociswap! - "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; - } - }, - address_reservation: None - } - ) - // Caviarnine liquidity receipt - .call_function( - RESOURCE_PACKAGE, - NON_FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT, - NON_FUNGIBLE_RESOURCE_MANAGER_CREATE_RUID_WITH_INITIAL_SUPPLY_IDENT, - NonFungibleResourceManagerCreateRuidWithInitialSupplyManifestInput { - owner_role: owner_role.clone(), - track_total_supply: true, - non_fungible_schema: NonFungibleDataSchema::new_local_without_self_package_replacement::>(), - entries: Vec::new(), - resource_roles: roles.clone(), - metadata: metadata! { - roles { - metadata_setter => protocol_owner_rule.clone(); - metadata_setter_updater => protocol_owner_rule.clone(); - metadata_locker => protocol_owner_rule.clone(); - metadata_locker_updater => protocol_owner_rule.clone(); - }, - init { - // TODO: Confirm with the exchanges what they want - // their name to be. - "name" => "Ignition LP: Caviarnine", updatable; - "description" => "Represents a particular contribution of liquidity to Caviarnine through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; - "tags" => vec!["lp token"], updatable; - "dapp_definitions" => vec![dapp_definition_account], updatable; - // TODO: Must get this from our design team - "icon_url" => UncheckedUrl::of("https://www.google.com"), updatable; - "DEX" => "Caviarnine", updatable; - // TODO: Must get this from Caviarnine! - "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; - } - }, - address_reservation: None - } - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let resource_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_resource_addresses; - ( - *resource_addresses.first().unwrap(), - *resource_addresses.get(1).unwrap(), - ) - }; - - // Creating the oracle and adapters. - let ( - ignition_component, - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) = { - let manifest = ManifestBuilder::new() - // Creating the oracle component - .call_function( - simple_oracle_package_address, - "SimpleOracle", - "instantiate", - ( - protocol_manager_rule.clone(), - metadata_init! { - "name" => "Ignition Oracle", updatable; - "description" => "The oracle used by the Ignition protocol.", updatable; - "tags" => vec!["oracle"], updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - // Creating the ociswap adapter v1 component - .call_function( - ociswap_v1_adapter_v1_package_address, - "OciswapV1Adapter", - "instantiate", - ( - metadata_init! { - "name" => "Ignition Ociswap Adapter", updatable; - "description" => "The adapter used by the Ignition protocol to communicate with Ociswap pools.", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - // Creating the ociswap adapter v1 component - .call_function( - caviarnine_v1_adapter_v1_package_address, - "CaviarnineV1Adapter", - "instantiate", - ( - metadata_init! { - "name" => "Ignition Caviarnine Adapter", updatable; - "description" => "The adapter used by the Ignition protocol to communicate with Caviarnine pools.", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - None::, - ), - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let component_addresses = transaction_service - .submit_manifest(manifest, ¬ary_private_key, &fee_handling)? - .new_component_addresses; - - let ( - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) = ( - *component_addresses.first().unwrap(), - *component_addresses.get(1).unwrap(), - *component_addresses.get(2).unwrap(), - ); - - // Instantiating the Ignition component - let manifest = ManifestBuilder::new() - // Instantiate Ignition. - .call_function( - ignition_package_address, - "Ignition", - "instantiate", - manifest_args!( - metadata_init! { - "name" => "Ignition", updatable; - "description" => "The Ignition protocol component", updatable; - "dapp_definition" => dapp_definition_account, updatable; - }, - owner_role.clone(), - protocol_owner_rule.clone(), - protocol_manager_rule.clone(), - protocol_resource, - simple_oracle_component, - MAXIMUM_ALLOWED_PRICE_STALENESS_IN_SECONDS, - MAXIMUM_ALLOWED_PRICE_DIFFERENCE_PERCENTAGE, - InitializationParametersManifest { - initial_pool_information: Some(indexmap! { - BlueprintId { - package_address: exchanges.caviarnine_v1.package, - blueprint_name: "QuantaSwap".to_owned() - } => PoolBlueprintInformation { - adapter: caviarnine_v1_adapter_v1_component, - allowed_pools: exchanges.caviarnine_v1.pools.into_iter().collect(), - liquidity_receipt: caviarnine_v1_liquidity_receipt_resource - }, - BlueprintId { - package_address: exchanges.ociswap_v1.package, - blueprint_name: "BasicPool".to_owned() - } => PoolBlueprintInformation { - adapter: ociswap_v1_adapter_v1_component, - // TODO: Fix this when we have actual - // ociswap pools. - allowed_pools: Default::default(), - // allowed_pools: exchanges.ociswap.pools.into_iter().collect(), - liquidity_receipt: ociswap_v1_liquidity_receipt_resource - } - }), - initial_user_resource_volatility: Some( - indexmap! { - resources.bitcoin => Volatility::Volatile, - resources.ethereum => Volatility::Volatile, - resources.usdc => Volatility::NonVolatile, - resources.usdt => Volatility::NonVolatile, - } - ), - initial_reward_rates: Some(reward_information), - initial_volatile_protocol_resources: None, - initial_non_volatile_protocol_resources: None, - initial_is_open_position_enabled: Some(true), - initial_is_close_position_enabled: Some(true), - }, - None:: - ) - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - let component_addresses = transaction_service - .submit_manifest( - manifest, - &ephemeral_private_key, - &fee_handling, - )? - .new_component_addresses; - - let ignition_component_address = - *component_addresses.first().unwrap(); - - ( - ignition_component_address, - simple_oracle_component, - ociswap_v1_adapter_v1_component, - caviarnine_v1_adapter_v1_component, - ) - }; - - // Updating the dapp definition account with the metadata that it - // should have. - { - let manifest = ManifestBuilder::new() - .set_metadata( - dapp_definition_account, - "account_type", - "dapp definition", - ) - .set_metadata( - dapp_definition_account, - "claimed_websites", - Vec::::new(), - ) - .set_metadata( - dapp_definition_account, - "dapp_definitions", - Vec::::new(), - ) - .set_metadata( - dapp_definition_account, - "claimed_entities", - vec![ - GlobalAddress::from(protocol_manager_badge), - GlobalAddress::from(protocol_owner_badge), - GlobalAddress::from(ignition_package_address), - GlobalAddress::from(simple_oracle_package_address), - GlobalAddress::from( - ociswap_v1_adapter_v1_package_address, - ), - GlobalAddress::from( - caviarnine_v1_adapter_v1_package_address, - ), - GlobalAddress::from( - ociswap_v1_liquidity_receipt_resource, - ), - GlobalAddress::from( - caviarnine_v1_liquidity_receipt_resource, - ), - GlobalAddress::from(ignition_component), - GlobalAddress::from(simple_oracle_component), - GlobalAddress::from(ociswap_v1_adapter_v1_component), - GlobalAddress::from(caviarnine_v1_adapter_v1_component), - ], - ) - .call_role_assignment_method( - dapp_definition_account, - ROLE_ASSIGNMENT_SET_OWNER_IDENT, - RoleAssignmentSetOwnerInput { - rule: protocol_owner_rule, - }, - ) - .build(); - - std::thread::sleep(std::time::Duration::from_secs(5)); - transaction_service.submit_manifest( - manifest, - &ephemeral_private_key, - &fee_handling, - )?; - } - - Ok(()) - } -} - -pub struct DexInformation { - pub pools: NameIndexedResourceInformation, - pub package: PackageAddress, -} diff --git a/tools/bootstrap/src/transaction_service.rs b/tools/bootstrap/src/transaction_service.rs deleted file mode 100644 index eecae6ee..00000000 --- a/tools/bootstrap/src/transaction_service.rs +++ /dev/null @@ -1,386 +0,0 @@ -use crate::error::*; -use gateway_client::apis::configuration::*; -use gateway_client::apis::status_api::*; -use gateway_client::apis::transaction_api::*; -use gateway_client::models::*; -use radix_engine::transaction::*; -use radix_engine_interface::blueprints::account::*; -use radix_engine_interface::prelude::*; -use std::thread::*; -use std::time::*; -use transaction::manifest::*; -use transaction::prelude::*; - -type NativePublicKey = radix_engine_interface::crypto::PublicKey; -type GatewayPublicKey = gateway_client::models::PublicKey; - -/// A transaction service that provides a higher-level abstraction over the -/// gateway API. -pub struct TransactionService<'a> { - /// The Bech32m encoders and decoders that the transaction service uses. - bech32m_coders: &'a Bech32mCoders<'a>, - - /// The base url of the gateway API. - gateway_api_base_url: String, - - /// Controls how often the transaction service should poll for the - /// transaction status. This defaults to 5 seconds which is 5,000 - /// milliseconds. - polling_frequency_in_milliseconds: u64, - - /// Controls how many polling attempts the transaction service should make - /// before considering that to be an error. This defaults to 12 attempts. - maximum_number_of_polling_attempts: u64, -} - -impl<'a> TransactionService<'a> { - pub fn new( - bech32m_coders: &'a Bech32mCoders, - gateway_api_base_url: impl Into, - ) -> Self { - Self::new_configurable(bech32m_coders, gateway_api_base_url, 5_000, 12) - } - - pub fn new_configurable( - bech32m_coders: &'a Bech32mCoders, - gateway_api_base_url: impl Into, - polling_frequency_in_milliseconds: u64, - maximum_number_of_polling_attempts: u64, - ) -> Self { - Self { - bech32m_coders, - gateway_api_base_url: gateway_api_base_url.into(), - polling_frequency_in_milliseconds, - maximum_number_of_polling_attempts, - } - } - - pub fn submit_manifest( - &self, - mut manifest: TransactionManifestV1, - notary_private_key: &PrivateKey, - fee_handling: &FeeHandling<'_>, - ) -> std::result::Result { - // Generating the nonce that will be used in submitting the transaction. - let nonce = rand::random::(); - - // Getting the epoch bounds of this transaction - let current_epoch = self.current_epoch()?; - let max_epoch = current_epoch.after(10).unwrap(); - - let additional_signatures = if let FeeHandling::EstimateAndLock { - fee_payer_private_key, - .. - } = fee_handling - { - let is_additional_fee_payer_signature_required = - match (notary_private_key, fee_payer_private_key) { - ( - PrivateKey::Secp256k1(notary), - PrivateKey::Secp256k1(fee_payer), - ) => notary.to_bytes() != fee_payer.to_bytes(), - ( - PrivateKey::Ed25519(notary), - PrivateKey::Ed25519(fee_payer), - ) => notary.to_bytes() != fee_payer.to_bytes(), - (PrivateKey::Secp256k1(..), PrivateKey::Ed25519(..)) - | (PrivateKey::Ed25519(..), PrivateKey::Secp256k1(..)) => { - true - } - }; - - if is_additional_fee_payer_signature_required { - vec![fee_payer_private_key] - } else { - vec![] - } - } else { - vec![] - }; - - // If we need to estimate the fees then we must get a preview of the - // manifest to estimate how much the fees will be. - if let FeeHandling::EstimateAndLock { - fee_payer_account, - fee_payer_private_key, - } = fee_handling - { - let decompiled_manifest = decompile( - &manifest.instructions, - self.bech32m_coders.network_definition, - )?; - - let fees = { - // Getting a preview of the manifest. - let preview_response = transaction_preview( - &self.gateway_config(), - TransactionPreviewRequest { - manifest: decompiled_manifest.clone(), - blobs_hex: Some( - manifest.blobs.values().map(hex::encode).collect(), - ), - start_epoch_inclusive: current_epoch.number() as i64, - end_epoch_exclusive: max_epoch.number() as i64, - notary_public_key: match notary_private_key.public_key() - { - NativePublicKey::Secp256k1(pk) => Some(Box::new( - GatewayPublicKey::EcdsaSecp256k1 { key: pk.0 }, - )), - NativePublicKey::Ed25519(pk) => { - Some(Box::new(GatewayPublicKey::EddsaEd25519 { - key: pk.0, - })) - } - }, - notary_is_signatory: Some(true), - tip_percentage: 0, - nonce: nonce as i64, - signer_public_keys: vec![match fee_payer_private_key { - PrivateKey::Secp256k1(pk) => { - GatewayPublicKey::EcdsaSecp256k1 { - key: pk.public_key().0, - } - } - PrivateKey::Ed25519(pk) => { - GatewayPublicKey::EddsaEd25519 { - key: pk.public_key().0, - } - } - }], - flags: Box::new(TransactionPreviewRequestFlags { - use_free_credit: true, - assume_all_signature_proofs: true, - skip_epoch_check: false, - }), - }, - )?; - - // Ensure that the transaction succeeded in preview. Getting the - // fees of a transaction that failed or was rejected has no - // point. - let receipt = scrypto_decode::( - &preview_response.encoded_receipt, - ) - .unwrap() - .into_latest(); - - if !receipt.is_commit_success() { - return Err(Error::PreviewFailed { - manifest: decompiled_manifest, - receipt, - }); - } - - receipt.fee_summary.total_execution_cost_in_xrd - + receipt.fee_summary.total_finalization_cost_in_xrd - + receipt.fee_summary.total_tipping_cost_in_xrd - + receipt.fee_summary.total_storage_cost_in_xrd - + receipt.fee_summary.total_royalty_cost_in_xrd - }; - - // Adding a 50% padding over the fees that were calculated. - let fees_to_lock = fees * dec!(1.5); - - // Adding the instruction to lock fees. - manifest.instructions.insert( - 0, - InstructionV1::CallMethod { - address: (*fee_payer_account).into(), - method_name: ACCOUNT_LOCK_FEE_IDENT.to_owned(), - args: manifest_args!(fees_to_lock).into(), - }, - ); - }; - - // Constructing the transaction and submitting it. - let mut builder = TransactionBuilder::new().manifest(manifest).header( - TransactionHeaderV1 { - network_id: self.bech32m_coders.network_definition.id, - start_epoch_inclusive: current_epoch, - end_epoch_exclusive: max_epoch, - nonce, - notary_public_key: notary_private_key.public_key(), - notary_is_signatory: true, - tip_percentage: 0, - }, - ); - for key in additional_signatures { - builder = builder.sign(*key); - } - let notarized_transaction = - builder.notarize(notary_private_key).build(); - - // Compiling the notarized transaction and submitting it to the gateway. - let compiled_notarized_transaction = - notarized_transaction.to_payload_bytes().unwrap(); - transaction_submit( - &self.gateway_config(), - TransactionSubmitRequest { - notarized_transaction: compiled_notarized_transaction, - }, - )?; - - // Getting the intent hash and starting to poll for the transaction. - let intent_hash = - notarized_transaction.prepare().unwrap().intent_hash(); - let bech32m_intent_hash = self - .bech32m_coders - .transaction_hash_encoder - .encode(&intent_hash) - .unwrap(); - println!("{bech32m_intent_hash}"); - - for _ in 0..self.maximum_number_of_polling_attempts { - match transaction_status( - &self.gateway_config(), - TransactionStatusRequest { - intent_hash: bech32m_intent_hash.clone(), - }, - ) { - Ok(TransactionStatusResponse { - status: TransactionStatus::CommittedSuccess, - .. - }) => { - // The transaction has been committed successfully. We can - // now get the transaction committed details with no issues. - let committed_details = transaction_committed_details( - &self.gateway_config(), - TransactionCommittedDetailsRequest { - intent_hash: bech32m_intent_hash.clone(), - opt_ins: Some(Box::new(TransactionDetailsOptIns { - raw_hex: Some(true), - receipt_state_changes: Some(true), - receipt_fee_summary: Some(true), - receipt_fee_source: Some(true), - receipt_fee_destination: Some(true), - receipt_costing_parameters: Some(true), - receipt_events: Some(true), - receipt_output: Some(true), - affected_global_entities: Some(true), - balance_changes: Some(true), - })), - at_ledger_state: None, - }, - )?; - - let state_updates = committed_details - .transaction - .receipt - .unwrap() - .state_updates - .unwrap(); - - let mut simplified_receipt = SimplifiedTransactionReceipt { - new_component_addresses: Default::default(), - new_resource_addresses: Default::default(), - new_package_addresses: Default::default(), - }; - - for entity in state_updates.new_global_entities { - let address_string = entity.entity_address; - - if let Some(address) = PackageAddress::try_from_bech32( - &self.bech32m_coders.address_decoder, - &address_string, - ) { - simplified_receipt - .new_package_addresses - .push(address) - } else if let Some(address) = - ResourceAddress::try_from_bech32( - &self.bech32m_coders.address_decoder, - &address_string, - ) - { - simplified_receipt - .new_resource_addresses - .push(address) - } else if let Some(address) = - ComponentAddress::try_from_bech32( - &self.bech32m_coders.address_decoder, - &address_string, - ) - { - simplified_receipt - .new_component_addresses - .push(address) - } - } - - return Ok(simplified_receipt); - } - Ok(TransactionStatusResponse { - status: - TransactionStatus::CommittedFailure - | TransactionStatus::Rejected, - .. - }) => { - return Err(Error::TransactionWasNotSuccessful { - intent_hash: bech32m_intent_hash, - }) - } - _ => {} - } - sleep(Duration::from_millis( - self.polling_frequency_in_milliseconds, - )); - } - - Err(Error::TransactionPollingYieldedNothing { - intent_hash: bech32m_intent_hash, - }) - } - - fn gateway_config(&self) -> Configuration { - Configuration { - base_path: self.gateway_api_base_url.clone(), - ..Default::default() - } - } - - fn current_epoch(&self) -> std::result::Result { - Ok(Epoch::of( - gateway_status(&self.gateway_config())?.ledger_state.epoch as u64, - )) - } -} - -pub struct SimplifiedTransactionReceipt { - pub new_component_addresses: Vec, - pub new_resource_addresses: Vec, - pub new_package_addresses: Vec, -} - -pub enum FeeHandling<'a> { - AlreadyHandled, - EstimateAndLock { - fee_payer_account: ComponentAddress, - fee_payer_private_key: &'a PrivateKey, - }, -} - -pub struct Bech32mCoders<'a> { - pub network_definition: &'a NetworkDefinition, - pub address_encoder: AddressBech32Encoder, - pub address_decoder: AddressBech32Decoder, - pub transaction_hash_encoder: TransactionHashBech32Encoder, - pub transaction_hash_decoder: TransactionHashBech32Decoder, -} - -impl<'a> Bech32mCoders<'a> { - pub fn from_network_definition( - network_definition: &'a NetworkDefinition, - ) -> Self { - Self { - network_definition, - address_encoder: AddressBech32Encoder::new(network_definition), - address_decoder: AddressBech32Decoder::new(network_definition), - transaction_hash_encoder: TransactionHashBech32Encoder::new( - network_definition, - ), - transaction_hash_decoder: TransactionHashBech32Decoder::new( - network_definition, - ), - } - } -} diff --git a/tools/bootstrap/src/types/mod.rs b/tools/bootstrap/src/types/mod.rs deleted file mode 100644 index e36e386e..00000000 --- a/tools/bootstrap/src/types/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod name_indexed_dex_information; -mod name_indexed_resource_information; - -pub use name_indexed_dex_information::*; -pub use name_indexed_resource_information::*; diff --git a/tools/bootstrap/src/types/name_indexed_dex_information.rs b/tools/bootstrap/src/types/name_indexed_dex_information.rs deleted file mode 100644 index 91302a39..00000000 --- a/tools/bootstrap/src/types/name_indexed_dex_information.rs +++ /dev/null @@ -1,30 +0,0 @@ -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct NameIndexedDexInformation { - pub ociswap_v1: T, - pub caviarnine_v1: T, -} - -impl NameIndexedDexInformation { - pub fn map(&self, mut map: F) -> NameIndexedDexInformation - where - F: FnMut(&T) -> O, - { - NameIndexedDexInformation:: { - ociswap_v1: map(&self.ociswap_v1), - caviarnine_v1: map(&self.caviarnine_v1), - } - } - - pub fn try_map( - &self, - mut map: F, - ) -> Result, E> - where - F: FnMut(&T) -> Result, - { - Ok(NameIndexedDexInformation:: { - ociswap_v1: map(&self.ociswap_v1)?, - caviarnine_v1: map(&self.caviarnine_v1)?, - }) - } -} diff --git a/tools/bootstrap/src/types/name_indexed_resource_information.rs b/tools/bootstrap/src/types/name_indexed_resource_information.rs deleted file mode 100644 index fc5ad2f7..00000000 --- a/tools/bootstrap/src/types/name_indexed_resource_information.rs +++ /dev/null @@ -1,45 +0,0 @@ -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct NameIndexedResourceInformation { - pub bitcoin: T, - pub ethereum: T, - pub usdc: T, - pub usdt: T, -} - -impl NameIndexedResourceInformation { - pub fn map(&self, mut map: F) -> NameIndexedResourceInformation - where - F: FnMut(&T) -> O, - { - NameIndexedResourceInformation:: { - bitcoin: map(&self.bitcoin), - ethereum: map(&self.ethereum), - usdc: map(&self.usdc), - usdt: map(&self.usdt), - } - } - - pub fn try_map( - &self, - mut map: F, - ) -> Result, E> - where - F: FnMut(&T) -> Result, - { - Ok(NameIndexedResourceInformation:: { - bitcoin: map(&self.bitcoin)?, - ethereum: map(&self.ethereum)?, - usdc: map(&self.usdc)?, - usdt: map(&self.usdt)?, - }) - } -} - -impl std::iter::IntoIterator for NameIndexedResourceInformation { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - vec![self.bitcoin, self.ethereum, self.usdc, self.usdt].into_iter() - } -} diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml index d48427a2..a3d462e3 100644 --- a/tools/publishing-tool/Cargo.toml +++ b/tools/publishing-tool/Cargo.toml @@ -17,17 +17,32 @@ common = { path = "../../libraries/common" } ignition = { path = "../../packages/ignition" } package-loader = { path = "../../libraries/package-loader" } gateway-client = { path = "../../libraries/gateway-client" } + ociswap-v1-adapter-v1 = { path = "../../packages/ociswap-v1-adapter-v1", features = [ "manifest-builder-stubs", ] } +ociswap-v2-adapter-v1 = { path = "../../packages/ociswap-v2-adapter-v1", features = [ + "manifest-builder-stubs", +] } +defiplaza-v2-adapter-v1 = { path = "../../packages/defiplaza-v2-adapter-v1", features = [ + "manifest-builder-stubs", +] } caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", features = [ "manifest-builder-stubs", ] } state-manager = { git = "https://github.com/radixdlt/babylon-node", rev = "63a8267196995fef0830e4fbf0271bea65c90ab1" } +sbor-json = { git = "https://github.com/radixdlt/radix-engine-toolkit", rev = "1cfe879c7370cfa497857ada7a8973f8a3388abc" } hex = { version = "0.4.3" } -rand = "0.8.5" +rand = { version = "0.8.5" } +macro_rules_attribute = { version = "0.2.0" } +log = "0.4.21" +env_logger = "0.11.2" +hex-literal = "0.4.1" +itertools = "0.12.1" +serde_json = "1.0.114" +clap = { version = "4.5.1", features = ["derive"] } [lints] workspace = true diff --git a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs new file mode 100644 index 00000000..0d15cbdc --- /dev/null +++ b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs @@ -0,0 +1,304 @@ +use common::prelude::*; + +use self::utils::*; +use crate::*; + +pub fn mainnet_testing( + notary_private_key: &PrivateKey, +) -> PublishingConfiguration { + let notary_account_address = + ComponentAddress::virtual_account_from_public_key( + ¬ary_private_key.public_key(), + ); + + // cSpell:disable + PublishingConfiguration { + protocol_configuration: ProtocolConfiguration { + protocol_resource: resource_address!( + "resource_rdx1t4dekrf58h0r28s3c93z92w3jt5ngx87jzd63mgc597zmf3534rxfv" + ), + user_resource_volatility: UserResourceIndexedData { + bitcoin: Volatility::Volatile, + ethereum: Volatility::Volatile, + usdc: Volatility::NonVolatile, + usdt: Volatility::NonVolatile, + }, + reward_rates: indexmap! { + LockupPeriod::from_minutes(0).unwrap() => dec!(0.125), // 12.5% + LockupPeriod::from_minutes(1).unwrap() => dec!(0.15), // 15.0% + }, + allow_opening_liquidity_positions: true, + allow_closing_liquidity_positions: true, + maximum_allowed_price_staleness: i64::MAX, + maximum_allowed_price_difference_percentage: Decimal::MAX, + entities_metadata: Entities { + protocol_entities: ProtocolIndexedData { + ignition: metadata_init! { + "name" => "Ignition", updatable; + "description" => "The main entrypoint into the Ignition liquidity incentive program.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + simple_oracle: metadata_init! { + "name" => "Ignition Oracle", updatable; + "description" => "The oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with Ociswap v2 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + defiplaza_v2: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with DefiPlaza v2 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + caviarnine_v1: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with Caviarnine v1 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + }, + dapp_definition_metadata: indexmap! { + "name".to_owned() => MetadataValue::String("Project Ignition".to_owned()), + "description".to_owned() => MetadataValue::String("A Radix liquidity incentives program, offered in partnership with select decentralized exchange dApps in the Radix ecosystem.".to_owned()), + "icon_url".to_owned() => MetadataValue::Url(UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png")) + }, + transaction_configuration: TransactionConfiguration { + notary: clone_private_key(¬ary_private_key), + fee_payer_information: AccountAndControllingKey::new_virtual_account( + clone_private_key(¬ary_private_key), + ), + }, + // TODO: Determine where they should be sent to. + badges: BadgeIndexedData { + oracle_manager_badge: BadgeHandling::CreateAndSend { + account_address: component_address!( + "account_rdx168nr5dwmll4k2x5apegw5dhrpejf3xac7khjhgjqyg4qddj9tg9v4d" + ), + metadata_init: metadata_init! { + "name" => "Ignition Oracle Manager", updatable; + "symbol" => "IGNOM", updatable; + "description" => "A badge with the authority to update the Oracle prices of the Ignition oracle.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_manager_badge: BadgeHandling::CreateAndSend { + account_address: notary_account_address, + metadata_init: metadata_init! { + "name" => "Ignition Protocol Manager", updatable; + "symbol" => "IGNPM", updatable; + "description" => "A badge with the authority to manage the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_owner_badge: BadgeHandling::CreateAndSend { + account_address: notary_account_address, + metadata_init: metadata_init! { + "name" => "Ignition Protocol Owner", updatable; + "symbol" => "IGNPO", updatable; + "description" => "A badge with owner authority over the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + // TODO: Not real resources, just the notXYZ resources. + user_resources: UserResourceIndexedData { + bitcoin: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1t58dla7ykxzxe5es89wlhgzatqla0gceukg0eeduzvtj4cxd55etn8" + ), + }, + ethereum: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1tkscrlztcyn82ej5z3n232f0qqp0qur69arjf279ppmg5usa3xhnsm" + ), + }, + usdc: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1th7nx2hy0cf6aea6mz7zhkdmy4p45s488xutltnp7296zxj8hwchpf" + ), + }, + usdt: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1tkafx32lu72mcxr85gjx0rh3rx9q89zqffg4phmv5rxdqg5fnd0w7s" + ), + }, + }, + packages: Entities { + protocol_entities: ProtocolIndexedData { + ignition: PackageHandling::LoadAndPublish { + crate_package_name: "ignition".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Package", updatable; + "description" => "The implementation of the Ignition protocol.", updatable; + "tags" => Vec::::new(), updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "Ignition".to_owned(), + }, + simple_oracle: PackageHandling::LoadAndPublish { + crate_package_name: "simple-oracle".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Simple Oracle Package", updatable; + "description" => "The implementation of the Oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "SimpleOracle".to_owned(), + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: PackageHandling::LoadAndPublish { + crate_package_name: "ociswap-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for Ociswap v2 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "OciswapV2Adapter".to_owned(), + }, + defiplaza_v2: PackageHandling::LoadAndPublish { + crate_package_name: "defiplaza-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for DefiPlaza v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "DefiPlazaV2Adapter".to_owned(), + }, + caviarnine_v1: PackageHandling::LoadAndPublish { + crate_package_name: "caviarnine-v1-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter Package", updatable; + "description" => "The implementation of an adapter for Caviarnine v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "CaviarnineV1Adapter".to_owned(), + }, + }, + }, + exchange_information: ExchangeIndexedData { + // No ociswap v2 currently on mainnet. + ociswap_v2: None, + defiplaza_v2: Some(ExchangeInformation { + blueprint_id: BlueprintId { + package_address: package_address!( + "package_rdx1p4dhfl7qwthqqu6p2267m5nedlqnzdvfxdl6q7h8g85dflx8n06p93" + ), + blueprint_name: "PlazaPair".to_owned(), + }, + pools: UserResourceIndexedData { + bitcoin: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpgyq8809z4mnc5rw2pvru3xdcjftjv45a5cgcwyqdqtg2xs35r58r" + ), + }, + ethereum: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpzf8pygechpgat29phu72nzn4gn6shu7x0fdjydjky6g683sl0azk" + ), + }, + usdc: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cq32fjfp8gu3hh8jau9m6syfaargxagmvcakwwx966ejy6cwczghw4" + ), + }, + usdt: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1crx4h8dljzufy9m3g5ez49d5ge2q0vwysfc77vxrp8x480rqq3qpre" + ), + }, + }, + liquidity_receipt: LiquidityReceiptHandling::CreateNew { + non_fungible_schema: + NonFungibleDataSchema::new_local_without_self_package_replacement::< + LiquidityReceipt, + >(), + metadata: metadata_init! { + "name" => "Ignition LP: DefiPlaza", updatable; + "description" => "Represents a particular contribution of liquidity to DefiPlaza through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; + "tags" => vec!["lp token"], updatable; + "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png"), updatable; + "DEX" => "DefiPlaza", updatable; + // TODO: Must get this from the DEX + "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; + }, + }, + }), + caviarnine_v1: Some(ExchangeInformation { + blueprint_id: BlueprintId { + package_address: package_address!( + "package_rdx1p4r9rkp0cq67wmlve544zgy0l45mswn6h798qdqm47x4762h383wa3" + ), + blueprint_name: "QuantaSwap".to_owned(), + }, + pools: UserResourceIndexedData { + bitcoin: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1crzl2c39m83lpe6fv62epgp3phqunxhc264ys23qz8xeemjcu8lln3" + ), + }, + ethereum: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cqk2ufmdq6pkcu7ed7r6u9hmdsht9gyd8y8wwtd7w5znefz9k54a7d" + ), + }, + usdc: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cq9q8umlpmngff6y4e534htz0n37te4m7vsj50u9zc58ys65zl6jv9" + ), + }, + usdt: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpl0v3lndt9d7g7uuepztxs9m7m24ly0yfhvcum2y7tm0vlzst0l5y" + ), + }, + }, + liquidity_receipt: LiquidityReceiptHandling::CreateNew { + non_fungible_schema: + NonFungibleDataSchema::new_local_without_self_package_replacement::< + LiquidityReceipt, + >(), + metadata: metadata_init! { + "name" => "Ignition LP: Caviarnine", updatable; + "description" => "Represents a particular contribution of liquidity to Caviarnine through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; + "tags" => vec!["lp token"], updatable; + "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png"), updatable; + "DEX" => "Caviarnine", updatable; + // TODO: Must get this from the DEX + "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; + }, + }, + }), + }, + additional_information: AdditionalInformation { + ociswap_v2_registry_component: None, + }, + // cSpell:enable + } +} diff --git a/tools/publishing-tool/src/cli/default_configurations/mod.rs b/tools/publishing-tool/src/cli/default_configurations/mod.rs new file mode 100644 index 00000000..49a6f636 --- /dev/null +++ b/tools/publishing-tool/src/cli/default_configurations/mod.rs @@ -0,0 +1,33 @@ +use crate::*; +use clap::*; +mod mainnet_testing; + +#[derive(ValueEnum, Clone, Copy, Debug)] +pub enum ConfigurationSelector { + MainnetTesting, +} + +impl ConfigurationSelector { + pub fn configuration( + self, + notary_private_key: &PrivateKey, + ) -> PublishingConfiguration { + match self { + Self::MainnetTesting => { + mainnet_testing::mainnet_testing(notary_private_key) + } + } + } + + pub fn gateway_base_url(self) -> String { + match self { + Self::MainnetTesting => "https://mainnet.radixdlt.com".to_owned(), + } + } + + pub fn network_definition(self) -> NetworkDefinition { + match self { + Self::MainnetTesting => NetworkDefinition::mainnet(), + } + } +} diff --git a/tools/publishing-tool/src/cli/mod.rs b/tools/publishing-tool/src/cli/mod.rs new file mode 100644 index 00000000..e9eb4a83 --- /dev/null +++ b/tools/publishing-tool/src/cli/mod.rs @@ -0,0 +1,18 @@ +mod default_configurations; +mod publish; + +use crate::Error; +use clap::Parser; + +#[derive(Parser, Debug)] +pub enum Cli { + Publish(publish::Publish), +} + +impl Cli { + pub fn run(self, out: &mut O) -> Result<(), Error> { + match self { + Self::Publish(cmd) => cmd.run(out), + } + } +} diff --git a/tools/publishing-tool/src/cli/publish.rs b/tools/publishing-tool/src/cli/publish.rs new file mode 100644 index 00000000..b5664862 --- /dev/null +++ b/tools/publishing-tool/src/cli/publish.rs @@ -0,0 +1,70 @@ +use super::default_configurations::*; +use crate::utils::*; +use crate::*; +use clap::Parser; +use common::prelude::LockupPeriod; +use defiplaza_v2_adapter_v1::DefiPlazaV2PoolInterfaceManifestBuilderExtensionTrait; +use radix_engine_common::prelude::*; +use state_manager::RocksDBStore; +use std::path::*; +use transaction::prelude::*; + +#[derive(Parser, Debug)] +pub struct Publish { + /// The configuration that the user wants to use when publishing. + configuration_selector: ConfigurationSelector, + + /// The path to the state manager database. + state_manager_database_path: PathBuf, + + /// The hex-encoded private key of the notary. + notary_ed25519_private_key_hex: String, +} + +impl Publish { + pub fn run(self, f: &mut O) -> Result<(), Error> { + // Loading the private key from the passed argument. + let notary_private_key = + hex::decode(self.notary_ed25519_private_key_hex) + .ok() + .and_then(|bytes| Ed25519PrivateKey::from_bytes(&bytes).ok()) + .map(PrivateKey::Ed25519) + .ok_or(Error::PrivateKeyError)?; + + // Loading the configuration to use for the deployment + let configuration = self + .configuration_selector + .configuration(¬ary_private_key); + + // Creating the network connection providers to use for the deployments + let network_definition = + self.configuration_selector.network_definition(); + let gateway_base_url = self.configuration_selector.gateway_base_url(); + let database = + RocksDBStore::new_read_only(self.state_manager_database_path) + .unwrap(); + let mut simulator_network_provider = SimulatorNetworkConnector::new( + &database, + network_definition.clone(), + ); + let mut gateway_network_provider = GatewayNetworkConnector::new( + gateway_base_url, + network_definition.clone(), + PollingConfiguration { + interval_in_seconds: 10, + retries: 10, + }, + ); + + // Running a dry run of the publishing process against the simulator + // network provider. + log::info!("Publishing against the simulator"); + publish(&configuration, &mut simulator_network_provider)?; + + // Running the transactions against the network. + log::info!("Publishing against the gateway"); + let receipt = publish(&configuration, &mut gateway_network_provider)?; + writeln!(f, "{}", to_json(&receipt, &network_definition)).unwrap(); + Ok(()) + } +} diff --git a/tools/publishing-tool/src/error.rs b/tools/publishing-tool/src/error.rs new file mode 100644 index 00000000..42cdceaa --- /dev/null +++ b/tools/publishing-tool/src/error.rs @@ -0,0 +1,20 @@ +use crate::*; + +#[derive(Debug)] +pub enum Error { + PrivateKeyError, + GatewayExecutorError(PublishingError), + SimulatorExecutorError(PublishingError), +} + +impl From> for Error { + fn from(value: PublishingError) -> Self { + Self::GatewayExecutorError(value) + } +} + +impl From> for Error { + fn from(value: PublishingError) -> Self { + Self::SimulatorExecutorError(value) + } +} diff --git a/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs b/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs deleted file mode 100644 index 337771d5..00000000 --- a/tools/publishing-tool/src/executor/mainnet_simulator_executor.rs +++ /dev/null @@ -1,105 +0,0 @@ -use super::*; -use crate::database_overlay::*; -use radix_engine::transaction::*; -use radix_engine::vm::*; -use scrypto_unit::*; -use state_manager::store::*; -use transaction::prelude::*; - -/// An [`Executor`] that simulates the transaction execution on mainnet state. -/// This requires having a mainnet database that the executor can read from. All -/// of the database changes from the transaction execution is written to an -/// overlay which means that the mainnet database's state remains unchanged. -pub struct MainnetSimulatorExecutor<'s>( - TestRunner< - NoExtension, - UnmergeableSubstateDatabaseOverlay<'s, RocksDBStore>, - >, -); - -impl<'s> MainnetSimulatorExecutor<'s> { - pub fn new(database: &'s RocksDBStore) -> Self { - let database = UnmergeableSubstateDatabaseOverlay::new(database); - let test_runner = TestRunnerBuilder::new() - .with_custom_database(database) - .without_trace() - .build(); - Self(test_runner) - } -} - -impl<'s> Executor for MainnetSimulatorExecutor<'s> { - type Error = MainnetSimulatorError; - - fn execute_transaction( - &mut self, - notarized_transaction: &NotarizedTransactionV1, - ) -> Result { - let network_definition = NetworkDefinition::mainnet(); - let raw_transaction = notarized_transaction.to_raw().map_err( - MainnetSimulatorError::NotarizedTransactionRawFormatError, - )?; - - let transaction_receipt = self - .0 - .execute_raw_transaction(&network_definition, &raw_transaction); - - let execution_receipt = match transaction_receipt.result { - TransactionResult::Commit(CommitResult { - outcome: TransactionOutcome::Success(..), - state_update_summary, - .. - }) => ExecutionReceipt::CommitSuccess { - new_entities: NewEntities { - new_component_addresses: state_update_summary - .new_components, - new_resource_addresses: state_update_summary.new_resources, - new_package_addresses: state_update_summary.new_packages, - }, - }, - TransactionResult::Commit(CommitResult { - outcome: TransactionOutcome::Failure(reason), - .. - }) => ExecutionReceipt::CommitFailure { - reason: format!("{:?}", reason), - }, - TransactionResult::Reject(RejectResult { reason }) => { - ExecutionReceipt::Rejection { - reason: format!("{:?}", reason), - } - } - TransactionResult::Abort(AbortResult { reason }) => { - ExecutionReceipt::Abort { - reason: format!("{:?}", reason), - } - } - }; - Ok(execution_receipt) - } - - fn preview_transaction( - &mut self, - preview_intent: PreviewIntentV1, - ) -> Result { - let network_definition = NetworkDefinition::mainnet(); - self.0 - .preview(preview_intent, &network_definition) - .map_err(MainnetSimulatorError::PreviewError) - } - - fn get_current_epoch(&mut self) -> Result { - Ok(self.0.get_current_epoch()) - } - - fn get_network_definition( - &mut self, - ) -> Result { - Ok(NetworkDefinition::mainnet()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MainnetSimulatorError { - NotarizedTransactionRawFormatError(EncodeError), - PreviewError(PreviewError), -} diff --git a/tools/publishing-tool/src/executor/mod.rs b/tools/publishing-tool/src/executor/mod.rs deleted file mode 100644 index 0f330d14..00000000 --- a/tools/publishing-tool/src/executor/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod execution_service; -mod gateway_executor; -mod mainnet_simulator_executor; -mod traits; - -pub use execution_service::*; -pub use gateway_executor::*; -pub use mainnet_simulator_executor::*; -pub use traits::*; diff --git a/tools/bootstrap/src/macros.rs b/tools/publishing-tool/src/macros.rs similarity index 87% rename from tools/bootstrap/src/macros.rs rename to tools/publishing-tool/src/macros.rs index d7fd9452..32b2c508 100644 --- a/tools/bootstrap/src/macros.rs +++ b/tools/publishing-tool/src/macros.rs @@ -53,11 +53,11 @@ macro_rules! global_address { #[macro_export] macro_rules! decode_to_node_id { ($address: expr) => { - ::radix_engine_interface::prelude::AddressBech32Decoder::validate_and_decode_ignore_hrp($address) - .ok() - .and_then(|(_, _, value)| - value.try_into().map(NodeId).ok() - ) - .unwrap() + ::radix_engine_interface::prelude::AddressBech32Decoder::validate_and_decode_ignore_hrp( + $address, + ) + .ok() + .and_then(|(_, _, value)| value.try_into().map(NodeId).ok()) + .unwrap() }; } diff --git a/tools/publishing-tool/src/main.rs b/tools/publishing-tool/src/main.rs index 3c495211..341c3d0c 100644 --- a/tools/publishing-tool/src/main.rs +++ b/tools/publishing-tool/src/main.rs @@ -1,6 +1,23 @@ +#![allow(dead_code, clippy::enum_variant_names)] + +mod cli; mod database_overlay; -mod executor; +mod error; +mod network_connection_provider; +mod publishing; +mod utils; +#[macro_use] +mod macros; + +use error::*; +use network_connection_provider::*; +use publishing::*; +use radix_engine_common::prelude::*; +use transaction::prelude::*; -fn main() { - println!("Hello, world!"); +fn main() -> Result<(), Error> { + env_logger::init(); + let mut out = std::io::stdout(); + let cli = ::parse(); + cli.run(&mut out) } diff --git a/tools/publishing-tool/src/executor/execution_service.rs b/tools/publishing-tool/src/network_connection_provider/execution_service.rs similarity index 62% rename from tools/publishing-tool/src/executor/execution_service.rs rename to tools/publishing-tool/src/network_connection_provider/execution_service.rs index 862853a0..d3108a1b 100644 --- a/tools/publishing-tool/src/executor/execution_service.rs +++ b/tools/publishing-tool/src/network_connection_provider/execution_service.rs @@ -1,6 +1,8 @@ +use itertools::*; use radix_engine::transaction::*; use radix_engine_common::prelude::*; use radix_engine_interface::blueprints::account::*; +use transaction::manifest::*; use transaction::model::*; use transaction::prelude::*; @@ -8,7 +10,7 @@ use super::*; /// A simple execution service whose main responsibilities is to construct, /// submit, and return the result of transactions. -pub struct ExecutionService<'e, E: Executor> { +pub struct ExecutionService<'e, E: NetworkConnectionProvider> { /// The executor that the service will use to execute transactions. executor: &'e mut E, /// The account to use for the payment of fees. @@ -16,37 +18,50 @@ pub struct ExecutionService<'e, E: Executor> { /// The notary of the transaction notary_private_key: &'e PrivateKey, /// The set of private keys that should sign the transaction. - signers_private_keys: &'e [PrivateKey], + signers_private_keys: &'e [&'e PrivateKey], } -impl<'e, E: Executor> ExecutionService<'e, E> { +impl<'e, E: NetworkConnectionProvider> ExecutionService<'e, E> { pub fn new( executor: &'e mut E, fee_payer_account_address: ComponentAddress, notary_private_key: &'e PrivateKey, - additional_signatures: &'e [PrivateKey], + signers_private_keys: &'e [&'e PrivateKey], ) -> Self { Self { executor, fee_payer_account_address, notary_private_key, - signers_private_keys: additional_signatures, + signers_private_keys, } } pub fn execute_manifest( &mut self, mut manifest: TransactionManifestV1, - ) -> Result> { + ) -> Result< + ExecutionReceiptSuccessContents, + ExecutionServiceError<::Error>, + > { + // If the manifest is empty (has no instructions) do no work + if manifest.instructions.is_empty() { + return Ok(ExecutionReceiptSuccessContents { + new_entities: Default::default(), + }); + } + // The signers for the transaction let notary_is_signatory = self.signers_private_keys.iter().any(|private_key| { private_key.public_key() == self.notary_private_key.public_key() }); - let signer_private_keys = - self.signers_private_keys.iter().filter(|private_key| { + let signer_private_keys = self + .signers_private_keys + .iter() + .filter(|private_key| { private_key.public_key() != self.notary_private_key.public_key() - }); + }) + .unique_by(|private_key| private_key.public_key()); // Getting the current network definition let network_definition = self @@ -88,12 +103,14 @@ impl<'e, E: Executor> ExecutionService<'e, E> { }, message: MessageV1::None, }, - signer_public_keys: signer_private_keys - .clone() + signer_public_keys: self + .signers_private_keys + .iter() .map(|private_key| private_key.public_key()) + .unique() .collect(), flags: PreviewFlags { - use_free_credit: false, + use_free_credit: true, assume_all_signature_proofs: false, skip_epoch_check: false, }, @@ -109,7 +126,9 @@ impl<'e, E: Executor> ExecutionService<'e, E> { ); } let total_fees = preview_receipt.fee_summary.total_cost(); - let total_fees_plus_padding = total_fees * dec!(1.20); + let total_fees_plus_padding = + total_fees + self.signers_private_keys.len() * dec!(0.5); + let total_fees_plus_padding = total_fees_plus_padding * dec!(1.10); // Adding a lock fee instruction to the manifest. manifest.instructions.insert( @@ -125,10 +144,11 @@ impl<'e, E: Executor> ExecutionService<'e, E> { ); // Constructing the transaction. - let mut transaction_builder = - TransactionBuilder::new().header(header).manifest(manifest); + let mut transaction_builder = TransactionBuilder::new() + .header(header) + .manifest(manifest.clone()); for signer_private_key in signer_private_keys { - transaction_builder = transaction_builder.sign(signer_private_key) + transaction_builder = transaction_builder.sign(*signer_private_key) } let transaction = transaction_builder .notarize(self.notary_private_key) @@ -140,13 +160,46 @@ impl<'e, E: Executor> ExecutionService<'e, E> { .execute_transaction(&transaction) .map_err(ExecutionServiceError::ExecutorError)?; - Ok(receipt) + // Do a match on the receipt and error out if execution failed. If it + // did not, then return the success contents. + match receipt { + ExecutionReceipt::CommitSuccess(success_contents) => { + Ok(success_contents) + } + ExecutionReceipt::CommitFailure { reason } + | ExecutionReceipt::Rejection { reason } + | ExecutionReceipt::Abort { reason } => { + let decompiled_manifest = + decompile(&manifest.instructions, &network_definition) + .map_err( + ExecutionServiceError::ManifestDecompilationFailed, + )?; + Err( + ExecutionServiceError::TransactionExecutionWasNotSuccessful { + manifest: decompiled_manifest, + reason, + }, + ) + } + } + } + + pub fn with_network_connection_provider(&mut self, callback: F) -> O + where + F: Fn(&mut E) -> O, + { + callback(self.executor) } } #[derive(Debug)] -pub enum ExecutionServiceError { - ExecutorError(::Error), +pub enum ExecutionServiceError { + ExecutorError(E), + ManifestDecompilationFailed(DecompileError), + TransactionExecutionWasNotSuccessful { + manifest: String, + reason: String, + }, TransactionPreviewWasNotSuccessful( TransactionManifestV1, TransactionReceipt, diff --git a/tools/publishing-tool/src/executor/gateway_executor.rs b/tools/publishing-tool/src/network_connection_provider/gateway_connector.rs similarity index 82% rename from tools/publishing-tool/src/executor/gateway_executor.rs rename to tools/publishing-tool/src/network_connection_provider/gateway_connector.rs index f3555b26..cfb8e810 100644 --- a/tools/publishing-tool/src/executor/gateway_executor.rs +++ b/tools/publishing-tool/src/network_connection_provider/gateway_connector.rs @@ -1,7 +1,8 @@ use super::*; use gateway_client::apis::configuration::*; -use gateway_client::apis::status_api::gateway_status; +use gateway_client::apis::state_api::*; use gateway_client::apis::status_api::GatewayStatusError; +use gateway_client::apis::status_api::*; use gateway_client::apis::transaction_api::*; use gateway_client::apis::Error as GatewayClientError; use gateway_client::models::*; @@ -9,7 +10,7 @@ use radix_engine::transaction::*; use transaction::manifest::*; use transaction::prelude::*; -pub struct GatewayExecutor { +pub struct GatewayNetworkConnector { /// The configuration to use when making gateway HTTP requests. pub configuration: Configuration, /// The network definition of the network that the gateway talks to. @@ -18,7 +19,7 @@ pub struct GatewayExecutor { pub polling_configuration: PollingConfiguration, } -impl GatewayExecutor { +impl GatewayNetworkConnector { pub fn new( base_url: impl ToOwned, network_definition: NetworkDefinition, @@ -35,7 +36,7 @@ impl GatewayExecutor { } } -impl Executor for GatewayExecutor { +impl NetworkConnectionProvider for GatewayNetworkConnector { type Error = GatewayExecutorError; fn execute_transaction( @@ -83,6 +84,11 @@ impl Executor for GatewayExecutor { | TransactionIntentStatus::CommitPendingOutcomeUnknown | TransactionIntentStatus::Pending => {} TransactionIntentStatus::CommittedSuccess => { + // We must wait for some time before requesting the commit + // details as I've observed that doing this too quickly can + // result in us not getting commit results back. + std::thread::sleep(std::time::Duration::from_secs(5)); + let transaction_committed_result_response = transaction_committed_details( &self.configuration, TransactionCommittedDetailsRequest { @@ -154,9 +160,9 @@ impl Executor for GatewayExecutor { new_entities }; - return Ok(ExecutionReceipt::CommitSuccess { - new_entities, - }); + return Ok(ExecutionReceipt::CommitSuccess( + ExecutionReceiptSuccessContents { new_entities }, + )); } TransactionIntentStatus::CommittedFailure => { return Ok(ExecutionReceipt::CommitFailure { @@ -257,6 +263,53 @@ impl Executor for GatewayExecutor { ) -> Result { Ok(self.network_definition.clone()) } + + fn read_component_state( + &mut self, + component_address: ComponentAddress, + ) -> Result { + let encoder = AddressBech32Encoder::new(&self.network_definition); + let encoded_component_address = encoder + .encode(&component_address.as_node_id().0) + .expect("Can't fail!"); + + let request = StateEntityDetailsRequest { + at_ledger_state: None, + opt_ins: Some(Box::new(StateEntityDetailsOptIns { + ancestor_identities: Some(true), + component_royalty_vault_balance: Some(true), + package_royalty_vault_balance: Some(true), + non_fungible_include_nfids: Some(true), + explicit_metadata: None, + })), + addresses: vec![encoded_component_address], + aggregation_level: None, + }; + + let response = state_entity_details(&self.configuration, request) + .map_err(GatewayExecutorError::StateEntityDetailsError)?; + + let details = serde_json::from_value::< + sbor_json::scrypto::programmatic::value::ProgrammaticScryptoValue, + >( + response + .items + .first() + .unwrap() + .clone() + .details + .unwrap() + .get("state") + .unwrap() + .clone(), + ) + .unwrap(); + let encoded_details = + scrypto_encode(&details.to_scrypto_value()).unwrap(); + + scrypto_decode(&encoded_details) + .map_err(GatewayExecutorError::StateScryptoDecodeError) + } } fn native_public_key_to_gateway_public_key( @@ -292,10 +345,12 @@ pub enum GatewayExecutorError { GatewayStatusError(GatewayClientError), TransactionStatusError(GatewayClientError), TransactionPreviewError(GatewayClientError), + StateEntityDetailsError(GatewayClientError), TransactionCommittedDetailsError( GatewayClientError, ), TransactionSubmissionError(GatewayClientError), + StateScryptoDecodeError(DecodeError), AddressBech32mDecodeError, Timeout, } diff --git a/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs b/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs new file mode 100644 index 00000000..4cf6f3d5 --- /dev/null +++ b/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs @@ -0,0 +1,140 @@ +use super::*; +use crate::database_overlay::*; +use radix_engine::system::system_substates::*; +use radix_engine::transaction::*; +use radix_engine::vm::*; +use radix_engine_store_interface::db_key_mapper::*; +use scrypto_unit::*; +use state_manager::store::*; +use transaction::prelude::*; + +/// A [`NetworkConnectionProvider`] that simulates the transaction execution on +/// any network so long as it can access the state manager's database. The most +/// common use of this is to simulate the transactions on mainnet prior to their +/// submission to ensure that they're all valid. The underlying database remains +/// unchanged since an overlay is used. +pub struct SimulatorNetworkConnector<'s> { + /// The id of the network + network_definition: NetworkDefinition, + + /// The simulator that transactions will be running against. + ledger_simulator: TestRunner< + NoExtension, + UnmergeableSubstateDatabaseOverlay<'s, RocksDBStore>, + >, +} + +impl<'s> SimulatorNetworkConnector<'s> { + pub fn new( + database: &'s RocksDBStore, + network_definition: NetworkDefinition, + ) -> Self { + let database = UnmergeableSubstateDatabaseOverlay::new(database); + let test_runner = TestRunnerBuilder::new() + .with_custom_database(database) + .without_trace() + .build_without_bootstrapping(); + Self { + ledger_simulator: test_runner, + network_definition, + } + } +} + +impl<'s> NetworkConnectionProvider for SimulatorNetworkConnector<'s> { + type Error = MainnetSimulatorError; + + fn execute_transaction( + &mut self, + notarized_transaction: &NotarizedTransactionV1, + ) -> Result { + let raw_transaction = notarized_transaction.to_raw().map_err( + MainnetSimulatorError::NotarizedTransactionRawFormatError, + )?; + + let transaction_receipt = + self.ledger_simulator.execute_raw_transaction( + &self.network_definition, + &raw_transaction, + ); + + let execution_receipt = match transaction_receipt.result { + TransactionResult::Commit(CommitResult { + outcome: TransactionOutcome::Success(..), + state_update_summary, + .. + }) => ExecutionReceipt::CommitSuccess( + ExecutionReceiptSuccessContents { + new_entities: NewEntities { + new_component_addresses: state_update_summary + .new_components, + new_resource_addresses: state_update_summary + .new_resources, + new_package_addresses: state_update_summary + .new_packages, + }, + }, + ), + TransactionResult::Commit(CommitResult { + outcome: TransactionOutcome::Failure(reason), + .. + }) => ExecutionReceipt::CommitFailure { + reason: format!("{:?}", reason), + }, + TransactionResult::Reject(RejectResult { reason }) => { + ExecutionReceipt::Rejection { + reason: format!("{:?}", reason), + } + } + TransactionResult::Abort(AbortResult { reason }) => { + ExecutionReceipt::Abort { + reason: format!("{:?}", reason), + } + } + }; + Ok(execution_receipt) + } + + fn preview_transaction( + &mut self, + preview_intent: PreviewIntentV1, + ) -> Result { + self.ledger_simulator + .preview(preview_intent, &self.network_definition) + .map_err(MainnetSimulatorError::PreviewError) + } + + fn get_current_epoch(&mut self) -> Result { + Ok(self.ledger_simulator.get_current_epoch()) + } + + fn get_network_definition( + &mut self, + ) -> Result { + Ok(self.network_definition.clone()) + } + + fn read_component_state( + &mut self, + component_address: ComponentAddress, + ) -> Result { + self.ledger_simulator + .substate_db() + .get_mapped::>( + component_address.as_node_id(), + MAIN_BASE_PARTITION, + &SubstateKey::Field(ComponentField::State0.into()), + ) + .ok_or(MainnetSimulatorError::CantReadComponentState( + component_address, + )) + .map(|value| value.into_payload()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MainnetSimulatorError { + NotarizedTransactionRawFormatError(EncodeError), + PreviewError(PreviewError), + CantReadComponentState(ComponentAddress), +} diff --git a/tools/publishing-tool/src/network_connection_provider/mod.rs b/tools/publishing-tool/src/network_connection_provider/mod.rs new file mode 100644 index 00000000..57f372aa --- /dev/null +++ b/tools/publishing-tool/src/network_connection_provider/mod.rs @@ -0,0 +1,9 @@ +mod execution_service; +mod gateway_connector; +mod mainnet_simulator_connector; +mod traits; + +pub use execution_service::*; +pub use gateway_connector::*; +pub use mainnet_simulator_connector::*; +pub use traits::*; diff --git a/tools/publishing-tool/src/executor/traits.rs b/tools/publishing-tool/src/network_connection_provider/traits.rs similarity index 57% rename from tools/publishing-tool/src/executor/traits.rs rename to tools/publishing-tool/src/network_connection_provider/traits.rs index 6a674fa3..7a1cc9a8 100644 --- a/tools/publishing-tool/src/executor/traits.rs +++ b/tools/publishing-tool/src/network_connection_provider/traits.rs @@ -1,12 +1,14 @@ use radix_engine::transaction::TransactionReceiptV1; use transaction::prelude::*; -/// A trait that can be implemented by various structs to execute transactions -/// and produce execution receipts. The executor could be object doing the -/// execution itself in case of a node or could delegate the execution to -/// another object like in the case of the gateway. This detail does not matter -/// for the executor. -pub trait Executor { +/// A standardized interface for objects that provide connection to the network +/// regardless of how these objects are implemented and how they provide such +/// connection. One implementation could choose to provide network connection +/// through the core-api, another might do it over the gateway-api, and another +/// might talk directly to a node. The implementation details are abstracted +/// away in the interface. The interface has a number of getter functions and +/// functions for executing transactions. +pub trait NetworkConnectionProvider { type Error: Debug; fn execute_transaction( @@ -24,6 +26,11 @@ pub trait Executor { fn get_network_definition( &mut self, ) -> Result; + + fn read_component_state( + &mut self, + component_address: ComponentAddress, + ) -> Result; } /// A simplified transaction receipt containing the key pieces of information @@ -31,7 +38,7 @@ pub trait Executor { /// that the node can give us. #[derive(Clone, Debug, PartialEq, Eq, ScryptoSbor)] pub enum ExecutionReceipt { - CommitSuccess { new_entities: NewEntities }, + CommitSuccess(ExecutionReceiptSuccessContents), CommitFailure { reason: String }, Rejection { reason: String }, Abort { reason: String }, @@ -43,3 +50,8 @@ pub struct NewEntities { pub new_resource_addresses: IndexSet, pub new_package_addresses: IndexSet, } + +#[derive(Clone, Debug, PartialEq, Eq, ScryptoSbor)] +pub struct ExecutionReceiptSuccessContents { + pub new_entities: NewEntities, +} diff --git a/tools/publishing-tool/src/publishing/configuration.rs b/tools/publishing-tool/src/publishing/configuration.rs new file mode 100644 index 00000000..9125fe9a --- /dev/null +++ b/tools/publishing-tool/src/publishing/configuration.rs @@ -0,0 +1,254 @@ +use super::macros::*; +use common::prelude::*; +use macro_rules_attribute::apply; +use radix_engine::prelude::*; +use transaction::prelude::*; + +pub struct PublishingConfiguration { + /// The configuration of the Ignition protocol. + pub protocol_configuration: ProtocolConfiguration, + + /// The metadata to use for the dapp definition that is created. + pub dapp_definition_metadata: IndexMap, + + /// Contains configurations for the transactions that will be submitted + /// such as the notary and the account to get the fees from. Information + /// that mostly pertains to signing. + pub transaction_configuration: TransactionConfiguration, + + /// Contains information on the various badges to use for publishing and + /// whether these badges already exist or should be created. + pub badges: BadgeIndexedData, + + /// Contains information on the user resources that this deployment will use + /// such as their addresses or information about their properties if they're + /// to be created during the publishing process. + pub user_resources: UserResourceIndexedData, + + /// Contains information on how each of the packages should be handled and + /// whether they should be compiled and published or if pre-existing ones + /// should be used. + pub packages: Entities, + + /// Information about the exchange will be supported in Ignition. This + /// contains information necessary for the publishing and bootstrapping + /// process of Ignition. As an example, the address of the exchange's + /// package, the name of the blueprint, and the pools that we wish to + /// support. This uses an [`Option`] to allow for cases when there are + /// some networks where these exchanges are not live and therefore their + /// information can't be provided as part of publishing. + pub exchange_information: ExchangeIndexedData< + Option>, + >, + + /// Additional information that doesn't quite fit into any of the above + /// categories nicely. + pub additional_information: AdditionalInformation, +} + +#[derive(Debug, Clone, ScryptoSbor)] +pub struct PublishingReceipt { + pub packages: Entities, + pub components: Entities, + pub exchange_information: ExchangeIndexedData< + Option>, + >, + pub protocol_configuration: ProtocolConfigurationReceipt, +} + +#[derive(Debug, Clone, ScryptoSbor)] +pub struct ProtocolConfigurationReceipt { + pub protocol_resource: ResourceAddress, + pub user_resource_volatility: UserResourceIndexedData, + pub reward_rates: IndexMap, + pub allow_opening_liquidity_positions: bool, + pub allow_closing_liquidity_positions: bool, + pub maximum_allowed_price_staleness: i64, + pub maximum_allowed_price_difference_percentage: Decimal, + pub user_resources: UserResourceIndexedData, + pub registered_pools: + ExchangeIndexedData>>, +} + +pub struct AdditionalInformation { + pub ociswap_v2_registry_component: Option, +} + +pub struct ProtocolConfiguration { + pub protocol_resource: ResourceAddress, + pub user_resource_volatility: UserResourceIndexedData, + pub reward_rates: IndexMap, + pub allow_opening_liquidity_positions: bool, + pub allow_closing_liquidity_positions: bool, + pub maximum_allowed_price_staleness: i64, + pub maximum_allowed_price_difference_percentage: Decimal, + pub entities_metadata: Entities, +} + +pub struct TransactionConfiguration { + pub notary: PrivateKey, + pub fee_payer_information: AccountAndControllingKey, +} + +pub struct AccountAndControllingKey { + pub account_address: ComponentAddress, + pub controlling_key: PrivateKey, +} + +impl AccountAndControllingKey { + pub fn new_virtual_account(controlling_key: PrivateKey) -> Self { + let account_address = ComponentAddress::virtual_account_from_public_key( + &controlling_key.public_key(), + ); + Self { + account_address, + controlling_key, + } + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub struct Entities { + pub protocol_entities: ProtocolIndexedData, + pub exchange_adapter_entities: ExchangeIndexedData, +} + +#[apply(name_indexed_struct)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub struct ProtocolIndexedData { + pub ignition: T, + pub simple_oracle: T, +} + +#[apply(name_indexed_struct)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub struct ExchangeIndexedData { + pub ociswap_v2: T, + pub defiplaza_v2: T, + pub caviarnine_v1: T, +} + +#[apply(name_indexed_struct)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub struct UserResourceIndexedData { + pub bitcoin: T, + pub ethereum: T, + pub usdc: T, + pub usdt: T, +} + +#[apply(name_indexed_struct)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub struct BadgeIndexedData { + pub oracle_manager_badge: T, + pub protocol_owner_badge: T, + pub protocol_manager_badge: T, +} + +pub enum BadgeHandling { + /// Creates a new badge and deposits it into the specified account. + CreateAndSend { + /// The account that the badges should be sent to. + account_address: ComponentAddress, + /// The metadata of the created badges. + metadata_init: MetadataInit, + }, + /// Use an existing badge that exists in some account. If the badge is + /// required in one of the operations then a proof of it will be created. + /// A signature of this account must be provided. + UseExisting { + /// The private key of the account that controlling the badge. This is + /// required for any proofs that need to be created. + controlling_private_key: PrivateKey, + /// The address of the holder + holder_account_address: ComponentAddress, + /// The address of the badge + badge_resource_address: ResourceAddress, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub struct ExchangeInformation { + /// The id of the pool blueprint of the exchange. + pub blueprint_id: BlueprintId, + /// The pools that we wish to support for the exchange. + pub pools: UserResourceIndexedData

, + /// The liquidity receipt to use for the exchange. + pub liquidity_receipt: R, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub enum PackageHandling { + /// The package should be compiled and published in the process. + LoadAndPublish { + /// The name of the crate that contains the package. This is the name + /// that will be used when instructing the package loader to get the + /// package. + crate_package_name: String, + /// The initial metadata to set on the package when it's being published + metadata: MetadataInit, + /// The name of the blueprint to use from this package. This is under + /// the assumption that each package is just a single blueprint. + blueprint_name: String, + }, + /// The package already exists on the desired network. + UseExisting { + /// The address of the package on the network and + package_address: BlueprintId, + }, +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScryptoSbor, +)] +pub enum PoolHandling { + /// A pool does not exist and should be created. + Create, + /// A pool already exists and should be used. + UseExisting { + /// The address of the pool to use + pool_address: ComponentAddress, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub enum UserResourceHandling { + /// Resources do not exist on the network and should be created + CreateFreelyMintableAndBurnable { + /// The divisibility to create the resource with + divisibility: u8, + /// The initial metadata to use for the resource + metadata: MetadataInit, + }, + /// Resources exist on the network and should be used. + UseExisting { + /// The address of the resource + resource_address: ResourceAddress, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +pub enum LiquidityReceiptHandling { + /// Create a new resource to use as the liquidity receipt + CreateNew { + /// The non-fungible data schema of the resource. + non_fungible_schema: NonFungibleDataSchema, + /// The initial metadata to use for the resource. + metadata: MetadataInit, + }, + /// Use an existing resource as the liquidity receipt of the exchange + UseExisting { + /// The address of the liquidity receipt resource + resource_address: ResourceAddress, + }, +} diff --git a/tools/publishing-tool/src/publishing/error.rs b/tools/publishing-tool/src/publishing/error.rs new file mode 100644 index 00000000..3d75c1eb --- /dev/null +++ b/tools/publishing-tool/src/publishing/error.rs @@ -0,0 +1,4 @@ +#[derive(Clone, Debug)] +pub struct KeyNotFound { + pub key: String, +} diff --git a/tools/publishing-tool/src/publishing/handler.rs b/tools/publishing-tool/src/publishing/handler.rs new file mode 100644 index 00000000..769af58e --- /dev/null +++ b/tools/publishing-tool/src/publishing/handler.rs @@ -0,0 +1,1412 @@ +#![allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)] + +use defiplaza_v2_adapter_v1::*; +use ignition::{InitializationParametersManifest, PoolBlueprintInformation}; +use itertools::*; +use package_loader::*; +use radix_engine::blueprints::package::*; +use radix_engine::types::node_modules::*; +use radix_engine_interface::blueprints::account::*; +use rand::prelude::*; +use transaction::prelude::*; + +use super::*; +use crate::network_connection_provider::*; + +pub fn publish( + configuration: &PublishingConfiguration, + network_provider: &mut N, +) -> Result< + PublishingReceipt, + PublishingError<::Error>, +> { + // A cryptographically secure random number generator. + let mut std_rng = rand::rngs::StdRng::from_entropy(); + + // Creating an ephemeral private key to use for the publishing process. This + // key will be mapped to an account that may store things during the process + // but will ultimately be discarded in the end. + let ephemeral_key_u64 = std_rng.next_u64(); + let ephemeral_private_key = PrivateKey::Ed25519( + Ed25519PrivateKey::from_u64(ephemeral_key_u64).unwrap(), + ); + let ephemeral_account = ComponentAddress::virtual_account_from_public_key( + &ephemeral_private_key.public_key(), + ); + log::info!("Ephemeral private key selected: {}", ephemeral_key_u64); + + // Finding the set of private keys to use for the signatures. This will be + // the notary, the fee payer, and all of the private keys that control the + // accounts with the badges. + let mut signer_private_keys = vec![ + &configuration.transaction_configuration.notary, + &ephemeral_private_key, + &configuration + .transaction_configuration + .fee_payer_information + .controlling_key, + ]; + + for badge_handling in configuration.badges.iter() { + if let BadgeHandling::UseExisting { + controlling_private_key, + .. + } = badge_handling + { + signer_private_keys.push(controlling_private_key) + } + } + + // Creating an execution service from the passed executor + let mut execution_service = ExecutionService::new( + network_provider, + configuration + .transaction_configuration + .fee_payer_information + .account_address, + &configuration.transaction_configuration.notary, + &signer_private_keys, + ); + + // Creating the dApp definition account. The owner role will be set to the + // ephemeral private key and then switched to the protocol owner and manager + // at the end + let dapp_definition_account = { + let manifest = ManifestBuilder::new() + .allocate_global_address( + ACCOUNT_PACKAGE, + ACCOUNT_BLUEPRINT, + "reservation", + "named_address", + ) + .then(|builder| { + let reservation = builder.address_reservation("reservation"); + let named_address = builder.named_address("named_address"); + + let mut builder = builder + .call_function( + ACCOUNT_PACKAGE, + ACCOUNT_BLUEPRINT, + ACCOUNT_CREATE_ADVANCED_IDENT, + AccountCreateAdvancedManifestInput { + address_reservation: Some(reservation), + owner_role: OwnerRole::Updatable(rule!(require( + NonFungibleGlobalId::from_public_key( + &ephemeral_private_key.public_key() + ) + ))), + }, + ) + .call_metadata_method( + named_address, + METADATA_SET_IDENT, + MetadataSetInput { + key: "account_type".to_owned(), + value: MetadataValue::String( + "dapp definition".to_owned(), + ), + }, + ) + .call_metadata_method( + named_address, + METADATA_SET_IDENT, + MetadataSetInput { + key: "claimed_websites".to_owned(), + value: MetadataValue::OriginArray(vec![]), + }, + ) + .call_metadata_method( + named_address, + METADATA_SET_IDENT, + MetadataSetInput { + key: "dapp_definitions".to_owned(), + value: MetadataValue::GlobalAddressArray(vec![]), + }, + ); + + for (key, value) in + configuration.dapp_definition_metadata.iter() + { + builder = builder.call_metadata_method( + named_address, + METADATA_SET_IDENT, + MetadataSetInput { + key: key.to_owned(), + value: value.clone(), + }, + ) + } + + builder + }) + .build(); + + execution_service + .execute_manifest(manifest.clone())? + .new_entities + .new_component_addresses + .first() + .copied() + .expect("Must succeed!") + }; + + // Handling the creation of the user resources if they need to be created. + let resolved_user_resources = { + let user_resources_map = configuration.user_resources.into_map(); + + let user_resources_already_created = + user_resources_map.iter().flat_map(|(key, handling)| { + if let UserResourceHandling::UseExisting { resource_address } = + handling + { + Some((*key, resource_address)) + } else { + None + } + }); + let user_resources_requiring_creation = user_resources_map + .iter() + .flat_map(|(key, handling)| { + if let UserResourceHandling::CreateFreelyMintableAndBurnable { + divisibility, + metadata, + } = handling + { + Some((*key, (divisibility, metadata))) + } else { + None + } + }) + .collect::>(); + + // Construct a manifest that creates the user resources. + let manifest = TransactionManifestV1 { + instructions: user_resources_requiring_creation + .values() + .map(|(divisibility, metadata)| InstructionV1::CallFunction { + package_address: RESOURCE_PACKAGE.into(), + blueprint_name: FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT + .to_string(), + function_name: FUNGIBLE_RESOURCE_MANAGER_CREATE_IDENT + .to_owned(), + args: to_manifest_value( + &FungibleResourceManagerCreateManifestInput { + owner_role: OwnerRole::None, + track_total_supply: true, + divisibility: **divisibility, + resource_roles: FungibleResourceRoles { + mint_roles: mint_roles! { + minter => rule!(allow_all); + minter_updater => rule!(deny_all); + }, + burn_roles: burn_roles! { + burner => rule!(allow_all); + burner_updater => rule!(deny_all); + }, + ..Default::default() + }, + metadata: ModuleConfig { + init: (*metadata).clone(), + roles: Default::default(), + }, + address_reservation: None, + }, + ) + .expect("Can't fail!"), + }) + .collect::>(), + blobs: Default::default(), + }; + let resource_addresses = execution_service + .execute_manifest(manifest)? + .new_entities + .new_resource_addresses; + + UserResourceIndexedData::from_map( + user_resources_already_created + .map(|(key, address)| (key, *address)) + .chain( + user_resources_requiring_creation + .iter() + .map(|value| *value.0) + .zip(resource_addresses), + ), + ) + .expect("Can't fail!") + }; + + // Handling the badge creation that is needed. + let resolved_badges = { + let already_existing_badges = + configuration.badges.into_map().into_iter().filter_map( + |(key, value)| { + if let BadgeHandling::UseExisting { + holder_account_address, + badge_resource_address, + .. + } = value + { + Some(( + key, + (*holder_account_address, *badge_resource_address), + )) + } else { + None + } + }, + ); + + let badges_requiring_creation = configuration + .badges + .into_map() + .into_iter() + .filter_map(|(key, value)| { + if let BadgeHandling::CreateAndSend { metadata_init, .. } = + value + { + Some((key, metadata_init)) + } else { + None + } + }); + + let mut manifest_builder = ManifestBuilder::new(); + let mut keys = vec![]; + for (key, metadata_init) in badges_requiring_creation { + let mut metadata_init = metadata_init.clone(); + metadata_init.data.insert( + "dapp_definitions".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddressArray(vec![ + dapp_definition_account.into(), + ])), + lock: false, + }, + ); + + keys.push(key); + manifest_builder = manifest_builder.create_fungible_resource( + OwnerRole::Updatable(rule!(require( + NonFungibleGlobalId::from_public_key( + &ephemeral_private_key.public_key() + ) + ))), + true, + 0, + FungibleResourceRoles { + mint_roles: mint_roles! { + minter => rule!(deny_all); + minter_updater => rule!(deny_all); + }, + burn_roles: burn_roles! { + burner => rule!(deny_all); + burner_updater => rule!(deny_all); + }, + freeze_roles: freeze_roles! { + freezer => rule!(deny_all); + freezer_updater => rule!(deny_all); + }, + recall_roles: recall_roles! { + recaller => rule!(deny_all); + recaller_updater => rule!(deny_all); + }, + withdraw_roles: withdraw_roles! { + withdrawer => rule!(allow_all); + withdrawer_updater => rule!(deny_all); + }, + deposit_roles: deposit_roles! { + depositor => rule!(allow_all); + depositor_updater => rule!(deny_all); + }, + }, + ModuleConfig { + roles: Default::default(), + init: metadata_init, + }, + Some(dec!(1)), + ) + } + let manifest = manifest_builder + .try_deposit_entire_worktop_or_abort(ephemeral_account, None) + .build(); + let badges = keys + .into_iter() + .zip( + execution_service + .execute_manifest(manifest.clone())? + .new_entities + .new_resource_addresses, + ) + .map(|(key, resource_address)| { + (key, (ephemeral_account, resource_address)) + }); + + BadgeIndexedData::from_map(already_existing_badges.chain(badges)) + .expect("Can't fail") + }; + + let resolved_rules = resolved_badges + .map(|(_, resource_address)| rule!(require(*resource_address))); + + // The resources created in the previous transaction have an updatable owner + // role that is set to the ephemeral private key. In this transaction the + // owner role is modified to be the protocol owner. + { + let mut manifest_builder = ManifestBuilder::new(); + for ((_, address), handling) in + resolved_badges.zip_borrowed(&configuration.badges).iter() + { + if let BadgeHandling::CreateAndSend { .. } = handling { + manifest_builder = manifest_builder + .create_proof_from_account_of_amount( + resolved_badges.protocol_owner_badge.0, + resolved_badges.protocol_owner_badge.1, + dec!(1), + ) + .set_owner_role( + *address, + resolved_rules.protocol_owner_badge.clone(), + ) + .lock_owner_role(*address); + } + } + let manifest = manifest_builder.build(); + + execution_service.execute_manifest(manifest.clone())?; + } + + // Publishing the packages that need to be published + let resolved_blueprint_ids = { + let mut map = configuration.packages.protocol_entities.into_map(); + map.extend(configuration.packages.exchange_adapter_entities.into_map()); + + let iterator = map + .into_iter() + .filter_map(|(key, package_handling)| { + if let PackageHandling::LoadAndPublish { + crate_package_name, + metadata, + blueprint_name, + } = package_handling + { + Some((key, (crate_package_name, metadata, blueprint_name))) + } else { + None + } + }) + .map(|(key, (crate_package_name, metadata, blueprint_name))| { + let (code, definition) = PackageLoader::get(crate_package_name); + + let mut metadata = metadata.clone(); + metadata.data.insert( + "dapp_definition".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddress( + dapp_definition_account.into(), + )), + lock: false, + }, + ); + + (key, (code, definition, metadata, blueprint_name.clone())) + }) + .sorted_by(|x, y| x.1 .0.len().cmp(&y.1 .0.len())); + + // We want to get as many packages into one transaction. Goal is to + // have each transaction be 980 kbs or less in size. If the addition + // of a package increases the size beyond that then it goes in the + // next batch. + let mut batches = vec![Vec::<( + String, + (Vec, PackageDefinition, MetadataInit, String), + )>::new()]; + for (key, (code, definition, metadata_init, blueprint_name)) in iterator + { + let latest_batch = batches.last_mut().expect("Impossible!"); + let total_code_size = latest_batch + .iter() + .map(|entry| entry.1 .0.len()) + .sum::(); + + let size_if_code_is_added_to_batch = total_code_size + code.len(); + // Add to next batch + if size_if_code_is_added_to_batch > 980 * 1024 { + batches.push(vec![( + key.to_owned(), + (code, definition, metadata_init, blueprint_name), + )]) + } + // Add to this batch + else { + latest_batch.push(( + key.to_owned(), + (code, definition, metadata_init, blueprint_name), + )); + } + } + + // Creating transactions of the batches + let mut addresses_map = IndexMap::::new(); + for batch in batches { + let mut manifest_builder = ManifestBuilder::new(); + for (_, (code, definition, metadata, _)) in batch.iter() { + manifest_builder = manifest_builder.publish_package_advanced( + None, + code.clone(), + definition.clone(), + metadata.clone(), + OwnerRole::Fixed( + resolved_rules.protocol_owner_badge.clone(), + ), + ); + } + let manifest = manifest_builder.build(); + + addresses_map.extend( + execution_service + .execute_manifest(manifest.clone())? + .new_entities + .new_package_addresses + .into_iter() + .zip(batch.into_iter()) + .map( + |( + package_address, + (key, (_, _, _, blueprint_name)), + )| { + ( + key, + BlueprintId { + package_address, + blueprint_name, + }, + ) + }, + ), + ); + } + + let addresses_map = configuration + .packages + .protocol_entities + .into_map() + .into_iter() + .filter_map(|(key, value)| { + if let PackageHandling::UseExisting { package_address } = value + { + Some((key.to_owned(), package_address.clone())) + } else { + None + } + }) + .chain(addresses_map) + .collect::>(); + + Entities { + protocol_entities: ProtocolIndexedData::from_map( + addresses_map.clone(), + ) + .expect("Can't fail!"), + exchange_adapter_entities: ExchangeIndexedData::from_map( + addresses_map, + ) + .expect("Can't fail!"), + } + }; + + // Computing the package global caller + let resolved_package_global_caller_rules = Entities { + protocol_entities: resolved_blueprint_ids.protocol_entities.map( + |blueprint_id| { + rule!(require(package_of_direct_caller( + blueprint_id.package_address + ))) + }, + ), + exchange_adapter_entities: resolved_blueprint_ids + .exchange_adapter_entities + .map(|blueprint_id| { + rule!(require(package_of_direct_caller( + blueprint_id.package_address + ))) + }), + }; + + let resolved_exchange_data = ExchangeIndexedData { + ociswap_v2: handle_ociswap_v2_exchange_information( + &mut execution_service, + configuration.exchange_information.ociswap_v2.as_ref(), + dapp_definition_account, + &resolved_rules, + &resolved_package_global_caller_rules, + &resolved_user_resources, + configuration.protocol_configuration.protocol_resource, + &configuration.additional_information, + )?, + defiplaza_v2: handle_defiplaza_v2_exchange_information( + &mut execution_service, + configuration.exchange_information.defiplaza_v2.as_ref(), + dapp_definition_account, + &resolved_rules, + &resolved_package_global_caller_rules, + &resolved_user_resources, + configuration.protocol_configuration.protocol_resource, + )?, + caviarnine_v1: handle_caviarnine_v1_exchange_information( + &mut execution_service, + configuration.exchange_information.caviarnine_v1.as_ref(), + dapp_definition_account, + &resolved_rules, + &resolved_package_global_caller_rules, + &resolved_user_resources, + configuration.protocol_configuration.protocol_resource, + )?, + }; + + // Creating the adapter components of the various exchange packages that we + // published. + let resolved_adapter_component_addresses = { + let adapter_instantiation_instructions = resolved_blueprint_ids + .exchange_adapter_entities + .clone() + .zip( + configuration + .protocol_configuration + .entities_metadata + .exchange_adapter_entities + .clone(), + ) + .map(|(adapter_package, metadata_init)| { + let mut metadata_init = metadata_init.clone(); + metadata_init.data.insert( + "dapp_definition".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddress( + dapp_definition_account.into(), + )), + lock: false, + }, + ); + + InstructionV1::CallFunction { + package_address: adapter_package.package_address.into(), + blueprint_name: adapter_package.blueprint_name.clone(), + function_name: "instantiate".to_owned(), + args: to_manifest_value(&( + resolved_rules.protocol_manager_badge.clone(), + resolved_rules.protocol_owner_badge.clone(), + metadata_init, + OwnerRole::Fixed( + resolved_rules.protocol_owner_badge.clone(), + ), + None::, + )) + .expect("Impossible!"), + } + }); + + let manifest = TransactionManifestV1 { + instructions: adapter_instantiation_instructions + .iter() + .cloned() + .collect(), + blobs: Default::default(), + }; + + ExchangeIndexedData::from_map( + adapter_instantiation_instructions + .into_map() + .into_iter() + .zip( + execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses, + ) + .map(|((key, _), component_address)| (key, component_address)), + ) + .expect("Cant fail!") + }; + + // Instantiating the oracle component + let oracle_component_address = { + let mut metadata_init = configuration + .protocol_configuration + .entities_metadata + .protocol_entities + .simple_oracle + .clone(); + + metadata_init.data.insert( + "dapp_definition".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddress( + dapp_definition_account.into(), + )), + lock: false, + }, + ); + + let manifest = ManifestBuilder::new() + .call_function( + resolved_blueprint_ids + .protocol_entities + .simple_oracle + .package_address, + resolved_blueprint_ids + .protocol_entities + .simple_oracle + .blueprint_name + .clone(), + "instantiate", + ( + resolved_rules.oracle_manager_badge.clone(), + metadata_init, + OwnerRole::Fixed( + resolved_rules.protocol_owner_badge.clone(), + ), + None::, + ), + ) + .build(); + + execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses + .first() + .copied() + .unwrap() + }; + + // Instantiating the Ignition component + let ignition_component_address = { + let mut metadata_init = configuration + .protocol_configuration + .entities_metadata + .protocol_entities + .ignition + .clone(); + + metadata_init.data.insert( + "dapp_definition".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddress( + dapp_definition_account.into(), + )), + lock: false, + }, + ); + + let ignition_initialization_parameters = + InitializationParametersManifest { + initial_pool_information: Some( + resolved_exchange_data + .clone() + .zip(resolved_adapter_component_addresses) + .iter() + .filter_map( + |(exchange_information, adapter_component)| { + exchange_information.as_ref().map( + |exchange_information| { + ( + exchange_information + .blueprint_id + .clone(), + PoolBlueprintInformation { + adapter: *adapter_component, + allowed_pools: + exchange_information + .pools + .iter() + .copied() + .collect(), + liquidity_receipt: + exchange_information + .liquidity_receipt, + }, + ) + }, + ) + }, + ) + .collect(), + ), + initial_user_resource_volatility: Some( + resolved_user_resources + .zip( + configuration + .protocol_configuration + .user_resource_volatility, + ) + .iter() + .map(|(address, volatility)| (*address, *volatility)) + .collect(), + ), + initial_reward_rates: Some( + configuration.protocol_configuration.reward_rates.clone(), + ), + initial_volatile_protocol_resources: None, + initial_non_volatile_protocol_resources: None, + initial_is_open_position_enabled: Some( + configuration + .protocol_configuration + .allow_opening_liquidity_positions, + ), + initial_is_close_position_enabled: Some( + configuration + .protocol_configuration + .allow_closing_liquidity_positions, + ), + }; + + let manifest = ManifestBuilder::new() + .call_function( + resolved_blueprint_ids + .protocol_entities + .ignition + .package_address, + resolved_blueprint_ids + .protocol_entities + .ignition + .blueprint_name + .clone(), + "instantiate", + ( + metadata_init, + OwnerRole::Fixed( + resolved_rules.protocol_owner_badge.clone(), + ), + resolved_rules.protocol_owner_badge.clone(), + resolved_rules.protocol_manager_badge.clone(), + configuration.protocol_configuration.protocol_resource, + oracle_component_address, + configuration + .protocol_configuration + .maximum_allowed_price_staleness, + configuration + .protocol_configuration + .maximum_allowed_price_difference_percentage, + ignition_initialization_parameters, + None::, + ), + ) + .build(); + execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses + .first() + .copied() + .unwrap() + }; + + let resolved_entity_component_addresses = Entities { + protocol_entities: ProtocolIndexedData { + ignition: ignition_component_address, + simple_oracle: oracle_component_address, + }, + exchange_adapter_entities: resolved_adapter_component_addresses, + }; + + // Submitting the defiplaza pool pair config to the adapter + { + if let Some(ref defiplaza_v2_exchange_information) = + resolved_exchange_data.defiplaza_v2 + { + let pair_config = execution_service + .with_network_connection_provider(|provider| { + defiplaza_v2_exchange_information.pools.try_map(|address| { + provider.read_component_state::(*address) + }) + })?; + + let pair_config_map = defiplaza_v2_exchange_information + .pools + .zip(pair_config) + .iter() + .map(|(pool_address, plaza_pair)| { + (*pool_address, plaza_pair.config) + }) + .collect::>(); + + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_amount( + resolved_badges.protocol_manager_badge.0, + resolved_badges.protocol_manager_badge.1, + dec!(1), + ) + .call_method( + resolved_adapter_component_addresses.defiplaza_v2, + "add_pair_config", + (pair_config_map,), + ) + .build(); + execution_service.execute_manifest(manifest)?; + } + } + + // Caching the information of the Caviarnine pools + { + if let Some(ExchangeInformation { pools, .. }) = + resolved_exchange_data.caviarnine_v1 + { + let instructions = pools + .iter() + .map(|address| InstructionV1::CallMethod { + address: resolved_adapter_component_addresses + .caviarnine_v1 + .into(), + method_name: "preload_pool_information".to_owned(), + args: to_manifest_value(&(*address,)).expect("Can't fail!"), + }) + .collect::>(); + let manifest = TransactionManifestV1 { + instructions, + blobs: Default::default(), + }; + execution_service.execute_manifest(manifest)?; + } + } + + // Setting the dApp definition metadata + { + let claimed_entities = resolved_badges + .iter() + .map(|(_, address)| GlobalAddress::from(*address)) + .chain(resolved_blueprint_ids.exchange_adapter_entities.iter().map( + |blueprint_id| { + GlobalAddress::from(blueprint_id.package_address) + }, + )) + .chain(resolved_blueprint_ids.protocol_entities.iter().map( + |blueprint_id| { + GlobalAddress::from(blueprint_id.package_address) + }, + )) + .chain(resolved_exchange_data.iter().filter_map(|information| { + information.as_ref().map(|information| { + GlobalAddress::from(information.liquidity_receipt) + }) + })) + .chain( + resolved_entity_component_addresses + .exchange_adapter_entities + .iter() + .map(|component_address| { + GlobalAddress::from(*component_address) + }), + ) + .chain( + resolved_entity_component_addresses + .protocol_entities + .iter() + .map(|component_address| { + GlobalAddress::from(*component_address) + }), + ) + .collect::>(); + + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_amount( + resolved_badges.protocol_owner_badge.0, + resolved_badges.protocol_owner_badge.1, + dec!(1), + ) + .set_metadata( + dapp_definition_account, + "claimed_entities", + claimed_entities, + ) + .set_owner_role( + dapp_definition_account, + resolved_rules.protocol_owner_badge.clone(), + ) + .lock_owner_role(dapp_definition_account) + .build(); + execution_service.execute_manifest(manifest)?; + } + + // Depositing the created badges into their accounts. + { + let mut manifest_builder = ManifestBuilder::new(); + for ((current_holder_address, resource_address), handling) in + resolved_badges.zip_borrowed(&configuration.badges).iter() + { + if let BadgeHandling::CreateAndSend { + account_address: destination_account_address, + .. + } = handling + { + manifest_builder = manifest_builder + .withdraw_from_account( + *current_holder_address, + *resource_address, + dec!(1), + ) + .try_deposit_entire_worktop_or_abort( + *destination_account_address, + None, + ) + } + } + let manifest = manifest_builder.build(); + execution_service.execute_manifest(manifest)?; + } + + Ok(PublishingReceipt { + packages: Entities { + protocol_entities: resolved_blueprint_ids + .protocol_entities + .map(|blueprint_id| blueprint_id.package_address), + exchange_adapter_entities: resolved_blueprint_ids + .exchange_adapter_entities + .map(|blueprint_id| blueprint_id.package_address), + }, + components: resolved_entity_component_addresses, + exchange_information: resolved_exchange_data.clone(), + protocol_configuration: ProtocolConfigurationReceipt { + protocol_resource: configuration + .protocol_configuration + .protocol_resource, + user_resource_volatility: configuration + .protocol_configuration + .user_resource_volatility, + reward_rates: configuration + .protocol_configuration + .reward_rates + .clone(), + allow_opening_liquidity_positions: configuration + .protocol_configuration + .allow_opening_liquidity_positions, + allow_closing_liquidity_positions: configuration + .protocol_configuration + .allow_closing_liquidity_positions, + maximum_allowed_price_staleness: configuration + .protocol_configuration + .maximum_allowed_price_staleness, + maximum_allowed_price_difference_percentage: configuration + .protocol_configuration + .maximum_allowed_price_difference_percentage, + user_resources: resolved_user_resources, + registered_pools: resolved_exchange_data.map(|information| { + information.as_ref().map(|information| information.pools) + }), + }, + }) +} + +fn handle_ociswap_v2_exchange_information( + execution_service: &mut ExecutionService, + exchange_information: Option< + &ExchangeInformation, + >, + dapp_definition: ComponentAddress, + badge_rules: &BadgeIndexedData, + entity_package_caller_rules: &Entities, + user_resources: &UserResourceIndexedData, + protocol_resource: ResourceAddress, + additional_information: &AdditionalInformation, +) -> Result< + Option>, + ExecutionServiceError<::Error>, +> { + // No ociswap registry component is passed even through it is needed. + let AdditionalInformation { + ociswap_v2_registry_component: Some(ociswap_v2_registry_component), + } = additional_information + else { + return Ok(None); + }; + + match exchange_information { + Some(exchange_information) => { + // Create the liquidity receipt if it needs to be created. + let liquidity_receipt = match exchange_information.liquidity_receipt + { + LiquidityReceiptHandling::CreateNew { + ref non_fungible_schema, + ref metadata, + } => handle_liquidity_receipt_creation( + execution_service, + non_fungible_schema, + metadata, + dapp_definition, + badge_rules, + entity_package_caller_rules, + )?, + LiquidityReceiptHandling::UseExisting { resource_address } => { + resource_address + } + }; + + // Creating the liquidity pools that need to be created + let pools = + exchange_information.pools.zip(*user_resources).try_map( + |(pool_handling, user_resource_address)| -> Result< + ComponentAddress, + ExecutionServiceError< + ::Error, + >, + > { + let (resource_x, resource_y) = + if *user_resource_address > protocol_resource { + (protocol_resource, *user_resource_address) + } else { + (*user_resource_address, protocol_resource) + }; + + match pool_handling { + PoolHandling::Create => { + let manifest = ManifestBuilder::new() + .call_function( + exchange_information + .blueprint_id + .package_address, + exchange_information + .blueprint_id + .blueprint_name + .clone(), + "instantiate", + ( + resource_x, + resource_y, + pdec!(1.4142135624), + dec!(0.01), + dec!(0.009), + ociswap_v2_registry_component, + Vec::<( + ComponentAddress, + ManifestBucket, + )>::new( + ), + // TODO: Specify their dapp definition? + FAUCET, + ), + ) + .build(); + + Ok(execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses + .first() + .copied() + .unwrap()) + } + PoolHandling::UseExisting { pool_address } => { + Ok(*pool_address) + } + } + }, + )?; + + Ok(Some(ExchangeInformation { + blueprint_id: exchange_information.blueprint_id.clone(), + pools, + liquidity_receipt, + })) + } + None => Ok(None), + } +} + +fn handle_defiplaza_v2_exchange_information( + execution_service: &mut ExecutionService, + exchange_information: Option< + &ExchangeInformation, + >, + dapp_definition: ComponentAddress, + badge_rules: &BadgeIndexedData, + entity_package_caller_rules: &Entities, + user_resources: &UserResourceIndexedData, + protocol_resource: ResourceAddress, +) -> Result< + Option>, + ExecutionServiceError<::Error>, +> { + match exchange_information { + Some(exchange_information) => { + // Create the liquidity receipt if it needs to be created. + let liquidity_receipt = match exchange_information.liquidity_receipt + { + LiquidityReceiptHandling::CreateNew { + ref non_fungible_schema, + ref metadata, + } => handle_liquidity_receipt_creation( + execution_service, + non_fungible_schema, + metadata, + dapp_definition, + badge_rules, + entity_package_caller_rules, + )?, + LiquidityReceiptHandling::UseExisting { resource_address } => { + resource_address + } + }; + + // Creating the liquidity pools that need to be created + let pools = + exchange_information.pools.zip(*user_resources).try_map( + |(pool_handling, user_resource_address)| -> Result< + ComponentAddress, + ExecutionServiceError< + ::Error, + >, + > { + match pool_handling { + PoolHandling::Create => { + let manifest = ManifestBuilder::new() + .call_function( + exchange_information + .blueprint_id + .package_address, + exchange_information + .blueprint_id + .blueprint_name + .clone(), + "instantiate_pair", + ( + OwnerRole::None, + user_resource_address, + protocol_resource, + PairConfig { + k_in: dec!(1), + k_out: dec!(1.5), + fee: dec!(0.01), + decay_factor: dec!(0.9995), + }, + dec!(1), + ), + ) + .build(); + + Ok(execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses + .first() + .copied() + .unwrap()) + } + PoolHandling::UseExisting { pool_address } => { + Ok(*pool_address) + } + } + }, + )?; + + Ok(Some(ExchangeInformation { + blueprint_id: exchange_information.blueprint_id.clone(), + pools, + liquidity_receipt, + })) + } + None => Ok(None), + } +} + +fn handle_caviarnine_v1_exchange_information( + execution_service: &mut ExecutionService, + exchange_information: Option< + &ExchangeInformation, + >, + dapp_definition: ComponentAddress, + badge_rules: &BadgeIndexedData, + entity_package_caller_rules: &Entities, + user_resources: &UserResourceIndexedData, + protocol_resource: ResourceAddress, +) -> Result< + Option>, + ExecutionServiceError<::Error>, +> { + match exchange_information { + Some(exchange_information) => { + // Create the liquidity receipt if it needs to be created. + let liquidity_receipt = match exchange_information.liquidity_receipt + { + LiquidityReceiptHandling::CreateNew { + ref non_fungible_schema, + ref metadata, + } => handle_liquidity_receipt_creation( + execution_service, + non_fungible_schema, + metadata, + dapp_definition, + badge_rules, + entity_package_caller_rules, + )?, + LiquidityReceiptHandling::UseExisting { resource_address } => { + resource_address + } + }; + + // Creating the liquidity pools that need to be created + let pools = + exchange_information.pools.zip(*user_resources).try_map( + |(pool_handling, user_resource_address)| -> Result< + ComponentAddress, + ExecutionServiceError< + ::Error, + >, + > { + match pool_handling { + PoolHandling::Create => { + let manifest = ManifestBuilder::new() + .call_function( + exchange_information + .blueprint_id + .package_address, + exchange_information + .blueprint_id + .blueprint_name + .clone(), + "new", + ( + rule!(allow_all), + rule!(allow_all), + user_resource_address, + protocol_resource, + 100u32, + None::, + ), + ) + .build(); + + Ok(execution_service + .execute_manifest(manifest)? + .new_entities + .new_component_addresses + .first() + .copied() + .unwrap()) + } + PoolHandling::UseExisting { pool_address } => { + Ok(*pool_address) + } + } + }, + )?; + + Ok(Some(ExchangeInformation { + blueprint_id: exchange_information.blueprint_id.clone(), + pools, + liquidity_receipt, + })) + } + None => Ok(None), + } +} + +fn handle_liquidity_receipt_creation( + execution_service: &mut ExecutionService, + non_fungible_schema: &NonFungibleDataSchema, + metadata_init: &MetadataInit, + dapp_definition_account: ComponentAddress, + badge_rules: &BadgeIndexedData, + entity_package_caller_rules: &Entities, +) -> Result< + ResourceAddress, + ExecutionServiceError<::Error>, +> { + // Adding the dapp definition to the metadata + let mut metadata_init = metadata_init.clone(); + metadata_init.data.insert( + "dapp_definitions".to_owned(), + KeyValueStoreInitEntry { + value: Some(MetadataValue::GlobalAddressArray(vec![ + dapp_definition_account.into(), + ])), + lock: false, + }, + ); + + let manifest = ManifestBuilder::new() + .call_function( + RESOURCE_PACKAGE, + NON_FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT.to_owned(), + NON_FUNGIBLE_RESOURCE_MANAGER_CREATE_RUID_WITH_INITIAL_SUPPLY_IDENT.to_owned(), + NonFungibleResourceManagerCreateRuidWithInitialSupplyManifestInput { + owner_role: OwnerRole::Fixed(badge_rules.protocol_owner_badge.clone()), + track_total_supply: true, + non_fungible_schema: non_fungible_schema.clone(), + entries: Default::default(), + resource_roles: NonFungibleResourceRoles { + // Mintable and burnable by the Ignition package and the + // protocol owner can update who can do that. + mint_roles: mint_roles! { + minter => entity_package_caller_rules.protocol_entities.ignition.clone(); + minter_updater => badge_rules.protocol_owner_badge.clone(); + }, + burn_roles: burn_roles! { + burner => entity_package_caller_rules.protocol_entities.ignition.clone(); + burner_updater => badge_rules.protocol_owner_badge.clone(); + }, + // The protocol owner reserves the rights to update the data + // of the non-fungibles as they see fit. + non_fungible_data_update_roles: non_fungible_data_update_roles! { + non_fungible_data_updater => rule!(deny_all); + non_fungible_data_updater_updater => badge_rules.protocol_owner_badge.clone(); + }, + // Everything else is deny all and can't be changed. + recall_roles: recall_roles! { + recaller => rule!(deny_all); + recaller_updater => rule!(deny_all); + }, + freeze_roles: freeze_roles! { + freezer => rule!(deny_all); + freezer_updater => rule!(deny_all); + }, + deposit_roles: deposit_roles! { + depositor => rule!(allow_all); + depositor_updater => rule!(deny_all); + }, + withdraw_roles: withdraw_roles! { + withdrawer => rule!(allow_all); + withdrawer_updater => rule!(deny_all); + }, + }, + metadata: ModuleConfig { + init: metadata_init, + roles: metadata_roles! { + metadata_setter => badge_rules.protocol_owner_badge.clone(); + metadata_setter_updater => badge_rules.protocol_owner_badge.clone(); + metadata_locker => badge_rules.protocol_owner_badge.clone(); + metadata_locker_updater => badge_rules.protocol_owner_badge.clone(); + } + }, + address_reservation: None, + }, + ) + .build(); + + execution_service + .execute_manifest(manifest) + .map(|new_entities| { + new_entities + .new_entities + .new_resource_addresses + .first() + .copied() + .unwrap() + }) +} + +#[derive(Debug)] +pub enum PublishingError { + NetworkConnectionProviderError(E), + ExecutionServiceError(ExecutionServiceError), +} + +impl From for PublishingError { + fn from(value: E) -> Self { + Self::NetworkConnectionProviderError(value) + } +} + +impl From> for PublishingError { + fn from(value: ExecutionServiceError) -> Self { + Self::ExecutionServiceError(value) + } +} diff --git a/tools/publishing-tool/src/publishing/macros.rs b/tools/publishing-tool/src/publishing/macros.rs new file mode 100644 index 00000000..d3e4f67e --- /dev/null +++ b/tools/publishing-tool/src/publishing/macros.rs @@ -0,0 +1,134 @@ +macro_rules! name_indexed_struct { + ( + $(#[$meta: meta])* + $struct_vis: vis struct $struct_ident: ident <$generic: ident> { + $( + $(#[$field_meta: meta])* + $field_vis: vis $field_ident: ident: $field_ty: ty + ),* $(,)? + } + ) => { + // Pass through the struct definition + $(#[$meta])* + $struct_vis struct $struct_ident <$generic> { + $( + $(#[$field_meta])* + $field_vis $field_ident: $field_ty + ),* + } + + impl<$generic> $struct_ident<$generic> { + // Map function + pub fn map(&self, mut map: F) -> $struct_ident + where + F: FnMut(&$generic) -> O, + { + $struct_ident:: { + $( + $field_ident: map(&self.$field_ident) + ),* + } + } + + // Map owned function + pub fn map_owned(self, mut map: F) -> $struct_ident + where + F: FnMut($generic) -> O, + { + $struct_ident:: { + $( + $field_ident: map(self.$field_ident) + ),* + } + } + + // Map function + pub fn try_map(&self, mut map: F) -> Result<$struct_ident, E> + where + F: FnMut(&$generic) -> Result, + { + Ok($struct_ident:: { + $( + $field_ident: map(&self.$field_ident)? + ),* + }) + } + + // Zip two together + pub fn zip(self, other: $struct_ident) -> $struct_ident<($generic, Other)> { + $struct_ident { + $( + $field_ident: (self.$field_ident, other.$field_ident) + ),* + } + } + + pub fn zip_borrowed(self, other: &$struct_ident) -> $struct_ident<($generic, &Other)> { + $struct_ident { + $( + $field_ident: (self.$field_ident, &other.$field_ident) + ),* + } + } + + // Creating from a map + pub fn from_map( + map: M + ) -> Result + where + M: IntoIterator, + S: AsRef + { + $( + let mut $field_ident = None::<$generic>; + )* + + for (key, value) in map.into_iter() { + match key.as_ref() { + $( + stringify!($field_ident) => { + $field_ident = Some(value) + } + ),* + _ => {} + } + } + + $( + let $field_ident = $field_ident + .ok_or_else( + || $crate::publishing::KeyNotFound { + key: stringify!($field_ident).to_owned() + } + )?; + )* + + Ok($struct_ident { + $( + $field_ident + ),* + }) + } + + pub fn iter(&self) -> impl Iterator { + vec![ + $( + &self.$field_ident + ),* + ].into_iter() + } + + // Creating a map of everything in the name indexed struct + pub fn into_map(&self) -> ::radix_engine_common::prelude::IndexMap<&'static str, &$generic> { + let mut map = ::radix_engine_common::prelude::IndexMap::<&'static str, &$generic>::new(); + + $( + map.insert(stringify!($field_ident), &self.$field_ident); + )* + + map + } + } + }; +} +pub(super) use name_indexed_struct; diff --git a/tools/publishing-tool/src/publishing/mod.rs b/tools/publishing-tool/src/publishing/mod.rs new file mode 100644 index 00000000..52b4e0c2 --- /dev/null +++ b/tools/publishing-tool/src/publishing/mod.rs @@ -0,0 +1,9 @@ +mod configuration; +#[macro_use] +mod macros; +mod error; +mod handler; + +pub use configuration::*; +pub use error::*; +pub use handler::*; diff --git a/tools/publishing-tool/src/utils.rs b/tools/publishing-tool/src/utils.rs new file mode 100644 index 00000000..c2e376fe --- /dev/null +++ b/tools/publishing-tool/src/utils.rs @@ -0,0 +1,39 @@ +use sbor::representations::SerializationParameters; +use transaction::prelude::*; + +pub fn clone_private_key(private_key: &PrivateKey) -> PrivateKey { + match private_key { + PrivateKey::Secp256k1(private_key) => PrivateKey::Secp256k1( + Secp256k1PrivateKey::from_bytes(&private_key.to_bytes()).unwrap(), + ), + PrivateKey::Ed25519(private_key) => PrivateKey::Ed25519( + Ed25519PrivateKey::from_bytes(&private_key.to_bytes()).unwrap(), + ), + } +} + +pub fn to_json( + value: &S, + network_definition: &NetworkDefinition, +) -> String { + let encoder = AddressBech32Encoder::new(network_definition); + + let (local_type_id, schema) = + generate_full_schema_from_single_type::(); + let schema = schema.into_latest(); + + let context = + ScryptoValueDisplayContext::with_optional_bech32(Some(&encoder)); + let payload = scrypto_encode(value).unwrap(); + let raw_payload = ScryptoRawPayload::new_from_valid_slice(&payload); + let serializable = + raw_payload.serializable(SerializationParameters::WithSchema { + mode: representations::SerializationMode::Natural, + custom_context: context, + schema: &schema, + type_id: local_type_id, + depth_limit: SCRYPTO_SBOR_V1_MAX_DEPTH, + }); + + serde_json::to_string_pretty(&serializable).unwrap() +} From 1870bb0a2f9366bdc91b46ecd030e6ac99805aa7 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 10:08:27 +0300 Subject: [PATCH 17/47] [Publishing Tool]: Misc changes. --- .../src/cli/default_configurations/mainnet_testing.rs | 4 ++-- tools/publishing-tool/src/cli/publish.rs | 2 -- tools/publishing-tool/src/main.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs index 0d15cbdc..f22b1969 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs +++ b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs @@ -75,9 +75,9 @@ pub fn mainnet_testing( "icon_url".to_owned() => MetadataValue::Url(UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png")) }, transaction_configuration: TransactionConfiguration { - notary: clone_private_key(¬ary_private_key), + notary: clone_private_key(notary_private_key), fee_payer_information: AccountAndControllingKey::new_virtual_account( - clone_private_key(¬ary_private_key), + clone_private_key(notary_private_key), ), }, // TODO: Determine where they should be sent to. diff --git a/tools/publishing-tool/src/cli/publish.rs b/tools/publishing-tool/src/cli/publish.rs index b5664862..3815d301 100644 --- a/tools/publishing-tool/src/cli/publish.rs +++ b/tools/publishing-tool/src/cli/publish.rs @@ -2,8 +2,6 @@ use super::default_configurations::*; use crate::utils::*; use crate::*; use clap::Parser; -use common::prelude::LockupPeriod; -use defiplaza_v2_adapter_v1::DefiPlazaV2PoolInterfaceManifestBuilderExtensionTrait; use radix_engine_common::prelude::*; use state_manager::RocksDBStore; use std::path::*; diff --git a/tools/publishing-tool/src/main.rs b/tools/publishing-tool/src/main.rs index 341c3d0c..dd1d9a88 100644 --- a/tools/publishing-tool/src/main.rs +++ b/tools/publishing-tool/src/main.rs @@ -1,4 +1,4 @@ -#![allow(dead_code, clippy::enum_variant_names)] +#![allow(dead_code, clippy::enum_variant_names, clippy::wrong_self_convention)] mod cli; mod database_overlay; From 7c0ddc09023512c94b1f27852ef6e14ea5c9a76a Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 11:47:03 +0300 Subject: [PATCH 18/47] [Defiplaza v2 Adapter v1]: Some cleanups and tests --- Cargo.lock | 13 +++ packages/defiplaza-v2-adapter-v1/src/lib.rs | 40 +++------ packages/ignition/src/blueprint.rs | 2 +- tests/tests/defiplaza_v2.rs | 94 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0461d07..6c9a7f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,19 @@ dependencies = [ "transaction", ] +[[package]] +name = "defiplaza-v2-adapter-v1" +version = "0.1.0" +dependencies = [ + "common", + "ports-interface", + "radix-engine-interface", + "sbor", + "scrypto", + "scrypto-interface", + "transaction", +] + [[package]] name = "deranged" version = "0.3.11" diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 38ca81fa..0a99dda0 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -6,8 +6,6 @@ use ports_interface::prelude::*; use scrypto::prelude::*; use scrypto_interface::*; -// TODO: Remove all logging. - macro_rules! define_error { ( $( @@ -59,7 +57,8 @@ pub mod adapter { /// The pair config of the various pools is constant but there is no /// getter function that can be used to get it on ledger. As such, the /// protocol owner or manager must submit this information to the - /// adapter for its operation. + /// adapter for its operation. This does not change, so, once set we + /// do not expect to remove it again. pair_config: KeyValueStore, } @@ -230,11 +229,6 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - info!("Doing the first one"); - info!( - "Shortage before first contribution: {:?}", - pool.get_state().shortage - ); let (first_pool_units, second_change) = match shortage_state { ShortageState::Equilibrium => ( pool.add_liquidity(first_bucket, None).0, @@ -244,10 +238,6 @@ pub mod adapter { pool.add_liquidity(first_bucket, Some(second_bucket)) } }; - info!( - "Shortage after first contribution: {:?}", - pool.get_state().shortage - ); // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets @@ -256,17 +246,19 @@ pub mod adapter { let second_original_target = second_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - info!("Doing the second one"); let (second_pool_units, change) = pool.add_liquidity(second_bucket, None); - info!( - "Shortage after second contribution: {:?}", - pool.get_state().shortage - ); - // TODO: Should we subtract the change from the second original - // target? Seems like we should if the price if not the same in - // some way? + // We've been told that the change should be zero. Therefore, we + // assert for it to make sure that everything is as we expect it + // to be. + assert_eq!( + change + .as_ref() + .map(|bucket| bucket.amount()) + .unwrap_or(Decimal::ZERO), + Decimal::ZERO + ); // A sanity check to make sure that everything is correct. The pool // units obtained from the first contribution should be different @@ -433,7 +425,6 @@ pub mod adapter { Global::::from(base_pool), Global::::from(quote_pool), ); - info!("bid ask = {bid_ask:?}"); let average_price = bid_ask .bid @@ -441,7 +432,6 @@ pub mod adapter { .and_then(|value| value.checked_div(dec!(2))) .expect(OVERFLOW_ERROR); - info!("average_price = {average_price}"); Price { base: base_resource_address, quote: quote_resource_address, @@ -488,8 +478,8 @@ impl From for AnyValue { // source code is licensed under the MIT license which allows us to do such // copies and modification of code. // -// This module exposes two main functions which are the entrypoints into this -// module's functionality which calculate the incoming and outgoing spot prices. +// The `calculate_pair_prices` function is the entrypoint into the module and is +// the function to calculate the current bid and ask prices of the pairs. #[allow(clippy::arithmetic_side_effects)] mod price_math { use super::*; @@ -564,8 +554,6 @@ mod price_math { let bid = incoming_spot; let ask = outgoing_spot; - info!("Shortage = {:?}", pair_state.shortage); - // TODO: What to do at equilibrium? match pair_state.shortage { Shortage::Equilibrium | Shortage::BaseShortage => { diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 6f1cb30f..edbe013a 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -579,7 +579,7 @@ mod ignition { value .checked_round(17, RoundingMode::ToPositiveInfinity) }) - .expect(OVERFLOW_ERROR); + .unwrap_or(Decimal::MAX); assert!( pool_reported_value_of_user_resource_in_protocol_resource <= maximum_amount, diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 78711179..ff3cabe6 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -819,3 +819,97 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_base_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = base_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = quote_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} From ecd6018ea778734d08a331415c47ba23730ba1ab Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 11:59:01 +0300 Subject: [PATCH 19/47] [Misc]: Fix cargo.lock issue --- Cargo.lock | 71 ++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c9a7f05..b7db6fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,10 +288,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" dependencies = [ + "jobserver", "libc", ] @@ -580,19 +581,6 @@ dependencies = [ "transaction", ] -[[package]] -name = "defiplaza-v2-adapter-v1" -version = "0.1.0" -dependencies = [ - "common", - "ports-interface", - "radix-engine-interface", - "sbor", - "scrypto", - "scrypto-interface", - "transaction", -] - [[package]] name = "deranged" version = "0.3.11" @@ -933,9 +921,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1116,11 +1104,20 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1222,9 +1219,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2842,9 +2839,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -2873,9 +2870,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2883,9 +2880,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -2898,9 +2895,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2910,9 +2907,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2920,9 +2917,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -2933,9 +2930,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -3067,9 +3064,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", From 230101eb86a8a9dab4d66c1328369147e1b0629b Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 12:25:02 +0300 Subject: [PATCH 20/47] [Defiplaza v2 Adapter v1]: Small correction --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 0a99dda0..ac106f4e 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -13,7 +13,7 @@ macro_rules! define_error { )* ) => { $( - pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v2]", " ", $item); + pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v1]", " ", $item); )* }; } From fd26ddf374192aa75dddf067ddc54745228f5c48 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 15:18:53 +0300 Subject: [PATCH 21/47] [Ociswap v2 Adapter v1]: Add the global id of the underlying LP. --- packages/ociswap-v2-adapter-v1/src/lib.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ociswap-v2-adapter-v1/src/lib.rs b/packages/ociswap-v2-adapter-v1/src/lib.rs index 3ad18d58..72099637 100644 --- a/packages/ociswap-v2-adapter-v1/src/lib.rs +++ b/packages/ociswap-v2-adapter-v1/src/lib.rs @@ -171,12 +171,22 @@ pub mod adapter { let (receipt, change_x, change_y) = pool.add_liquidity(lower_tick, upper_tick, bucket_x, bucket_y); + let non_fungible_global_id = NonFungibleGlobalId::new( + receipt.as_non_fungible().resource_address(), + receipt.as_non_fungible().non_fungible_local_id(), + ); + OpenLiquidityPositionOutput { pool_units: IndexedBuckets::from_bucket(receipt), change: IndexedBuckets::from_buckets([change_x, change_y]), others: Default::default(), - adapter_specific_information: AnyValue::from_typed(&()) - .expect(UNEXPECTED_ERROR), + adapter_specific_information: AnyValue::from_typed( + &OciswapV2AdapterSpecificInformation { + liquidity_receipt_non_fungible_global_id: + non_fungible_global_id.clone(), + }, + ) + .expect(UNEXPECTED_ERROR), } } @@ -246,7 +256,10 @@ pub mod adapter { } #[derive(ScryptoSbor, Debug, Clone)] -pub struct OciswapV2AdapterSpecificInformation {} +pub struct OciswapV2AdapterSpecificInformation { + /// Stores the non-fungible global id of the liquidity receipt. + pub liquidity_receipt_non_fungible_global_id: NonFungibleGlobalId, +} impl From for AnyValue { fn from(value: OciswapV2AdapterSpecificInformation) -> Self { From e150d04bc8289128a0f4e776403e8ed469e5d1ce Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 16:37:04 +0300 Subject: [PATCH 22/47] [Publishing Tool]: Allow the publishing tool to run against stokenet --- Cargo.lock | 1 + tools/publishing-tool/Cargo.toml | 1 + .../default_configurations/mainnet_testing.rs | 1 + .../src/cli/default_configurations/mod.rs | 7 + .../stokenet_testing.rs | 243 ++++++++++++++++++ tools/publishing-tool/src/cli/publish.rs | 53 ++-- tools/publishing-tool/src/error.rs | 3 + .../src/publishing/configuration.rs | 23 ++ .../publishing-tool/src/publishing/handler.rs | 50 ++++ 9 files changed, 360 insertions(+), 22 deletions(-) create mode 100644 tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs diff --git a/Cargo.lock b/Cargo.lock index 617daa46..13486e6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2050,6 +2050,7 @@ dependencies = [ name = "publishing-tool" version = "0.1.0" dependencies = [ + "bitflags 2.4.2", "caviarnine-v1-adapter-v1", "clap", "common", diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml index a3d462e3..c7735f98 100644 --- a/tools/publishing-tool/Cargo.toml +++ b/tools/publishing-tool/Cargo.toml @@ -43,6 +43,7 @@ hex-literal = "0.4.1" itertools = "0.12.1" serde_json = "1.0.114" clap = { version = "4.5.1", features = ["derive"] } +bitflags = "2.4.2" [lints] workspace = true diff --git a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs index f22b1969..20d6f239 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs +++ b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs @@ -299,6 +299,7 @@ pub fn mainnet_testing( additional_information: AdditionalInformation { ociswap_v2_registry_component: None, }, + additional_operation_flags: AdditionalOperationFlags::empty() // cSpell:enable } } diff --git a/tools/publishing-tool/src/cli/default_configurations/mod.rs b/tools/publishing-tool/src/cli/default_configurations/mod.rs index 49a6f636..fa6d4188 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mod.rs +++ b/tools/publishing-tool/src/cli/default_configurations/mod.rs @@ -1,10 +1,12 @@ use crate::*; use clap::*; mod mainnet_testing; +mod stokenet_testing; #[derive(ValueEnum, Clone, Copy, Debug)] pub enum ConfigurationSelector { MainnetTesting, + StokenetTesting, } impl ConfigurationSelector { @@ -16,18 +18,23 @@ impl ConfigurationSelector { Self::MainnetTesting => { mainnet_testing::mainnet_testing(notary_private_key) } + Self::StokenetTesting => { + stokenet_testing::stokenet_testing(notary_private_key) + } } } pub fn gateway_base_url(self) -> String { match self { Self::MainnetTesting => "https://mainnet.radixdlt.com".to_owned(), + Self::StokenetTesting => "https://stokenet.radixdlt.com".to_owned(), } } pub fn network_definition(self) -> NetworkDefinition { match self { Self::MainnetTesting => NetworkDefinition::mainnet(), + Self::StokenetTesting => NetworkDefinition::stokenet(), } } } diff --git a/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs new file mode 100644 index 00000000..9a273334 --- /dev/null +++ b/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs @@ -0,0 +1,243 @@ +use common::prelude::*; + +use self::utils::*; +use crate::*; + +pub fn stokenet_testing( + notary_private_key: &PrivateKey, +) -> PublishingConfiguration { + let notary_account_address = + ComponentAddress::virtual_account_from_public_key( + ¬ary_private_key.public_key(), + ); + + // cSpell:disable + PublishingConfiguration { + protocol_configuration: ProtocolConfiguration { + protocol_resource: XRD, + user_resource_volatility: UserResourceIndexedData { + bitcoin: Volatility::Volatile, + ethereum: Volatility::Volatile, + usdc: Volatility::NonVolatile, + usdt: Volatility::NonVolatile, + }, + reward_rates: indexmap! { + LockupPeriod::from_minutes(0).unwrap() => dec!(0.125), // 12.5% + LockupPeriod::from_minutes(1).unwrap() => dec!(0.15), // 15.0% + }, + allow_opening_liquidity_positions: true, + allow_closing_liquidity_positions: true, + maximum_allowed_price_staleness: i64::MAX, + maximum_allowed_price_difference_percentage: Decimal::MAX, + entities_metadata: Entities { + protocol_entities: ProtocolIndexedData { + ignition: metadata_init! { + "name" => "Ignition", updatable; + "description" => "The main entrypoint into the Ignition liquidity incentive program.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + simple_oracle: metadata_init! { + "name" => "Ignition Oracle", updatable; + "description" => "The oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with Ociswap v2 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + defiplaza_v2: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with DefiPlaza v2 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + caviarnine_v1: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol to communicate with Caviarnine v1 pools.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + }, + dapp_definition_metadata: indexmap! { + "name".to_owned() => MetadataValue::String("Project Ignition".to_owned()), + "description".to_owned() => MetadataValue::String("A Radix liquidity incentives program, offered in partnership with select decentralized exchange dApps in the Radix ecosystem.".to_owned()), + "icon_url".to_owned() => MetadataValue::Url(UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png")) + }, + transaction_configuration: TransactionConfiguration { + notary: clone_private_key(notary_private_key), + fee_payer_information: AccountAndControllingKey::new_virtual_account( + clone_private_key(notary_private_key), + ), + }, + // TODO: Determine where they should be sent to. + badges: BadgeIndexedData { + oracle_manager_badge: BadgeHandling::CreateAndSend { + account_address: notary_account_address, + metadata_init: metadata_init! { + "name" => "Ignition Oracle Manager", updatable; + "symbol" => "IGNOM", updatable; + "description" => "A badge with the authority to update the Oracle prices of the Ignition oracle.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_manager_badge: BadgeHandling::CreateAndSend { + account_address: notary_account_address, + metadata_init: metadata_init! { + "name" => "Ignition Protocol Manager", updatable; + "symbol" => "IGNPM", updatable; + "description" => "A badge with the authority to manage the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_owner_badge: BadgeHandling::CreateAndSend { + account_address: notary_account_address, + metadata_init: metadata_init! { + "name" => "Ignition Protocol Owner", updatable; + "symbol" => "IGNPO", updatable; + "description" => "A badge with owner authority over the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + // TODO: Not real resources, just the notXYZ resources. + user_resources: UserResourceIndexedData { + bitcoin: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_tdx_2_1thltk578jr4v7axqpu5ceznhlha6ca2qtzcflqdmytgtf37xncu7l9" + ), + }, + ethereum: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_tdx_2_1t59gx963vzd6u6fz63h5de2zh9nmgwxc8y832edmr6pxvz98wg6zu3" + ), + }, + usdc: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_tdx_2_1thfv477eqwlh8x4wt6xsc62myt4z0zxmdpr4ea74fa8jnxh243y60r" + ), + }, + usdt: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_tdx_2_1t4p3ytx933n576pdps4ua7jkjh36zrh36a543u0tfcsu2vthavlqg8" + ), + }, + }, + packages: Entities { + protocol_entities: ProtocolIndexedData { + ignition: PackageHandling::LoadAndPublish { + crate_package_name: "ignition".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Package", updatable; + "description" => "The implementation of the Ignition protocol.", updatable; + "tags" => Vec::::new(), updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "Ignition".to_owned(), + }, + simple_oracle: PackageHandling::LoadAndPublish { + crate_package_name: "simple-oracle".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Simple Oracle Package", updatable; + "description" => "The implementation of the Oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "SimpleOracle".to_owned(), + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: PackageHandling::LoadAndPublish { + crate_package_name: "ociswap-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for Ociswap v2 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "OciswapV2Adapter".to_owned(), + }, + defiplaza_v2: PackageHandling::LoadAndPublish { + crate_package_name: "defiplaza-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for DefiPlaza v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "DefiPlazaV2Adapter".to_owned(), + }, + caviarnine_v1: PackageHandling::LoadAndPublish { + crate_package_name: "caviarnine-v1-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter Package", updatable; + "description" => "The implementation of an adapter for Caviarnine v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "CaviarnineV1Adapter".to_owned(), + }, + }, + }, + exchange_information: ExchangeIndexedData { + // No ociswap v2 currently on mainnet. + ociswap_v2: Some(ExchangeInformation { + blueprint_id: BlueprintId { + package_address: package_address!( + "package_tdx_2_1phgf5er6zx60wu4jjhtps97akqjpv787f6k7rjqkxgdpacng89a4uz" + ), + blueprint_name: "LiquidityPool".to_owned(), + }, + pools: UserResourceIndexedData { + bitcoin: PoolHandling::Create, + ethereum: PoolHandling::Create, + usdc: PoolHandling::Create, + usdt: PoolHandling::Create, + }, + liquidity_receipt: LiquidityReceiptHandling::CreateNew { + non_fungible_schema: + NonFungibleDataSchema::new_local_without_self_package_replacement::< + LiquidityReceipt, + >(), + metadata: metadata_init! { + "name" => "Ignition LP: Ociswap", updatable; + "description" => "Represents a particular contribution of liquidity to Ociswap through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; + "tags" => vec!["lp token"], updatable; + "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png"), updatable; + "DEX" => "Ociswap", updatable; + // TODO: Must get this from the DEX + "redeem_url" => UncheckedUrl::of("https://www.google.com"), updatable; + }, + }, + }), + caviarnine_v1: None, + defiplaza_v2: None, + }, + additional_information: AdditionalInformation { + ociswap_v2_registry_component: Some(component_address!( + "component_tdx_2_1cpwm3sjxr48gmsnh7lgmh5de3eqqzthqkazztc4qv6n3fvedgjepwk" + )), + }, + additional_operation_flags: AdditionalOperationFlags::SUBMIT_ORACLE_PRICES_OF_ONE + // cSpell:enable + } +} diff --git a/tools/publishing-tool/src/cli/publish.rs b/tools/publishing-tool/src/cli/publish.rs index 3815d301..31e1daeb 100644 --- a/tools/publishing-tool/src/cli/publish.rs +++ b/tools/publishing-tool/src/cli/publish.rs @@ -12,11 +12,15 @@ pub struct Publish { /// The configuration that the user wants to use when publishing. configuration_selector: ConfigurationSelector, - /// The path to the state manager database. - state_manager_database_path: PathBuf, - /// The hex-encoded private key of the notary. notary_ed25519_private_key_hex: String, + + /// The path to the state manager database. If no path is provided for the + /// state manager database then it will be assumed that the user does not + /// wish to do a simulation before publishing and is comfortable doing an + /// actual run straightaway. + #[clap(short, long)] + state_manager_database_path: Option, } impl Publish { @@ -33,18 +37,31 @@ impl Publish { let configuration = self .configuration_selector .configuration(¬ary_private_key); - - // Creating the network connection providers to use for the deployments let network_definition = self.configuration_selector.network_definition(); + + // Creating the network connection providers to use for the deployments + if let Some(state_manager_database_path) = + self.state_manager_database_path + { + let database = + RocksDBStore::new_read_only(state_manager_database_path) + .map_err(Error::RocksDbOpenError)?; + + let mut simulator_network_provider = SimulatorNetworkConnector::new( + &database, + network_definition.clone(), + ); + + // Running a dry run of the publishing process against the simulator + // network provider. + log::info!("Publishing against the simulator"); + publish(&configuration, &mut simulator_network_provider)?; + } + + // Running the transactions against the network. + log::info!("Publishing against the gateway"); let gateway_base_url = self.configuration_selector.gateway_base_url(); - let database = - RocksDBStore::new_read_only(self.state_manager_database_path) - .unwrap(); - let mut simulator_network_provider = SimulatorNetworkConnector::new( - &database, - network_definition.clone(), - ); let mut gateway_network_provider = GatewayNetworkConnector::new( gateway_base_url, network_definition.clone(), @@ -53,16 +70,8 @@ impl Publish { retries: 10, }, ); - - // Running a dry run of the publishing process against the simulator - // network provider. - log::info!("Publishing against the simulator"); - publish(&configuration, &mut simulator_network_provider)?; - - // Running the transactions against the network. - log::info!("Publishing against the gateway"); let receipt = publish(&configuration, &mut gateway_network_provider)?; - writeln!(f, "{}", to_json(&receipt, &network_definition)).unwrap(); - Ok(()) + writeln!(f, "{}", to_json(&receipt, &network_definition)) + .map_err(Error::IoError) } } diff --git a/tools/publishing-tool/src/error.rs b/tools/publishing-tool/src/error.rs index 42cdceaa..20ecbca7 100644 --- a/tools/publishing-tool/src/error.rs +++ b/tools/publishing-tool/src/error.rs @@ -1,10 +1,13 @@ use crate::*; +use state_manager::traits::*; #[derive(Debug)] pub enum Error { PrivateKeyError, GatewayExecutorError(PublishingError), SimulatorExecutorError(PublishingError), + IoError(std::io::Error), + RocksDbOpenError(DatabaseConfigValidationError), } impl From> for Error { diff --git a/tools/publishing-tool/src/publishing/configuration.rs b/tools/publishing-tool/src/publishing/configuration.rs index 9125fe9a..510734d2 100644 --- a/tools/publishing-tool/src/publishing/configuration.rs +++ b/tools/publishing-tool/src/publishing/configuration.rs @@ -44,6 +44,10 @@ pub struct PublishingConfiguration { /// Additional information that doesn't quite fit into any of the above /// categories nicely. pub additional_information: AdditionalInformation, + + /// Bit flags for additional operations that can be done by the publishing + /// logic during the publishing process. + pub additional_operation_flags: AdditionalOperationFlags, } #[derive(Debug, Clone, ScryptoSbor)] @@ -54,6 +58,25 @@ pub struct PublishingReceipt { Option>, >, pub protocol_configuration: ProtocolConfigurationReceipt, + pub badges: BadgeIndexedData, +} + +bitflags::bitflags! { + /// Additional operations that the publishing process can be instructed to + /// perform. + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct AdditionalOperationFlags: u8 { + /// Submits prices to the oracle that are just one for all of the assets + /// supported in the deployment. + const SUBMIT_ORACLE_PRICES_OF_ONE = 0b00000001; + + /// Provide initial liquidity to Ignition. How this is done depends on + /// the selected protocol resource. If it is XRD then the publisher will + /// attempt to get the XRD from the faucet. If otherwise then it will + /// attempt to mint it. + const PROVIDE_INITIAL_IGNITION_LIQUIDITY = 0b00000010; + } } #[derive(Debug, Clone, ScryptoSbor)] diff --git a/tools/publishing-tool/src/publishing/handler.rs b/tools/publishing-tool/src/publishing/handler.rs index 769af58e..f53e9358 100644 --- a/tools/publishing-tool/src/publishing/handler.rs +++ b/tools/publishing-tool/src/publishing/handler.rs @@ -927,6 +927,55 @@ pub fn publish( execution_service.execute_manifest(manifest)?; } + // Processing the additional operations specified in the publishing config + { + // Submitting prices to the oracle with (user_asset, protocol_asset) + // and (protocol_asset, user_asset) where the price of both is equal + // to one. + if configuration + .additional_operation_flags + .contains(AdditionalOperationFlags::SUBMIT_ORACLE_PRICES_OF_ONE) + { + let price_updates = resolved_user_resources + .iter() + .copied() + .flat_map(|address| { + [ + ( + address, + configuration + .protocol_configuration + .protocol_resource, + ), + ( + configuration + .protocol_configuration + .protocol_resource, + address, + ), + ] + }) + .map(|address_pair| (address_pair, dec!(1))) + .collect::>(); + + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_amount( + resolved_badges.oracle_manager_badge.0, + resolved_badges.oracle_manager_badge.1, + dec!(1), + ) + .call_method( + resolved_entity_component_addresses + .protocol_entities + .simple_oracle, + "set_price_batch", + (price_updates,), + ) + .build(); + execution_service.execute_manifest(manifest)?; + } + } + // Depositing the created badges into their accounts. { let mut manifest_builder = ManifestBuilder::new(); @@ -993,6 +1042,7 @@ pub fn publish( information.as_ref().map(|information| information.pools) }), }, + badges: resolved_badges.map(|(_, address)| *address), }) } From 12692831529a6985c7b760a82d5ceb815b913d8e Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 16:46:16 +0300 Subject: [PATCH 23/47] [Ociswap v2 Adapter v1]: Add non-fungible data to adapter specific data. --- packages/ociswap-v2-adapter-v1/src/lib.rs | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/ociswap-v2-adapter-v1/src/lib.rs b/packages/ociswap-v2-adapter-v1/src/lib.rs index 72099637..7e03d93d 100644 --- a/packages/ociswap-v2-adapter-v1/src/lib.rs +++ b/packages/ociswap-v2-adapter-v1/src/lib.rs @@ -171,10 +171,11 @@ pub mod adapter { let (receipt, change_x, change_y) = pool.add_liquidity(lower_tick, upper_tick, bucket_x, bucket_y); - let non_fungible_global_id = NonFungibleGlobalId::new( - receipt.as_non_fungible().resource_address(), - receipt.as_non_fungible().non_fungible_local_id(), - ); + let non_fungible = receipt + .as_non_fungible() + .non_fungible::(); + let non_fungible_data = non_fungible.data(); + let non_fungible_global_id = non_fungible.global_id().clone(); OpenLiquidityPositionOutput { pool_units: IndexedBuckets::from_bucket(receipt), @@ -183,7 +184,8 @@ pub mod adapter { adapter_specific_information: AnyValue::from_typed( &OciswapV2AdapterSpecificInformation { liquidity_receipt_non_fungible_global_id: - non_fungible_global_id.clone(), + non_fungible_global_id, + liquidity_receipt_data: non_fungible_data, }, ) .expect(UNEXPECTED_ERROR), @@ -259,6 +261,9 @@ pub mod adapter { pub struct OciswapV2AdapterSpecificInformation { /// Stores the non-fungible global id of the liquidity receipt. pub liquidity_receipt_non_fungible_global_id: NonFungibleGlobalId, + + /// The data of the underlying liquidity receipt + pub liquidity_receipt_data: LiquidityPosition, } impl From for AnyValue { @@ -266,3 +271,15 @@ impl From for AnyValue { AnyValue::from_typed(&value).unwrap() } } + +#[derive(NonFungibleData, ScryptoSbor, Debug, Clone)] +pub struct LiquidityPosition { + liquidity: PreciseDecimal, + left_bound: i32, + right_bound: i32, + shape_id: Option, + x_fee_checkpoint: PreciseDecimal, + y_fee_checkpoint: PreciseDecimal, + x_total_fee_checkpoint: PreciseDecimal, + y_total_fee_checkpoint: PreciseDecimal, +} From 0842a6bb69b89250d04d6f5fbb004b40ef4e1ab3 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 20:54:30 +0300 Subject: [PATCH 24/47] [Publishing Tool]: Have a lib and bin in same crate --- tools/publishing-tool/Cargo.toml | 7 + tools/publishing-tool/src/cli/bin.rs | 29 +++++ .../default_configurations/mainnet_testing.rs | 13 +- .../src/cli/default_configurations/mod.rs | 6 +- .../stokenet_testing.rs | 26 +++- tools/publishing-tool/src/cli/mod.rs | 18 --- tools/publishing-tool/src/cli/publish.rs | 6 +- tools/publishing-tool/src/error.rs | 3 +- tools/publishing-tool/src/lib.rs | 6 + tools/publishing-tool/src/main.rs | 23 ---- .../src/publishing/configuration.rs | 8 +- .../publishing-tool/src/publishing/handler.rs | 123 +++++++++++++++++- 12 files changed, 205 insertions(+), 63 deletions(-) create mode 100644 tools/publishing-tool/src/cli/bin.rs delete mode 100644 tools/publishing-tool/src/cli/mod.rs create mode 100644 tools/publishing-tool/src/lib.rs delete mode 100644 tools/publishing-tool/src/main.rs diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml index c7735f98..70229bb8 100644 --- a/tools/publishing-tool/Cargo.toml +++ b/tools/publishing-tool/Cargo.toml @@ -47,3 +47,10 @@ bitflags = "2.4.2" [lints] workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[[bin]] +name = "publishing-tool" +path = "src/cli/bin.rs" \ No newline at end of file diff --git a/tools/publishing-tool/src/cli/bin.rs b/tools/publishing-tool/src/cli/bin.rs new file mode 100644 index 00000000..083e5238 --- /dev/null +++ b/tools/publishing-tool/src/cli/bin.rs @@ -0,0 +1,29 @@ +#![allow(dead_code, clippy::enum_variant_names, clippy::wrong_self_convention)] + +mod default_configurations; +mod publish; + +use clap::Parser; +use publishing_tool::error::*; +use radix_engine_common::prelude::*; +use transaction::prelude::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut out = std::io::stdout(); + let cli = ::parse(); + cli.run(&mut out) +} + +#[derive(Parser, Debug)] +pub enum Cli { + Publish(publish::Publish), +} + +impl Cli { + pub fn run(self, out: &mut O) -> Result<(), Error> { + match self { + Self::Publish(cmd) => cmd.run(out), + } + } +} diff --git a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs index 20d6f239..e03ae788 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs +++ b/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs @@ -1,7 +1,9 @@ use common::prelude::*; - -use self::utils::*; -use crate::*; +use publishing_tool::publishing::*; +use publishing_tool::utils::*; +use publishing_tool::*; +use radix_engine_interface::prelude::*; +use transaction::prelude::*; pub fn mainnet_testing( notary_private_key: &PrivateKey, @@ -297,9 +299,8 @@ pub fn mainnet_testing( }), }, additional_information: AdditionalInformation { - ociswap_v2_registry_component: None, + ociswap_v2_registry_component_and_dapp_definition: None, }, - additional_operation_flags: AdditionalOperationFlags::empty() - // cSpell:enable + additional_operation_flags: AdditionalOperationFlags::empty(), // cSpell:enable } } diff --git a/tools/publishing-tool/src/cli/default_configurations/mod.rs b/tools/publishing-tool/src/cli/default_configurations/mod.rs index fa6d4188..d5f6e946 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mod.rs +++ b/tools/publishing-tool/src/cli/default_configurations/mod.rs @@ -1,8 +1,10 @@ -use crate::*; -use clap::*; mod mainnet_testing; mod stokenet_testing; +use clap::*; +use publishing_tool::publishing::*; +use transaction::prelude::*; + #[derive(ValueEnum, Clone, Copy, Debug)] pub enum ConfigurationSelector { MainnetTesting, diff --git a/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs b/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs index 9a273334..eee3d1f6 100644 --- a/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs +++ b/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs @@ -1,7 +1,9 @@ use common::prelude::*; - -use self::utils::*; -use crate::*; +use publishing_tool::publishing::*; +use publishing_tool::utils::*; +use publishing_tool::*; +use radix_engine_interface::prelude::*; +use transaction::prelude::*; pub fn stokenet_testing( notary_private_key: &PrivateKey, @@ -233,11 +235,21 @@ pub fn stokenet_testing( defiplaza_v2: None, }, additional_information: AdditionalInformation { - ociswap_v2_registry_component: Some(component_address!( - "component_tdx_2_1cpwm3sjxr48gmsnh7lgmh5de3eqqzthqkazztc4qv6n3fvedgjepwk" + ociswap_v2_registry_component_and_dapp_definition: Some(( + component_address!( + "component_tdx_2_1cpwm3sjxr48gmsnh7lgmh5de3eqqzthqkazztc4qv6n3fvedgjepwk" + ), + component_address!( + "account_tdx_2_12yhfrtak5j0pmaju5l3p752wpye4z4nzua679ypns0094hmu66p2yk" + ), )), }, - additional_operation_flags: AdditionalOperationFlags::SUBMIT_ORACLE_PRICES_OF_ONE - // cSpell:enable + additional_operation_flags: + AdditionalOperationFlags::SUBMIT_ORACLE_PRICES_OF_ONE + .union(AdditionalOperationFlags::PROVIDE_INITIAL_IGNITION_LIQUIDITY) + .union( + AdditionalOperationFlags::PROVIDE_INITIAL_LIQUIDITY_TO_OCISWAP_BY_MINTING_USER_RESOURCE + ) } + // cSpell:enable } diff --git a/tools/publishing-tool/src/cli/mod.rs b/tools/publishing-tool/src/cli/mod.rs deleted file mode 100644 index e9eb4a83..00000000 --- a/tools/publishing-tool/src/cli/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod default_configurations; -mod publish; - -use crate::Error; -use clap::Parser; - -#[derive(Parser, Debug)] -pub enum Cli { - Publish(publish::Publish), -} - -impl Cli { - pub fn run(self, out: &mut O) -> Result<(), Error> { - match self { - Self::Publish(cmd) => cmd.run(out), - } - } -} diff --git a/tools/publishing-tool/src/cli/publish.rs b/tools/publishing-tool/src/cli/publish.rs index 31e1daeb..51faba50 100644 --- a/tools/publishing-tool/src/cli/publish.rs +++ b/tools/publishing-tool/src/cli/publish.rs @@ -1,7 +1,9 @@ -use super::default_configurations::*; -use crate::utils::*; +use crate::default_configurations::*; use crate::*; use clap::Parser; +use publishing_tool::network_connection_provider::*; +use publishing_tool::publishing::*; +use publishing_tool::utils::*; use radix_engine_common::prelude::*; use state_manager::RocksDBStore; use std::path::*; diff --git a/tools/publishing-tool/src/error.rs b/tools/publishing-tool/src/error.rs index 20ecbca7..d428f402 100644 --- a/tools/publishing-tool/src/error.rs +++ b/tools/publishing-tool/src/error.rs @@ -1,4 +1,5 @@ -use crate::*; +use crate::network_connection_provider::*; +use crate::publishing::*; use state_manager::traits::*; #[derive(Debug)] diff --git a/tools/publishing-tool/src/lib.rs b/tools/publishing-tool/src/lib.rs new file mode 100644 index 00000000..57f31540 --- /dev/null +++ b/tools/publishing-tool/src/lib.rs @@ -0,0 +1,6 @@ +pub mod database_overlay; +pub mod error; +pub mod macros; +pub mod network_connection_provider; +pub mod publishing; +pub mod utils; diff --git a/tools/publishing-tool/src/main.rs b/tools/publishing-tool/src/main.rs deleted file mode 100644 index dd1d9a88..00000000 --- a/tools/publishing-tool/src/main.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![allow(dead_code, clippy::enum_variant_names, clippy::wrong_self_convention)] - -mod cli; -mod database_overlay; -mod error; -mod network_connection_provider; -mod publishing; -mod utils; -#[macro_use] -mod macros; - -use error::*; -use network_connection_provider::*; -use publishing::*; -use radix_engine_common::prelude::*; -use transaction::prelude::*; - -fn main() -> Result<(), Error> { - env_logger::init(); - let mut out = std::io::stdout(); - let cli = ::parse(); - cli.run(&mut out) -} diff --git a/tools/publishing-tool/src/publishing/configuration.rs b/tools/publishing-tool/src/publishing/configuration.rs index 510734d2..1defbba4 100644 --- a/tools/publishing-tool/src/publishing/configuration.rs +++ b/tools/publishing-tool/src/publishing/configuration.rs @@ -76,6 +76,11 @@ bitflags::bitflags! { /// attempt to get the XRD from the faucet. If otherwise then it will /// attempt to mint it. const PROVIDE_INITIAL_IGNITION_LIQUIDITY = 0b00000010; + + /// Provides initial liquidity to ociswap v2 pools by minting the user + /// asset. If the protocol asset is mintable then it mints them in the + /// process and if they're not then it gets them from the faucet. + const PROVIDE_INITIAL_LIQUIDITY_TO_OCISWAP_BY_MINTING_USER_RESOURCE = 0b00000100; } } @@ -94,7 +99,8 @@ pub struct ProtocolConfigurationReceipt { } pub struct AdditionalInformation { - pub ociswap_v2_registry_component: Option, + pub ociswap_v2_registry_component_and_dapp_definition: + Option<(ComponentAddress, ComponentAddress)>, } pub struct ProtocolConfiguration { diff --git a/tools/publishing-tool/src/publishing/handler.rs b/tools/publishing-tool/src/publishing/handler.rs index f53e9358..98c170c6 100644 --- a/tools/publishing-tool/src/publishing/handler.rs +++ b/tools/publishing-tool/src/publishing/handler.rs @@ -3,6 +3,7 @@ use defiplaza_v2_adapter_v1::*; use ignition::{InitializationParametersManifest, PoolBlueprintInformation}; use itertools::*; +use ociswap_v2_adapter_v1::OciswapV2PoolInterfaceManifestBuilderExtensionTrait; use package_loader::*; use radix_engine::blueprints::package::*; use radix_engine::types::node_modules::*; @@ -974,6 +975,119 @@ pub fn publish( .build(); execution_service.execute_manifest(manifest)?; } + + // Seeding Ignition with the initial set of XRD if requested. + if configuration.additional_operation_flags.contains( + AdditionalOperationFlags::PROVIDE_INITIAL_IGNITION_LIQUIDITY, + ) { + let total_amount_of_protocol_resource = dec!(10_000); + let mut manifest_builder = ManifestBuilder::new() + .create_proof_from_account_of_amount( + resolved_badges.protocol_owner_badge.0, + resolved_badges.protocol_owner_badge.1, + dec!(1), + ); + if configuration.protocol_configuration.protocol_resource == XRD { + manifest_builder = manifest_builder.get_free_xrd_from_faucet() + } else { + manifest_builder = manifest_builder.mint_fungible( + configuration.protocol_configuration.protocol_resource, + total_amount_of_protocol_resource, + ) + } + + let manifest = manifest_builder + .take_from_worktop( + XRD, + total_amount_of_protocol_resource / 2, + "volatile", + ) + .take_from_worktop( + XRD, + total_amount_of_protocol_resource / 2, + "non_volatile", + ) + .with_name_lookup(|builder, _| { + let volatile = builder.bucket("volatile"); + let non_volatile = builder.bucket("non_volatile"); + + builder + .call_method( + resolved_entity_component_addresses + .protocol_entities + .ignition, + "deposit_protocol_resources", + (volatile, common::prelude::Volatility::Volatile), + ) + .call_method( + resolved_entity_component_addresses + .protocol_entities + .ignition, + "deposit_protocol_resources", + ( + non_volatile, + common::prelude::Volatility::NonVolatile, + ), + ) + }) + .build(); + execution_service.execute_manifest(manifest)?; + } + + // Contributing initial liquidity to Ociswap if requested + if configuration.additional_operation_flags.contains( + AdditionalOperationFlags::PROVIDE_INITIAL_LIQUIDITY_TO_OCISWAP_BY_MINTING_USER_RESOURCE, + ) { + if let Some(ExchangeInformation { pools, .. }) = resolved_exchange_data.ociswap_v2 { + for (pool_address, user_resource_address) in + pools.zip_borrowed(&resolved_user_resources).iter() + { + let (pool_address, user_resource_address) = + (*pool_address, **user_resource_address); + + let mut manifest_builder = ManifestBuilder::new(); + if configuration.protocol_configuration.protocol_resource == XRD { + manifest_builder = manifest_builder.get_free_xrd_from_faucet() + } else { + manifest_builder = manifest_builder.mint_fungible( + configuration.protocol_configuration.protocol_resource, + dec!(10_000), + ) + } + let manifest = manifest_builder + .mint_fungible(user_resource_address, dec!(10_000)) + .take_all_from_worktop( + configuration.protocol_configuration.protocol_resource, + "protocol", + ) + .take_all_from_worktop(user_resource_address, "user") + .then(|builder| { + let protocol_resource = builder.bucket("protocol"); + let user_resource = builder.bucket("user"); + + let (x_bucket, y_bucket) = + if configuration.protocol_configuration.protocol_resource + < user_resource_address + { + (protocol_resource, user_resource) + } else { + (user_resource, protocol_resource) + }; + + builder.ociswap_v2_pool_add_liquidity( + pool_address, + -3921i32, + 9942i32, + x_bucket, + y_bucket, + ) + }) + .try_deposit_entire_worktop_or_abort(ephemeral_account, None) + .build(); + execution_service.execute_manifest(manifest)?; + } + } + } } // Depositing the created badges into their accounts. @@ -1063,7 +1177,11 @@ fn handle_ociswap_v2_exchange_information( > { // No ociswap registry component is passed even through it is needed. let AdditionalInformation { - ociswap_v2_registry_component: Some(ociswap_v2_registry_component), + ociswap_v2_registry_component_and_dapp_definition: + Some(( + ociswap_v2_registry_component, + ociswap_v2_dapp_definition_account, + )), } = additional_information else { return Ok(None); @@ -1130,8 +1248,7 @@ fn handle_ociswap_v2_exchange_information( ManifestBucket, )>::new( ), - // TODO: Specify their dapp definition? - FAUCET, + ociswap_v2_dapp_definition_account, ), ) .build(); From e51f29d72415601fad7826cc6a5d341c3b8f8df7 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 09:47:30 +0300 Subject: [PATCH 25/47] [Tests]: Directory restructure --- Cargo.lock | 26 ++++++++++ Cargo.toml | 5 +- testing/stateful-tests/Cargo.toml | 45 ++++++++++++++++ testing/stateful-tests/src/main.rs | 3 ++ testing/tests/Cargo.toml | 49 ++++++++++++++++++ .../tests}/assets/defiplaza_v2.rpd | Bin .../tests}/assets/defiplaza_v2.wasm | Bin .../tests}/assets/ociswap_v2_pool.rpd | Bin .../tests}/assets/ociswap_v2_pool.wasm | Bin .../tests}/assets/ociswap_v2_registry.rpd | Bin .../tests}/assets/ociswap_v2_registry.wasm | Bin {tests => testing/tests}/assets/state | Bin {tests => testing/tests}/build.rs | 0 {tests => testing/tests}/src/environment.rs | 0 {tests => testing/tests}/src/errors.rs | 0 {tests => testing/tests}/src/extensions.rs | 0 {tests => testing/tests}/src/lib.rs | 0 {tests => testing/tests}/src/prelude.rs | 0 .../tests}/tests/caviarnine_v1.rs | 0 .../tests}/tests/caviarnine_v1_simulation.rs | 0 .../tests}/tests/defiplaza_v2.rs | 0 {tests => testing/tests}/tests/ociswap_v1.rs | 0 {tests => testing/tests}/tests/ociswap_v2.rs | 0 {tests => testing/tests}/tests/protocol.rs | 0 tests/Cargo.toml | 49 ------------------ 25 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 testing/stateful-tests/Cargo.toml create mode 100644 testing/stateful-tests/src/main.rs create mode 100644 testing/tests/Cargo.toml rename {tests => testing/tests}/assets/defiplaza_v2.rpd (100%) rename {tests => testing/tests}/assets/defiplaza_v2.wasm (100%) rename {tests => testing/tests}/assets/ociswap_v2_pool.rpd (100%) rename {tests => testing/tests}/assets/ociswap_v2_pool.wasm (100%) rename {tests => testing/tests}/assets/ociswap_v2_registry.rpd (100%) rename {tests => testing/tests}/assets/ociswap_v2_registry.wasm (100%) rename {tests => testing/tests}/assets/state (100%) rename {tests => testing/tests}/build.rs (100%) rename {tests => testing/tests}/src/environment.rs (100%) rename {tests => testing/tests}/src/errors.rs (100%) rename {tests => testing/tests}/src/extensions.rs (100%) rename {tests => testing/tests}/src/lib.rs (100%) rename {tests => testing/tests}/src/prelude.rs (100%) rename {tests => testing/tests}/tests/caviarnine_v1.rs (100%) rename {tests => testing/tests}/tests/caviarnine_v1_simulation.rs (100%) rename {tests => testing/tests}/tests/defiplaza_v2.rs (100%) rename {tests => testing/tests}/tests/ociswap_v1.rs (100%) rename {tests => testing/tests}/tests/ociswap_v2.rs (100%) rename {tests => testing/tests}/tests/protocol.rs (100%) delete mode 100644 tests/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 13486e6a..cc61ec50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,6 +2977,32 @@ dependencies = [ "utils", ] +[[package]] +name = "stateful-tests" +version = "0.1.0" +dependencies = [ + "caviarnine-v1-adapter-v1", + "common", + "defiplaza-v2-adapter-v1", + "extend", + "gateway-client", + "ignition", + "ociswap-v1-adapter-v1", + "ociswap-v2-adapter-v1", + "package-loader", + "paste", + "ports-interface", + "publishing-tool", + "radix-engine", + "radix-engine-common", + "radix-engine-interface", + "sbor", + "scrypto-test", + "scrypto-unit", + "simple-oracle", + "transaction", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1b59d8db..27c8f5bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ members = [ # Tools "tools/publishing-tool", # Tests - "tests" + "testing/tests", + "testing/stateful-tests" ] [workspace.package] @@ -68,4 +69,4 @@ radix-engine-queries = { git = "https://www.github.com/radixdlt/radixdlt-scrypto radix-engine-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } radix-engine-store-interface = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } scrypto-unit = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } -scrypto-test = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } \ No newline at end of file +scrypto-test = { git = "https://www.github.com/radixdlt/radixdlt-scrypto.git", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } diff --git a/testing/stateful-tests/Cargo.toml b/testing/stateful-tests/Cargo.toml new file mode 100644 index 00000000..ee182059 --- /dev/null +++ b/testing/stateful-tests/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "stateful-tests" +version.workspace = true +edition.workspace = true +description = "A crate that tests Ignition against the current mainnet state" + +[dependencies] +sbor = { workspace = true } +transaction = { workspace = true } +scrypto-test = { workspace = true } +scrypto-unit = { workspace = true } +radix-engine = { workspace = true } +radix-engine-common = { workspace = true } +radix-engine-interface = { workspace = true } + +common = { path = "../../libraries/common" } +ignition = { path = "../../packages/ignition", features = ["test"] } +simple-oracle = { path = "../../packages/simple-oracle", features = ["test"] } +ports-interface = { path = "../../libraries/ports-interface" } +ociswap-v1-adapter-v1 = { path = "../../packages/ociswap-v1-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +ociswap-v2-adapter-v1 = { path = "../../packages/ociswap-v2-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +defiplaza-v2-adapter-v1 = { path = "../../packages/defiplaza-v2-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } + +package-loader = { path = "../../libraries/package-loader" } +gateway-client = { path = "../../libraries/gateway-client" } +publishing-tool = { path = "../../tools/publishing-tool" } + +paste = { version = "1.0.14" } +extend = { version = "1.2.0" } + +[lints] +workspace = true \ No newline at end of file diff --git a/testing/stateful-tests/src/main.rs b/testing/stateful-tests/src/main.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/testing/stateful-tests/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/testing/tests/Cargo.toml b/testing/tests/Cargo.toml new file mode 100644 index 00000000..ca1b49d0 --- /dev/null +++ b/testing/tests/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "tests" +version.workspace = true +edition.workspace = true +description = "A crate with unit and integration tests for Ignition" +build = "build.rs" + +[dependencies] +sbor = { workspace = true } +transaction = { workspace = true } +scrypto-test = { workspace = true } +scrypto-unit = { workspace = true } +radix-engine = { workspace = true } +radix-engine-common = { workspace = true } +radix-engine-interface = { workspace = true } + +common = { path = "../../libraries/common" } +ignition = { path = "../../packages/ignition", features = ["test"] } +simple-oracle = { path = "../../packages/simple-oracle", features = ["test"] } +ports-interface = { path = "../../libraries/ports-interface" } +ociswap-v1-adapter-v1 = { path = "../../packages/ociswap-v1-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +ociswap-v2-adapter-v1 = { path = "../../packages/ociswap-v2-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +defiplaza-v2-adapter-v1 = { path = "../../packages/defiplaza-v2-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } +caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", features = [ + "test", + "manifest-builder-stubs" +] } + +package-loader = { path = "../../libraries/package-loader" } +gateway-client = { path = "../../libraries/gateway-client" } + +paste = { version = "1.0.14" } +extend = { version = "1.2.0" } +lazy_static = "1.4.0" + +[build-dependencies] +flate2 = { version = "1.0.28" } + +[lints] +workspace = true \ No newline at end of file diff --git a/tests/assets/defiplaza_v2.rpd b/testing/tests/assets/defiplaza_v2.rpd similarity index 100% rename from tests/assets/defiplaza_v2.rpd rename to testing/tests/assets/defiplaza_v2.rpd diff --git a/tests/assets/defiplaza_v2.wasm b/testing/tests/assets/defiplaza_v2.wasm similarity index 100% rename from tests/assets/defiplaza_v2.wasm rename to testing/tests/assets/defiplaza_v2.wasm diff --git a/tests/assets/ociswap_v2_pool.rpd b/testing/tests/assets/ociswap_v2_pool.rpd similarity index 100% rename from tests/assets/ociswap_v2_pool.rpd rename to testing/tests/assets/ociswap_v2_pool.rpd diff --git a/tests/assets/ociswap_v2_pool.wasm b/testing/tests/assets/ociswap_v2_pool.wasm similarity index 100% rename from tests/assets/ociswap_v2_pool.wasm rename to testing/tests/assets/ociswap_v2_pool.wasm diff --git a/tests/assets/ociswap_v2_registry.rpd b/testing/tests/assets/ociswap_v2_registry.rpd similarity index 100% rename from tests/assets/ociswap_v2_registry.rpd rename to testing/tests/assets/ociswap_v2_registry.rpd diff --git a/tests/assets/ociswap_v2_registry.wasm b/testing/tests/assets/ociswap_v2_registry.wasm similarity index 100% rename from tests/assets/ociswap_v2_registry.wasm rename to testing/tests/assets/ociswap_v2_registry.wasm diff --git a/tests/assets/state b/testing/tests/assets/state similarity index 100% rename from tests/assets/state rename to testing/tests/assets/state diff --git a/tests/build.rs b/testing/tests/build.rs similarity index 100% rename from tests/build.rs rename to testing/tests/build.rs diff --git a/tests/src/environment.rs b/testing/tests/src/environment.rs similarity index 100% rename from tests/src/environment.rs rename to testing/tests/src/environment.rs diff --git a/tests/src/errors.rs b/testing/tests/src/errors.rs similarity index 100% rename from tests/src/errors.rs rename to testing/tests/src/errors.rs diff --git a/tests/src/extensions.rs b/testing/tests/src/extensions.rs similarity index 100% rename from tests/src/extensions.rs rename to testing/tests/src/extensions.rs diff --git a/tests/src/lib.rs b/testing/tests/src/lib.rs similarity index 100% rename from tests/src/lib.rs rename to testing/tests/src/lib.rs diff --git a/tests/src/prelude.rs b/testing/tests/src/prelude.rs similarity index 100% rename from tests/src/prelude.rs rename to testing/tests/src/prelude.rs diff --git a/tests/tests/caviarnine_v1.rs b/testing/tests/tests/caviarnine_v1.rs similarity index 100% rename from tests/tests/caviarnine_v1.rs rename to testing/tests/tests/caviarnine_v1.rs diff --git a/tests/tests/caviarnine_v1_simulation.rs b/testing/tests/tests/caviarnine_v1_simulation.rs similarity index 100% rename from tests/tests/caviarnine_v1_simulation.rs rename to testing/tests/tests/caviarnine_v1_simulation.rs diff --git a/tests/tests/defiplaza_v2.rs b/testing/tests/tests/defiplaza_v2.rs similarity index 100% rename from tests/tests/defiplaza_v2.rs rename to testing/tests/tests/defiplaza_v2.rs diff --git a/tests/tests/ociswap_v1.rs b/testing/tests/tests/ociswap_v1.rs similarity index 100% rename from tests/tests/ociswap_v1.rs rename to testing/tests/tests/ociswap_v1.rs diff --git a/tests/tests/ociswap_v2.rs b/testing/tests/tests/ociswap_v2.rs similarity index 100% rename from tests/tests/ociswap_v2.rs rename to testing/tests/tests/ociswap_v2.rs diff --git a/tests/tests/protocol.rs b/testing/tests/tests/protocol.rs similarity index 100% rename from tests/tests/protocol.rs rename to testing/tests/tests/protocol.rs diff --git a/tests/Cargo.toml b/tests/Cargo.toml deleted file mode 100644 index 929462f2..00000000 --- a/tests/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "tests" -version.workspace = true -edition.workspace = true -description.workspace = true -build = "build.rs" - -[dependencies] -sbor = { workspace = true } -transaction = { workspace = true } -scrypto-test = { workspace = true } -scrypto-unit = { workspace = true } -radix-engine = { workspace = true } -radix-engine-common = { workspace = true } -radix-engine-interface = { workspace = true } - -common = { path = "../libraries/common" } -ignition = { path = "../packages/ignition", features = ["test"] } -simple-oracle = { path = "../packages/simple-oracle", features = ["test"] } -ports-interface = { path = "../libraries/ports-interface" } -ociswap-v1-adapter-v1 = { path = "../packages/ociswap-v1-adapter-v1", features = [ - "test", - "manifest-builder-stubs" -] } -ociswap-v2-adapter-v1 = { path = "../packages/ociswap-v2-adapter-v1", features = [ - "test", - "manifest-builder-stubs" -] } -defiplaza-v2-adapter-v1 = { path = "../packages/defiplaza-v2-adapter-v1", features = [ - "test", - "manifest-builder-stubs" -] } -caviarnine-v1-adapter-v1 = { path = "../packages/caviarnine-v1-adapter-v1", features = [ - "test", - "manifest-builder-stubs" -] } - -package-loader = { path = "../libraries/package-loader" } -gateway-client = { path = "../libraries/gateway-client" } - -paste = { version = "1.0.14" } -extend = { version = "1.2.0" } -lazy_static = "1.4.0" - -[build-dependencies] -flate2 = { version = "1.0.28" } - -[lints] -workspace = true \ No newline at end of file From 3108a79d1c0904b6dc954c3221be5faa533463e4 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 15:26:42 +0300 Subject: [PATCH 26/47] [Publishing Tool]: Make certain modules publicly accessible in lib --- testing/stateful-tests/Cargo.toml | 2 ++ tools/publishing-tool/Cargo.toml | 3 ++- tools/publishing-tool/src/cli/bin.rs | 1 - tools/publishing-tool/src/cli/publish.rs | 2 +- .../mainnet_testing.rs | 6 ++--- .../mod.rs | 12 +++++++--- .../stokenet_testing.rs | 6 ++--- tools/publishing-tool/src/lib.rs | 1 + .../mainnet_simulator_connector.rs | 22 +++++++++++++++++++ 9 files changed, 43 insertions(+), 12 deletions(-) rename tools/publishing-tool/src/{cli/default_configurations => configuration_selector}/mainnet_testing.rs (99%) rename tools/publishing-tool/src/{cli/default_configurations => configuration_selector}/mod.rs (73%) rename tools/publishing-tool/src/{cli/default_configurations => configuration_selector}/stokenet_testing.rs (99%) diff --git a/testing/stateful-tests/Cargo.toml b/testing/stateful-tests/Cargo.toml index ee182059..f527dbb8 100644 --- a/testing/stateful-tests/Cargo.toml +++ b/testing/stateful-tests/Cargo.toml @@ -13,6 +13,8 @@ radix-engine = { workspace = true } radix-engine-common = { workspace = true } radix-engine-interface = { workspace = true } +state-manager = { workspace = true } + common = { path = "../../libraries/common" } ignition = { path = "../../packages/ignition", features = ["test"] } simple-oracle = { path = "../../packages/simple-oracle", features = ["test"] } diff --git a/tools/publishing-tool/Cargo.toml b/tools/publishing-tool/Cargo.toml index 70229bb8..ca560630 100644 --- a/tools/publishing-tool/Cargo.toml +++ b/tools/publishing-tool/Cargo.toml @@ -13,6 +13,8 @@ radix-engine-common = { workspace = true } radix-engine-interface = { workspace = true } radix-engine-store-interface = { workspace = true } +state-manager = { workspace = true } + common = { path = "../../libraries/common" } ignition = { path = "../../packages/ignition" } package-loader = { path = "../../libraries/package-loader" } @@ -31,7 +33,6 @@ caviarnine-v1-adapter-v1 = { path = "../../packages/caviarnine-v1-adapter-v1", f "manifest-builder-stubs", ] } -state-manager = { git = "https://github.com/radixdlt/babylon-node", rev = "63a8267196995fef0830e4fbf0271bea65c90ab1" } sbor-json = { git = "https://github.com/radixdlt/radix-engine-toolkit", rev = "1cfe879c7370cfa497857ada7a8973f8a3388abc" } hex = { version = "0.4.3" } diff --git a/tools/publishing-tool/src/cli/bin.rs b/tools/publishing-tool/src/cli/bin.rs index 083e5238..3337c79a 100644 --- a/tools/publishing-tool/src/cli/bin.rs +++ b/tools/publishing-tool/src/cli/bin.rs @@ -1,6 +1,5 @@ #![allow(dead_code, clippy::enum_variant_names, clippy::wrong_self_convention)] -mod default_configurations; mod publish; use clap::Parser; diff --git a/tools/publishing-tool/src/cli/publish.rs b/tools/publishing-tool/src/cli/publish.rs index 51faba50..968ba830 100644 --- a/tools/publishing-tool/src/cli/publish.rs +++ b/tools/publishing-tool/src/cli/publish.rs @@ -1,6 +1,6 @@ -use crate::default_configurations::*; use crate::*; use clap::Parser; +use publishing_tool::configuration_selector::*; use publishing_tool::network_connection_provider::*; use publishing_tool::publishing::*; use publishing_tool::utils::*; diff --git a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs b/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs similarity index 99% rename from tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs rename to tools/publishing-tool/src/configuration_selector/mainnet_testing.rs index e03ae788..d7b4ceaf 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mainnet_testing.rs +++ b/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs @@ -1,7 +1,7 @@ +use crate::publishing::*; +use crate::utils::*; +use crate::*; use common::prelude::*; -use publishing_tool::publishing::*; -use publishing_tool::utils::*; -use publishing_tool::*; use radix_engine_interface::prelude::*; use transaction::prelude::*; diff --git a/tools/publishing-tool/src/cli/default_configurations/mod.rs b/tools/publishing-tool/src/configuration_selector/mod.rs similarity index 73% rename from tools/publishing-tool/src/cli/default_configurations/mod.rs rename to tools/publishing-tool/src/configuration_selector/mod.rs index d5f6e946..97065c0b 100644 --- a/tools/publishing-tool/src/cli/default_configurations/mod.rs +++ b/tools/publishing-tool/src/configuration_selector/mod.rs @@ -1,13 +1,14 @@ mod mainnet_testing; mod stokenet_testing; +use crate::publishing::*; use clap::*; -use publishing_tool::publishing::*; use transaction::prelude::*; #[derive(ValueEnum, Clone, Copy, Debug)] pub enum ConfigurationSelector { MainnetTesting, + MainnetProduction, StokenetTesting, } @@ -17,6 +18,7 @@ impl ConfigurationSelector { notary_private_key: &PrivateKey, ) -> PublishingConfiguration { match self { + Self::MainnetProduction => todo!(), Self::MainnetTesting => { mainnet_testing::mainnet_testing(notary_private_key) } @@ -28,14 +30,18 @@ impl ConfigurationSelector { pub fn gateway_base_url(self) -> String { match self { - Self::MainnetTesting => "https://mainnet.radixdlt.com".to_owned(), + Self::MainnetProduction | Self::MainnetTesting => { + "https://mainnet.radixdlt.com".to_owned() + } Self::StokenetTesting => "https://stokenet.radixdlt.com".to_owned(), } } pub fn network_definition(self) -> NetworkDefinition { match self { - Self::MainnetTesting => NetworkDefinition::mainnet(), + Self::MainnetProduction | Self::MainnetTesting => { + NetworkDefinition::mainnet() + } Self::StokenetTesting => NetworkDefinition::stokenet(), } } diff --git a/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs b/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs similarity index 99% rename from tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs rename to tools/publishing-tool/src/configuration_selector/stokenet_testing.rs index eee3d1f6..e4603907 100644 --- a/tools/publishing-tool/src/cli/default_configurations/stokenet_testing.rs +++ b/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs @@ -1,7 +1,7 @@ +use crate::publishing::*; +use crate::utils::*; +use crate::*; use common::prelude::*; -use publishing_tool::publishing::*; -use publishing_tool::utils::*; -use publishing_tool::*; use radix_engine_interface::prelude::*; use transaction::prelude::*; diff --git a/tools/publishing-tool/src/lib.rs b/tools/publishing-tool/src/lib.rs index 57f31540..016a34d9 100644 --- a/tools/publishing-tool/src/lib.rs +++ b/tools/publishing-tool/src/lib.rs @@ -1,3 +1,4 @@ +pub mod configuration_selector; pub mod database_overlay; pub mod error; pub mod macros; diff --git a/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs b/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs index 4cf6f3d5..f111df66 100644 --- a/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs +++ b/tools/publishing-tool/src/network_connection_provider/mainnet_simulator_connector.rs @@ -39,6 +39,28 @@ impl<'s> SimulatorNetworkConnector<'s> { network_definition, } } + + pub fn new_with_test_runner( + ledger_simulator: TestRunner< + NoExtension, + UnmergeableSubstateDatabaseOverlay<'s, RocksDBStore>, + >, + network_definition: NetworkDefinition, + ) -> Self { + Self { + ledger_simulator, + network_definition, + } + } + + pub fn into_test_runner( + self, + ) -> TestRunner< + NoExtension, + UnmergeableSubstateDatabaseOverlay<'s, RocksDBStore>, + > { + self.ledger_simulator + } } impl<'s> NetworkConnectionProvider for SimulatorNetworkConnector<'s> { From deaa4f130c63626679b9fabdff8d285bb9e942ff Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:01:28 +0300 Subject: [PATCH 27/47] [Tests]: Update dependencies --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cc61ec50..350da9ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3000,6 +3000,7 @@ dependencies = [ "scrypto-test", "scrypto-unit", "simple-oracle", + "state-manager", "transaction", ] diff --git a/Cargo.toml b/Cargo.toml index 27c8f5bd..fd9163db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,8 @@ radix-engine-store-interface = { git = "https://github.com/radixdlt/radixdlt-scr scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "4887c5e4be2603433592ed290b70b1a0c03cced3" } +state-manager = { git = "https://github.com/radixdlt/babylon-node", rev = "63a8267196995fef0830e4fbf0271bea65c90ab1" } + [profile.release] opt-level = 'z' lto = true From 0428c5a3a7998efc06aafe8ff2650af34d10c4be Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:02:22 +0300 Subject: [PATCH 28/47] [Defiplaza v2 Adapter v1]: Rename `add_pair_config` to plural --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 4 ++-- tests/src/environment.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ac106f4e..ee007db5 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -43,7 +43,7 @@ pub mod adapter { protocol_manager => updatable_by: [protocol_manager, protocol_owner]; }, methods { - add_pair_config => restrict_to: [protocol_manager, protocol_owner]; + add_pair_configs => restrict_to: [protocol_manager, protocol_owner]; /* User methods */ price => PUBLIC; resource_addresses => PUBLIC; @@ -96,7 +96,7 @@ pub mod adapter { .globalize() } - pub fn add_pair_config( + pub fn add_pair_configs( &mut self, pair_config: IndexMap, ) { diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 066c8be1..e1d271ef 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -541,7 +541,7 @@ impl ScryptoTestEnv { )?; // Registering all of pair configs to the adapter. - defiplaza_v2_adapter_v1.add_pair_config( + defiplaza_v2_adapter_v1.add_pair_configs( defiplaza_v2_pools .iter() .map(|pool| ComponentAddress::try_from(pool).unwrap()) @@ -1308,7 +1308,7 @@ impl ScryptoUnitEnv { .lock_fee_from_faucet() .call_method( defiplaza_v2_adapter_v1, - "add_pair_config", + "add_pair_configs", (defiplaza_v2_pools .iter() .map(|address| { From f97f4fafbbb1e34cd41b09a894a3cb1a338beb5a Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:04:47 +0300 Subject: [PATCH 29/47] [Defiplaza v2 Adapter v1]: Rename first and second to shortage and surplus assets --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ee007db5..6e3fcfab 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -199,7 +199,7 @@ pub mod adapter { let shortage = pair_state.shortage; let shortage_state = ShortageState::from(shortage); - let [(first_resource_address, first_bucket), (second_resource_address, second_bucket)] = + let [(shortage_asset_resource_address, shortage_asset_bucket), (surplus_asset_resource_address, surplus_asset_bucket)] = match shortage_state { ShortageState::Equilibrium => [ (base_resource_address, base_bucket), @@ -218,9 +218,9 @@ pub mod adapter { // Step 3: Calculate tate.target_ratio * bucket1.amount() where // bucket1 is the bucket currently in shortage or the resource that // will be contributed first. - let first_original_target = pair_state + let shortage_asset_original_target = pair_state .target_ratio - .checked_mul(first_bucket.amount()) + .checked_mul(shortage_asset_bucket.amount()) .expect(OVERFLOW_ERROR); // Step 4: Contribute to the pool. The first bucket to provide the @@ -229,25 +229,28 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - let (first_pool_units, second_change) = match shortage_state { - ShortageState::Equilibrium => ( - pool.add_liquidity(first_bucket, None).0, - Some(second_bucket), - ), - ShortageState::Shortage(_) => { - pool.add_liquidity(first_bucket, Some(second_bucket)) - } - }; + let (shortage_asset_pool_units, surplus_asset_change) = + match shortage_state { + ShortageState::Equilibrium => ( + pool.add_liquidity(shortage_asset_bucket, None).0, + Some(surplus_asset_bucket), + ), + ShortageState::Shortage(_) => pool.add_liquidity( + shortage_asset_bucket, + Some(surplus_asset_bucket), + ), + }; // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets // that are in the remainder (change) bucket. - let second_bucket = second_change.expect(UNEXPECTED_ERROR); - let second_original_target = second_bucket.amount(); + let surplus_asset_bucket = + surplus_asset_change.expect(UNEXPECTED_ERROR); + let surplus_asset_original_target = surplus_asset_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - let (second_pool_units, change) = - pool.add_liquidity(second_bucket, None); + let (surplus_asset_pool_units, change) = + pool.add_liquidity(surplus_asset_bucket, None); // We've been told that the change should be zero. Therefore, we // assert for it to make sure that everything is as we expect it @@ -264,16 +267,16 @@ pub mod adapter { // units obtained from the first contribution should be different // from those obtained in the second contribution. assert_ne!( - first_pool_units.resource_address(), - second_pool_units.resource_address(), + shortage_asset_pool_units.resource_address(), + surplus_asset_pool_units.resource_address(), ); // The procedure for adding liquidity to the pool is now complete. // We can now construct the output. OpenLiquidityPositionOutput { pool_units: IndexedBuckets::from_buckets([ - first_pool_units, - second_pool_units, + shortage_asset_pool_units, + surplus_asset_pool_units, ]), change: change .map(IndexedBuckets::from_bucket) @@ -282,8 +285,8 @@ pub mod adapter { adapter_specific_information: DefiPlazaV2AdapterSpecificInformation { original_targets: indexmap! { - first_resource_address => first_original_target, - second_resource_address => second_original_target + shortage_asset_resource_address => shortage_asset_original_target, + surplus_asset_resource_address => surplus_asset_original_target }, } .into(), From 727818d4c532e41f11c458953da4a0cd61d6b6d6 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 23:27:33 +0300 Subject: [PATCH 30/47] [Caviarnine v1 Adapter v1]: Optimize fees for opening positions. --- packages/caviarnine-v1-adapter-v1/src/lib.rs | 11 +- tests/tests/caviarnine_v1.rs | 293 +++++++++++++++++++ 2 files changed, 297 insertions(+), 7 deletions(-) diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index 6895d31b..9097e364 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -472,7 +472,7 @@ pub mod adapter { } let (receipt, change_x, change_y) = - pool.add_liquidity(bucket_x, bucket_y, positions); + pool.add_liquidity(bucket_x, bucket_y, positions.clone()); let receipt_global_id = { let resource_address = receipt.resource_address(); @@ -483,14 +483,11 @@ pub mod adapter { let adapter_specific_information = CaviarnineV1AdapterSpecificInformation { - bin_contributions: pool - .get_redemption_bin_values( - receipt_global_id.local_id().clone(), - ) + bin_contributions: positions .into_iter() - .map(|(tick, amount_x, amount_y)| { + .map(|(bin, amount_x, amount_y)| { ( - tick, + bin, ResourceIndexedData { resource_x: amount_x, resource_y: amount_y, diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 9bf45fb9..1cf187a8 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1466,3 +1466,296 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let pool = caviarnine_v1.pools.bitcoin; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement1( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(user_resource) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement2( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(XRD) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +fn round_down_to_5_decimal_places(decimal: Decimal) -> Decimal { + decimal + .checked_round(5, RoundingMode::ToNegativeInfinity) + .unwrap() +} From a89d5387fd9b682831874e22335627a71b07c002 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 11:51:12 +0300 Subject: [PATCH 31/47] [Tests]: Add basis for defiplaza v2 fee tests --- tests/src/environment.rs | 14 +-- tests/tests/defiplaza_v2.rs | 194 ++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 10 deletions(-) diff --git a/tests/src/environment.rs b/tests/src/environment.rs index e1d271ef..f6cc53cc 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -430,16 +430,10 @@ impl ScryptoTestEnv { Self::publish_package("defiplaza-v2-adapter-v1", &mut env)?; let defiplaza_v2_pools = resource_addresses.try_map(|resource_address| { - let (resource_x, resource_y) = if XRD < *resource_address { - (XRD, *resource_address) - } else { - (*resource_address, XRD) - }; - let mut defiplaza_pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( OwnerRole::None, - resource_x, - resource_y, + *resource_address, + XRD, // This pair config is obtained from DefiPlaza's // repo. PairConfig { @@ -454,9 +448,9 @@ impl ScryptoTestEnv { )?; let resource_x = - ResourceManager(resource_x).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(*resource_address).mint_fungible(dec!(100_000_000), &mut env)?; let resource_y = - ResourceManager(resource_y).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(XRD).mint_fungible(dec!(100_000_000), &mut env)?; let (_, change1) = defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index ff3cabe6..7644d463 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -913,3 +913,197 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ Ok(()) } + +#[test] +#[ignore = "Awaiting defiplaza response"] +fn exact_fee_test1() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + AssetIndexedData { + protocol_resource: dec!(100_000), + user_resource: dec!(100_000), + }, + // Initial price of the pool + dec!(1), + // The fee percentage of the pool + dec!(0.03), + // User contribution to the pool. This would mean that the user would + // own 0.1% of the pool + AssetIndexedData { + user_resource: dec!(100), + protocol_resource: dec!(100), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(1_000))], + // The fees to expect - with 0.1% pool ownership of the pool and fees of + // 3% then we expect to see 0.03 of the protocol resource in fees (as it + // was the input in the swap) and none of the user resource in fees. + AssetIndexedData { + user_resource: EqualityCheck::ExactlyEquals(dec!(0)), + protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + }, + ) + .expect("Should not fail!") +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Asset { + UserResource, + ProtocolResource, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct AssetIndexedData { + user_resource: T, + protocol_resource: T, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum EqualityCheck { + ExactlyEquals(T), + ApproximatelyEquals { value: T, acceptable_difference: T }, +} + +fn test_exact_defiplaza_fees_amounts( + // The initial amount of liquidity to provide when creating the liquidity + // pool. + initial_liquidity: AssetIndexedData, + // The price to set as the initial price of the pool. + initial_price: Decimal, + // The fee percentage of the pool + fee_percentage: Decimal, + // The contribution that the user will make to the pool + user_contribution: AssetIndexedData, + // The swaps to perform on the pool. + swaps: Vec<(Asset, Decimal)>, + // Equality checks to perform when closing the liquidity position. + expected_fees: AssetIndexedData>, +) -> Result<(), RuntimeError> { + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources: ResourceInformation { bitcoin, .. }, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let resources = AssetIndexedData { + user_resource: bitcoin, + protocol_resource: XRD, + }; + + // Creating a new defiplaza pair so we can initialize it the way that we + // desire and without any constraints from the environment. + let mut pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( + OwnerRole::None, + resources.user_resource, + resources.protocol_resource, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: fee_percentage, + decay_factor: dec!("0.9512"), + }, + initial_price, + defiplaza_v2.package, + env, + )?; + + // Providing the desired initial contribution to the pool. + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + + // Providing the user's contribution to the pool through the adapter + let [bucket_x, bucket_y] = [ + ( + resources.protocol_resource, + user_contribution.protocol_resource, + ), + (resources.user_resource, user_contribution.user_resource), + ] + .map(|(resource_address, amount)| { + ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap() + }); + let OpenLiquidityPositionOutput { + pool_units, + change, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (bucket_x, bucket_y), + env, + )?; + + // Asset the user got back no change in this contribution + for bucket in change.into_values() { + let amount = bucket.amount(env)?; + assert_eq!(amount, Decimal::ZERO); + } + + // Performing the swaps specified by the user + for (asset, amount) in swaps.into_iter() { + let address = match asset { + Asset::ProtocolResource => resources.protocol_resource, + Asset::UserResource => resources.user_resource, + }; + let bucket = + ResourceManager(address).mint_fungible(amount, env).unwrap(); + let _ = pool.swap(bucket, env)?; + } + + // Close the liquidity position + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + pool.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert that the fees is what's expected. + for (resource_address, equality_check) in [ + (resources.protocol_resource, expected_fees.protocol_resource), + (resources.user_resource, expected_fees.user_resource), + ] { + // Get the fees + let resource_fees = fees.get(&resource_address).copied().unwrap(); + + // Perform the assertion + match equality_check { + EqualityCheck::ExactlyEquals(value) => { + assert_eq!(resource_fees, value) + } + EqualityCheck::ApproximatelyEquals { + value, + acceptable_difference, + } => { + assert!( + (resource_fees - value).checked_abs().unwrap() + <= acceptable_difference + ) + } + } + } + + Ok(()) +} From d3f9ce82d4768f46c2a3e6a6ac9c2afda17d0a4b Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 13:30:21 +0300 Subject: [PATCH 32/47] [Tests]: Fix tests --- tests/tests/caviarnine_v1.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index e14b8b34..59dea3d9 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1511,6 +1511,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1608,6 +1611,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1705,6 +1711,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() From 1ae3819f5171dc331eb93c1c34d5f90e9e30e2be Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 14:25:58 +0300 Subject: [PATCH 33/47] [Publishing Tool]: Mainnet configuration --- packages/ignition/src/blueprint.rs | 17 +- testing/tests/src/environment.rs | 8 +- testing/tests/tests/caviarnine_v1.rs | 9 + testing/tests/tests/protocol.rs | 28 +- .../mainnet_production.rs | 318 ++++++++++++++++++ .../configuration_selector/mainnet_testing.rs | 2 +- .../src/configuration_selector/mod.rs | 1 + .../stokenet_testing.rs | 2 +- .../src/publishing/configuration.rs | 4 +- .../publishing-tool/src/publishing/handler.rs | 6 +- 10 files changed, 363 insertions(+), 32 deletions(-) create mode 100644 tools/publishing-tool/src/configuration_selector/mainnet_production.rs diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 47845e4a..a4a6032a 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -122,7 +122,7 @@ mod ignition { protocol_owner, protocol_manager ]; - set_maximum_allowed_price_staleness => restrict_to: [ + set_maximum_allowed_price_staleness_in_seconds => restrict_to: [ protocol_owner, protocol_manager ]; @@ -289,7 +289,7 @@ mod ignition { /// The maximum allowed staleness of prices in seconds. If a price is /// found to be older than this then it is deemed to be invalid. - maximum_allowed_price_staleness: i64, + maximum_allowed_price_staleness_in_seconds: i64, /// The maximum percentage of price difference the protocol is willing /// to accept before deeming the price difference to be too much. This @@ -310,7 +310,7 @@ mod ignition { /* Initial Configuration */ protocol_resource: ResourceManager, oracle_adapter: ComponentAddress, - maximum_allowed_price_staleness: i64, + maximum_allowed_price_staleness_in_seconds: i64, maximum_allowed_price_difference_percentage: Decimal, /* Initializers */ initialization_parameters: InitializationParameters, @@ -344,7 +344,7 @@ mod ignition { reward_rates: KeyValueStore::new_with_registered_type(), is_open_position_enabled: false, is_close_position_enabled: false, - maximum_allowed_price_staleness, + maximum_allowed_price_staleness_in_seconds, maximum_allowed_price_difference_percentage, user_resource_volatility: KeyValueStore::new_with_registered_type(), @@ -1546,9 +1546,12 @@ mod ignition { /// /// * `value`: [`i64`] - The maximum allowed staleness period in /// seconds. - pub fn set_maximum_allowed_price_staleness(&mut self, value: i64) { + pub fn set_maximum_allowed_price_staleness_in_seconds( + &mut self, + value: i64, + ) { assert!(value >= 0, "{}", INVALID_MAXIMUM_PRICE_STALENESS); - self.maximum_allowed_price_staleness = value + self.maximum_allowed_price_staleness_in_seconds = value } /// Adds a rewards rate to the protocol. @@ -1801,7 +1804,7 @@ mod ignition { let (price, last_update) = self.oracle_adapter.get_price(base, quote); let final_price_validity = last_update - .add_seconds(self.maximum_allowed_price_staleness) + .add_seconds(self.maximum_allowed_price_staleness_in_seconds) .unwrap_or(Instant::new(i64::MAX)); // Check for staleness diff --git a/testing/tests/src/environment.rs b/testing/tests/src/environment.rs index 438cf334..3515c509 100644 --- a/testing/tests/src/environment.rs +++ b/testing/tests/src/environment.rs @@ -489,7 +489,7 @@ impl ScryptoTestEnv { protocol_manager_rule, XRD.into(), simple_oracle.try_into().unwrap(), - configuration.maximum_allowed_price_staleness_seconds, + configuration.maximum_allowed_price_staleness_in_seconds_seconds, configuration.maximum_allowed_relative_price_difference, InitializationParameters::default(), None, @@ -1245,7 +1245,7 @@ impl ScryptoUnitEnv { XRD, simple_oracle, configuration - .maximum_allowed_price_staleness_seconds, + .maximum_allowed_price_staleness_in_seconds_seconds, configuration .maximum_allowed_relative_price_difference, InitializationParametersManifest::default(), @@ -1617,7 +1617,7 @@ impl ResourceInformation { #[derive(Clone, Debug)] pub struct Configuration { pub fees: Decimal, - pub maximum_allowed_price_staleness_seconds: i64, + pub maximum_allowed_price_staleness_in_seconds_seconds: i64, pub maximum_allowed_relative_price_difference: Decimal, } @@ -1627,7 +1627,7 @@ impl Default for Configuration { // 1% fees: dec!(0.01), // 5 Minutes - maximum_allowed_price_staleness_seconds: 300i64, + maximum_allowed_price_staleness_in_seconds_seconds: 300i64, // 1% maximum_allowed_relative_price_difference: dec!(0.01), } diff --git a/testing/tests/tests/caviarnine_v1.rs b/testing/tests/tests/caviarnine_v1.rs index cfd0a7f9..57645299 100644 --- a/testing/tests/tests/caviarnine_v1.rs +++ b/testing/tests/tests/caviarnine_v1.rs @@ -1512,6 +1512,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1609,6 +1612,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1706,6 +1712,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() diff --git a/testing/tests/tests/protocol.rs b/testing/tests/tests/protocol.rs index 6ab80614..af0c86eb 100644 --- a/testing/tests/tests/protocol.rs +++ b/testing/tests/tests/protocol.rs @@ -105,7 +105,7 @@ fn can_add_a_positive_upfront_reward_percentage() -> Result<(), RuntimeError> { } #[test] -fn cant_set_the_maximum_allowed_price_staleness_to_a_negative_number( +fn cant_set_the_maximum_allowed_price_staleness_in_seconds_to_a_negative_number( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -117,7 +117,7 @@ fn cant_set_the_maximum_allowed_price_staleness_to_a_negative_number( // Act let rtn = protocol .ignition - .set_maximum_allowed_price_staleness(-1, env); + .set_maximum_allowed_price_staleness_in_seconds(-1, env); // Assert assert_is_ignition_invalid_maximum_price_staleness(&rtn); @@ -126,7 +126,7 @@ fn cant_set_the_maximum_allowed_price_staleness_to_a_negative_number( } #[test] -fn can_set_the_maximum_allowed_price_staleness_to_zero( +fn can_set_the_maximum_allowed_price_staleness_in_seconds_to_zero( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -138,7 +138,7 @@ fn can_set_the_maximum_allowed_price_staleness_to_zero( // Act let rtn = protocol .ignition - .set_maximum_allowed_price_staleness(0, env); + .set_maximum_allowed_price_staleness_in_seconds(0, env); // Assert assert!(rtn.is_ok()); @@ -147,7 +147,7 @@ fn can_set_the_maximum_allowed_price_staleness_to_zero( } #[test] -fn can_set_the_maximum_allowed_price_staleness_to_a_positive_number( +fn can_set_the_maximum_allowed_price_staleness_in_seconds_to_a_positive_number( ) -> Result<(), RuntimeError> { // Arrange let Environment { @@ -159,7 +159,7 @@ fn can_set_the_maximum_allowed_price_staleness_to_a_positive_number( // Act let rtn = protocol .ignition - .set_maximum_allowed_price_staleness(1, env); + .set_maximum_allowed_price_staleness_in_seconds(1, env); // Assert assert!(rtn.is_ok()); @@ -337,7 +337,7 @@ fn can_open_a_liquidity_position_before_the_price_is_stale( resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: 5 * 60, + maximum_allowed_price_staleness_in_seconds_seconds: 5 * 60, ..Default::default() })?; @@ -370,7 +370,7 @@ fn can_open_a_liquidity_position_right_before_price_goes_stale( resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: 5 * 60, + maximum_allowed_price_staleness_in_seconds_seconds: 5 * 60, ..Default::default() })?; @@ -406,7 +406,7 @@ fn cant_open_a_liquidity_position_right_after_price_goes_stale( resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: 5 * 60, + maximum_allowed_price_staleness_in_seconds_seconds: 5 * 60, ..Default::default() })?; @@ -1718,7 +1718,7 @@ fn protocol_owner_can_perform_forced_liquidation() -> Result<(), RuntimeError> { resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: i64::MAX, + maximum_allowed_price_staleness_in_seconds_seconds: i64::MAX, ..Default::default() })?; env.enable_auth_module(); @@ -1769,7 +1769,7 @@ fn protocol_owner_can_perform_forced_liquidation_even_when_liquidation_is_closed resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: i64::MAX, + maximum_allowed_price_staleness_in_seconds_seconds: i64::MAX, ..Default::default() })?; env.enable_auth_module(); @@ -1824,7 +1824,7 @@ fn protocol_owner_cant_perform_forced_liquidation_before_maturity_date( resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: i64::MAX, + maximum_allowed_price_staleness_in_seconds_seconds: i64::MAX, ..Default::default() })?; env.enable_auth_module(); @@ -1872,7 +1872,7 @@ fn forcefully_liquidated_resources_can_be_claimed_when_closing_liquidity_positio resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: i64::MAX, + maximum_allowed_price_staleness_in_seconds_seconds: i64::MAX, ..Default::default() })?; @@ -1923,7 +1923,7 @@ fn forcefully_liquidated_resources_can_be_claimed_when_closing_liquidity_positio resources, .. } = ScryptoTestEnv::new_with_configuration(Configuration { - maximum_allowed_price_staleness_seconds: i64::MAX, + maximum_allowed_price_staleness_in_seconds_seconds: i64::MAX, ..Default::default() })?; diff --git a/tools/publishing-tool/src/configuration_selector/mainnet_production.rs b/tools/publishing-tool/src/configuration_selector/mainnet_production.rs new file mode 100644 index 00000000..1c94ddfb --- /dev/null +++ b/tools/publishing-tool/src/configuration_selector/mainnet_production.rs @@ -0,0 +1,318 @@ +use crate::publishing::*; +use crate::utils::*; +use crate::*; +use common::prelude::*; +use radix_engine_interface::prelude::*; +use transaction::prelude::*; + +pub fn mainnet_production( + notary_private_key: &PrivateKey, +) -> PublishingConfiguration { + // cSpell:disable + PublishingConfiguration { + protocol_configuration: ProtocolConfiguration { + // The protocol resource to use is XRD. + protocol_resource: XRD, + user_resource_volatility: UserResourceIndexedData { + bitcoin: Volatility::Volatile, + ethereum: Volatility::Volatile, + usdc: Volatility::NonVolatile, + usdt: Volatility::NonVolatile, + }, + reward_rates: indexmap! { + LockupPeriod::from_months(9).unwrap() => dec!(0.125), // 12.5% + LockupPeriod::from_months(10).unwrap() => dec!(0.145), // 14.5% + LockupPeriod::from_months(11).unwrap() => dec!(0.17), // 17.0% + LockupPeriod::from_months(12).unwrap() => dec!(0.2), // 20.0% + }, + // When Ignition is first deployed nobody is allowed to open or + // close positions. + allow_opening_liquidity_positions: false, + allow_closing_liquidity_positions: false, + // The maximum allowed price staleness is 60 seconds + maximum_allowed_price_staleness_in_seconds: 60, + // The maximum allowed price difference percentage is 5% from the + // oracle price. + maximum_allowed_price_difference_percentage: dec!(0.05), + entities_metadata: Entities { + protocol_entities: ProtocolIndexedData { + ignition: metadata_init! { + "name" => "Ignition", updatable; + "description" => "The main entrypoint into the Ignition liquidity incentive program.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + simple_oracle: metadata_init! { + "name" => "Ignition Oracle", updatable; + "description" => "The oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol for Ociswap v2 interactions.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + defiplaza_v2: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol for DefiPlaza v2 interactions.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + caviarnine_v1: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter", updatable; + "description" => "An adapter used by the Ignition protocol for Caviarnine v1 interactions.", updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + }, + dapp_definition_metadata: indexmap! { + "name".to_owned() => MetadataValue::String("Project Ignition".to_owned()), + "description".to_owned() => MetadataValue::String("A Radix liquidity incentives program, offered in partnership with select decentralized exchange dApps in the Radix ecosystem.".to_owned()), + "icon_url".to_owned() => MetadataValue::Url(UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png")) + }, + transaction_configuration: TransactionConfiguration { + // Whoever notarizes this transaction will also be handling the + // payment of fees for it. + notary: clone_private_key(notary_private_key), + fee_payer_information: AccountAndControllingKey::new_virtual_account( + clone_private_key(notary_private_key), + ), + }, + badges: BadgeIndexedData { + oracle_manager_badge: BadgeHandling::CreateAndSend { + // TODO: Confirm this address with Devops + // This is the account of devops that runs the oracle software + account_address: component_address!( + "account_rdx168nr5dwmll4k2x5apegw5dhrpejf3xac7khjhgjqyg4qddj9tg9v4d" + ), + metadata_init: metadata_init! { + "name" => "Ignition Oracle Manager", updatable; + "symbol" => "IGNOM", updatable; + "description" => "A badge with the authority to update the Oracle prices of the Ignition oracle.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_manager_badge: BadgeHandling::CreateAndSend { + // TODO: This is currently RTJL20 which might be incorrect + account_address: component_address!( + "account_rdx16ykaehfl0suwzy9tvtlhgds7td8ynwx4jk3q4czaucpf6m4pps9yr4" + ), + metadata_init: metadata_init! { + "name" => "Ignition Protocol Manager", updatable; + "symbol" => "IGNPM", updatable; + "description" => "A badge with the authority to manage the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + protocol_owner_badge: BadgeHandling::CreateAndSend { + // Thee RTJL20 account + account_address: component_address!( + "account_rdx16ykaehfl0suwzy9tvtlhgds7td8ynwx4jk3q4czaucpf6m4pps9yr4" + ), + metadata_init: metadata_init! { + "name" => "Ignition Protocol Owner", updatable; + "symbol" => "IGNPO", updatable; + "description" => "A badge with owner authority over the Ignition protocol.", updatable; + "tags" => vec!["badge"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + }, + }, + user_resources: UserResourceIndexedData { + bitcoin: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1t580qxc7upat7lww4l2c4jckacafjeudxj5wpjrrct0p3e82sq4y75" + ), + }, + ethereum: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1th88qcj5syl9ghka2g9l7tw497vy5x6zaatyvgfkwcfe8n9jt2npww" + ), + }, + usdc: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1t4upr78guuapv5ept7d7ptekk9mqhy605zgms33mcszen8l9fac8vf" + ), + }, + usdt: UserResourceHandling::UseExisting { + resource_address: resource_address!( + "resource_rdx1thrvr3xfs2tarm2dl9emvs26vjqxu6mqvfgvqjne940jv0lnrrg7rw" + ), + }, + }, + packages: Entities { + protocol_entities: ProtocolIndexedData { + ignition: PackageHandling::LoadAndPublish { + crate_package_name: "ignition".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Package", updatable; + "description" => "The implementation of the Ignition protocol.", updatable; + "tags" => Vec::::new(), updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "Ignition".to_owned(), + }, + simple_oracle: PackageHandling::LoadAndPublish { + crate_package_name: "simple-oracle".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Simple Oracle Package", updatable; + "description" => "The implementation of the Oracle used by the Ignition protocol.", updatable; + "tags" => vec!["oracle"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "SimpleOracle".to_owned(), + }, + }, + exchange_adapter_entities: ExchangeIndexedData { + ociswap_v2: PackageHandling::LoadAndPublish { + crate_package_name: "ociswap-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Ociswap v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for Ociswap v2 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "OciswapV2Adapter".to_owned(), + }, + defiplaza_v2: PackageHandling::LoadAndPublish { + crate_package_name: "defiplaza-v2-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition DefiPlaza v2 Adapter Package", updatable; + "description" => "The implementation of an adapter for DefiPlaza v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "DefiPlazaV2Adapter".to_owned(), + }, + caviarnine_v1: PackageHandling::LoadAndPublish { + crate_package_name: "caviarnine-v1-adapter-v1".to_owned(), + metadata: metadata_init! { + "name" => "Ignition Caviarnine v1 Adapter Package", updatable; + "description" => "The implementation of an adapter for Caviarnine v1 for the Ignition protocol.", updatable; + "tags" => vec!["adapter"], updatable; + // Dapp definition will be automatically added by the + // publisher accordingly. + }, + blueprint_name: "CaviarnineV1Adapter".to_owned(), + }, + }, + }, + exchange_information: ExchangeIndexedData { + // No ociswap v2 currently on mainnet. + ociswap_v2: None, + defiplaza_v2: Some(ExchangeInformation { + blueprint_id: BlueprintId { + package_address: package_address!( + "package_rdx1p4dhfl7qwthqqu6p2267m5nedlqnzdvfxdl6q7h8g85dflx8n06p93" + ), + blueprint_name: "PlazaPair".to_owned(), + }, + pools: UserResourceIndexedData { + bitcoin: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpv5g5a86qezw0g46w2ph8ydlu2m7jnzxw9p4lx6593qn9fmnwerta" + ), + }, + ethereum: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1crwdzvlv7djtkug9gmvp9ejun0gm0w6cvkpfqycw8fcp4gg82eftjc" + ), + }, + usdc: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpw85pmjl8ujjq7kp50lgh3ej5hz3ky9x65q2cjqvg4efnhcmfpz27" + ), + }, + usdt: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1czr2hzfv2xnxdsts4a02dglkn05clv3a2t9uk04709utehau8gjv8h" + ), + }, + }, + liquidity_receipt: LiquidityReceiptHandling::CreateNew { + non_fungible_schema: + NonFungibleDataSchema::new_local_without_self_package_replacement::< + LiquidityReceipt, + >(), + metadata: metadata_init! { + "name" => "Ignition LP: DefiPlaza", updatable; + "description" => "Represents a particular contribution of liquidity to DefiPlaza through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; + "tags" => vec!["lp token"], updatable; + "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png"), updatable; + "DEX" => "DefiPlaza", updatable; + // I have confirmed this with DefiPlaza to be the + // correct link. + "redeem_url" => UncheckedUrl::of("https://radix.defiplaza.net/ignition"), updatable; + }, + }, + }), + caviarnine_v1: Some(ExchangeInformation { + blueprint_id: BlueprintId { + package_address: package_address!( + "package_rdx1p4r9rkp0cq67wmlve544zgy0l45mswn6h798qdqm47x4762h383wa3" + ), + blueprint_name: "QuantaSwap".to_owned(), + }, + pools: UserResourceIndexedData { + bitcoin: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cp9w8443uyz2jtlaxnkcq84q5a5ndqpg05wgckzrnd3lgggpa080ed" + ), + }, + ethereum: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cpsvw207842gafeyvf6tc0gdnq47u3mn74kvzszqlhc03lrns52v82" + ), + }, + usdc: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cr6lxkr83gzhmyg4uxg49wkug5s4wwc3c7cgmhxuczxraa09a97wcu" + ), + }, + usdt: PoolHandling::UseExisting { + pool_address: component_address!( + "component_rdx1cqs338cyje65rk44zgmjvvy42qcszrhk9ewznedtkqd8l3crtgnmh5" + ), + }, + }, + liquidity_receipt: LiquidityReceiptHandling::CreateNew { + non_fungible_schema: + NonFungibleDataSchema::new_local_without_self_package_replacement::< + LiquidityReceipt, + >(), + metadata: metadata_init! { + "name" => "Ignition LP: Caviarnine", updatable; + "description" => "Represents a particular contribution of liquidity to Caviarnine through the Ignition liquidity incentives program. See the redeem_url metadata for where to redeem these NFTs.", updatable; + "tags" => vec!["lp token"], updatable; + "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-Ignition-LP.png"), updatable; + "DEX" => "Caviarnine", updatable; + // I have confirmed this with Caviarnine to be the + // correct link. + "redeem_url" => UncheckedUrl::of("https://www.caviarnine.com/ignition"), updatable; + }, + }, + }), + }, + additional_information: AdditionalInformation { + ociswap_v2_registry_component_and_dapp_definition: None, + }, + additional_operation_flags: AdditionalOperationFlags::empty(), + } + // cSpell:enable +} diff --git a/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs b/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs index d7b4ceaf..e23d1b07 100644 --- a/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs +++ b/tools/publishing-tool/src/configuration_selector/mainnet_testing.rs @@ -31,7 +31,7 @@ pub fn mainnet_testing( }, allow_opening_liquidity_positions: true, allow_closing_liquidity_positions: true, - maximum_allowed_price_staleness: i64::MAX, + maximum_allowed_price_staleness_in_seconds: i64::MAX, maximum_allowed_price_difference_percentage: Decimal::MAX, entities_metadata: Entities { protocol_entities: ProtocolIndexedData { diff --git a/tools/publishing-tool/src/configuration_selector/mod.rs b/tools/publishing-tool/src/configuration_selector/mod.rs index 97065c0b..36b89edf 100644 --- a/tools/publishing-tool/src/configuration_selector/mod.rs +++ b/tools/publishing-tool/src/configuration_selector/mod.rs @@ -1,3 +1,4 @@ +mod mainnet_production; mod mainnet_testing; mod stokenet_testing; diff --git a/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs b/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs index e4603907..8bd6ccf8 100644 --- a/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs +++ b/tools/publishing-tool/src/configuration_selector/stokenet_testing.rs @@ -29,7 +29,7 @@ pub fn stokenet_testing( }, allow_opening_liquidity_positions: true, allow_closing_liquidity_positions: true, - maximum_allowed_price_staleness: i64::MAX, + maximum_allowed_price_staleness_in_seconds: i64::MAX, maximum_allowed_price_difference_percentage: Decimal::MAX, entities_metadata: Entities { protocol_entities: ProtocolIndexedData { diff --git a/tools/publishing-tool/src/publishing/configuration.rs b/tools/publishing-tool/src/publishing/configuration.rs index 1defbba4..a8f2de78 100644 --- a/tools/publishing-tool/src/publishing/configuration.rs +++ b/tools/publishing-tool/src/publishing/configuration.rs @@ -91,7 +91,7 @@ pub struct ProtocolConfigurationReceipt { pub reward_rates: IndexMap, pub allow_opening_liquidity_positions: bool, pub allow_closing_liquidity_positions: bool, - pub maximum_allowed_price_staleness: i64, + pub maximum_allowed_price_staleness_in_seconds: i64, pub maximum_allowed_price_difference_percentage: Decimal, pub user_resources: UserResourceIndexedData, pub registered_pools: @@ -109,7 +109,7 @@ pub struct ProtocolConfiguration { pub reward_rates: IndexMap, pub allow_opening_liquidity_positions: bool, pub allow_closing_liquidity_positions: bool, - pub maximum_allowed_price_staleness: i64, + pub maximum_allowed_price_staleness_in_seconds: i64, pub maximum_allowed_price_difference_percentage: Decimal, pub entities_metadata: Entities, } diff --git a/tools/publishing-tool/src/publishing/handler.rs b/tools/publishing-tool/src/publishing/handler.rs index 98c170c6..2417382a 100644 --- a/tools/publishing-tool/src/publishing/handler.rs +++ b/tools/publishing-tool/src/publishing/handler.rs @@ -784,7 +784,7 @@ pub fn publish( oracle_component_address, configuration .protocol_configuration - .maximum_allowed_price_staleness, + .maximum_allowed_price_staleness_in_seconds, configuration .protocol_configuration .maximum_allowed_price_difference_percentage, @@ -1145,9 +1145,9 @@ pub fn publish( allow_closing_liquidity_positions: configuration .protocol_configuration .allow_closing_liquidity_positions, - maximum_allowed_price_staleness: configuration + maximum_allowed_price_staleness_in_seconds: configuration .protocol_configuration - .maximum_allowed_price_staleness, + .maximum_allowed_price_staleness_in_seconds, maximum_allowed_price_difference_percentage: configuration .protocol_configuration .maximum_allowed_price_difference_percentage, From 1aafa79c6e515655c42041cdf5732c65ad599284 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 8 Mar 2024 10:26:02 +0300 Subject: [PATCH 34/47] [Tests]: Add stateful tests for opening and closing liquidity positions --- Cargo.lock | 2 + testing/stateful-tests/Cargo.toml | 5 +- testing/stateful-tests/src/lib.rs | 336 ++++++++++++++ testing/stateful-tests/src/main.rs | 3 - testing/stateful-tests/tests/lib.rs | 438 ++++++++++++++++++ .../src/configuration_selector/mod.rs | 4 +- .../src/publishing/configuration.rs | 2 + .../publishing-tool/src/publishing/handler.rs | 4 +- 8 files changed, 788 insertions(+), 6 deletions(-) create mode 100644 testing/stateful-tests/src/lib.rs delete mode 100644 testing/stateful-tests/src/main.rs create mode 100644 testing/stateful-tests/tests/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a2757cc7..dab19594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2997,6 +2997,8 @@ dependencies = [ "extend", "gateway-client", "ignition", + "lazy_static", + "macro_rules_attribute", "ociswap-v1-adapter-v1", "ociswap-v2-adapter-v1", "package-loader", diff --git a/testing/stateful-tests/Cargo.toml b/testing/stateful-tests/Cargo.toml index f527dbb8..9c6959dd 100644 --- a/testing/stateful-tests/Cargo.toml +++ b/testing/stateful-tests/Cargo.toml @@ -43,5 +43,8 @@ publishing-tool = { path = "../../tools/publishing-tool" } paste = { version = "1.0.14" } extend = { version = "1.2.0" } +macro_rules_attribute = { version = "0.2.0" } +lazy_static = "1.4.0" + [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/testing/stateful-tests/src/lib.rs b/testing/stateful-tests/src/lib.rs new file mode 100644 index 00000000..9995dcb6 --- /dev/null +++ b/testing/stateful-tests/src/lib.rs @@ -0,0 +1,336 @@ +use common::prelude::*; +use extend::*; +use publishing_tool::component_address; +use publishing_tool::configuration_selector::*; +use publishing_tool::database_overlay::*; +use publishing_tool::network_connection_provider::*; +use publishing_tool::publishing::*; +use radix_engine::system::system_modules::*; +use radix_engine::transaction::*; +use radix_engine::vm::*; +use radix_engine_interface::blueprints::account::*; +use scrypto_unit::*; +use state_manager::RocksDBStore; +use std::ops::*; +use transaction::prelude::*; + +lazy_static::lazy_static! { + /// The substate manager database is a lazy-static since it takes a lot of + /// time to be opened for read-only and this had a very negative impact on + /// tests. Keep in mind that this now means that we should keep all of the + /// tests to one module and that we should use `cargo test` and not nextest. + static ref SUBSTATE_MANAGER_DATABASE: RocksDBStore = { + const STATE_MANAGER_DATABASE_PATH_ENVIRONMENT_VARIABLE: &str = + "STATE_MANAGER_DATABASE_PATH"; + let Ok(state_manager_database_path) = + std::env::var(STATE_MANAGER_DATABASE_PATH_ENVIRONMENT_VARIABLE) + .map(std::path::PathBuf::from) + else { + panic!( + "The `{}` environment variable is not set", + STATE_MANAGER_DATABASE_PATH_ENVIRONMENT_VARIABLE + ); + }; + RocksDBStore::new_read_only(state_manager_database_path).expect( + "Failed to create a new instance of the state manager database", + ) + }; +} + +pub type StatefulTestRunner<'a> = TestRunner< + NoExtension, + UnmergeableSubstateDatabaseOverlay<'a, RocksDBStore>, +>; + +pub fn execute_test_within_environment(test_function: F) -> O +where + F: Fn( + AccountAndControllingKey, + PublishingConfiguration, + PublishingReceipt, + ComponentAddress, + &mut StatefulTestRunner<'_>, + ) -> O, +{ + // Creating the database and the necessary overlays to run the tests. + + let overlayed_state_manager_database = + UnmergeableSubstateDatabaseOverlay::new_unmergeable( + SUBSTATE_MANAGER_DATABASE.deref(), + ); + + // Creating a test runner from the overlayed state manager database + let mut test_runner = TestRunnerBuilder::new() + .with_custom_database(overlayed_state_manager_database) + .without_trace() + .build_without_bootstrapping(); + + // Creating a new account which we will be using as the notary and funding + // it with XRD. Since there is no faucet on mainnet, the only way we can + // fund this account is by disabling the auth module and minting XRD. + let notary_private_key = + PrivateKey::Ed25519(Ed25519PrivateKey::from_u64(1).unwrap()); + let notary_account_address = + ComponentAddress::virtual_account_from_public_key( + ¬ary_private_key.public_key(), + ); + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .mint_fungible(XRD, dec!(100_000_000_000)) + .deposit_batch(notary_account_address) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::COSTING + & !EnabledModules::AUTH, + ) + .expect_commit_success(); + + // The notary account address now has the fees required to be able to pay + // for the deployment of Ignition. We now get the configuration and run the + // deployment. + let mut simulator_network_connection_provider = + SimulatorNetworkConnector::new_with_test_runner( + test_runner, + NetworkDefinition::mainnet(), + ); + let publishing_configuration = ConfigurationSelector::MainnetProduction + .configuration(¬ary_private_key); + let publishing_receipt = publish( + &publishing_configuration, + &mut simulator_network_connection_provider, + ) + .expect("Publishing of Ignition must succeed!"); + let mut test_runner = + simulator_network_connection_provider.into_test_runner(); + + // Modifying the Ignition component state so that we can use it in tests. + // What we will modify is the Oracle to use where we will be using an actual + // oracle that is live on mainnet that prices are being submitted to. We + // also need to allow for opening and closing of liquidity positions. + // Additionally, we fund Ignition with XRD. + let oracle = component_address!( + "component_rdx1crty68w9d6ud4ecreewvpsvgyq0u9ta8syqrmuzelem593putyu79e" + ); + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .call_method( + publishing_receipt.components.protocol_entities.ignition, + "set_oracle_adapter", + (oracle,), + ) + .call_method( + publishing_receipt.components.protocol_entities.ignition, + "set_is_open_position_enabled", + (true,), + ) + .call_method( + publishing_receipt.components.protocol_entities.ignition, + "set_is_close_position_enabled", + (true,), + ) + .mint_fungible(XRD, dec!(200_000_000_000_000)) + .take_from_worktop(XRD, dec!(100_000_000_000_000), "volatile") + .take_from_worktop( + XRD, + dec!(100_000_000_000_000), + "non_volatile", + ) + .with_name_lookup(|builder, _| { + let volatile = builder.bucket("volatile"); + let non_volatile = builder.bucket("non_volatile"); + + builder + .call_method( + publishing_receipt + .components + .protocol_entities + .ignition, + "deposit_protocol_resources", + (volatile, Volatility::Volatile), + ) + .call_method( + publishing_receipt + .components + .protocol_entities + .ignition, + "deposit_protocol_resources", + (non_volatile, Volatility::NonVolatile), + ) + }) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::COSTING + & !EnabledModules::AUTH, + ) + .expect_commit_success(); + + // Creating an account to use for the testing that has some of each of the + // resources + let test_account_private_key = + PrivateKey::Ed25519(Ed25519PrivateKey::from_u64(2).unwrap()); + let test_account_address = + ComponentAddress::virtual_account_from_public_key( + &test_account_private_key.public_key(), + ); + test_runner + .execute_manifest_with_enabled_modules( + TransactionManifestV1 { + instructions: std::iter::once(XRD) + .chain(publishing_receipt.user_resources.iter().copied()) + .map(|resource_address| InstructionV1::CallMethod { + address: resource_address.into(), + method_name: FUNGIBLE_RESOURCE_MANAGER_MINT_IDENT + .to_owned(), + args: to_manifest_value( + &FungibleResourceManagerMintInput { + amount: dec!(100_000_000_000), + }, + ) + .unwrap(), + }) + .chain(std::iter::once(InstructionV1::CallMethod { + address: test_account_address.into(), + method_name: ACCOUNT_DEPOSIT_BATCH_IDENT.into(), + args: to_manifest_value(&( + ManifestExpression::EntireWorktop, + )) + .unwrap(), + })) + .collect(), + blobs: Default::default(), + }, + EnabledModules::for_notarized_transaction() + & !EnabledModules::COSTING + & !EnabledModules::AUTH, + ) + .expect_commit_success(); + let test_account = + AccountAndControllingKey::new_virtual_account(test_account_private_key); + + // We are now ready to execute the function callback and return its output + // back + test_function( + test_account, + publishing_configuration, + publishing_receipt, + oracle, + &mut test_runner, + ) +} + +/// This macro can be applied to any function to turn it into a test function +/// that takes arguments. The arguments given is the mainnet state after the +/// publishing of Ignition to the network. The following is an example: +/// +/// ```no_run +/// use macro_rules_attribute::apply; +/// +/// #[apply(mainnet_test)] +/// fn example( +/// _test_account: AccountAndControllingKey, +/// _configuration: PublishingConfiguration, +/// _receipt: PublishingReceipt, +/// _test_runner: &mut StatefulTestRunner<'_>, +/// ) -> Result<(), RuntimeError> { +/// assert!(false); +/// Ok(()) +/// } +/// ``` +/// +/// The above function will be treated as a test and will be discoverable by +/// the testing harness. +#[macro_export] +macro_rules! mainnet_test { + ( + $(#[$meta: meta])* + $fn_vis: vis fn $fn_ident: ident ( + $($tokens: tt)* + ) $(-> $return_type: ty)? $block: block + ) => { + #[test] + $(#[$meta])* + $fn_vis fn $fn_ident () $(-> $return_type)? { + $crate::execute_test_within_environment(| + $($tokens)* + | -> $crate::resolve_return_type!($(-> $return_type)?) { + $block + }) + } + }; +} + +#[macro_export] +macro_rules! resolve_return_type { + () => { + () + }; + (-> $type: ty) => { + $type + }; +} + +#[ext] +pub impl<'a> StatefulTestRunner<'a> { + fn execute_manifest_without_auth( + &mut self, + manifest: TransactionManifestV1, + ) -> TransactionReceiptV1 { + self.execute_manifest_with_enabled_modules( + manifest, + EnabledModules::for_notarized_transaction() & !EnabledModules::AUTH, + ) + } + + fn execute_manifest_with_enabled_modules( + &mut self, + manifest: TransactionManifestV1, + enabled_modules: EnabledModules, + ) -> TransactionReceiptV1 { + let mut execution_config = ExecutionConfig::for_notarized_transaction( + NetworkDefinition::mainnet(), + ); + execution_config.enabled_modules = enabled_modules; + + let nonce = self.next_transaction_nonce(); + let test_transaction = TestTransaction::new_from_nonce(manifest, nonce); + let prepared_transaction = test_transaction.prepare().unwrap(); + let executable = + prepared_transaction.get_executable(Default::default()); + self.execute_transaction( + executable, + Default::default(), + execution_config, + ) + } + + /// Constructs a notarized transaction and executes it. This is primarily + /// used in the testing of fees to make sure that they're approximated in + /// the best way. + fn construct_and_execute_notarized_transaction( + &mut self, + manifest: TransactionManifestV1, + notary_private_key: &PrivateKey, + ) -> TransactionReceiptV1 { + let network_definition = NetworkDefinition::simulator(); + let current_epoch = self.get_current_epoch(); + let transaction = TransactionBuilder::new() + .header(TransactionHeaderV1 { + network_id: network_definition.id, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch.after(10).unwrap(), + nonce: self.next_transaction_nonce(), + notary_public_key: notary_private_key.public_key(), + notary_is_signatory: true, + tip_percentage: 0, + }) + .manifest(manifest) + .notarize(notary_private_key) + .build(); + self.execute_raw_transaction( + &network_definition, + &transaction.to_raw().unwrap(), + ) + } +} diff --git a/testing/stateful-tests/src/main.rs b/testing/stateful-tests/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/testing/stateful-tests/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/testing/stateful-tests/tests/lib.rs b/testing/stateful-tests/tests/lib.rs new file mode 100644 index 00000000..18fb4652 --- /dev/null +++ b/testing/stateful-tests/tests/lib.rs @@ -0,0 +1,438 @@ +#![allow(clippy::arithmetic_side_effects)] + +use common::prelude::*; +use macro_rules_attribute::apply; +use publishing_tool::publishing::*; +use radix_engine::blueprints::consensus_manager::*; +use radix_engine::blueprints::models::*; +use radix_engine::system::system_db_reader::*; +use radix_engine::system::system_modules::EnabledModules; +use radix_engine::types::*; +use radix_engine_interface::blueprints::consensus_manager::*; +use stateful_tests::*; +use transaction::prelude::*; + +#[apply(mainnet_test)] +fn all_ignition_entities_are_linked_to_the_dapp_definition_in_accordance_with_the_metadata_standard( + _: AccountAndControllingKey, + _: PublishingConfiguration, + receipt: PublishingReceipt, + _: ComponentAddress, + test_runner: &mut StatefulTestRunner<'_>, +) { + // Collecting all of the entities into an array + let ignition_entities = receipt + .badges + .iter() + .copied() + .map(GlobalAddress::from) + .chain( + receipt + .packages + .protocol_entities + .iter() + .copied() + .map(GlobalAddress::from), + ) + .chain( + receipt + .packages + .exchange_adapter_entities + .iter() + .copied() + .map(GlobalAddress::from), + ) + .chain( + receipt + .components + .protocol_entities + .iter() + .copied() + .map(GlobalAddress::from), + ) + .chain( + receipt + .components + .exchange_adapter_entities + .iter() + .copied() + .map(GlobalAddress::from), + ) + .chain( + receipt + .exchange_information + .iter() + .filter_map(|information| { + information + .as_ref() + .map(|information| information.liquidity_receipt) + }) + .map(GlobalAddress::from), + ) + .collect::>(); + + // Validating that the dapp definition account has the correct fields and + // metadata values. + let dapp_definition_account = receipt.dapp_definition_account; + let Some(MetadataValue::String(dapp_definition_account_type)) = test_runner + .get_metadata(dapp_definition_account.into(), "account_type") + else { + panic!("Dapp definition account type either does not exist or isn't a string") + }; + let Some(MetadataValue::GlobalAddressArray( + dapp_definition_claimed_entities, + )) = test_runner + .get_metadata(dapp_definition_account.into(), "claimed_entities") + else { + panic!("Dapp definition claimed entities either does not exist or is not an array") + }; + assert_eq!(dapp_definition_account_type, "dapp definition"); + assert_eq!( + dapp_definition_claimed_entities + .into_iter() + .collect::>(), + ignition_entities.iter().copied().collect::>() + ); + + // Validating the dapp definition of components and packages. They have the + // metadata field "dapp_definition" (not plural) and its just an address and + // not an array. + for entity_address in ignition_entities.iter().filter(|address| { + PackageAddress::try_from(**address).is_ok() + || ComponentAddress::try_from(**address).is_ok() + }) { + let Some(MetadataValue::GlobalAddress(linked_dapp_definition_account)) = + test_runner.get_metadata(*entity_address, "dapp_definition") + else { + panic!("Dapp definition key does not exist on package or component") + }; + assert_eq!( + linked_dapp_definition_account, + dapp_definition_account.into() + ) + } + + // Validating the dapp definition of resources. They have the metadata field + // "dapp_definitions" (plural) and its an array of dapp definitions. + for entity_address in ignition_entities + .iter() + .filter(|address| ResourceAddress::try_from(**address).is_ok()) + { + let Some(MetadataValue::GlobalAddressArray( + linked_dapp_definition_account, + )) = test_runner.get_metadata(*entity_address, "dapp_definitions") + else { + panic!( + "Dapp definition key does not exist on resource: {}", + entity_address.to_hex() + ) + }; + assert_eq!( + linked_dapp_definition_account.first().copied().unwrap(), + dapp_definition_account.into() + ) + } +} + +macro_rules! define_open_and_close_liquidity_position_tests { + ( + $( + $exchange_ident: ident => [ + $( + $resource_ident: ident + ),* $(,)? + ] + ),* $(,)? + ) => { + $( + $( + define_open_and_close_liquidity_position_tests!($exchange_ident, $resource_ident, 9); + define_open_and_close_liquidity_position_tests!($exchange_ident, $resource_ident, 10); + define_open_and_close_liquidity_position_tests!($exchange_ident, $resource_ident, 11); + define_open_and_close_liquidity_position_tests!($exchange_ident, $resource_ident, 12); + )* + )* + }; + ( + $exchange_ident: ident, + $resource_ident: ident, + $lockup_period: expr + ) => { + paste::paste! { + #[apply(mainnet_test)] + fn [< can_open_an_ignition_position_in_ $exchange_ident _ $resource_ident _pool_with_ $lockup_period _months_in_lock_up >]( + AccountAndControllingKey { + account_address: test_account, + controlling_key: test_account_private_key, + }: AccountAndControllingKey, + _: PublishingConfiguration, + receipt: PublishingReceipt, + _: ComponentAddress, + test_runner: &mut StatefulTestRunner<'_>, + ) { + // Arrange + let Some(ExchangeInformation { pools, .. }) = + receipt.exchange_information.$exchange_ident + else { + panic!("No {} pools", stringify!($exchange_ident)); + }; + let pool = pools.$resource_ident; + let user_resource = receipt.user_resources.$resource_ident; + + let current_epoch = test_runner.get_current_epoch(); + + // Act + let transaction = TransactionBuilder::new() + .header(TransactionHeaderV1 { + network_id: 1, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch.after(10).unwrap(), + nonce: test_runner.next_transaction_nonce(), + notary_public_key: test_account_private_key.public_key(), + notary_is_signatory: true, + tip_percentage: 0, + }) + .manifest( + ManifestBuilder::new() + .lock_fee(test_account, dec!(10)) + .withdraw_from_account(test_account, user_resource, dec!(1000)) + .take_all_from_worktop(user_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + receipt.components.protocol_entities.ignition, + "open_liquidity_position", + (bucket, pool, LockupPeriod::from_months($lockup_period).unwrap()), + ) + }) + .deposit_batch(test_account) + .build(), + ) + .notarize(&test_account_private_key) + .build(); + let receipt = test_runner.execute_raw_transaction( + &NetworkDefinition::mainnet(), + &transaction.to_raw().unwrap(), + ); + + // Assert + receipt.expect_commit_success(); + println!( + "Opening a position in {} {} pool costs {} XRD in total with {} XRD in execution", + stringify!($exchange_ident), + stringify!($resource_ident), + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); + + } + + #[apply(mainnet_test)] + fn [< can_open_and_close_an_ignition_position_in_ $exchange_ident _ $resource_ident _pool_with_ $lockup_period _months_in_lock_up >]( + AccountAndControllingKey { + account_address: test_account, + controlling_key: test_account_private_key, + }: AccountAndControllingKey, + _: PublishingConfiguration, + receipt: PublishingReceipt, + oracle: ComponentAddress, + test_runner: &mut StatefulTestRunner<'_>, + ) { + // Arrange + let Some(ExchangeInformation { pools, liquidity_receipt, .. }) = + receipt.exchange_information.$exchange_ident + else { + panic!("No {} pools", stringify!($exchange_ident)); + }; + let pool = pools.$resource_ident; + let user_resource = receipt.user_resources.$resource_ident; + + let current_epoch = test_runner.get_current_epoch(); + + let transaction = TransactionBuilder::new() + .header(TransactionHeaderV1 { + network_id: 1, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch.after(10).unwrap(), + nonce: test_runner.next_transaction_nonce(), + notary_public_key: test_account_private_key.public_key(), + notary_is_signatory: true, + tip_percentage: 0, + }) + .manifest( + ManifestBuilder::new() + .lock_fee(test_account, dec!(10)) + .withdraw_from_account(test_account, user_resource, dec!(1000)) + .take_all_from_worktop(user_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + receipt.components.protocol_entities.ignition, + "open_liquidity_position", + (bucket, pool, LockupPeriod::from_months($lockup_period).unwrap()), + ) + }) + .deposit_batch(test_account) + .build(), + ) + .notarize(&test_account_private_key) + .build(); + let transaction_receipt = test_runner.execute_raw_transaction( + &NetworkDefinition::mainnet(), + &transaction.to_raw().unwrap(), + ); + + transaction_receipt.expect_commit_success(); + + // Set the current time to be 6 months from now. + { + let current_time = + test_runner.get_current_time(TimePrecisionV2::Minute); + let maturity_instant = current_time + .add_seconds( + *LockupPeriod::from_months($lockup_period).unwrap().seconds() as i64 + ) + .unwrap(); + let db = test_runner.substate_db_mut(); + let mut writer = SystemDatabaseWriter::new(db); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMilliTimestamp.field_index(), + ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, + ), + ) + .unwrap(); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMinuteTimestamp.field_index(), + ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( + ProposerMinuteTimestampSubstate { + epoch_minute: i32::try_from( + maturity_instant.seconds_since_unix_epoch / 60, + ) + .unwrap(), + }, + ), + ) + .unwrap(); + } + + { + let (price, _) = test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .call_method( + oracle, + "get_price", + (user_resource, XRD), + ) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success() + .output::<(Decimal, Instant)>(0); + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .call_method( + oracle, + "set_price", + (user_resource, XRD, price), + ) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success(); + } + + let current_epoch = test_runner.get_current_epoch(); + + // Act + let transaction = TransactionBuilder::new() + .header(TransactionHeaderV1 { + network_id: 1, + start_epoch_inclusive: current_epoch, + end_epoch_exclusive: current_epoch.after(10).unwrap(), + nonce: test_runner.next_transaction_nonce(), + notary_public_key: test_account_private_key.public_key().into(), + notary_is_signatory: true, + tip_percentage: 0, + }) + .manifest( + ManifestBuilder::new() + .lock_fee(test_account, dec!(10)) + .withdraw_from_account( + test_account, + liquidity_receipt, + dec!(1), + ) + .take_all_from_worktop( + liquidity_receipt, + "bucket", + ) + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + receipt.components.protocol_entities.ignition, + "close_liquidity_position", + (bucket,), + ) + }) + .deposit_batch(test_account) + .build(), + ) + .notarize(&test_account_private_key) + .build(); + let receipt = test_runner.execute_raw_transaction( + &NetworkDefinition::mainnet(), + &transaction.to_raw().unwrap(), + ); + + // Assert + receipt.expect_commit_success(); + println!( + "Closing a position in {} {} pool costs {} XRD in total with {} XRD in execution", + stringify!($exchange_ident), + stringify!($resource_ident), + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); + } + } + }; +} + +define_open_and_close_liquidity_position_tests! { + caviarnine_v1 => [ + bitcoin, + ethereum, + usdc, + usdt + ], + // TODO: Enable once Defiplaza's prices are inline with what the oracle is + // reporting. + // defiplaza_v2 => [ + // bitcoin, + // ethereum, + // usdc, + // usdt + // ], + // TODO: Enable once Ociswap v2 is live on mainnet and once they have their + // pools inline with the oracle prices. + // ociswap_v2 => [ + // bitcoin, + // ethereum, + // usdc, + // usdt + // ] +} diff --git a/tools/publishing-tool/src/configuration_selector/mod.rs b/tools/publishing-tool/src/configuration_selector/mod.rs index 36b89edf..e6b2fb4d 100644 --- a/tools/publishing-tool/src/configuration_selector/mod.rs +++ b/tools/publishing-tool/src/configuration_selector/mod.rs @@ -19,7 +19,9 @@ impl ConfigurationSelector { notary_private_key: &PrivateKey, ) -> PublishingConfiguration { match self { - Self::MainnetProduction => todo!(), + Self::MainnetProduction => { + mainnet_production::mainnet_production(notary_private_key) + } Self::MainnetTesting => { mainnet_testing::mainnet_testing(notary_private_key) } diff --git a/tools/publishing-tool/src/publishing/configuration.rs b/tools/publishing-tool/src/publishing/configuration.rs index a8f2de78..ce655337 100644 --- a/tools/publishing-tool/src/publishing/configuration.rs +++ b/tools/publishing-tool/src/publishing/configuration.rs @@ -52,12 +52,14 @@ pub struct PublishingConfiguration { #[derive(Debug, Clone, ScryptoSbor)] pub struct PublishingReceipt { + pub dapp_definition_account: ComponentAddress, pub packages: Entities, pub components: Entities, pub exchange_information: ExchangeIndexedData< Option>, >, pub protocol_configuration: ProtocolConfigurationReceipt, + pub user_resources: UserResourceIndexedData, pub badges: BadgeIndexedData, } diff --git a/tools/publishing-tool/src/publishing/handler.rs b/tools/publishing-tool/src/publishing/handler.rs index 2417382a..d3b129f5 100644 --- a/tools/publishing-tool/src/publishing/handler.rs +++ b/tools/publishing-tool/src/publishing/handler.rs @@ -839,7 +839,7 @@ pub fn publish( ) .call_method( resolved_adapter_component_addresses.defiplaza_v2, - "add_pair_config", + "add_pair_configs", (pair_config_map,), ) .build(); @@ -1118,6 +1118,7 @@ pub fn publish( } Ok(PublishingReceipt { + dapp_definition_account, packages: Entities { protocol_entities: resolved_blueprint_ids .protocol_entities @@ -1156,6 +1157,7 @@ pub fn publish( information.as_ref().map(|information| information.pools) }), }, + user_resources: resolved_user_resources, badges: resolved_badges.map(|(_, address)| *address), }) } From d06a1a095074dc906d079b8cc8846de99078aea4 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 8 Mar 2024 10:57:21 +0300 Subject: [PATCH 35/47] [Tests]: Do not run stateful tests in CI since there is no DB. --- .github/workflows/test.yml | 2 +- testing/stateful-tests/src/lib.rs | 2 +- .../src/configuration_selector/mainnet_production.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab59ecb0..e61e88e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.3 - name: Run tests - run: cargo test --features package-loader/build-time-blueprints + run: cargo test --workspace --features package-loader/build-time-blueprints --exclude stateful-tests env: # Enable sccache SCCACHE_GHA_ENABLED: "true" diff --git a/testing/stateful-tests/src/lib.rs b/testing/stateful-tests/src/lib.rs index 9995dcb6..7fe45542 100644 --- a/testing/stateful-tests/src/lib.rs +++ b/testing/stateful-tests/src/lib.rs @@ -224,7 +224,7 @@ where /// that takes arguments. The arguments given is the mainnet state after the /// publishing of Ignition to the network. The following is an example: /// -/// ```no_run +/// ```norun /// use macro_rules_attribute::apply; /// /// #[apply(mainnet_test)] diff --git a/tools/publishing-tool/src/configuration_selector/mainnet_production.rs b/tools/publishing-tool/src/configuration_selector/mainnet_production.rs index 1c94ddfb..44ad446c 100644 --- a/tools/publishing-tool/src/configuration_selector/mainnet_production.rs +++ b/tools/publishing-tool/src/configuration_selector/mainnet_production.rs @@ -312,7 +312,7 @@ pub fn mainnet_production( additional_information: AdditionalInformation { ociswap_v2_registry_component_and_dapp_definition: None, }, - additional_operation_flags: AdditionalOperationFlags::empty(), + additional_operation_flags: AdditionalOperationFlags::empty(), } // cSpell:enable } From 381a3c06236a0dc889c8e35d8cedd5cadf171726 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 14:44:36 +0300 Subject: [PATCH 36/47] [Defiplaza v2 Adapter v1]: Fix fee calculations and add exact fee tests. --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7db6fc0..892e2728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,9 +209,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecount" @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" +checksum = "635179be18797d7e10edb9cd06c859580237750c7351f39ed9b298bfc17544ad" dependencies = [ "cc", "cxxbridge-flags", @@ -503,9 +503,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" +checksum = "9324397d262f63ef77eb795d900c0d682a34a43ac0932bec049ed73055d52f63" dependencies = [ "cc", "codespan-reporting", @@ -518,15 +518,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" +checksum = "a87ff7342ffaa54b7c61618e0ce2bbcf827eba6d55b923b83d82551acbbecfe5" [[package]] name = "cxxbridge-macro" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" +checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1" dependencies = [ "proc-macro2", "quote", From 42cd445ee3911e067fbbac262cf2a2f2ea267cf9 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 14:45:14 +0300 Subject: [PATCH 37/47] [Defiplaza v2 Adapter v1]: Update the fee calculation and add tests for fees --- libraries/common/src/indexed_buckets.rs | 7 + packages/defiplaza-v2-adapter-v1/src/lib.rs | 301 +++++++++++++----- tests/tests/defiplaza_v2.rs | 336 +++++++++++++++----- 3 files changed, 502 insertions(+), 142 deletions(-) diff --git a/libraries/common/src/indexed_buckets.rs b/libraries/common/src/indexed_buckets.rs index b675e82f..b20cfe5d 100644 --- a/libraries/common/src/indexed_buckets.rs +++ b/libraries/common/src/indexed_buckets.rs @@ -130,6 +130,13 @@ impl IndexedBuckets { pub fn into_inner(self) -> IndexMap { self.0 } + + pub fn combine(mut self, other: Self) -> Self { + for bucket in other.0.into_values() { + self.insert(bucket) + } + self + } } impl Default for IndexedBuckets { diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 6e3fcfab..8ff2c0de 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -326,89 +326,248 @@ pub mod adapter { (pool_units_bucket1, pool_units_bucket2) }; - // Getting the base and quote assets - let (base_resource_address, quote_resource_address) = - pool.get_tokens(); - // Decoding the adapter specific information as the type we expect // it to be. let DefiPlazaV2AdapterSpecificInformation { original_targets } = adapter_specific_information.as_typed().unwrap(); - let [old_base_target, old_quote_target] = - [base_resource_address, quote_resource_address].map( - |address| original_targets.get(&address).copied().unwrap(), - ); - - // Step 1: Get the pair's state - let pair_state = pool.get_state(); - - // Step 2 & 3: Determine which of the resources is in shortage and - // based on that determine what the new target should be. - let claimed_tokens = IndexedBuckets::from_buckets( - [pool_units1, pool_units2].into_iter().flat_map(|bucket| { - let resource_manager = bucket.resource_manager(); - let entry = ComponentAddress::try_from( - resource_manager - .get_metadata::<_, GlobalAddress>("pool") - .unwrap() - .unwrap(), - ) - .unwrap(); - let mut two_resource_pool = - Global::::from(entry); - let (bucket1, bucket2) = two_resource_pool.redeem(bucket); - [bucket1, bucket2] - }), - ); - let base_bucket = - claimed_tokens.get(&base_resource_address).unwrap(); - let quote_bucket = - claimed_tokens.get("e_resource_address).unwrap(); - let base_bucket_amount = base_bucket.amount(); - let quote_bucket_amount = quote_bucket.amount(); + // We have gotten two pools units, one of the base pool and + // another for the quote pool. We need to determine which is + // which and overall split things according to shortage and + // surplus. So, instead of referring to them as base and quote + // we would figure out what is in shortage and what is surplus. + + // First thing we do is store the pool units in a map where the + // key the address of the pool and the value is the pool units + // bucket. We find the address of the pool through metadata on + // the pool units since there is currently no other way to find + // this information. + let mut pool_component_to_pool_unit_mapping = + [pool_units1, pool_units2] + .into_iter() + .map(|bucket| { + let resource_manager = bucket.resource_manager(); + let pool_address = ComponentAddress::try_from( + resource_manager + .get_metadata::<_, GlobalAddress>("pool") + .unwrap() + .unwrap(), + ) + .unwrap(); + (pool_address, bucket) + }) + .collect::>(); + + // With the way we combined them above we now want to split them + // into base pool units and quote pool units. This is simple to + // do, just get the address of the base and quote pools and then + // do a simple `remove` from the above map. + let (base_pool_component, quote_pool_component) = pool.get_pools(); + let base_pool_units = pool_component_to_pool_unit_mapping + .remove(&base_pool_component) + .unwrap(); + let quote_pool_units = pool_component_to_pool_unit_mapping + .remove("e_pool_component) + .unwrap(); - let shortage = pair_state.shortage; - let shortage_state = ShortageState::from(shortage); - let (new_base_target, new_quote_target) = match shortage_state { + // At this point we have the the base and quote token addresses, + // pool addresses, and pool units. We can now split things as + // shortage and quote and stop referring to things as base and + // quote. + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + let pair_state = pool.get_state(); + let (claimed_resources, fees) = match ShortageState::from( + pair_state.shortage, + ) { + // The pool is in equilibrium, none of the assets are in + // shortage so there is no need to multiply anything by the + // target ratio. ShortageState::Equilibrium => { - (base_bucket_amount, quote_bucket_amount) + // Claiming the assets from the pools. + let [resources_claimed_from_base_resource_pool, resources_claimed_from_quote_resource_pool] = + [ + (base_pool_component, base_pool_units), + (quote_pool_component, quote_pool_units), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the two resources is just the amount we + // got back when closing the liquidity position. + let new_target_of_base_resource = + resources_claimed_from_base_resource_pool + .get(&base_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + let new_target_of_quote_resource = + resources_claimed_from_quote_resource_pool + .get("e_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the base and quote + // resources we can calculate the fees. + let base_resource_fees = original_targets + .get(&base_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_base_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let quote_resource_fees = original_targets + .get("e_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_quote_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + base_resource_address => base_resource_fees, + quote_resource_address => quote_resource_fees, + }; + + let claimed_resources = + resources_claimed_from_base_resource_pool.combine( + resources_claimed_from_quote_resource_pool, + ); + + (claimed_resources, fees) + } + // One of the assets is in shortage and the other is in + // surplus. Determine which is which and sort the info. + ShortageState::Shortage(asset) => { + let ( + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + shortage_asset_resource_address, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + surplus_asset_resource_address, + ), + ) = match asset { + Asset::Base => ( + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ), + Asset::Quote => ( + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ), + }; + + // We have now split them into shortage and surplus and + // we can now close the liquidity positions and compute + // the new targets for the base and shortage assets. + let [resources_claimed_from_shortage_asset_pool, resources_claimed_from_surplus_asset_pool] = + [ + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + ), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the shortage asset can be calculated by + // multiplying the amount we got back from closing the + // position in the shortage pool by the target ratio of + // the pool in the current state. + let new_target_of_shortage_asset = + resources_claimed_from_shortage_asset_pool + .get(&shortage_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR) + .checked_mul(pair_state.target_ratio) + .expect(OVERFLOW_ERROR); + + // The target of the surplus asset is simple, its the + // amount we got back when we closed the position in + // the surplus pool. + let new_target_of_surplus_asset = + resources_claimed_from_surplus_asset_pool + .get(&surplus_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the shortage and + // surplus assets we can calculate the fees earned on + // those assets. Its calculated by subtracting the + // new targets from the original targets. + let shortage_asset_fees = new_target_of_shortage_asset + .checked_sub( + original_targets + .get(&shortage_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let surplus_asset_fees = new_target_of_surplus_asset + .checked_sub( + original_targets + .get(&surplus_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + shortage_asset_resource_address => shortage_asset_fees, + surplus_asset_resource_address => surplus_asset_fees, + }; + + let claimed_resources = + resources_claimed_from_shortage_asset_pool + .combine(resources_claimed_from_surplus_asset_pool); + + (claimed_resources, fees) } - ShortageState::Shortage(Asset::Base) => ( - base_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - quote_bucket_amount, - ), - ShortageState::Shortage(Asset::Quote) => ( - base_bucket_amount, - quote_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - ), }; - // Steps 4 and 5 - let base_fees = std::cmp::max( - new_base_target - .checked_sub(old_base_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - let quote_fees = std::cmp::max( - new_quote_target - .checked_sub(old_quote_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - CloseLiquidityPositionOutput { - resources: claimed_tokens, + resources: claimed_resources, others: vec![], - fees: indexmap! { - base_resource_address => base_fees, - quote_resource_address => quote_fees, - }, + fees, } } diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 7644d463..bb3f4fb4 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -915,37 +915,231 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ } #[test] -#[ignore = "Awaiting defiplaza response"] fn exact_fee_test1() { test_exact_defiplaza_fees_amounts( // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::UserResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test2() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test3() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test4() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test5() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - protocol_resource: dec!(100_000), - user_resource: dec!(100_000), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test6() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, // Initial price of the pool dec!(1), - // The fee percentage of the pool - dec!(0.03), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, // User contribution to the pool. This would mean that the user would - // own 0.1% of the pool + // own 100% of the pool. AssetIndexedData { - user_resource: dec!(100), - protocol_resource: dec!(100), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, // The swaps to perform - the asset you see is the input asset - vec![(Asset::ProtocolResource, dec!(1_000))], - // The fees to expect - with 0.1% pool ownership of the pool and fees of - // 3% then we expect to see 0.03 of the protocol resource in fees (as it - // was the input in the swap) and none of the user resource in fees. + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test7() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - user_resource: EqualityCheck::ExactlyEquals(dec!(0)), - protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(3996)), + (Asset::UserResource, dec!(898)), + (Asset::ProtocolResource, dec!(7953)), + (Asset::ProtocolResource, dec!(3390)), + (Asset::ProtocolResource, dec!(4297)), + (Asset::ProtocolResource, dec!(2252)), + (Asset::UserResource, dec!(5835)), + (Asset::ProtocolResource, dec!(5585)), + (Asset::UserResource, dec!(7984)), + (Asset::ProtocolResource, dec!(8845)), + (Asset::ProtocolResource, dec!(4511)), + (Asset::UserResource, dec!(1407)), + (Asset::UserResource, dec!(4026)), + (Asset::UserResource, dec!(8997)), + (Asset::ProtocolResource, dec!(1950)), + (Asset::UserResource, dec!(8016)), + (Asset::UserResource, dec!(8322)), + (Asset::UserResource, dec!(5149)), + (Asset::ProtocolResource, dec!(6411)), + (Asset::ProtocolResource, dec!(1013)), + (Asset::ProtocolResource, dec!(3333)), + (Asset::ProtocolResource, dec!(4130)), + (Asset::UserResource, dec!(2786)), + (Asset::UserResource, dec!(5828)), + (Asset::UserResource, dec!(8974)), + (Asset::UserResource, dec!(6476)), + (Asset::ProtocolResource, dec!(8942)), + (Asset::UserResource, dec!(2159)), + (Asset::UserResource, dec!(8387)), + (Asset::UserResource, dec!(2830)), + ], ) .expect("Should not fail!") } +#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Asset { UserResource, @@ -958,26 +1152,24 @@ struct AssetIndexedData { protocol_resource: T, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum EqualityCheck { - ExactlyEquals(T), - ApproximatelyEquals { value: T, acceptable_difference: T }, -} - +/// This test will open a position for the user in a Defiplaza liquidity +/// pool and then perform a bunch of swaps to generate fees and then asset +/// that the amount of fees obtained as reported by the adapter matches the +/// amount that we expect the fees to be. An important note, Defiplaza fees +/// are collected on the output token and not the input token, so they're a +/// percentage of the output amount. fn test_exact_defiplaza_fees_amounts( // The initial amount of liquidity to provide when creating the liquidity // pool. - initial_liquidity: AssetIndexedData, + initial_liquidity: Option>, // The price to set as the initial price of the pool. initial_price: Decimal, - // The fee percentage of the pool - fee_percentage: Decimal, + // The pair configuration of the defiplaza pool + pair_configuration: PairConfig, // The contribution that the user will make to the pool user_contribution: AssetIndexedData, // The swaps to perform on the pool. swaps: Vec<(Asset, Decimal)>, - // Equality checks to perform when closing the liquidity position. - expected_fees: AssetIndexedData>, ) -> Result<(), RuntimeError> { let Environment { environment: ref mut env, @@ -1000,35 +1192,32 @@ fn test_exact_defiplaza_fees_amounts( OwnerRole::None, resources.user_resource, resources.protocol_resource, - PairConfig { - k_in: dec!("0.4"), - k_out: dec!("1"), - fee: fee_percentage, - decay_factor: dec!("0.9512"), - }, + pair_configuration, initial_price, defiplaza_v2.package, env, )?; // Providing the desired initial contribution to the pool. - [ - (resources.user_resource, initial_liquidity.user_resource), - ( - resources.protocol_resource, - initial_liquidity.protocol_resource, - ), - ] - .map(|(resource_address, amount)| { - let bucket = ResourceManager(resource_address) - .mint_fungible(amount, env) - .unwrap(); - let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); - let change_amount = change - .map(|bucket| bucket.amount(env).unwrap()) - .unwrap_or(Decimal::ZERO); - assert_eq!(change_amount, Decimal::ZERO); - }); + if let Some(initial_liquidity) = initial_liquidity { + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + } // Providing the user's contribution to the pool through the adapter let [bucket_x, bucket_y] = [ @@ -1061,6 +1250,7 @@ fn test_exact_defiplaza_fees_amounts( } // Performing the swaps specified by the user + let mut expected_fee_amounts = IndexMap::::new(); for (asset, amount) in swaps.into_iter() { let address = match asset { Asset::ProtocolResource => resources.protocol_resource, @@ -1068,7 +1258,14 @@ fn test_exact_defiplaza_fees_amounts( }; let bucket = ResourceManager(address).mint_fungible(amount, env).unwrap(); - let _ = pool.swap(bucket, env)?; + let (output, _) = pool.swap(bucket, env)?; + let output_resource_address = output.resource_address(env)?; + let swap_output_amount = output.amount(env)?; + let fee = swap_output_amount / (Decimal::ONE - pair_configuration.fee) + * pair_configuration.fee; + *expected_fee_amounts + .entry(output_resource_address) + .or_default() += fee; } // Close the liquidity position @@ -1081,28 +1278,25 @@ fn test_exact_defiplaza_fees_amounts( )?; // Assert that the fees is what's expected. - for (resource_address, equality_check) in [ - (resources.protocol_resource, expected_fees.protocol_resource), - (resources.user_resource, expected_fees.user_resource), - ] { - // Get the fees - let resource_fees = fees.get(&resource_address).copied().unwrap(); - - // Perform the assertion - match equality_check { - EqualityCheck::ExactlyEquals(value) => { - assert_eq!(resource_fees, value) - } - EqualityCheck::ApproximatelyEquals { - value, - acceptable_difference, - } => { - assert!( - (resource_fees - value).checked_abs().unwrap() - <= acceptable_difference - ) - } - } + for resource_address in + [resources.protocol_resource, resources.user_resource] + { + let expected_fees = expected_fee_amounts + .get(&resource_address) + .copied() + .unwrap_or_default(); + let fees = fees.get(&resource_address).copied().unwrap_or_default(); + + let resource_name = if resource_address == resources.protocol_resource { + "protocol" + } else { + "user" + }; + + assert!( + expected_fees - fees <= dec!(0.000001), + "{resource_name} resource assertion failed. Expected: {expected_fees}, Actual: {fees}" + ); } Ok(()) From cc5b086bd92289707998e350c0bc36672934f461 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 11:47:03 +0300 Subject: [PATCH 38/47] [Defiplaza v2 Adapter v1]: Some cleanups and tests --- Cargo.lock | 13 +++ packages/defiplaza-v2-adapter-v1/src/lib.rs | 40 +++------ tests/tests/defiplaza_v2.rs | 94 +++++++++++++++++++++ 3 files changed, 121 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13486e6a..0e69f336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,19 @@ dependencies = [ "transaction", ] +[[package]] +name = "defiplaza-v2-adapter-v1" +version = "0.1.0" +dependencies = [ + "common", + "ports-interface", + "radix-engine-interface", + "sbor", + "scrypto", + "scrypto-interface", + "transaction", +] + [[package]] name = "deranged" version = "0.3.11" diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 38ca81fa..0a99dda0 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -6,8 +6,6 @@ use ports_interface::prelude::*; use scrypto::prelude::*; use scrypto_interface::*; -// TODO: Remove all logging. - macro_rules! define_error { ( $( @@ -59,7 +57,8 @@ pub mod adapter { /// The pair config of the various pools is constant but there is no /// getter function that can be used to get it on ledger. As such, the /// protocol owner or manager must submit this information to the - /// adapter for its operation. + /// adapter for its operation. This does not change, so, once set we + /// do not expect to remove it again. pair_config: KeyValueStore, } @@ -230,11 +229,6 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - info!("Doing the first one"); - info!( - "Shortage before first contribution: {:?}", - pool.get_state().shortage - ); let (first_pool_units, second_change) = match shortage_state { ShortageState::Equilibrium => ( pool.add_liquidity(first_bucket, None).0, @@ -244,10 +238,6 @@ pub mod adapter { pool.add_liquidity(first_bucket, Some(second_bucket)) } }; - info!( - "Shortage after first contribution: {:?}", - pool.get_state().shortage - ); // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets @@ -256,17 +246,19 @@ pub mod adapter { let second_original_target = second_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - info!("Doing the second one"); let (second_pool_units, change) = pool.add_liquidity(second_bucket, None); - info!( - "Shortage after second contribution: {:?}", - pool.get_state().shortage - ); - // TODO: Should we subtract the change from the second original - // target? Seems like we should if the price if not the same in - // some way? + // We've been told that the change should be zero. Therefore, we + // assert for it to make sure that everything is as we expect it + // to be. + assert_eq!( + change + .as_ref() + .map(|bucket| bucket.amount()) + .unwrap_or(Decimal::ZERO), + Decimal::ZERO + ); // A sanity check to make sure that everything is correct. The pool // units obtained from the first contribution should be different @@ -433,7 +425,6 @@ pub mod adapter { Global::::from(base_pool), Global::::from(quote_pool), ); - info!("bid ask = {bid_ask:?}"); let average_price = bid_ask .bid @@ -441,7 +432,6 @@ pub mod adapter { .and_then(|value| value.checked_div(dec!(2))) .expect(OVERFLOW_ERROR); - info!("average_price = {average_price}"); Price { base: base_resource_address, quote: quote_resource_address, @@ -488,8 +478,8 @@ impl From for AnyValue { // source code is licensed under the MIT license which allows us to do such // copies and modification of code. // -// This module exposes two main functions which are the entrypoints into this -// module's functionality which calculate the incoming and outgoing spot prices. +// The `calculate_pair_prices` function is the entrypoint into the module and is +// the function to calculate the current bid and ask prices of the pairs. #[allow(clippy::arithmetic_side_effects)] mod price_math { use super::*; @@ -564,8 +554,6 @@ mod price_math { let bid = incoming_spot; let ask = outgoing_spot; - info!("Shortage = {:?}", pair_state.shortage); - // TODO: What to do at equilibrium? match pair_state.shortage { Shortage::Equilibrium | Shortage::BaseShortage => { diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 323bc054..c058189e 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -822,3 +822,97 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_base_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = base_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = quote_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} From c4910fc9cd7e4c0a889b8b23970f4c36efdd77be Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 15:21:19 +0300 Subject: [PATCH 39/47] [Misc]: Fix cargo.lock issue --- Cargo.lock | 117 ++++++++++++++++++++++++++--------------------------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e69f336..c7e03397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecount" @@ -328,10 +328,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ + "jobserver", "libc", ] @@ -358,9 +359,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -382,9 +383,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -392,9 +393,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -566,9 +567,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" +checksum = "635179be18797d7e10edb9cd06c859580237750c7351f39ed9b298bfc17544ad" dependencies = [ "cc", "cxxbridge-flags", @@ -578,9 +579,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" +checksum = "9324397d262f63ef77eb795d900c0d682a34a43ac0932bec049ed73055d52f63" dependencies = [ "cc", "codespan-reporting", @@ -593,15 +594,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" +checksum = "a87ff7342ffaa54b7c61618e0ce2bbcf827eba6d55b923b83d82551acbbecfe5" [[package]] name = "cxxbridge-macro" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" +checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1" dependencies = [ "proc-macro2", "quote", @@ -669,19 +670,6 @@ dependencies = [ "transaction", ] -[[package]] -name = "defiplaza-v2-adapter-v1" -version = "0.1.0" -dependencies = [ - "common", - "ports-interface", - "radix-engine-interface", - "sbor", - "scrypto", - "scrypto-interface", - "transaction", -] - [[package]] name = "deranged" version = "0.3.11" @@ -786,9 +774,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", @@ -1104,9 +1092,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1345,11 +1333,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1389,9 +1386,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", "windows-targets 0.52.4", @@ -2384,9 +2381,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" dependencies = [ "base64", "bytes", @@ -3076,20 +3073,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3637,9 +3634,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3647,9 +3644,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -3662,9 +3659,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3674,9 +3671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3684,9 +3681,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -3697,9 +3694,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -3831,9 +3828,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", From ae47f63bcf22d8d79c267ad021b06c474ea0bea1 Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 12:25:02 +0300 Subject: [PATCH 40/47] [Defiplaza v2 Adapter v1]: Small correction --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 0a99dda0..ac106f4e 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -13,7 +13,7 @@ macro_rules! define_error { )* ) => { $( - pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v2]", " ", $item); + pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v1]", " ", $item); )* }; } From 20faa96f75ff8bf4638f922bb63e35de8790cb55 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:02:22 +0300 Subject: [PATCH 41/47] [Defiplaza v2 Adapter v1]: Rename `add_pair_config` to plural --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 4 ++-- tests/src/environment.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ac106f4e..ee007db5 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -43,7 +43,7 @@ pub mod adapter { protocol_manager => updatable_by: [protocol_manager, protocol_owner]; }, methods { - add_pair_config => restrict_to: [protocol_manager, protocol_owner]; + add_pair_configs => restrict_to: [protocol_manager, protocol_owner]; /* User methods */ price => PUBLIC; resource_addresses => PUBLIC; @@ -96,7 +96,7 @@ pub mod adapter { .globalize() } - pub fn add_pair_config( + pub fn add_pair_configs( &mut self, pair_config: IndexMap, ) { diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 9b77ab26..4d2cacfc 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -540,7 +540,7 @@ impl ScryptoTestEnv { )?; // Registering all of pair configs to the adapter. - defiplaza_v2_adapter_v1.add_pair_config( + defiplaza_v2_adapter_v1.add_pair_configs( defiplaza_v2_pools .iter() .map(|pool| ComponentAddress::try_from(pool).unwrap()) @@ -1307,7 +1307,7 @@ impl ScryptoUnitEnv { .lock_fee_from_faucet() .call_method( defiplaza_v2_adapter_v1, - "add_pair_config", + "add_pair_configs", (defiplaza_v2_pools .iter() .map(|address| { From 731c9515591ee0814b7de4e7a2b791424deb529d Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:04:47 +0300 Subject: [PATCH 42/47] [Defiplaza v2 Adapter v1]: Rename first and second to shortage and surplus assets --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ee007db5..6e3fcfab 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -199,7 +199,7 @@ pub mod adapter { let shortage = pair_state.shortage; let shortage_state = ShortageState::from(shortage); - let [(first_resource_address, first_bucket), (second_resource_address, second_bucket)] = + let [(shortage_asset_resource_address, shortage_asset_bucket), (surplus_asset_resource_address, surplus_asset_bucket)] = match shortage_state { ShortageState::Equilibrium => [ (base_resource_address, base_bucket), @@ -218,9 +218,9 @@ pub mod adapter { // Step 3: Calculate tate.target_ratio * bucket1.amount() where // bucket1 is the bucket currently in shortage or the resource that // will be contributed first. - let first_original_target = pair_state + let shortage_asset_original_target = pair_state .target_ratio - .checked_mul(first_bucket.amount()) + .checked_mul(shortage_asset_bucket.amount()) .expect(OVERFLOW_ERROR); // Step 4: Contribute to the pool. The first bucket to provide the @@ -229,25 +229,28 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - let (first_pool_units, second_change) = match shortage_state { - ShortageState::Equilibrium => ( - pool.add_liquidity(first_bucket, None).0, - Some(second_bucket), - ), - ShortageState::Shortage(_) => { - pool.add_liquidity(first_bucket, Some(second_bucket)) - } - }; + let (shortage_asset_pool_units, surplus_asset_change) = + match shortage_state { + ShortageState::Equilibrium => ( + pool.add_liquidity(shortage_asset_bucket, None).0, + Some(surplus_asset_bucket), + ), + ShortageState::Shortage(_) => pool.add_liquidity( + shortage_asset_bucket, + Some(surplus_asset_bucket), + ), + }; // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets // that are in the remainder (change) bucket. - let second_bucket = second_change.expect(UNEXPECTED_ERROR); - let second_original_target = second_bucket.amount(); + let surplus_asset_bucket = + surplus_asset_change.expect(UNEXPECTED_ERROR); + let surplus_asset_original_target = surplus_asset_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - let (second_pool_units, change) = - pool.add_liquidity(second_bucket, None); + let (surplus_asset_pool_units, change) = + pool.add_liquidity(surplus_asset_bucket, None); // We've been told that the change should be zero. Therefore, we // assert for it to make sure that everything is as we expect it @@ -264,16 +267,16 @@ pub mod adapter { // units obtained from the first contribution should be different // from those obtained in the second contribution. assert_ne!( - first_pool_units.resource_address(), - second_pool_units.resource_address(), + shortage_asset_pool_units.resource_address(), + surplus_asset_pool_units.resource_address(), ); // The procedure for adding liquidity to the pool is now complete. // We can now construct the output. OpenLiquidityPositionOutput { pool_units: IndexedBuckets::from_buckets([ - first_pool_units, - second_pool_units, + shortage_asset_pool_units, + surplus_asset_pool_units, ]), change: change .map(IndexedBuckets::from_bucket) @@ -282,8 +285,8 @@ pub mod adapter { adapter_specific_information: DefiPlazaV2AdapterSpecificInformation { original_targets: indexmap! { - first_resource_address => first_original_target, - second_resource_address => second_original_target + shortage_asset_resource_address => shortage_asset_original_target, + surplus_asset_resource_address => surplus_asset_original_target }, } .into(), From 14c3cfe35451f49eb2896b3a4de61ef88148e182 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 23:27:33 +0300 Subject: [PATCH 43/47] [Caviarnine v1 Adapter v1]: Optimize fees for opening positions. --- packages/caviarnine-v1-adapter-v1/src/lib.rs | 11 +- tests/tests/caviarnine_v1.rs | 293 +++++++++++++++++++ 2 files changed, 297 insertions(+), 7 deletions(-) diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index d379a79b..ad5f3560 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -475,7 +475,7 @@ pub mod adapter { } let (receipt, change_x, change_y) = - pool.add_liquidity(bucket_x, bucket_y, positions); + pool.add_liquidity(bucket_x, bucket_y, positions.clone()); let receipt_global_id = { let resource_address = receipt.resource_address(); @@ -486,14 +486,11 @@ pub mod adapter { let adapter_specific_information = CaviarnineV1AdapterSpecificInformation { - bin_contributions: pool - .get_redemption_bin_values( - receipt_global_id.local_id().clone(), - ) + bin_contributions: positions .into_iter() - .map(|(tick, amount_x, amount_y)| { + .map(|(bin, amount_x, amount_y)| { ( - tick, + bin, ResourceIndexedData { resource_x: amount_x, resource_y: amount_y, diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 6a8ca582..cfd0a7f9 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1472,3 +1472,296 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let pool = caviarnine_v1.pools.bitcoin; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement1( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(user_resource) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement2( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(XRD) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +fn round_down_to_5_decimal_places(decimal: Decimal) -> Decimal { + decimal + .checked_round(5, RoundingMode::ToNegativeInfinity) + .unwrap() +} From 7160d6d811bc40bfd0f3fa0d34f2317c1b87c2b0 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 11:51:12 +0300 Subject: [PATCH 44/47] [Tests]: Add basis for defiplaza v2 fee tests --- tests/src/environment.rs | 14 +-- tests/tests/defiplaza_v2.rs | 194 ++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 10 deletions(-) diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 4d2cacfc..438cf334 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -430,16 +430,10 @@ impl ScryptoTestEnv { Self::publish_package("defiplaza-v2-adapter-v1", &mut env)?; let defiplaza_v2_pools = resource_addresses.try_map(|resource_address| { - let (resource_x, resource_y) = if XRD < *resource_address { - (XRD, *resource_address) - } else { - (*resource_address, XRD) - }; - let mut defiplaza_pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( OwnerRole::None, - resource_x, - resource_y, + *resource_address, + XRD, // This pair config is obtained from DefiPlaza's // repo. PairConfig { @@ -454,9 +448,9 @@ impl ScryptoTestEnv { )?; let resource_x = - ResourceManager(resource_x).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(*resource_address).mint_fungible(dec!(100_000_000), &mut env)?; let resource_y = - ResourceManager(resource_y).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(XRD).mint_fungible(dec!(100_000_000), &mut env)?; let (_, change1) = defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; let (_, change2) = defiplaza_pool.add_liquidity(resource_y, None, &mut env)?; diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index c058189e..04f9e792 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -916,3 +916,197 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ Ok(()) } + +#[test] +#[ignore = "Awaiting defiplaza response"] +fn exact_fee_test1() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + AssetIndexedData { + protocol_resource: dec!(100_000), + user_resource: dec!(100_000), + }, + // Initial price of the pool + dec!(1), + // The fee percentage of the pool + dec!(0.03), + // User contribution to the pool. This would mean that the user would + // own 0.1% of the pool + AssetIndexedData { + user_resource: dec!(100), + protocol_resource: dec!(100), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(1_000))], + // The fees to expect - with 0.1% pool ownership of the pool and fees of + // 3% then we expect to see 0.03 of the protocol resource in fees (as it + // was the input in the swap) and none of the user resource in fees. + AssetIndexedData { + user_resource: EqualityCheck::ExactlyEquals(dec!(0)), + protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + }, + ) + .expect("Should not fail!") +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Asset { + UserResource, + ProtocolResource, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct AssetIndexedData { + user_resource: T, + protocol_resource: T, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum EqualityCheck { + ExactlyEquals(T), + ApproximatelyEquals { value: T, acceptable_difference: T }, +} + +fn test_exact_defiplaza_fees_amounts( + // The initial amount of liquidity to provide when creating the liquidity + // pool. + initial_liquidity: AssetIndexedData, + // The price to set as the initial price of the pool. + initial_price: Decimal, + // The fee percentage of the pool + fee_percentage: Decimal, + // The contribution that the user will make to the pool + user_contribution: AssetIndexedData, + // The swaps to perform on the pool. + swaps: Vec<(Asset, Decimal)>, + // Equality checks to perform when closing the liquidity position. + expected_fees: AssetIndexedData>, +) -> Result<(), RuntimeError> { + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources: ResourceInformation { bitcoin, .. }, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let resources = AssetIndexedData { + user_resource: bitcoin, + protocol_resource: XRD, + }; + + // Creating a new defiplaza pair so we can initialize it the way that we + // desire and without any constraints from the environment. + let mut pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( + OwnerRole::None, + resources.user_resource, + resources.protocol_resource, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: fee_percentage, + decay_factor: dec!("0.9512"), + }, + initial_price, + defiplaza_v2.package, + env, + )?; + + // Providing the desired initial contribution to the pool. + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + + // Providing the user's contribution to the pool through the adapter + let [bucket_x, bucket_y] = [ + ( + resources.protocol_resource, + user_contribution.protocol_resource, + ), + (resources.user_resource, user_contribution.user_resource), + ] + .map(|(resource_address, amount)| { + ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap() + }); + let OpenLiquidityPositionOutput { + pool_units, + change, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (bucket_x, bucket_y), + env, + )?; + + // Asset the user got back no change in this contribution + for bucket in change.into_values() { + let amount = bucket.amount(env)?; + assert_eq!(amount, Decimal::ZERO); + } + + // Performing the swaps specified by the user + for (asset, amount) in swaps.into_iter() { + let address = match asset { + Asset::ProtocolResource => resources.protocol_resource, + Asset::UserResource => resources.user_resource, + }; + let bucket = + ResourceManager(address).mint_fungible(amount, env).unwrap(); + let _ = pool.swap(bucket, env)?; + } + + // Close the liquidity position + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + pool.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert that the fees is what's expected. + for (resource_address, equality_check) in [ + (resources.protocol_resource, expected_fees.protocol_resource), + (resources.user_resource, expected_fees.user_resource), + ] { + // Get the fees + let resource_fees = fees.get(&resource_address).copied().unwrap(); + + // Perform the assertion + match equality_check { + EqualityCheck::ExactlyEquals(value) => { + assert_eq!(resource_fees, value) + } + EqualityCheck::ApproximatelyEquals { + value, + acceptable_difference, + } => { + assert!( + (resource_fees - value).checked_abs().unwrap() + <= acceptable_difference + ) + } + } + } + + Ok(()) +} From ed3d11cd715934b2bcd956c87d6f3de839960a4f Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 13:30:21 +0300 Subject: [PATCH 45/47] [Tests]: Fix tests --- tests/tests/caviarnine_v1.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index cfd0a7f9..57645299 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1512,6 +1512,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1609,6 +1612,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1706,6 +1712,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() From 14241898a852927644001bd17f9da1475cf1a087 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 14:45:14 +0300 Subject: [PATCH 46/47] [Defiplaza v2 Adapter v1]: Update the fee calculation and add tests for fees --- libraries/common/src/indexed_buckets.rs | 7 + packages/defiplaza-v2-adapter-v1/src/lib.rs | 301 +++++++++++++----- tests/tests/defiplaza_v2.rs | 336 +++++++++++++++----- 3 files changed, 502 insertions(+), 142 deletions(-) diff --git a/libraries/common/src/indexed_buckets.rs b/libraries/common/src/indexed_buckets.rs index b675e82f..b20cfe5d 100644 --- a/libraries/common/src/indexed_buckets.rs +++ b/libraries/common/src/indexed_buckets.rs @@ -130,6 +130,13 @@ impl IndexedBuckets { pub fn into_inner(self) -> IndexMap { self.0 } + + pub fn combine(mut self, other: Self) -> Self { + for bucket in other.0.into_values() { + self.insert(bucket) + } + self + } } impl Default for IndexedBuckets { diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 6e3fcfab..8ff2c0de 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -326,89 +326,248 @@ pub mod adapter { (pool_units_bucket1, pool_units_bucket2) }; - // Getting the base and quote assets - let (base_resource_address, quote_resource_address) = - pool.get_tokens(); - // Decoding the adapter specific information as the type we expect // it to be. let DefiPlazaV2AdapterSpecificInformation { original_targets } = adapter_specific_information.as_typed().unwrap(); - let [old_base_target, old_quote_target] = - [base_resource_address, quote_resource_address].map( - |address| original_targets.get(&address).copied().unwrap(), - ); - - // Step 1: Get the pair's state - let pair_state = pool.get_state(); - - // Step 2 & 3: Determine which of the resources is in shortage and - // based on that determine what the new target should be. - let claimed_tokens = IndexedBuckets::from_buckets( - [pool_units1, pool_units2].into_iter().flat_map(|bucket| { - let resource_manager = bucket.resource_manager(); - let entry = ComponentAddress::try_from( - resource_manager - .get_metadata::<_, GlobalAddress>("pool") - .unwrap() - .unwrap(), - ) - .unwrap(); - let mut two_resource_pool = - Global::::from(entry); - let (bucket1, bucket2) = two_resource_pool.redeem(bucket); - [bucket1, bucket2] - }), - ); - let base_bucket = - claimed_tokens.get(&base_resource_address).unwrap(); - let quote_bucket = - claimed_tokens.get("e_resource_address).unwrap(); - let base_bucket_amount = base_bucket.amount(); - let quote_bucket_amount = quote_bucket.amount(); + // We have gotten two pools units, one of the base pool and + // another for the quote pool. We need to determine which is + // which and overall split things according to shortage and + // surplus. So, instead of referring to them as base and quote + // we would figure out what is in shortage and what is surplus. + + // First thing we do is store the pool units in a map where the + // key the address of the pool and the value is the pool units + // bucket. We find the address of the pool through metadata on + // the pool units since there is currently no other way to find + // this information. + let mut pool_component_to_pool_unit_mapping = + [pool_units1, pool_units2] + .into_iter() + .map(|bucket| { + let resource_manager = bucket.resource_manager(); + let pool_address = ComponentAddress::try_from( + resource_manager + .get_metadata::<_, GlobalAddress>("pool") + .unwrap() + .unwrap(), + ) + .unwrap(); + (pool_address, bucket) + }) + .collect::>(); + + // With the way we combined them above we now want to split them + // into base pool units and quote pool units. This is simple to + // do, just get the address of the base and quote pools and then + // do a simple `remove` from the above map. + let (base_pool_component, quote_pool_component) = pool.get_pools(); + let base_pool_units = pool_component_to_pool_unit_mapping + .remove(&base_pool_component) + .unwrap(); + let quote_pool_units = pool_component_to_pool_unit_mapping + .remove("e_pool_component) + .unwrap(); - let shortage = pair_state.shortage; - let shortage_state = ShortageState::from(shortage); - let (new_base_target, new_quote_target) = match shortage_state { + // At this point we have the the base and quote token addresses, + // pool addresses, and pool units. We can now split things as + // shortage and quote and stop referring to things as base and + // quote. + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + let pair_state = pool.get_state(); + let (claimed_resources, fees) = match ShortageState::from( + pair_state.shortage, + ) { + // The pool is in equilibrium, none of the assets are in + // shortage so there is no need to multiply anything by the + // target ratio. ShortageState::Equilibrium => { - (base_bucket_amount, quote_bucket_amount) + // Claiming the assets from the pools. + let [resources_claimed_from_base_resource_pool, resources_claimed_from_quote_resource_pool] = + [ + (base_pool_component, base_pool_units), + (quote_pool_component, quote_pool_units), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the two resources is just the amount we + // got back when closing the liquidity position. + let new_target_of_base_resource = + resources_claimed_from_base_resource_pool + .get(&base_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + let new_target_of_quote_resource = + resources_claimed_from_quote_resource_pool + .get("e_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the base and quote + // resources we can calculate the fees. + let base_resource_fees = original_targets + .get(&base_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_base_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let quote_resource_fees = original_targets + .get("e_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_quote_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + base_resource_address => base_resource_fees, + quote_resource_address => quote_resource_fees, + }; + + let claimed_resources = + resources_claimed_from_base_resource_pool.combine( + resources_claimed_from_quote_resource_pool, + ); + + (claimed_resources, fees) + } + // One of the assets is in shortage and the other is in + // surplus. Determine which is which and sort the info. + ShortageState::Shortage(asset) => { + let ( + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + shortage_asset_resource_address, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + surplus_asset_resource_address, + ), + ) = match asset { + Asset::Base => ( + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ), + Asset::Quote => ( + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ), + }; + + // We have now split them into shortage and surplus and + // we can now close the liquidity positions and compute + // the new targets for the base and shortage assets. + let [resources_claimed_from_shortage_asset_pool, resources_claimed_from_surplus_asset_pool] = + [ + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + ), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the shortage asset can be calculated by + // multiplying the amount we got back from closing the + // position in the shortage pool by the target ratio of + // the pool in the current state. + let new_target_of_shortage_asset = + resources_claimed_from_shortage_asset_pool + .get(&shortage_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR) + .checked_mul(pair_state.target_ratio) + .expect(OVERFLOW_ERROR); + + // The target of the surplus asset is simple, its the + // amount we got back when we closed the position in + // the surplus pool. + let new_target_of_surplus_asset = + resources_claimed_from_surplus_asset_pool + .get(&surplus_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the shortage and + // surplus assets we can calculate the fees earned on + // those assets. Its calculated by subtracting the + // new targets from the original targets. + let shortage_asset_fees = new_target_of_shortage_asset + .checked_sub( + original_targets + .get(&shortage_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let surplus_asset_fees = new_target_of_surplus_asset + .checked_sub( + original_targets + .get(&surplus_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + shortage_asset_resource_address => shortage_asset_fees, + surplus_asset_resource_address => surplus_asset_fees, + }; + + let claimed_resources = + resources_claimed_from_shortage_asset_pool + .combine(resources_claimed_from_surplus_asset_pool); + + (claimed_resources, fees) } - ShortageState::Shortage(Asset::Base) => ( - base_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - quote_bucket_amount, - ), - ShortageState::Shortage(Asset::Quote) => ( - base_bucket_amount, - quote_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - ), }; - // Steps 4 and 5 - let base_fees = std::cmp::max( - new_base_target - .checked_sub(old_base_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - let quote_fees = std::cmp::max( - new_quote_target - .checked_sub(old_quote_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - CloseLiquidityPositionOutput { - resources: claimed_tokens, + resources: claimed_resources, others: vec![], - fees: indexmap! { - base_resource_address => base_fees, - quote_resource_address => quote_fees, - }, + fees, } } diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 04f9e792..2df0e7b2 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -918,37 +918,231 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ } #[test] -#[ignore = "Awaiting defiplaza response"] fn exact_fee_test1() { test_exact_defiplaza_fees_amounts( // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::UserResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test2() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test3() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test4() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test5() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - protocol_resource: dec!(100_000), - user_resource: dec!(100_000), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test6() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, // Initial price of the pool dec!(1), - // The fee percentage of the pool - dec!(0.03), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, // User contribution to the pool. This would mean that the user would - // own 0.1% of the pool + // own 100% of the pool. AssetIndexedData { - user_resource: dec!(100), - protocol_resource: dec!(100), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, // The swaps to perform - the asset you see is the input asset - vec![(Asset::ProtocolResource, dec!(1_000))], - // The fees to expect - with 0.1% pool ownership of the pool and fees of - // 3% then we expect to see 0.03 of the protocol resource in fees (as it - // was the input in the swap) and none of the user resource in fees. + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test7() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - user_resource: EqualityCheck::ExactlyEquals(dec!(0)), - protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(3996)), + (Asset::UserResource, dec!(898)), + (Asset::ProtocolResource, dec!(7953)), + (Asset::ProtocolResource, dec!(3390)), + (Asset::ProtocolResource, dec!(4297)), + (Asset::ProtocolResource, dec!(2252)), + (Asset::UserResource, dec!(5835)), + (Asset::ProtocolResource, dec!(5585)), + (Asset::UserResource, dec!(7984)), + (Asset::ProtocolResource, dec!(8845)), + (Asset::ProtocolResource, dec!(4511)), + (Asset::UserResource, dec!(1407)), + (Asset::UserResource, dec!(4026)), + (Asset::UserResource, dec!(8997)), + (Asset::ProtocolResource, dec!(1950)), + (Asset::UserResource, dec!(8016)), + (Asset::UserResource, dec!(8322)), + (Asset::UserResource, dec!(5149)), + (Asset::ProtocolResource, dec!(6411)), + (Asset::ProtocolResource, dec!(1013)), + (Asset::ProtocolResource, dec!(3333)), + (Asset::ProtocolResource, dec!(4130)), + (Asset::UserResource, dec!(2786)), + (Asset::UserResource, dec!(5828)), + (Asset::UserResource, dec!(8974)), + (Asset::UserResource, dec!(6476)), + (Asset::ProtocolResource, dec!(8942)), + (Asset::UserResource, dec!(2159)), + (Asset::UserResource, dec!(8387)), + (Asset::UserResource, dec!(2830)), + ], ) .expect("Should not fail!") } +#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Asset { UserResource, @@ -961,26 +1155,24 @@ struct AssetIndexedData { protocol_resource: T, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum EqualityCheck { - ExactlyEquals(T), - ApproximatelyEquals { value: T, acceptable_difference: T }, -} - +/// This test will open a position for the user in a Defiplaza liquidity +/// pool and then perform a bunch of swaps to generate fees and then asset +/// that the amount of fees obtained as reported by the adapter matches the +/// amount that we expect the fees to be. An important note, Defiplaza fees +/// are collected on the output token and not the input token, so they're a +/// percentage of the output amount. fn test_exact_defiplaza_fees_amounts( // The initial amount of liquidity to provide when creating the liquidity // pool. - initial_liquidity: AssetIndexedData, + initial_liquidity: Option>, // The price to set as the initial price of the pool. initial_price: Decimal, - // The fee percentage of the pool - fee_percentage: Decimal, + // The pair configuration of the defiplaza pool + pair_configuration: PairConfig, // The contribution that the user will make to the pool user_contribution: AssetIndexedData, // The swaps to perform on the pool. swaps: Vec<(Asset, Decimal)>, - // Equality checks to perform when closing the liquidity position. - expected_fees: AssetIndexedData>, ) -> Result<(), RuntimeError> { let Environment { environment: ref mut env, @@ -1003,35 +1195,32 @@ fn test_exact_defiplaza_fees_amounts( OwnerRole::None, resources.user_resource, resources.protocol_resource, - PairConfig { - k_in: dec!("0.4"), - k_out: dec!("1"), - fee: fee_percentage, - decay_factor: dec!("0.9512"), - }, + pair_configuration, initial_price, defiplaza_v2.package, env, )?; // Providing the desired initial contribution to the pool. - [ - (resources.user_resource, initial_liquidity.user_resource), - ( - resources.protocol_resource, - initial_liquidity.protocol_resource, - ), - ] - .map(|(resource_address, amount)| { - let bucket = ResourceManager(resource_address) - .mint_fungible(amount, env) - .unwrap(); - let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); - let change_amount = change - .map(|bucket| bucket.amount(env).unwrap()) - .unwrap_or(Decimal::ZERO); - assert_eq!(change_amount, Decimal::ZERO); - }); + if let Some(initial_liquidity) = initial_liquidity { + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + } // Providing the user's contribution to the pool through the adapter let [bucket_x, bucket_y] = [ @@ -1064,6 +1253,7 @@ fn test_exact_defiplaza_fees_amounts( } // Performing the swaps specified by the user + let mut expected_fee_amounts = IndexMap::::new(); for (asset, amount) in swaps.into_iter() { let address = match asset { Asset::ProtocolResource => resources.protocol_resource, @@ -1071,7 +1261,14 @@ fn test_exact_defiplaza_fees_amounts( }; let bucket = ResourceManager(address).mint_fungible(amount, env).unwrap(); - let _ = pool.swap(bucket, env)?; + let (output, _) = pool.swap(bucket, env)?; + let output_resource_address = output.resource_address(env)?; + let swap_output_amount = output.amount(env)?; + let fee = swap_output_amount / (Decimal::ONE - pair_configuration.fee) + * pair_configuration.fee; + *expected_fee_amounts + .entry(output_resource_address) + .or_default() += fee; } // Close the liquidity position @@ -1084,28 +1281,25 @@ fn test_exact_defiplaza_fees_amounts( )?; // Assert that the fees is what's expected. - for (resource_address, equality_check) in [ - (resources.protocol_resource, expected_fees.protocol_resource), - (resources.user_resource, expected_fees.user_resource), - ] { - // Get the fees - let resource_fees = fees.get(&resource_address).copied().unwrap(); - - // Perform the assertion - match equality_check { - EqualityCheck::ExactlyEquals(value) => { - assert_eq!(resource_fees, value) - } - EqualityCheck::ApproximatelyEquals { - value, - acceptable_difference, - } => { - assert!( - (resource_fees - value).checked_abs().unwrap() - <= acceptable_difference - ) - } - } + for resource_address in + [resources.protocol_resource, resources.user_resource] + { + let expected_fees = expected_fee_amounts + .get(&resource_address) + .copied() + .unwrap_or_default(); + let fees = fees.get(&resource_address).copied().unwrap_or_default(); + + let resource_name = if resource_address == resources.protocol_resource { + "protocol" + } else { + "user" + }; + + assert!( + expected_fees - fees <= dec!(0.000001), + "{resource_name} resource assertion failed. Expected: {expected_fees}, Actual: {fees}" + ); } Ok(()) From 07b30f8d878d30c46fd99d4af26ca5715e7b0d69 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 20:01:56 +0300 Subject: [PATCH 47/47] [Defiplaza v2 Adapter v1]: Fee tests --- tests/tests/defiplaza_v2.rs | 100 +++--------------------------------- 1 file changed, 7 insertions(+), 93 deletions(-) diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 2df0e7b2..27bdf5cc 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -307,99 +307,10 @@ fn fees_are_zero_when_no_swaps_take_place() -> Result<(), RuntimeError> { )?; // Assert - assert!(fees.values().all(|value| *value == Decimal::ZERO)); - - Ok(()) -} - -#[test] -fn a_swap_with_xrd_input_produces_xrd_fees() -> Result<(), RuntimeError> { - // Arrange - let Environment { - environment: ref mut env, - mut defiplaza_v2, - resources, - .. - } = ScryptoTestEnv::new()?; - - let [bitcoin_bucket, xrd_bucket] = [resources.bitcoin, XRD] - .map(ResourceManager) - .map(|mut resource_manager| { - resource_manager.mint_fungible(dec!(100), env).unwrap() - }); - - let OpenLiquidityPositionOutput { - pool_units, - adapter_specific_information, - .. - } = defiplaza_v2.adapter.open_liquidity_position( - defiplaza_v2.pools.bitcoin.try_into().unwrap(), - (bitcoin_bucket, xrd_bucket), - env, - )?; - - let _ = ResourceManager(XRD) - .mint_fungible(dec!(100_000), env) - .and_then(|bucket| defiplaza_v2.pools.bitcoin.swap(bucket, env))?; - - // Act - let CloseLiquidityPositionOutput { fees, .. } = - defiplaza_v2.adapter.close_liquidity_position( - defiplaza_v2.pools.bitcoin.try_into().unwrap(), - pool_units.into_values().collect(), - adapter_specific_information, - env, - )?; - - // Assert - assert_eq!(*fees.get(&resources.bitcoin).unwrap(), dec!(0)); - assert_ne!(*fees.get(&XRD).unwrap(), dec!(0)); - - Ok(()) -} - -#[test] -fn a_swap_with_btc_input_produces_btc_fees() -> Result<(), RuntimeError> { - // Arrange - let Environment { - environment: ref mut env, - mut defiplaza_v2, - resources, - .. - } = ScryptoTestEnv::new()?; - - let [bitcoin_bucket, xrd_bucket] = [resources.bitcoin, XRD] - .map(ResourceManager) - .map(|mut resource_manager| { - resource_manager.mint_fungible(dec!(100), env).unwrap() - }); - - let OpenLiquidityPositionOutput { - pool_units, - adapter_specific_information, - .. - } = defiplaza_v2.adapter.open_liquidity_position( - defiplaza_v2.pools.bitcoin.try_into().unwrap(), - (bitcoin_bucket, xrd_bucket), - env, - )?; - - let _ = ResourceManager(resources.bitcoin) - .mint_fungible(dec!(100_000), env) - .and_then(|bucket| defiplaza_v2.pools.bitcoin.swap(bucket, env))?; - - // Act - let CloseLiquidityPositionOutput { fees, .. } = - defiplaza_v2.adapter.close_liquidity_position( - defiplaza_v2.pools.bitcoin.try_into().unwrap(), - pool_units.into_values().collect(), - adapter_specific_information, - env, - )?; - - // Assert - assert_ne!(*fees.get(&resources.bitcoin).unwrap(), dec!(0)); - assert_eq!(*fees.get(&XRD).unwrap(), dec!(0)); + assert!(fees.values().all(|value| value + .checked_round(5, RoundingMode::ToZero) + .unwrap() + == Decimal::ZERO)); Ok(()) } @@ -584,6 +495,9 @@ fn non_strict_testing_of_fees( match price_of_user_asset { // User asset price goes down - i.e., we inject it into the pool. Movement::Down => { + let xrd_bucket = + ResourceManager(XRD).mint_fungible(dec!(10_000), env)?; + let _ = defiplaza_v2.pools.bitcoin.swap(xrd_bucket, env)?; let bitcoin_bucket = ResourceManager(resources.bitcoin) .mint_fungible(dec!(10_000_000), env)?; let _ = defiplaza_v2.pools.bitcoin.swap(bitcoin_bucket, env)?;