diff --git a/aleph-client/src/contract/convertible_value.rs b/aleph-client/src/contract/convertible_value.rs index 7cbcd58bbe..ff27c894d6 100644 --- a/aleph-client/src/contract/convertible_value.rs +++ b/aleph-client/src/contract/convertible_value.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use contract_transcode::Value; use sp_core::crypto::Ss58Codec; @@ -71,3 +71,36 @@ impl TryFrom for AccountId { } } } + +impl TryFrom for Result +where + ConvertibleValue: TryInto, +{ + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result, Self::Error> { + if let Value::Tuple(tuple) = &value.0 { + match tuple.ident() { + Some(x) if x == "Ok" => { + if tuple.values().count() == 1 { + let item = + ConvertibleValue(tuple.values().next().unwrap().clone()).try_into()?; + return Ok(Ok(item)); + } else { + bail!("Unexpected number of elements in Ok variant: {:?}", &value); + } + } + Some(x) if x == "Err" => { + if tuple.values().count() == 1 { + return Ok(Err(anyhow!(value.to_string()))); + } else { + bail!("Unexpected number of elements in Err variant: {:?}", &value); + } + } + _ => (), + } + } + + bail!("Expected {:?} to be an Ok(_) or Err(_) tuple.", value); + } +} diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index cb26dcc361..d0e8aa0388 100644 --- a/aleph-client/src/contract/mod.rs +++ b/aleph-client/src/contract/mod.rs @@ -96,15 +96,15 @@ impl ContractInstance { conn: &C, message: &str, ) -> Result { - self.contract_read(conn, message, &[]) + self.contract_read::(conn, message, &[]) } /// Reads the value of a read-only call via RPC. - pub fn contract_read( + pub fn contract_read + Debug>( &self, conn: &C, message: &str, - args: &[&str], + args: &[S], ) -> Result { let payload = self.encode(message, args)?; let request = self.contract_read_request(&payload); @@ -121,15 +121,15 @@ impl ContractInstance { /// Executes a 0-argument contract call. pub fn contract_exec0(&self, conn: &SignedConnection, message: &str) -> Result<()> { - self.contract_exec(conn, message, &[]) + self.contract_exec::(conn, message, &[]) } /// Executes a contract call. - pub fn contract_exec( + pub fn contract_exec + Debug>( &self, conn: &SignedConnection, message: &str, - args: &[&str], + args: &[S], ) -> Result<()> { let data = self.encode(message, args)?; let xt = compose_extrinsic!( @@ -164,7 +164,7 @@ impl ContractInstance { }) } - fn encode(&self, message: &str, args: &[&str]) -> Result> { + fn encode + Debug>(&self, message: &str, args: &[S]) -> Result> { ContractMessageTranscoder::new(&self.ink_project).encode(message, args) } diff --git a/contracts/scripts/test.sh b/contracts/scripts/test.sh index 8bfc27de82..96b7061d0f 100755 --- a/contracts/scripts/test.sh +++ b/contracts/scripts/test.sh @@ -7,10 +7,12 @@ CONTRACTS_PATH=$(pwd)/contracts EARLY_BIRD_SPECIAL=$(jq --raw-output ".early_bird_special" < "$CONTRACTS_PATH"/addresses.json) THE_PRESSIAH_COMETH=$(jq --raw-output ".the_pressiah_cometh" < "$CONTRACTS_PATH"/addresses.json) BACK_TO_THE_FUTURE=$(jq --raw-output ".back_to_the_future" < "$CONTRACTS_PATH"/addresses.json) +SIMPLE_DEX=$(jq --raw-output ".simple_dex" < "$CONTRACTS_PATH"/addresses.json) pushd "$E2E_PATH" RUST_LOG="aleph_e2e_client=info" cargo run --release -- \ + --test-cases simple_dex \ --test-cases marketplace \ --test-cases button_game_reset \ --test-cases early_bird_special \ @@ -19,9 +21,11 @@ RUST_LOG="aleph_e2e_client=info" cargo run --release -- \ --early-bird-special "$EARLY_BIRD_SPECIAL" \ --the-pressiah-cometh "$THE_PRESSIAH_COMETH" \ --back-to-the-future "$BACK_TO_THE_FUTURE" \ + --simple-dex "$SIMPLE_DEX" \ --button-game-metadata ../contracts/button/target/ink/metadata.json \ --ticket-token-metadata ../contracts/ticket_token/target/ink/metadata.json \ --reward-token-metadata ../contracts/game_token/target/ink/metadata.json \ - --marketplace-metadata ../contracts/marketplace/target/ink/metadata.json + --marketplace-metadata ../contracts/marketplace/target/ink/metadata.json \ + --simple-dex-metadata ../contracts/simple_dex/target/ink/metadata.json exit $? diff --git a/e2e-tests/src/cases.rs b/e2e-tests/src/cases.rs index d871760c70..bdbdb83bd3 100644 --- a/e2e-tests/src/cases.rs +++ b/e2e-tests/src/cases.rs @@ -15,7 +15,7 @@ use crate::{ force_new_era as test_force_new_era, marketplace as test_marketplace, points_basic as test_points_basic, points_stake_change as test_points_stake_change, schedule_doomed_version_change_and_verify_finalization_stopped as test_schedule_doomed_version_change_and_verify_finalization_stopped, - schedule_version_change as test_schedule_version_change, + schedule_version_change as test_schedule_version_change, simple_dex as test_simple_dex, staking_era_payouts as test_staking_era_payouts, staking_new_validator as test_staking_new_validator, the_pressiah_cometh as test_the_pressiah_cometh, token_transfer as test_token_transfer, @@ -72,6 +72,7 @@ pub fn possible_test_cases() -> PossibleTestCases { ("back_to_the_future", test_back_to_the_future as TestCase), ("the_pressiah_cometh", test_the_pressiah_cometh as TestCase), ("marketplace", test_marketplace as TestCase), + ("simple_dex", test_simple_dex as TestCase), ("ban_automatic", test_ban_automatic as TestCase), ("ban_manual", test_ban_manual as TestCase), ( diff --git a/e2e-tests/src/config.rs b/e2e-tests/src/config.rs index cf3c047853..788581655a 100644 --- a/e2e-tests/src/config.rs +++ b/e2e-tests/src/config.rs @@ -81,6 +81,10 @@ pub struct TestCaseParams { #[clap(long)] pub the_pressiah_cometh: Option, + /// Address of the simple dex contract. + #[clap(long)] + pub simple_dex: Option, + /// Path to the button game metadata file. Only used by button tests. #[clap(long)] pub button_game_metadata: Option, @@ -97,6 +101,10 @@ pub struct TestCaseParams { #[clap(long)] pub marketplace_metadata: Option, + /// Path to the simple_dex metadata file. Only used by button tests. + #[clap(long)] + pub simple_dex_metadata: Option, + /// Version for the VersionUpgrade test. #[clap(long)] pub upgrade_to_version: Option, diff --git a/e2e-tests/src/test/button_game/contracts.rs b/e2e-tests/src/test/button_game/contracts.rs index 8bcde90b04..8dc6188840 100644 --- a/e2e-tests/src/test/button_game/contracts.rs +++ b/e2e-tests/src/test/button_game/contracts.rs @@ -6,6 +6,121 @@ use sp_core::crypto::{AccountId32 as AccountId, Ss58Codec}; use crate::Config; +/// A wrapper around the simple dex contract. +/// +/// The methods on this type match contract methods. +#[derive(Debug)] +pub(super) struct SimpleDexInstance { + contract: ContractInstance, +} + +impl<'a> From<&'a SimpleDexInstance> for &'a ContractInstance { + fn from(dex: &'a SimpleDexInstance) -> Self { + &dex.contract + } +} + +impl<'a> From<&'a SimpleDexInstance> for AccountId { + fn from(dex: &'a SimpleDexInstance) -> Self { + dex.contract.address().clone() + } +} + +impl SimpleDexInstance { + pub fn new(config: &Config) -> Result { + let dex_address = config + .test_case_params + .simple_dex + .clone() + .context("Simple dex address not set.")?; + let dex_address = AccountId::from_string(&dex_address)?; + let metadata_path = config + .test_case_params + .simple_dex_metadata + .clone() + .context("Simple dex metadata not set")?; + + Ok(Self { + contract: ContractInstance::new(dex_address, &metadata_path)?, + }) + } + + pub fn add_swap_pair( + &self, + conn: &SignedConnection, + from: AccountId, + to: AccountId, + ) -> Result<()> { + self.contract + .contract_exec(conn, "add_swap_pair", &[&from.to_string(), &to.to_string()]) + } + + pub fn deposit( + &self, + conn: &SignedConnection, + amounts: &[(&PSP22TokenInstance, Balance)], + ) -> Result<()> { + let deposits = amounts + .iter() + .map(|(token, amount)| { + let address: AccountId = (*token).try_into()?; + Ok(format!("deposits ({:}, {:})", address, amount)) + }) + .collect::>>()?; + + self.contract + .contract_exec(conn, "deposit", &[format!("[{:}]", deposits.join(","))]) + } + + pub fn out_given_in( + &self, + conn: &C, + token_in: &PSP22TokenInstance, + token_out: &PSP22TokenInstance, + amount_token_in: Balance, + min_amount_token_out: Option, + ) -> Result { + let token_in: AccountId = token_in.into(); + let token_out: AccountId = token_out.into(); + + self.contract + .contract_read( + conn, + "out_given_in", + &[ + token_in.to_string(), + token_out.to_string(), + amount_token_in.to_string(), + min_amount_token_out.map_or("None".to_string(), |x| format!("Some({:})", x)), + ], + )? + .try_into()? + } + + pub fn swap( + &self, + conn: &SignedConnection, + token_in: &PSP22TokenInstance, + amount_token_in: Balance, + token_out: &PSP22TokenInstance, + min_amount_token_out: Balance, + ) -> Result<()> { + let token_in: AccountId = token_in.into(); + let token_out: AccountId = token_out.into(); + + self.contract.contract_exec( + conn, + "swap", + &[ + token_in.to_string(), + token_out.to_string(), + amount_token_in.to_string(), + min_amount_token_out.to_string(), + ], + ) + } +} + /// A wrapper around a button game contract. /// /// The methods on this type match contract methods. @@ -99,7 +214,7 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22::transfer", - &[to.to_string().as_str(), amount.to_string().as_str(), "0x00"], + &[to.to_string(), amount.to_string(), "0x00".to_string()], ) } @@ -107,7 +222,7 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22Mintable::mint", - &[to.to_string().as_str(), amount.to_string().as_str()], + &[to.to_string(), amount.to_string()], ) } @@ -120,13 +235,13 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22::approve", - &[spender.to_string().as_str(), value.to_string().as_str()], + &[spender.to_string(), value.to_string()], ) } pub fn balance_of(&self, conn: &Connection, account: &AccountId) -> Result { self.contract - .contract_read(conn, "PSP22::balance_of", &[account.to_string().as_str()])? + .contract_read(conn, "PSP22::balance_of", &[account.to_string()])? .try_into() } } diff --git a/e2e-tests/src/test/button_game/helpers.rs b/e2e-tests/src/test/button_game/helpers.rs index 2ca2e0ae14..638c25f33c 100644 --- a/e2e-tests/src/test/button_game/helpers.rs +++ b/e2e-tests/src/test/button_game/helpers.rs @@ -19,8 +19,10 @@ use log::{info, warn}; use rand::Rng; use sp_core::Pair; -use super::contracts::{ButtonInstance, PSP22TokenInstance}; -use crate::{test::button_game::contracts::MarketplaceInstance, Config}; +use super::contracts::{ + ButtonInstance, MarketplaceInstance, PSP22TokenInstance, SimpleDexInstance, +}; +use crate::Config; /// A wrapper around a KeyPair for purposes of converting to an account id in tests. pub struct KeyPairWrapper(KeyPair); @@ -103,6 +105,11 @@ pub fn alephs(basic_unit_amount: Balance) -> Balance { basic_unit_amount * 1_000_000_000_000 } +/// Returns the given number multiplied by 10^6. +pub fn mega(x: Balance) -> Balance { + x * 1_000_000 +} + pub(super) struct ButtonTestContext { pub button: Arc, pub ticket_token: Arc, @@ -118,6 +125,78 @@ pub(super) struct ButtonTestContext { pub player: KeyPairWrapper, } +pub(super) struct DexTestContext { + pub conn: Connection, + /// An authority with the power to mint tokens and manage the dex. + pub authority: KeyPairWrapper, + /// A random account with some money for fees. + pub account: KeyPairWrapper, + pub dex: Arc, + pub token1: Arc, + pub token2: Arc, + pub token3: Arc, + /// A [BufferedReceiver] preconfigured to listen for events of `dex`, `token1`, `token2`, and `token3`. + pub events: BufferedReceiver>, +} + +pub(super) fn setup_dex_test(config: &Config) -> Result { + let conn = config.get_first_signed_connection().as_connection(); + let authority = KeyPairWrapper(aleph_client::keypair_from_string(&config.sudo_seed)); + let account = random_account(); + + let dex = Arc::new(SimpleDexInstance::new(config)?); + let token1 = + reward_token_for_button(config, &conn, &config.test_case_params.early_bird_special)?; + let token2 = + reward_token_for_button(config, &conn, &config.test_case_params.the_pressiah_cometh)?; + let token3 = + reward_token_for_button(config, &conn, &config.test_case_params.back_to_the_future)?; + + let c1 = dex.clone(); + let c2 = token1.clone(); + let c3 = token2.clone(); + let c4 = token3.clone(); + + let subscription = subscribe_events(&conn)?; + let (events_tx, events_rx) = channel(); + + thread::spawn(move || { + let contract_metadata = vec![ + c1.as_ref().into(), + c2.as_ref().into(), + c3.as_ref().into(), + c4.as_ref().into(), + ]; + + listen_contract_events(subscription, &contract_metadata, None, |event| { + let _ = events_tx.send(event); + }); + }); + + let events = BufferedReceiver::new(events_rx, Duration::from_secs(3)); + transfer(&conn, &authority, &account, alephs(100)); + + Ok(DexTestContext { + conn, + authority, + account, + dex, + token1, + token2, + token3, + events, + }) +} + +fn reward_token_for_button( + config: &Config, + conn: &Connection, + button_contract_address: &Option, +) -> Result> { + let button = ButtonInstance::new(config, button_contract_address)?; + Ok(Arc::new(reward_token(conn, &button, config)?)) +} + /// Sets up a number of objects commonly used in button game tests. pub(super) fn setup_button_test( config: &Config, diff --git a/e2e-tests/src/test/button_game/mod.rs b/e2e-tests/src/test/button_game/mod.rs index a2857c1667..38919df53f 100644 --- a/e2e-tests/src/test/button_game/mod.rs +++ b/e2e-tests/src/test/button_game/mod.rs @@ -5,11 +5,12 @@ use anyhow::Result; use assert2::{assert, let_assert}; use helpers::sign; use log::info; +use sp_core::Pair; use crate::{ test::button_game::helpers::{ - assert_recv, assert_recv_id, refute_recv_id, setup_button_test, wait_for_death, - ButtonTestContext, + assert_recv, assert_recv_id, mega, refute_recv_id, setup_button_test, setup_dex_test, + wait_for_death, ButtonTestContext, DexTestContext, }, Config, }; @@ -17,6 +18,114 @@ use crate::{ mod contracts; mod helpers; +/// Test trading on simple_dex. +/// +/// The scenario does the following (given 3 tokens A, B, C): +/// +/// 1. Enables A <-> B, and A -> C swaps. +/// 2. Adds (A, 2000M), (B, 5000M), (C, 10000M) of liquidity. +/// 3. Makes a swap A -> B and then B -> A for the amount of B received in the first swap. +/// 4. Makes a swap A -> B expecting negative slippage (this should fail). +/// 5. Checks that the price after the two swaps is the same as before (with a dust allowance of 1 for rounding). +/// 6. Checks that it's possible to make an A -> C swap, but impossible to make a C -> A swap. +pub fn simple_dex(config: &Config) -> Result<()> { + let DexTestContext { + conn, + authority, + account, + dex, + token1, + token2, + token3, + mut events, + } = setup_dex_test(config)?; + let authority_conn = &sign(&conn, &authority); + let account_conn = &sign(&conn, &account); + let token1 = token1.as_ref(); + let token2 = token2.as_ref(); + let token3 = token3.as_ref(); + let dex = dex.as_ref(); + + dex.add_swap_pair(authority_conn, token1.into(), token2.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + dex.add_swap_pair(authority_conn, token2.into(), token1.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + dex.add_swap_pair(authority_conn, token1.into(), token3.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + token1.mint(authority_conn, &authority.public().into(), mega(3000))?; + token2.mint(authority_conn, &authority.public().into(), mega(5000))?; + token3.mint(authority_conn, &authority.public().into(), mega(10000))?; + + token1.approve(authority_conn, &dex.into(), mega(3000))?; + token2.approve(authority_conn, &dex.into(), mega(5000))?; + token3.approve(authority_conn, &dex.into(), mega(10000))?; + dex.deposit( + authority_conn, + &[ + (token1, mega(3000)), + (token2, mega(5000)), + (token3, mega(10000)), + ], + )?; + + let more_than_liquidity = mega(1_000_000); + assert!(dex + .out_given_in(account_conn, token1, token2, 100, Some(more_than_liquidity)) + .is_err()); + + let initial_amount = mega(100); + token1.mint(authority_conn, &account.public().into(), initial_amount)?; + let expected_output = dex.out_given_in(account_conn, token1, token2, initial_amount, None)?; + assert!(expected_output > 0); + + let at_most_10_percent_slippage = expected_output * 9 / 10; + token1.approve(account_conn, &dex.into(), initial_amount)?; + dex.swap( + account_conn, + token1, + initial_amount, + token2, + at_most_10_percent_slippage, + )?; + assert_recv_id(&mut events, "Swapped"); + assert!(token2.balance_of(&conn, &account.public().into())? == expected_output); + + token2.approve(account_conn, &dex.into(), expected_output)?; + dex.swap(account_conn, token2, expected_output, token1, mega(90))?; + assert_recv_id(&mut events, "Swapped"); + + let balance_after = token1.balance_of(&conn, &account.public().into())?; + assert!(initial_amount.abs_diff(balance_after) <= 1); + assert!( + dex.out_given_in(account_conn, token1, token2, initial_amount, None)? + .abs_diff(expected_output) + <= 1 + ); + + token1.approve(account_conn, &dex.into(), balance_after)?; + let unreasonable_slippage = expected_output * 11 / 10; + dex.swap( + account_conn, + token1, + balance_after, + token2, + unreasonable_slippage, + )?; + refute_recv_id(&mut events, "Swapped"); + + dex.swap(account_conn, token1, balance_after, token3, mega(90))?; + assert_recv_id(&mut events, "Swapped"); + let balance_token3 = token3.balance_of(&conn, &account.public().into())?; + token3.approve(account_conn, &dex.into(), balance_token3)?; + dex.swap(account_conn, token3, balance_token3, token1, mega(90))?; + refute_recv_id(&mut events, "Swapped"); + + Ok(()) +} + /// Tests trading on the marketplace. /// /// The scenario: diff --git a/e2e-tests/src/test/mod.rs b/e2e-tests/src/test/mod.rs index 55332b3822..b7a718e2d8 100644 --- a/e2e-tests/src/test/mod.rs +++ b/e2e-tests/src/test/mod.rs @@ -1,6 +1,7 @@ pub use ban::{ban_automatic, ban_manual, clearing_session_count}; pub use button_game::{ - back_to_the_future, button_game_reset, early_bird_special, marketplace, the_pressiah_cometh, + back_to_the_future, button_game_reset, early_bird_special, marketplace, simple_dex, + the_pressiah_cometh, }; pub use electing_validators::authorities_are_staking; pub use era_payout::era_payouts_calculated_correctly;