diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eeffe866e..6fa2472bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,18 +24,23 @@ jobs: - name: Install toolchain uses: actions-rs/toolchain@v1 with: - profile: minimal toolchain: nightly-2020-05-07 - components: rustfmt + components: rustfmt, clippy target: wasm32-unknown-unknown override: true default: true + - name: Show rustc version + run: rustc --version + - name: Show clippy version + run: cargo clippy --version - name: Install Wasm toolchain run: rustup target add wasm32-unknown-unknown - name: Check format run: make dev-format-check - - name: Install clippy - run: rustup component add clippy + - name: TEMP cargo clean + run: cargo clean + - name: TEMP cargo check + run: cargo check --all - name: Run clippy run: cargo clippy -- -D warnings - name: Update diff --git a/Cargo.dev.toml b/Cargo.dev.toml index f3faebb04..79992ad62 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -10,5 +10,6 @@ members = [ "traits", "utilities", "vesting", + "xtokens", "rewards", ] diff --git a/xtokens/Cargo.toml b/xtokens/Cargo.toml new file mode 100644 index 000000000..2681492ba --- /dev/null +++ b/xtokens/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "orml-xtokens" +description = "Crosschain token transfer" +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/tokens" +license = "Apache-2.0" +version = "0.1.3-dev" +authors = ["Laminar Developers "] +edition = "2018" + +[dependencies] +serde = { version = "1.0.111", optional = true } +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } +sp-runtime = { version = "2.0.0-rc6", default-features = false } +sp-io = { version = "2.0.0-rc6", default-features = false } +sp-std = { version = "2.0.0-rc6", default-features = false } + +frame-support = { version = "2.0.0-rc6", default-features = false } +frame-system = { version = "2.0.0-rc6", default-features = false } + +orml-traits = { path = "../traits", version = "0.1.3-dev", default-features = false } +orml-utilities = { path = "../utilities", version = "0.1.3-dev", default-features = false } + +cumulus-primitives = { git = "https://github.com/paritytech/cumulus", default-features = false } +cumulus-upward-message = { git = "https://github.com/paritytech/cumulus", default-features = false } + +# TODO: switch to polkadot master branch once ready +polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch = "rococo-branch", default-features = false } + +[dev-dependencies] +sp-core = { version = "2.0.0-rc6", default-features = false } +clear_on_drop = { version = "0.2.4", features = ["no_cc"] } # https://github.com/paritytech/substrate/issues/4179 +polkadot-core-primitives = { git = "https://github.com/paritytech/polkadot", branch = "rococo-branch", default-features = false } +orml-tokens = { path = "../tokens", version = "0.1.3-dev" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "frame-support/std", + "frame-system/std", + "orml-traits/std", + "cumulus-primitives/std", + "cumulus-upward-message/std", + "polkadot-parachain/std", +] diff --git a/xtokens/src/lib.rs b/xtokens/src/lib.rs new file mode 100644 index 000000000..f0a897f1a --- /dev/null +++ b/xtokens/src/lib.rs @@ -0,0 +1,372 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::{decl_error, decl_event, decl_module, decl_storage, traits::Get, Parameter}; +use frame_system::ensure_signed; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, CheckedSub, Convert, MaybeSerializeDeserialize, Member, Saturating}, + DispatchResult, RuntimeDebug, +}; +use sp_std::{ + convert::{TryFrom, TryInto}, + prelude::*, +}; + +use cumulus_primitives::{ + relay_chain::{Balance as RelayChainBalance, DownwardMessage}, + xcmp::{XCMPMessageHandler, XCMPMessageSender}, + DownwardMessageHandler, ParaId, UpwardMessageOrigin, UpwardMessageSender, +}; +use cumulus_upward_message::BalancesMessage; +use polkadot_parachain::primitives::AccountIdConversion; + +use orml_traits::MultiCurrency; +use orml_utilities::with_transaction_result; + +mod mock; +mod tests; + +#[derive(Encode, Decode, Eq, PartialEq, Clone, Copy, RuntimeDebug)] +/// Identity of chain. +pub enum ChainId { + /// The relay chain. + RelayChain, + /// A parachain. + ParaChain(ParaId), +} + +#[derive(Encode, Decode, Eq, PartialEq, Clone, RuntimeDebug)] +/// Identity of cross chain currency. +pub struct XCurrencyId { + /// The owner chain of the currency. For instance, the owner chain of DOT is + /// Polkadot. + pub chain_id: ChainId, + /// The identity of the currency. + pub currency_id: Vec, +} + +#[cfg(test)] +impl XCurrencyId { + pub fn new(chain_id: ChainId, currency_id: Vec) -> Self { + XCurrencyId { chain_id, currency_id } + } +} + +#[derive(Encode, Decode, Eq, PartialEq, Clone, RuntimeDebug)] +pub enum XCMPTokenMessage { + /// Token transfer. [x_currency_id, para_id, dest, amount] + Transfer(XCurrencyId, ParaId, AccountId, Balance), +} + +pub trait Trait: frame_system::Trait { + type Event: From> + Into<::Event>; + + /// The balance type. + type Balance: Parameter + Member + AtLeast32BitUnsigned + Default + Copy + MaybeSerializeDeserialize; + + /// Convertor `RelayChainBalance` to `Balance`. + type FromRelayChainBalance: Convert; + + /// Convertor `Balance` to `RelayChainBalance`. + type ToRelayChainBalance: Convert; + + /// The currency ID type + type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord + Into> + TryFrom>; + + /// Currency Id of relay chain. + type RelayChainCurrencyId: Get; + + /// The `MultiCurrency` impl for tokens. + type Currency: MultiCurrency; + + /// Parachain ID. + type ParaId: Get; + + /// The sender of XCMP message. + type XCMPMessageSender: XCMPMessageSender>; + + /// The sender of upward message(to relay chain). + type UpwardMessageSender: UpwardMessageSender; + + /// The upward message type used by parachain runtime. + type UpwardMessage: codec::Codec + BalancesMessage; +} + +decl_storage! { + trait Store for Module as XTokens { + /// Balances of currencies not known to self parachain. + pub UnknownBalances get(fn unknown_balances): double_map hasher(blake2_128_concat) T::AccountId, hasher(blake2_128_concat) Vec => T::Balance; + } +} + +decl_event! { + pub enum Event where + ::AccountId, + ::Balance, + XCurrencyId = XCurrencyId, + { + /// Transferred to relay chain. [src, dest, amount] + TransferredToRelayChain(AccountId, AccountId, Balance), + + /// Received transfer from relay chain. [dest, amount] + ReceivedTransferFromRelayChain(AccountId, Balance), + + /// Transferred to parachain. [x_currency_id, src, para_id, dest, amount] + TransferredToParachain(XCurrencyId, AccountId, ParaId, AccountId, Balance), + + /// Received transfer from parachain. [x_currency_id, para_id, dest, amount] + ReceivedTransferFromParachain(XCurrencyId, ParaId, AccountId, Balance), + } +} + +decl_error! { + /// Error for xtokens module. + pub enum Error for Module { + /// Insufficient balance to transfer. + InsufficientBalance, + /// Invalid currency ID. + InvalidCurrencyId, + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + fn deposit_event() = default; + + /// Transfer relay chain tokens to relay chain. + #[weight = 10] + pub fn transfer_to_relay_chain(origin, dest: T::AccountId, amount: T::Balance) { + with_transaction_result(|| { + let who = ensure_signed(origin)?; + Self::do_transfer_to_relay_chain(&who, &dest, amount)?; + Self::deposit_event(Event::::TransferredToRelayChain(who, dest, amount)); + Ok(()) + })?; + } + + /// Transfer tokens to parachain. + #[weight = 10] + pub fn transfer_to_parachain( + origin, + x_currency_id: XCurrencyId, + para_id: ParaId, + dest: T::AccountId, + amount: T::Balance, + ) { + with_transaction_result(|| { + let who = ensure_signed(origin)?; + + if para_id == T::ParaId::get() { + return Ok(()); + } + + Self::do_transfer_to_parachain(x_currency_id.clone(), &who, para_id, &dest, amount)?; + Self::deposit_event(Event::::TransferredToParachain(x_currency_id, who, para_id, dest, amount)); + + Ok(()) + })?; + } + } +} + +impl Module { + fn do_transfer_to_relay_chain(who: &T::AccountId, dest: &T::AccountId, amount: T::Balance) -> DispatchResult { + T::Currency::withdraw(T::RelayChainCurrencyId::get(), who, amount)?; + let msg = T::UpwardMessage::transfer(dest.clone(), T::ToRelayChainBalance::convert(amount)); + T::UpwardMessageSender::send_upward_message(&msg, UpwardMessageOrigin::Signed).expect("Should not fail; qed"); + Ok(()) + } + + fn do_transfer_to_parachain( + x_currency_id: XCurrencyId, + src: &T::AccountId, + para_id: ParaId, + dest: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + match x_currency_id.chain_id { + ChainId::RelayChain => { + Self::transfer_relay_chain_tokens_to_parachain(x_currency_id, src, para_id, dest, amount) + } + ChainId::ParaChain(token_owner) => { + if T::ParaId::get() == token_owner { + Self::transfer_owned_tokens_to_parachain(x_currency_id, src, para_id, dest, amount) + } else { + Self::transfer_non_owned_tokens_to_parachain(token_owner, x_currency_id, src, para_id, dest, amount) + } + } + } + } + + /// Transfer relay chain tokens to another parachain. + /// + /// 1. Withdraw `src` balance. + /// 2. Transfer in relay chain: from self parachain account to `para_id` + /// account. 3. Notify `para_id` the transfer. + fn transfer_relay_chain_tokens_to_parachain( + x_currency_id: XCurrencyId, + src: &T::AccountId, + para_id: ParaId, + dest: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let para_account = para_id.into_account(); + + T::Currency::withdraw(T::RelayChainCurrencyId::get(), src, amount)?; + + let msg = T::UpwardMessage::transfer(para_account, T::ToRelayChainBalance::convert(amount)); + T::UpwardMessageSender::send_upward_message(&msg, UpwardMessageOrigin::Signed).expect("Should not fail; qed"); + + T::XCMPMessageSender::send_xcmp_message( + para_id, + &XCMPTokenMessage::Transfer(x_currency_id, para_id, dest.clone(), amount), + ) + .expect("Should not fail; qed"); + + Ok(()) + } + + /// Transfer parachain tokens "owned" by self parachain to another + /// parachain. + /// + /// 1. Transfer from `src` to `para_id` account. + /// 2. Notify `para_id` the transfer. + /// + /// NOTE - `para_id` must not be self parachain. + fn transfer_owned_tokens_to_parachain( + x_currency_id: XCurrencyId, + src: &T::AccountId, + para_id: ParaId, + dest: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + let para_account = para_id.into_account(); + let currency_id: T::CurrencyId = x_currency_id + .currency_id + .clone() + .try_into() + .map_err(|_| Error::::InvalidCurrencyId)?; + T::Currency::transfer(currency_id, src, ¶_account, amount)?; + + T::XCMPMessageSender::send_xcmp_message( + para_id, + &XCMPTokenMessage::Transfer(x_currency_id, para_id, dest.clone(), amount), + ) + .expect("Should not fail; qed"); + + Ok(()) + } + + /// Transfer parachain tokens not "owned" by self chain to another + /// parachain. + /// + /// 1. Withdraw from `src`. + /// 2. Notify token owner parachain the transfer. (Token owner chain would + /// further notify `para_id`) + fn transfer_non_owned_tokens_to_parachain( + token_owner: ParaId, + x_currency_id: XCurrencyId, + src: &T::AccountId, + para_id: ParaId, + dest: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + if let Ok(currency_id) = x_currency_id.currency_id.clone().try_into() { + // Known currency, withdraw from src. + T::Currency::withdraw(currency_id, src, amount)?; + } else { + // Unknown currency, update balance. + UnknownBalances::::try_mutate(src, &x_currency_id.currency_id, |total| -> DispatchResult { + *total = total.checked_sub(&amount).ok_or(Error::::InsufficientBalance)?; + Ok(()) + })?; + } + + T::XCMPMessageSender::send_xcmp_message( + token_owner, + &XCMPTokenMessage::Transfer(x_currency_id, para_id, dest.clone(), amount), + ) + .expect("Should not fail; qed"); + + Ok(()) + } +} + +/// This is a hack to convert from one generic type to another where we are sure +/// that both are the same type/use the same encoding. +fn convert_hack(input: &impl Encode) -> O { + input.using_encoded(|e| Decode::decode(&mut &e[..]).expect("Must be compatible; qed")) +} + +impl DownwardMessageHandler for Module { + fn handle_downward_message(msg: &DownwardMessage) { + if let DownwardMessage::TransferInto(dest, amount, _) = msg { + let dest: T::AccountId = convert_hack(dest); + let amount: T::Balance = T::FromRelayChainBalance::convert(*amount); + // Should not fail, but if it does, there is nothing can be done. + let _ = T::Currency::deposit(T::RelayChainCurrencyId::get(), &dest, amount); + + Self::deposit_event(Event::::ReceivedTransferFromRelayChain(dest, amount)); + } + } +} + +impl XCMPMessageHandler> for Module { + fn handle_xcmp_message(src: ParaId, msg: &XCMPTokenMessage) { + match msg { + XCMPTokenMessage::Transfer(x_currency_id, para_id, dest, amount) => { + match x_currency_id.chain_id { + ChainId::RelayChain => { + // Relay chain tokens. Should not fail, but if it does, there is nothing we + // could do. + let _ = T::Currency::deposit(T::RelayChainCurrencyId::get(), &dest, *amount); + } + ChainId::ParaChain(token_owner) => { + if T::ParaId::get() == token_owner { + // Handle owned tokens: + // If `para_id` is self parachain: + // 1. Transfer from `src` para account to `dest` account. + // else (`para_id` is not self parachain): + // 1. transfer between para accounts + // 2. notify the `para_id` + let src_para_account = src.into_account(); + if *para_id == T::ParaId::get() { + if let Ok(currency_id) = x_currency_id.currency_id.clone().try_into() { + // Should not fail, but if it does, there is nothing can be done. + let _ = T::Currency::transfer(currency_id, &src_para_account, dest, *amount); + } + } else { + // Should not fail, but if it does, there is nothing can be done. + let _ = Self::transfer_owned_tokens_to_parachain( + x_currency_id.clone(), + &src_para_account, + *para_id, + dest, + *amount, + ); + } + } else if let Ok(currency_id) = x_currency_id.currency_id.clone().try_into() { + // Handle known tokens. + // Should not fail, but if it does, there is nothing can be done. + let _ = T::Currency::deposit(currency_id, dest, *amount); + } else { + // Handle unknown tokens. + UnknownBalances::::mutate(dest, x_currency_id.currency_id.clone(), |total| { + *total = total.saturating_add(*amount) + }); + } + } + } + + Self::deposit_event(Event::::ReceivedTransferFromParachain( + x_currency_id.clone(), + src, + dest.clone(), + *amount, + )); + } + } + } +} diff --git a/xtokens/src/mock.rs b/xtokens/src/mock.rs new file mode 100644 index 000000000..189aa6ec0 --- /dev/null +++ b/xtokens/src/mock.rs @@ -0,0 +1,249 @@ +//! Mocks for the xtokens module. + +#![cfg(test)] + +use frame_support::{impl_outer_event, impl_outer_origin, parameter_types}; +use frame_system as system; +use serde::{Deserialize, Serialize}; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill}; +use sp_std::cell::RefCell; + +use super::*; + +type AccountId = u128; +pub type Balance = u128; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +mod xtokens { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + frame_system, + orml_tokens, + xtokens, + } +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +impl frame_system::Trait for Runtime { + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); +} +pub type System = system::Module; + +#[repr(u8)] +#[derive(Encode, Decode, Serialize, Deserialize, Eq, PartialEq, Copy, Clone, RuntimeDebug, PartialOrd, Ord)] +pub enum CurrencyId { + Owned = 0, + BTC, + DOT, +} +impl Into> for CurrencyId { + fn into(self) -> Vec { + vec![self as u8] + } +} + +impl TryFrom> for CurrencyId { + type Error = (); + + fn try_from(v: Vec) -> Result { + if v.len() == 1 { + let num = v[0]; + match num { + 0 => return Ok(CurrencyId::Owned), + 1 => return Ok(CurrencyId::BTC), + 2 => return Ok(CurrencyId::DOT), + _ => return Err(()), + }; + } + Err(()) + } +} + +pub fn unknown_currency_id() -> Vec { + vec![10] +} + +impl orml_tokens::Trait for Runtime { + type Event = TestEvent; + type Balance = Balance; + type Amount = i128; + type CurrencyId = CurrencyId; + type OnReceived = (); + type WeightInfo = (); +} +pub type Tokens = orml_tokens::Module; + +parameter_types! { + pub const RelayChainCurrencyId: CurrencyId = CurrencyId::DOT; + pub MockParaId: ParaId = 0.into(); +} + +impl Trait for Runtime { + type Event = TestEvent; + type Balance = Balance; + type ToRelayChainBalance = BalanceConvertor; + type FromRelayChainBalance = BalanceConvertor; + type CurrencyId = CurrencyId; + type RelayChainCurrencyId = RelayChainCurrencyId; + type Currency = Tokens; + type ParaId = MockParaId; + type XCMPMessageSender = MockXCMPMessageSender; + type UpwardMessageSender = MockUpwardMessageSender; + type UpwardMessage = MockUpwardMessage; +} +pub type XTokens = Module; + +thread_local! { + static XCMP_MESSAGES: RefCell)>> = RefCell::new(None); +} + +pub struct MockXCMPMessageSender; +impl MockXCMPMessageSender { + pub fn msg_sent(dest: ParaId, msg: XCMPTokenMessage) -> bool { + XCMP_MESSAGES.with(|v| v.borrow().clone()) == Some((dest, msg)) + } +} +impl XCMPMessageSender> for MockXCMPMessageSender { + fn send_xcmp_message(dest: ParaId, msg: &XCMPTokenMessage) -> Result<(), ()> { + XCMP_MESSAGES.with(|v| *v.borrow_mut() = Some((dest, msg.clone()))); + Ok(()) + } +} + +#[derive(Encode, Decode, Eq, PartialEq, Clone)] +pub struct MockUpwardMessage(pub AccountId, pub Balance); +impl BalancesMessage for MockUpwardMessage { + fn transfer(dest: AccountId, amount: Balance) -> Self { + MockUpwardMessage(dest, amount) + } +} + +thread_local! { + static UPWARD_MESSAGES: RefCell> = RefCell::new(None); +} +pub struct MockUpwardMessageSender; +impl MockUpwardMessageSender { + pub fn msg_sent(msg: MockUpwardMessage) -> bool { + UPWARD_MESSAGES.with(|v| v.borrow().clone()) == Some(msg) + } +} +impl UpwardMessageSender for MockUpwardMessageSender { + fn send_upward_message(msg: &MockUpwardMessage, _origin: UpwardMessageOrigin) -> Result<(), ()> { + UPWARD_MESSAGES.with(|v| *v.borrow_mut() = Some(msg.clone())); + Ok(()) + } +} + +pub struct BalanceConvertor; +impl Convert for BalanceConvertor { + fn convert(x: u128) -> u128 { + x + } +} + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; + +pub const PARA_ONE_ID: u32 = 1; + +pub fn para_one_id() -> ParaId { + PARA_ONE_ID.into() +} + +pub fn para_one_account() -> AccountId { + para_one_id().into_account() +} + +pub const PARA_TWO_ID: u32 = 2; + +pub fn para_two_id() -> ParaId { + PARA_TWO_ID.into() +} + +pub fn para_two_account() -> AccountId { + para_two_id().into_account() +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + endowed_accounts: vec![], + } + } +} + +impl ExtBuilder { + pub fn balances(mut self, endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.endowed_accounts = endowed_accounts; + self + } + + pub fn one_hundred_for_alice(self) -> Self { + self.balances(vec![ + (ALICE, CurrencyId::Owned, 100), + (ALICE, CurrencyId::DOT, 100), + (ALICE, CurrencyId::BTC, 100), + ]) + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + orml_tokens::GenesisConfig:: { + endowed_accounts: self.endowed_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + XCMP_MESSAGES.with(|v| *v.borrow_mut() = None); + UPWARD_MESSAGES.with(|v| *v.borrow_mut() = None); + + t.into() + } +} diff --git a/xtokens/src/tests.rs b/xtokens/src/tests.rs new file mode 100644 index 000000000..d923abe3d --- /dev/null +++ b/xtokens/src/tests.rs @@ -0,0 +1,369 @@ +//! Unit tests for the xtokens module. + +#![cfg(test)] + +use super::*; +use mock::*; + +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn transfer_to_relay_chain_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + System::set_block_number(1); + + assert_ok!(XTokens::transfer_to_relay_chain(Origin::signed(ALICE), BOB, 50)); + + assert_eq!(Tokens::free_balance(CurrencyId::DOT, &ALICE), 50); + assert!(MockUpwardMessageSender::msg_sent(MockUpwardMessage(BOB, 50))); + + let event = TestEvent::xtokens(RawEvent::TransferredToRelayChain(ALICE, BOB, 50)); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn transfer_to_relay_chain_fails_if_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + XTokens::transfer_to_relay_chain(Origin::signed(ALICE), BOB, 50), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn transfer_relay_chain_tokens_to_parachain_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::RelayChain, vec![0]); + assert_ok!(XTokens::transfer_to_parachain( + Origin::signed(ALICE), + x_currency_id.clone(), + para_one_id(), + BOB, + 50 + )); + + assert_eq!(Tokens::free_balance(CurrencyId::DOT, &ALICE), 50); + assert!(MockUpwardMessageSender::msg_sent(MockUpwardMessage( + para_one_account(), + 50 + ))); + assert!(MockXCMPMessageSender::msg_sent( + para_one_id(), + XCMPTokenMessage::Transfer(x_currency_id.clone(), para_one_id(), BOB, 50) + )); + + let event = TestEvent::xtokens(RawEvent::TransferredToParachain( + x_currency_id, + ALICE, + para_one_id(), + BOB, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn transfer_relay_chain_tokens_to_parachain_fails_if_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let x_currency_id = XCurrencyId::new(ChainId::RelayChain, vec![0]); + assert_noop!( + XTokens::transfer_to_parachain(Origin::signed(ALICE), x_currency_id, para_one_id(), BOB, 50), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn transfer_owned_tokens_to_parachain_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), CurrencyId::Owned.into()); + assert_ok!(XTokens::transfer_to_parachain( + Origin::signed(ALICE), + x_currency_id.clone(), + para_one_id(), + BOB, + 50 + )); + + assert_eq!(Tokens::free_balance(CurrencyId::Owned, &ALICE), 50); + assert_eq!(Tokens::free_balance(CurrencyId::Owned, ¶_one_account()), 50); + assert!(MockXCMPMessageSender::msg_sent( + para_one_id(), + XCMPTokenMessage::Transfer(x_currency_id.clone(), para_one_id(), BOB, 50) + )); + + let event = TestEvent::xtokens(RawEvent::TransferredToParachain( + x_currency_id, + ALICE, + para_one_id(), + BOB, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn transfer_owned_tokens_to_parachain_fails_if_unrecognized_currency_id() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), unknown_currency_id()); + assert_noop!( + XTokens::transfer_to_parachain(Origin::signed(ALICE), x_currency_id, para_one_id(), BOB, 50), + Error::::InvalidCurrencyId + ); + }); +} + +#[test] +fn transfer_owned_tokens_to_parachain_fails_if_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), CurrencyId::Owned.into()); + assert_noop!( + XTokens::transfer_to_parachain(Origin::signed(ALICE), x_currency_id, para_one_id(), BOB, 50), + orml_tokens::Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn transfer_known_non_owned_tokens_to_parachain_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), CurrencyId::BTC.into()); + assert_ok!(XTokens::transfer_to_parachain( + Origin::signed(ALICE), + x_currency_id.clone(), + para_two_id(), + BOB, + 50 + )); + + assert_eq!(Tokens::free_balance(CurrencyId::BTC, &ALICE), 50); + assert!(MockXCMPMessageSender::msg_sent( + para_one_id(), + XCMPTokenMessage::Transfer(x_currency_id.clone(), para_two_id(), BOB, 50) + )); + + let event = TestEvent::xtokens(RawEvent::TransferredToParachain( + x_currency_id, + ALICE, + para_two_id(), + BOB, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn transfer_known_non_owned_tokens_fails_if_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), CurrencyId::BTC.into()); + assert_noop!( + XTokens::transfer_to_parachain(Origin::signed(ALICE), x_currency_id, para_two_id(), BOB, 50), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn transfer_unknown_non_owned_tokens_to_parachain_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + System::set_block_number(1); + + >::insert(ALICE, unknown_currency_id(), 100); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), unknown_currency_id()); + assert_ok!(XTokens::transfer_to_parachain( + Origin::signed(ALICE), + x_currency_id.clone(), + para_two_id(), + BOB, + 50 + )); + + assert_eq!(XTokens::unknown_balances(ALICE, unknown_currency_id()), 50); + assert!(MockXCMPMessageSender::msg_sent( + para_one_id(), + XCMPTokenMessage::Transfer(x_currency_id.clone(), para_two_id(), BOB, 50) + )); + + let event = TestEvent::xtokens(RawEvent::TransferredToParachain( + x_currency_id, + ALICE, + para_two_id(), + BOB, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn transfer_unknown_non_owned_tokens_fails_if_insufficient_balance() { + ExtBuilder::default().build().execute_with(|| { + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), unknown_currency_id()); + assert_noop!( + XTokens::transfer_to_parachain(Origin::signed(ALICE), x_currency_id, para_two_id(), BOB, 50), + Error::::InsufficientBalance + ); + }); +} + +#[test] +fn handle_downward_message_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let dest: polkadot_core_primitives::AccountId = [0; 32].into(); + let msg = DownwardMessage::TransferInto(dest.clone(), 50, [0; 32]); + XTokens::handle_downward_message(&msg); + + let dest_account = convert_hack(&dest); + assert_eq!(Tokens::free_balance(CurrencyId::DOT, &dest_account), 50); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromRelayChain(dest_account, 50)); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn handle_xcmp_message_works_for_relay_chain_tokens() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::RelayChain, vec![0]); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), MockParaId::get(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + + assert_eq!(Tokens::free_balance(CurrencyId::DOT, &ALICE), 50); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromParachain( + x_currency_id, + para_one_id(), + ALICE, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn handle_xcmp_message_works_for_owned_parachain_tokens() { + // transfer from para_one to para_two + ExtBuilder::default() + .balances(vec![(para_one_account(), CurrencyId::Owned, 100)]) + .build() + .execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), CurrencyId::Owned.into()); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), para_two_id(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + + assert_eq!(Tokens::free_balance(CurrencyId::Owned, ¶_one_account()), 50); + assert_eq!(Tokens::free_balance(CurrencyId::Owned, ¶_two_account()), 50); + + MockXCMPMessageSender::msg_sent(para_two_id(), msg); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromParachain( + x_currency_id, + para_one_id(), + ALICE, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn handle_xcmp_message_works_for_owned_parachain_tokens_and_self_parachain_as_dest() { + // transfer from para_one to self parachain + ExtBuilder::default() + .balances(vec![(para_one_account(), CurrencyId::Owned, 100)]) + .build() + .execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), CurrencyId::Owned.into()); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), MockParaId::get(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + + assert_eq!(Tokens::free_balance(CurrencyId::Owned, ¶_one_account()), 50); + assert_eq!(Tokens::free_balance(CurrencyId::Owned, &ALICE), 50); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromParachain( + x_currency_id, + para_one_id(), + ALICE, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn handle_xcmp_message_works_for_owned_parachain_tokens_with_invalid_currency() { + ExtBuilder::default() + .balances(vec![(para_one_account(), CurrencyId::Owned, 100)]) + .build() + .execute_with(|| { + fn handle() -> sp_std::result::Result<(), ()> { + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(MockParaId::get()), unknown_currency_id()); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), MockParaId::get(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + Err(()) + } + assert_noop!(handle(), ()); + }); +} + +#[test] +fn handle_xcmp_message_works_for_non_owned_known_parachain_tokens() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), CurrencyId::BTC.into()); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), MockParaId::get(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + + assert_eq!(Tokens::free_balance(CurrencyId::BTC, &ALICE), 50); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromParachain( + x_currency_id, + para_one_id(), + ALICE, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +} + +#[test] +fn handle_xcmp_message_works_for_non_owned_unknown_parachain_tokens() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let x_currency_id = XCurrencyId::new(ChainId::ParaChain(para_one_id()), unknown_currency_id()); + let msg = XCMPTokenMessage::Transfer(x_currency_id.clone(), MockParaId::get(), ALICE, 50); + XTokens::handle_xcmp_message(para_one_id(), &msg); + + assert_eq!(XTokens::unknown_balances(ALICE, unknown_currency_id()), 50); + + let event = TestEvent::xtokens(RawEvent::ReceivedTransferFromParachain( + x_currency_id, + para_one_id(), + ALICE, + 50, + )); + assert!(System::events().iter().any(|record| record.event == event)); + }); +}