Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Market: region unlist #19

Merged
merged 42 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7c7863b
init coretime-market
Szegoo Jan 15, 2024
d39bfb4
Merge remote-tracking branch 'origin' into coretime-market
Szegoo Jan 15, 2024
2cccfad
add storage
Szegoo Jan 15, 2024
e9ec056
cross-contract calling
Szegoo Jan 16, 2024
4c6a7ea
list_region
Szegoo Jan 17, 2024
6bcb9a4
init e2e tests
Szegoo Jan 17, 2024
e1d65e7
add enumeration
Szegoo Jan 17, 2024
4303e85
XcRegions: fix remove
Szegoo Jan 17, 2024
553bfc1
deposit docs
Szegoo Jan 17, 2024
441efa2
current_timeslice
Szegoo Jan 17, 2024
2ca666d
list_region untested
Szegoo Jan 18, 2024
5a81152
purchase_region implemented
Szegoo Jan 20, 2024
a05c3de
fix
Szegoo Jan 20, 2024
82fb8f7
calculate_region_price wip
Szegoo Jan 21, 2024
84fa1d9
fix calculate_region_price
Szegoo Jan 21, 2024
03942ac
current_timeslice | wip
Szegoo Jan 21, 2024
435c6f2
use sp-arithmetic
Szegoo Jan 24, 2024
1a9eb96
fix attempt
Szegoo Jan 24, 2024
950f39e
introduce reference points
Szegoo Jan 26, 2024
934154b
init e2e js tests
Szegoo Jan 26, 2024
de9a339
constructors work
Szegoo Jan 26, 2024
d3a0834
Merge branch 'main' of https://github.com/RegionX-Labs/RegionX into c…
Szegoo Jan 26, 2024
fdfcd0a
Merge branch 'main' into coretime-market
Szegoo Jan 26, 2024
b44ddde
extrinsic calls work
Szegoo Jan 26, 2024
ed1bbcd
listing works
Szegoo Jan 27, 2024
3364256
listing fully tested
Szegoo Jan 27, 2024
f317a30
init purchase tests
Szegoo Jan 27, 2024
d888fc1
use block-number provider
Szegoo Jan 28, 2024
286d1d4
tests passing
Szegoo Jan 28, 2024
8dece31
more tests
Szegoo Jan 28, 2024
578297b
check events
Szegoo Jan 28, 2024
5f2c2a1
timeslice based pricing
Szegoo Feb 1, 2024
fc739c2
e2e tests passing
Szegoo Feb 1, 2024
6d7352e
fixes
Szegoo Feb 3, 2024
a59c672
remove artifacts & types
Szegoo Feb 3, 2024
0dbc312
remove unused
Szegoo Feb 3, 2024
b208cb0
clippy
Szegoo Feb 3, 2024
f09d905
Market: unlist region
Szegoo Feb 4, 2024
613a953
merge
Szegoo Feb 4, 2024
c8b7982
add e2e test
Szegoo Feb 4, 2024
66443e9
config
Szegoo Feb 5, 2024
53b3cc4
fix tests
Szegoo Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 109 additions & 43 deletions contracts/coretime_market/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ mod types;

#[openbrush::contract(env = environment::ExtendedEnvironment)]
pub mod coretime_market {
use crate::types::{Listing, MarketError};
use crate::types::{Config, Listing, MarketError};
use block_number_extension::BlockNumberProviderExtension;
use environment::ExtendedEnvironment;
use ink::{
Expand All @@ -49,7 +49,7 @@ pub mod coretime_market {
};
use openbrush::{contracts::traits::psp34::Id, storage::Mapping, traits::Storage};
use primitives::{
coretime::{RawRegionId, Region, Timeslice, CORE_MASK_BIT_LEN, TIMESLICE_PERIOD},
coretime::{RawRegionId, Region, Timeslice, CORE_MASK_BIT_LEN},
ensure, Version,
};
use sp_arithmetic::{traits::SaturatedConversion, FixedPointNumber, FixedU128};
Expand All @@ -58,20 +58,13 @@ pub mod coretime_market {
#[ink(storage)]
#[derive(Storage)]
pub struct CoretimeMarket {
/// A mapping that holds information about each region listed for sale.
/// A mapping that holds information about each region listed on sale.
pub listings: Mapping<RawRegionId, Listing>,
/// A vector containing all the region ids of regions listed on sale.
///
/// TODO: incentivize the removal of expired regions.
/// A vector containing all the regions listed on sale.
pub listed_regions: Vec<RawRegionId>,
/// The `AccountId` of the xc-regions contract.
///
/// Set on contract initialization. Can't be changed afterwards.
pub xc_regions_contract: AccountId,
/// The deposit required to list a region on sale.
///
/// Set on contract initialization. Can't be changed afterwards.
pub listing_deposit: Balance,
/// The configuration of the market. Set on contract initialization. Can't be changed
/// afterwards.
pub config: Config,
}

#[ink(event)]
Expand All @@ -89,6 +82,15 @@ pub mod coretime_market {
pub(crate) metadata_version: Version,
}

#[ink(event)]
pub struct RegionUnlisted {
/// The identifier of the region that got listed on sale.
#[ink(topic)]
pub(crate) region_id: RawRegionId,
/// The account that removed the region from sale.
pub(crate) caller: AccountId,
}

#[ink(event)]
pub struct RegionPurchased {
/// The identifier of the region that got listed on sale.
Expand All @@ -102,18 +104,21 @@ pub mod coretime_market {

impl CoretimeMarket {
#[ink(constructor)]
pub fn new(xc_regions_contract: AccountId, listing_deposit: Balance) -> Self {
pub fn new(
xc_regions_contract: AccountId,
listing_deposit: Balance,
timeslice_period: BlockNumber,
) -> Self {
Self {
listings: Default::default(),
listed_regions: Default::default(),
xc_regions_contract,
listing_deposit,
config: Config { xc_regions_contract, listing_deposit, timeslice_period },
}
}

#[ink(message)]
pub fn xc_regions_contract(&self) -> AccountId {
self.xc_regions_contract
self.config.xc_regions_contract
}

#[ink(message)]
Expand All @@ -131,8 +136,9 @@ pub mod coretime_market {
pub fn region_price(&self, id: Id) -> Result<Balance, MarketError> {
let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) };

let metadata = RegionMetadataRef::get_metadata(&self.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;
let metadata =
RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;
let listing = self.listings.get(&region_id).ok_or(MarketError::RegionNotListed)?;

self.calculate_region_price(metadata.region, listing)
Expand Down Expand Up @@ -167,22 +173,28 @@ pub mod coretime_market {
let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) };

// Ensure that the region exists and its metadata is set.
let metadata = RegionMetadataRef::get_metadata(&self.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;
let metadata =
RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;

let current_timeslice = self.current_timeslice();

// It doesn't make sense to list a region that expired.
ensure!(metadata.region.end > current_timeslice, MarketError::RegionExpired);

ensure!(
self.env().transferred_value() == self.listing_deposit,
self.env().transferred_value() == self.config.listing_deposit,
MarketError::MissingDeposit
);

// Transfer the region to the market.
PSP34Ref::transfer(&self.xc_regions_contract, market, id.clone(), Default::default())
.map_err(MarketError::XcRegionsPsp34Error)?;
PSP34Ref::transfer(
&self.config.xc_regions_contract,
market,
id.clone(),
Default::default(),
)
.map_err(MarketError::XcRegionsPsp34Error)?;

let sale_recepient = sale_recepient.unwrap_or(caller);

Expand Down Expand Up @@ -213,9 +225,49 @@ pub mod coretime_market {
/// ## Arguments:
/// - `region_id`: The `u128` encoded identifier of the region that the caller intends to
/// unlist from sale.
///
/// In case the region is expired, this is callable by anyone and the caller will receive
/// the listing deposit as a reward.
#[ink(message)]
pub fn unlist_region(&self, _region_id: RawRegionId) -> Result<(), MarketError> {
todo!()
pub fn unlist_region(&mut self, id: Id) -> Result<(), MarketError> {
let caller = self.env().caller();

let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) };

let listing = self.listings.get(&region_id).ok_or(MarketError::RegionNotListed)?;
let metadata =
RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;

let current_timeslice = self.current_timeslice();

// If the region is expired this is callable by anyone, otherwise only the seller can
// unlist the region from the market.
ensure!(
caller == listing.seller || current_timeslice > metadata.region.end,
MarketError::NotAllowed
);

// Transfer the region to the seller.
PSP34Ref::transfer(
&self.config.xc_regions_contract,
listing.seller,
id.clone(),
Default::default(),
)
.map_err(MarketError::XcRegionsPsp34Error)?;

// Remove the region from sale:
self.remove_from_sale(region_id)?;

// Reward the caller with listing deposit.
self.env()
.transfer(caller, self.config.listing_deposit)
.map_err(|_| MarketError::TransferFailed)?;

self.emit_event(RegionUnlisted { region_id, caller });

Ok(())
}

/// A function for updating a listed region's bit price.
Expand Down Expand Up @@ -254,28 +306,26 @@ pub mod coretime_market {
let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) };
let listing = self.listings.get(&region_id).ok_or(MarketError::RegionNotListed)?;

let metadata = RegionMetadataRef::get_metadata(&self.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;
let metadata =
RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, region_id)
.map_err(MarketError::XcRegionsMetadataError)?;

let price = self.calculate_region_price(metadata.region, listing.clone())?;
ensure!(transferred_value >= price, MarketError::InsufficientFunds);

ensure!(listing.metadata_version == metadata_version, MarketError::MetadataNotMatching);

// Transfer the region to the buyer.
PSP34Ref::transfer(&self.xc_regions_contract, caller, id.clone(), Default::default())
.map_err(MarketError::XcRegionsPsp34Error)?;
PSP34Ref::transfer(
&self.config.xc_regions_contract,
caller,
id.clone(),
Default::default(),
)
.map_err(MarketError::XcRegionsPsp34Error)?;

// Remove the region from sale:

let region_index = self
.listed_regions
.iter()
.position(|r| *r == region_id)
.ok_or(MarketError::RegionNotListed)?;

self.listed_regions.remove(region_index);
self.listings.remove(&region_id);
self.remove_from_sale(region_id)?;

// Transfer the tokens to the sale recipient.
self.env()
Expand Down Expand Up @@ -320,17 +370,31 @@ pub mod coretime_market {
Ok(price)
}

// Remove a region from sale
fn remove_from_sale(&mut self, region_id: RawRegionId) -> Result<(), MarketError> {
let region_index = self
.listed_regions
.iter()
.position(|r| *r == region_id)
.ok_or(MarketError::RegionNotListed)?;

self.listed_regions.remove(region_index);
self.listings.remove(&region_id);

Ok(())
}

#[cfg(not(test))]
pub(crate) fn current_timeslice(&self) -> Timeslice {
let latest_rc_block =
self.env().extension().relay_chain_block_number().unwrap_or_default();
(latest_rc_block / TIMESLICE_PERIOD).saturated_into()
(latest_rc_block / self.config.timeslice_period).saturated_into()
}

#[cfg(test)]
pub(crate) fn current_timeslice(&self) -> Timeslice {
let latest_block = self.env().block_number();
(latest_block / TIMESLICE_PERIOD).saturated_into()
(latest_block / self.config.timeslice_period).saturated_into()
}

fn emit_event<Event: Into<<CoretimeMarket as ContractEventBase>::Type>>(&self, e: Event) {
Expand All @@ -346,6 +410,7 @@ pub mod coretime_market {
use super::*;
use environment::ExtendedEnvironment;
use ink_e2e::MessageBuilder;
use primitives::coretime::TIMESLICE_PERIOD;
use xc_regions::xc_regions::XcRegionsRef;

type E2EResult<T> = Result<T, Box<dyn std::error::Error>>;
Expand All @@ -361,7 +426,8 @@ pub mod coretime_market {
.expect("instantiate failed")
.account_id;

let constructor = CoretimeMarketRef::new(xc_regions_acc_id, REQUIRED_DEPOSIT);
let constructor =
CoretimeMarketRef::new(xc_regions_acc_id, REQUIRED_DEPOSIT, TIMESLICE_PERIOD);
let market_acc_id = client
.instantiate("coretime-market", &ink_e2e::alice(), constructor, 0, None)
.await
Expand Down
2 changes: 1 addition & 1 deletion contracts/coretime_market/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use primitives::coretime::{CoreMask, Region, Timeslice, TIMESLICE_PERIOD};
fn calculate_region_price_works() {
let DefaultAccounts::<DefaultEnvironment> { charlie, .. } = get_default_accounts();

let market = CoretimeMarket::new(charlie, 0);
let market = CoretimeMarket::new(charlie, 0, TIMESLICE_PERIOD);
// Works for regions which haven't yet started.

// complete coremask, so 80 active bits.
Expand Down
20 changes: 19 additions & 1 deletion contracts/coretime_market/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,25 @@
// You should have received a copy of the GNU General Public License
// along with RegionX. If not, see <https://www.gnu.org/licenses/>.

use openbrush::{contracts::traits::psp34::PSP34Error, traits::AccountId};
use openbrush::{
contracts::traits::psp34::PSP34Error,
traits::{AccountId, BlockNumber},
};
use primitives::{Balance, Version};
use xc_regions::types::XcRegionsError;

/// The configuration of the coretime market
#[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
pub struct Config {
/// The `AccountId` of the xc-regions contract.
pub xc_regions_contract: AccountId,
/// The deposit required to list a region on sale.
pub listing_deposit: Balance,
/// The duration of a timeslice in block numbers.
pub timeslice_period: BlockNumber,
}

#[derive(scale::Decode, scale::Encode, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum MarketError {
Expand All @@ -36,6 +51,8 @@ pub enum MarketError {
MetadataNotMatching,
/// Failed to transfer the tokens to the seller.
TransferFailed,
/// The caller tried to perform an operation that they have no permission for.
NotAllowed,
/// An error occured when calling the xc-regions contract through the psp34 interface.
XcRegionsPsp34Error(PSP34Error),
/// An error occured when calling the xc-regions contract through the metadata interface.
Expand All @@ -53,6 +70,7 @@ impl core::fmt::Display for MarketError {
MarketError::InsufficientFunds => write!(f, "InsufficientFunds"),
MarketError::MetadataNotMatching => write!(f, "MetadataNotMatching"),
MarketError::TransferFailed => write!(f, "TransferFailed"),
MarketError::NotAllowed => write!(f, "NotAllowed"),
MarketError::XcRegionsPsp34Error(e) => write!(f, "{:?}", e),
MarketError::XcRegionsMetadataError(e) => write!(f, "{}", e),
}
Expand Down
13 changes: 7 additions & 6 deletions tests/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { Region } from 'coretime-utils';
const REGION_COLLECTION_ID = 42;

export async function createRegionCollection(api: ApiPromise, caller: KeyringPair): Promise<void> {
console.log(`Creating the region collection`);

const createCollectionCall = api.tx.uniques.create(REGION_COLLECTION_ID, caller.address);

const callTx = async (resolve: () => void, reject: ({ reason }) => void) => {
Expand Down Expand Up @@ -49,8 +47,6 @@ export async function mintRegion(
caller: KeyringPair,
region: Region,
): Promise<void> {
console.log(`Minting a region`);

const rawRegionId = region.getEncodedRegionId(api);
const mintCall = api.tx.uniques.mint(REGION_COLLECTION_ID, rawRegionId, caller.address);

Expand All @@ -76,8 +72,6 @@ export async function approveTransfer(
region: Region,
delegate: string,
): Promise<void> {
console.log(`Approving region to ${delegate}`);

const rawRegionId = region.getEncodedRegionId(api);
const approveCall = api.tx.uniques.approveTransfer(REGION_COLLECTION_ID, rawRegionId, delegate);

Expand Down Expand Up @@ -129,6 +123,13 @@ export async function balanceOf(api: ApiPromise, acc: string): Promise<number> {
return parseHNString(account.data.free);
}

export async function getBlockNumber(api: ApiPromise): Promise<number> {
const num = (await api.query.system.number()).toHuman();
return parseHNString(num.toString());
}

export function parseHNString(str: string): number {
return parseInt(str.replace(/,/g, ''));
}

export const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
Loading
Loading