From a8ecc4a7324d6c463aacbaae974adfb66e5161a9 Mon Sep 17 00:00:00 2001 From: Shady Khalifa Date: Wed, 11 Dec 2024 20:54:21 +0200 Subject: [PATCH 1/4] feat: track pending payments to MBSM --- pallets/services/src/lib.rs | 63 +++++++++++++++++++++++++++++++++- primitives/src/services/mod.rs | 47 ++++++++++++++++--------- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index 6b25508d..4ea3910f 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -622,6 +622,15 @@ pub mod module { OperatorProfile, ResultQuery::OperatorProfileNotFound>, >; + /// Holds the service payment information for a service request. + /// Once the service is initiated, the payment is transferred to the MBSM and this + /// information is removed. + /// + /// Service Requst ID -> Service Payment + #[pallet::storage] + #[pallet::getter(fn service_payment)] + pub type StagingServicePayments = + StorageMap<_, Identity, u64, StagingServicePayment>>; #[pallet::call] impl Pallet { @@ -871,6 +880,7 @@ pub mod module { } let mut native_value = Zero::zero(); + let request_id = NextServiceRequestId::::get(); if value != Zero::zero() { // Payment transfer @@ -900,9 +910,18 @@ pub mod module { ensure!(success, Error::::ERC20TransferFailed); }, }; + + // Save the payment information for the service request. + let payment = StagingServicePayment { + request_id, + owner: caller.clone(), + asset: payment_asset, + amount: value, + }; + + StagingServicePayments::::insert(request_id, payment); } - let request_id = NextServiceRequestId::::get(); let (allowed, _weight) = Self::on_request_hook( &blueprint, blueprint_id, @@ -1068,6 +1087,14 @@ pub mod module { .map_err(|_| Error::::MaxServicesPerUserExceeded) })?; + // Payment + if let Some(payment) = Self::service_payment(request_id) { + // TODO: handle the payment to MBSM. + + // Remove the payment information. + StagingServicePayments::::remove(request_id); + } + let (allowed, _weight) = Self::on_service_init_hook( &blueprint, blueprint_id, @@ -1131,6 +1158,40 @@ pub mod module { request_id, }); + // Refund the payment + if let Some(payment) = Self::service_payment(request_id) { + match payment.asset { + Asset::Custom(asset_id) if asset_id == Zero::zero() => { + T::Currency::transfer( + &Self::account_id(), + &payment.owner, + payment.amount, + ExistenceRequirement::KeepAlive, + )?; + }, + Asset::Custom(asset_id) => { + T::Fungibles::transfer( + asset_id, + &Self::account_id(), + &payment.owner, + payment.amount, + Preservation::Preserve, + )?; + }, + Asset::Erc20(token) => { + // TODO: handle the refund of the ERC20 token. + // let (success, _weight) = Self::erc20_transfer( + // token, + // Self::address(), + // &payment.owner, + // payment.amount, + // )?; + // ensure!(success, Error::::ERC20TransferFailed); + }, + } + StagingServicePayments::::remove(request_id); + } + // TODO: make use of the returned weight from the hook. Ok(PostDispatchInfo { actual_weight: None, pays_fee: Pays::Yes }) } diff --git a/primitives/src/services/mod.rs b/primitives/src/services/mod.rs index 46574ff6..e440168b 100644 --- a/primitives/src/services/mod.rs +++ b/primitives/src/services/mod.rs @@ -14,22 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . -// This file is part of Tangle. -// Copyright (C) 2022-2024 Tangle Foundation. -// -// Tangle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Tangle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Tangle. If not, see . - //! Services primitives. use educe::Educe; @@ -561,6 +545,37 @@ impl } } +/// A staging service payment is a payment that is made for a service request +/// but will be paid when the service is created or refunded if the service is rejected. +#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Copy, Clone, MaxEncodedLen)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct StagingServicePayment { + /// The service request ID. + pub request_id: u64, + /// The requester (the payer). + pub owner: AccountId, + /// The Asset used in the payment. + pub asset: Asset, + /// The amount of the asset that is paid. + pub amount: Balance, +} + +impl Default for StagingServicePayment +where + AccountId: Default, + AssetId: sp_runtime::traits::Zero, + Balance: Default, +{ + fn default() -> Self { + Self { + request_id: Default::default(), + owner: Default::default(), + asset: Asset::default(), + amount: Default::default(), + } + } +} + /// A Service is an instance of a service blueprint. #[derive(Educe, Encode, Decode, TypeInfo, MaxEncodedLen)] #[educe( From 3dcd4a201cb97b4eb82b6379881465d25454554e Mon Sep 17 00:00:00 2001 From: Shady Khalifa Date: Thu, 12 Dec 2024 21:19:56 +0200 Subject: [PATCH 2/4] feat: send payments to MBSM --- pallets/services/src/functions.rs | 3 +- pallets/services/src/lib.rs | 85 +++++++++++++++++++++++----- pallets/services/src/mock.rs | 29 ++++++++-- pallets/services/src/tests.rs | 10 +++- precompiles/services/src/lib.rs | 4 +- primitives/src/services/mod.rs | 12 ++-- primitives/src/types.rs | 94 +++++++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 30 deletions(-) diff --git a/pallets/services/src/functions.rs b/pallets/services/src/functions.rs index 9311ab08..283f5425 100644 --- a/pallets/services/src/functions.rs +++ b/pallets/services/src/functions.rs @@ -968,11 +968,10 @@ impl Pallet { /// Moves a `value` amount of tokens from the caller's account to `to`. pub fn erc20_transfer( erc20: H160, - caller: &T::AccountId, + from: H160, to: H160, value: BalanceOf, ) -> Result<(bool, Weight), DispatchErrorWithPostInfo> { - let from = T::EvmAddressMapping::into_address(caller.clone()); #[allow(deprecated)] let transfer_fn = Function { name: String::from("transfer"), diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index 4ea3910f..1435adc1 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -66,7 +66,7 @@ pub mod module { use sp_runtime::Percent; use sp_std::vec::Vec; use tangle_primitives::services::MasterBlueprintServiceManagerRevision; - use tangle_primitives::{services::*, MultiAssetDelegationInfo}; + use tangle_primitives::{services::*, Account, MultiAssetDelegationInfo}; use types::*; #[pallet::config] @@ -291,6 +291,12 @@ pub mod module { MaxMasterBlueprintServiceManagerVersionsExceeded, /// The ERC20 transfer failed. ERC20TransferFailed, + /// Missing EVM Origin for the EVM execution. + MissingEVMOrigin, + /// Expected the account to be an EVM address. + ExpectedEVMAddress, + /// Expected the account to be an account ID. + ExpectedAccountId, } #[pallet::event] @@ -855,6 +861,7 @@ pub mod module { #[pallet::weight(T::WeightInfo::request())] pub fn request( origin: OriginFor, + evm_origin: Option, #[pallet::compact] blueprint_id: u64, permitted_callers: Vec, operators: Vec, @@ -884,7 +891,7 @@ pub mod module { if value != Zero::zero() { // Payment transfer - match payment_asset { + let refund_to = match payment_asset { // Handle the case of native currency. Asset::Custom(asset_id) if asset_id == Zero::zero() => { T::Currency::transfer( @@ -894,6 +901,7 @@ pub mod module { ExistenceRequirement::KeepAlive, )?; native_value = value; + Account::id(caller.clone()) }, Asset::Custom(asset_id) => { T::Fungibles::transfer( @@ -903,18 +911,24 @@ pub mod module { value, Preservation::Preserve, )?; + Account::id(caller.clone()) }, Asset::Erc20(token) => { + // origin check. + let evm_origin = evm_origin.ok_or(Error::::MissingEVMOrigin)?; + let mapped_origin = T::EvmAddressMapping::into_account_id(evm_origin); + ensure!(mapped_origin == caller, DispatchError::BadOrigin); let (success, _weight) = - Self::erc20_transfer(token, &caller, Self::address(), value)?; + Self::erc20_transfer(token, evm_origin, Self::address(), value)?; ensure!(success, Error::::ERC20TransferFailed); + Account::from(evm_origin) }, }; // Save the payment information for the service request. let payment = StagingServicePayment { request_id, - owner: caller.clone(), + refund_to, asset: payment_asset, amount: value, }; @@ -1089,7 +1103,37 @@ pub mod module { // Payment if let Some(payment) = Self::service_payment(request_id) { - // TODO: handle the payment to MBSM. + // send payments to the MBSM + let mbsm_address = Self::mbsm_address_of(&blueprint)?; + let mbsm_account_id = T::EvmAddressMapping::into_account_id(mbsm_address); + match payment.asset { + Asset::Custom(asset_id) if asset_id == Zero::zero() => { + T::Currency::transfer( + &Self::account_id(), + &mbsm_account_id, + payment.amount, + ExistenceRequirement::AllowDeath, + )?; + }, + Asset::Custom(asset_id) => { + T::Fungibles::transfer( + asset_id, + &Self::account_id(), + &mbsm_account_id, + payment.amount, + Preservation::Expendable, + )?; + }, + Asset::Erc20(token) => { + let (success, _weight) = Self::erc20_transfer( + token, + Self::address(), + mbsm_address, + payment.amount, + )?; + ensure!(success, Error::::ERC20TransferFailed); + }, + } // Remove the payment information. StagingServicePayments::::remove(request_id); @@ -1162,31 +1206,42 @@ pub mod module { if let Some(payment) = Self::service_payment(request_id) { match payment.asset { Asset::Custom(asset_id) if asset_id == Zero::zero() => { + let refund_to = payment + .refund_to + .try_into_account_id() + .map_err(|_| Error::::ExpectedAccountId)?; T::Currency::transfer( &Self::account_id(), - &payment.owner, + &refund_to, payment.amount, ExistenceRequirement::KeepAlive, )?; }, Asset::Custom(asset_id) => { + let refund_to = payment + .refund_to + .try_into_account_id() + .map_err(|_| Error::::ExpectedAccountId)?; T::Fungibles::transfer( asset_id, &Self::account_id(), - &payment.owner, + &refund_to, payment.amount, Preservation::Preserve, )?; }, Asset::Erc20(token) => { - // TODO: handle the refund of the ERC20 token. - // let (success, _weight) = Self::erc20_transfer( - // token, - // Self::address(), - // &payment.owner, - // payment.amount, - // )?; - // ensure!(success, Error::::ERC20TransferFailed); + let refund_to = payment + .refund_to + .try_into_address() + .map_err(|_| Error::::ExpectedEVMAddress)?; + let (success, _weight) = Self::erc20_transfer( + token, + Self::address(), + refund_to, + payment.amount, + )?; + ensure!(success, Error::::ERC20TransferFailed); }, } StagingServicePayments::::remove(request_id); diff --git a/pallets/services/src/mock.rs b/pallets/services/src/mock.rs index d8ec9dd1..405f106d 100644 --- a/pallets/services/src/mock.rs +++ b/pallets/services/src/mock.rs @@ -232,11 +232,7 @@ impl EvmAddressMapping for PalletEVMAddressMapping { } fn into_address(account_id: AccountId) -> H160 { - account_id.using_encoded(|b| { - let mut addr = [0u8; 20]; - addr.copy_from_slice(&b[0..20]); - H160(addr) - }) + H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&account_id)[0..20]) } } @@ -473,7 +469,16 @@ pub fn mock_pub_key(id: u8) -> AccountId { } pub fn mock_address(id: u8) -> H160 { - H160([id; 20]) + H160::from_slice(&[id; 20]) +} + +pub fn account_id_to_address(account_id: AccountId) -> H160 { + H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&account_id)[0..20]) +} + +pub fn address_to_account_id(address: H160) -> AccountId { + use pallet_evm::AddressMapping; + ::AddressMapping::into_account_id(address) } pub fn mock_authorities(vec: Vec) -> Vec { @@ -569,6 +574,18 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE ); } + for a in &authorities { + evm_accounts.insert( + account_id_to_address(a.clone()), + fp_evm::GenesisAccount { + code: vec![], + storage: Default::default(), + nonce: Default::default(), + balance: Uint::from(1_000).mul(Uint::from(10).pow(Uint::from(18))), + }, + ); + } + let evm_config = pallet_evm::GenesisConfig:: { accounts: evm_accounts, ..Default::default() }; diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs index 32749c6b..3db3b056 100644 --- a/pallets/services/src/tests.rs +++ b/pallets/services/src/tests.rs @@ -400,6 +400,7 @@ fn request_service() { let eve = mock_pub_key(EVE); assert_ok!(Services::request( RuntimeOrigin::signed(eve.clone()), + None, 0, vec![alice.clone()], vec![bob.clone(), charlie.clone(), dave.clone()], @@ -501,6 +502,7 @@ fn request_service_with_no_assets() { assert_err!( Services::request( RuntimeOrigin::signed(eve.clone()), + None, 0, vec![alice.clone()], vec![bob.clone()], @@ -536,6 +538,7 @@ fn request_service_with_payment_asset() { let charlie = mock_pub_key(CHARLIE); assert_ok!(Services::request( RuntimeOrigin::signed(charlie.clone()), + None, 0, vec![], vec![bob.clone()], @@ -586,7 +589,8 @@ fn request_service_with_payment_token() { let charlie = mock_pub_key(CHARLIE); assert_ok!(Services::request( - RuntimeOrigin::signed(charlie.clone()), + RuntimeOrigin::signed(address_to_account_id(mock_address(CHARLIE))), + Some(account_id_to_address(charlie.clone())), 0, vec![], vec![bob.clone()], @@ -656,6 +660,7 @@ fn job_calls() { let eve = mock_pub_key(EVE); assert_ok!(Services::request( RuntimeOrigin::signed(eve.clone()), + None, 0, vec![alice.clone()], vec![bob.clone(), charlie.clone(), dave.clone()], @@ -749,6 +754,7 @@ fn job_result() { let eve = mock_pub_key(EVE); assert_ok!(Services::request( RuntimeOrigin::signed(eve.clone()), + None, 0, vec![alice.clone()], vec![bob.clone(), charlie.clone(), dave.clone()], @@ -867,6 +873,7 @@ fn deploy() -> Deployment { let service_id = Services::next_instance_id(); assert_ok!(Services::request( RuntimeOrigin::signed(eve.clone()), + None, blueprint_id, vec![alice.clone()], vec![bob.clone()], @@ -1173,6 +1180,7 @@ fn hooks() { // OnRequest hook should be called assert_ok!(Services::request( RuntimeOrigin::signed(charlie.clone()), + None, 0, vec![alice.clone()], vec![bob.clone()], diff --git a/precompiles/services/src/lib.rs b/precompiles/services/src/lib.rs index 50946f49..af04c33d 100644 --- a/precompiles/services/src/lib.rs +++ b/precompiles/services/src/lib.rs @@ -152,7 +152,8 @@ where amount: U256, ) -> EvmResult { handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let msg_sender = handle.context().caller; + let origin = Runtime::AddressMapping::into_account_id(msg_sender); let blueprint_id: u64 = blueprint_id.as_u64(); let permitted_callers_data: Vec = permitted_callers_data.into(); @@ -222,6 +223,7 @@ where }; let call = pallet_services::Call::::request { + evm_origin: Some(msg_sender), blueprint_id, permitted_callers, operators, diff --git a/primitives/src/services/mod.rs b/primitives/src/services/mod.rs index e440168b..34dba71f 100644 --- a/primitives/src/services/mod.rs +++ b/primitives/src/services/mod.rs @@ -20,7 +20,7 @@ use educe::Educe; use frame_support::pallet_prelude::*; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use sp_core::{ecdsa, RuntimeDebug}; +use sp_core::{ecdsa, ByteArray, RuntimeDebug}; use sp_runtime::Percent; #[cfg(not(feature = "std"))] @@ -29,6 +29,8 @@ use alloc::{string::String, vec, vec::Vec}; pub mod field; pub use field::*; +use super::Account; + /// A Higher level abstraction of all the constraints. pub trait Constraints { /// Maximum number of fields in a job call. @@ -552,8 +554,8 @@ impl pub struct StagingServicePayment { /// The service request ID. pub request_id: u64, - /// The requester (the payer). - pub owner: AccountId, + /// Where the refund should go. + pub refund_to: Account, /// The Asset used in the payment. pub asset: Asset, /// The amount of the asset that is paid. @@ -562,14 +564,14 @@ pub struct StagingServicePayment { impl Default for StagingServicePayment where - AccountId: Default, + AccountId: ByteArray, AssetId: sp_runtime::traits::Zero, Balance: Default, { fn default() -> Self { Self { request_id: Default::default(), - owner: Default::default(), + refund_to: Account::default(), asset: Asset::default(), amount: Default::default(), } diff --git a/primitives/src/types.rs b/primitives/src/types.rs index caf185b5..fd80dfab 100644 --- a/primitives/src/types.rs +++ b/primitives/src/types.rs @@ -14,6 +14,10 @@ // limitations under the License. // use super::*; +use frame_support::pallet_prelude::*; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_core::{ByteArray, RuntimeDebug}; use sp_runtime::{generic, AccountId32, OpaqueExtrinsic}; /// Block header type as expected by this runtime. @@ -70,3 +74,93 @@ impl From for sp_core::sr25519::Public { sp_core::sr25519::Public::from_raw(x.0) } } + +/// Different Account kinds +#[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + Copy, + Clone, + MaxEncodedLen, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Account { + #[codec(index = 0)] + Id(AccountId), + #[codec(index = 1)] + Address(sp_core::H160), +} + +impl Default for Account +where + AccountId: ByteArray, +{ + fn default() -> Self { + // This should be good enough to make the account for any account id type. + let empty = [0u8; 64]; + let account_id = + AccountId::from_slice(&empty[0..AccountId::LEN]).expect("never fails; qed"); + Account::Id(account_id) + } +} + +impl Account { + /// Create a new account from an AccountId. + pub fn id(account_id: AccountId) -> Self { + Self::Id(account_id) + } + + /// Create a new account from an EVM address. + pub fn address(address: sp_core::H160) -> Self { + Self::Address(address) + } + /// Returns `true` if the account is native (a la [`Id`]). + /// + /// [`Id`]: Account::Id + #[must_use] + #[doc(alias = "is_id", alias = "is_account_id")] + pub fn is_native(&self) -> bool { + matches!(self, Self::Id(..)) + } + + /// Returns `true` if the account is [`Address`]. + /// + /// [`Address`]: Account::Address + #[must_use] + #[doc(alias = "is_evm")] + pub fn is_address(&self) -> bool { + matches!(self, Self::Address(..)) + } + + /// Try to convert into an EVM address. + #[doc(alias = "try_into_evm")] + pub fn try_into_address(self) -> Result { + if let Self::Address(v) = self { + Ok(v) + } else { + Err(self) + } + } + + /// Try to convert into an AccountId. + #[doc(alias = "try_into_native")] + pub fn try_into_account_id(self) -> Result { + if let Self::Id(v) = self { + Ok(v) + } else { + Err(self) + } + } +} + +impl From for Account { + fn from(v: sp_core::H160) -> Self { + Self::Address(v) + } +} From 8fe8983b30de7628baad66aac181012610a27489 Mon Sep 17 00:00:00 2001 From: Shady Khalifa Date: Fri, 13 Dec 2024 17:05:12 +0200 Subject: [PATCH 3/4] fix: add tests --- pallets/services/src/tests.rs | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs index 3db3b056..1855313a 100644 --- a/pallets/services/src/tests.rs +++ b/pallets/services/src/tests.rs @@ -525,7 +525,10 @@ fn request_service_with_payment_asset() { let alice = mock_pub_key(ALICE); let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); let bob = mock_pub_key(BOB); assert_ok!(Services::register( RuntimeOrigin::signed(bob.clone()), @@ -535,6 +538,7 @@ fn request_service_with_payment_asset() { 0, )); + let payment = 5 * 10u128.pow(6); // 5 USDC let charlie = mock_pub_key(CHARLIE); assert_ok!(Services::request( RuntimeOrigin::signed(charlie.clone()), @@ -546,13 +550,13 @@ fn request_service_with_payment_asset() { vec![TNT, USDC, WETH], 100, Asset::Custom(USDC), - 5 * 10u128.pow(6), // 5 USDC + payment, )); assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); // The Pallet account now has 5 USDC - assert_eq!(Assets::balance(USDC, Services::account_id()), 5 * 10u128.pow(6)); + assert_eq!(Assets::balance(USDC, Services::account_id()), payment); // Bob approves the request assert_ok!(Services::approve( @@ -564,6 +568,13 @@ fn request_service_with_payment_asset() { // The request is now fully approved assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + // The Payment should be now transferred to the MBSM. + let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); + let mbsm_account_id = address_to_account_id(mbsm_address); + assert_eq!(Assets::balance(USDC, mbsm_account_id), payment); + // Pallet account should have 0 USDC + assert_eq!(Assets::balance(USDC, Services::account_id()), 0); + // Now the service should be initiated assert!(Instances::::contains_key(0)); }); @@ -577,7 +588,10 @@ fn request_service_with_payment_token() { let alice = mock_pub_key(ALICE); let blueprint = cggmp21_blueprint(); - assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); let bob = mock_pub_key(BOB); assert_ok!(Services::register( RuntimeOrigin::signed(bob.clone()), @@ -587,6 +601,7 @@ fn request_service_with_payment_token() { 0, )); + let payment = 5 * 10u128.pow(6); // 5 USDC let charlie = mock_pub_key(CHARLIE); assert_ok!(Services::request( RuntimeOrigin::signed(address_to_account_id(mock_address(CHARLIE))), @@ -598,7 +613,7 @@ fn request_service_with_payment_token() { vec![TNT, USDC, WETH], 100, Asset::Erc20(USDC_ERC20), - 5 * 10u128.pow(6), // 5 USDC + payment, )); assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); @@ -606,7 +621,7 @@ fn request_service_with_payment_token() { // The Pallet address now has 5 USDC assert_ok!( Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), - U256::from(5 * 10u128.pow(6)) + U256::from(payment) ); // Bob approves the request @@ -619,6 +634,18 @@ fn request_service_with_payment_token() { // The request is now fully approved assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + // The Payment should be now transferred to the MBSM. + let mbsm_address = Pallet::::mbsm_address_of(&blueprint).unwrap(); + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, mbsm_address).map(|(b, _)| b), + U256::from(payment) + ); + // Pallet account should have 0 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), + U256::from(0) + ); + // Now the service should be initiated assert!(Instances::::contains_key(0)); }); From 4ec3603a5a10fd4f1b2f12a155f1cdfc1f4ed847 Mon Sep 17 00:00:00 2001 From: Shady Khalifa Date: Fri, 13 Dec 2024 17:17:12 +0200 Subject: [PATCH 4/4] fix: add tests for refunds --- pallets/services/src/lib.rs | 4 +- pallets/services/src/tests.rs | 125 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index 1435adc1..005a84f1 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -1214,7 +1214,7 @@ pub mod module { &Self::account_id(), &refund_to, payment.amount, - ExistenceRequirement::KeepAlive, + ExistenceRequirement::AllowDeath, )?; }, Asset::Custom(asset_id) => { @@ -1227,7 +1227,7 @@ pub mod module { &Self::account_id(), &refund_to, payment.amount, - Preservation::Preserve, + Preservation::Expendable, )?; }, Asset::Erc20(token) => { diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs index 1855313a..d90ebcc8 100644 --- a/pallets/services/src/tests.rs +++ b/pallets/services/src/tests.rs @@ -651,6 +651,131 @@ fn request_service_with_payment_token() { }); } +#[test] +fn reject_service_with_payment_token() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: zero_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie_address = mock_address(CHARLIE); + let charlie_evm_account_id = address_to_account_id(charlie_address); + let before_balance = Services::query_erc20_balance_of(USDC_ERC20, charlie_address) + .map(|(b, _)| b) + .unwrap_or_default(); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie_evm_account_id), + Some(charlie_address), + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![TNT, USDC, WETH], + 100, + Asset::Erc20(USDC_ERC20), + payment, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet address now has 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), + U256::from(payment) + ); + // Charlie Balance should be decreased by 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), + before_balance - U256::from(payment) + ); + + // Bob rejects the request + assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); + + // The Payment should be now refunded to the requester. + // Pallet account should have 0 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), + U256::from(0) + ); + // Charlie Balance should be back to the original + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, charlie_address).map(|(b, _)| b), + before_balance + ); + }); +} + +#[test] +fn reject_service_with_payment_asset() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint( + RuntimeOrigin::signed(alice.clone()), + blueprint.clone() + )); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: zero_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let payment = 5 * 10u128.pow(6); // 5 USDC + let charlie = mock_pub_key(CHARLIE); + let before_balance = Assets::balance(USDC, charlie.clone()); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + None, + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![TNT, USDC, WETH], + 100, + Asset::Custom(USDC), + payment, + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet account now has 5 USDC + assert_eq!(Assets::balance(USDC, Services::account_id()), payment); + // Charlie Balance should be decreased by 5 USDC + assert_eq!(Assets::balance(USDC, charlie.clone()), before_balance - payment); + + // Bob rejects the request + assert_ok!(Services::reject(RuntimeOrigin::signed(bob.clone()), 0)); + + // The Payment should be now refunded to the requester. + // Pallet account should have 0 USDC + assert_eq!(Assets::balance(USDC, Services::account_id()), 0); + // Charlie Balance should be back to the original + assert_eq!(Assets::balance(USDC, charlie), before_balance); + }); +} + #[test] fn job_calls() { new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| {