├── .prettierignore ├── config.json ├── rust-toolchain.toml ├── .prettierrc ├── tsconfig.json ├── .gitignore ├── Cargo.toml ├── .rustfmt.toml ├── LICENSE ├── primitives ├── src │ ├── lib.rs │ ├── macros.rs │ ├── uniques.rs │ └── coretime.rs └── Cargo.toml ├── extension ├── uniques-extension │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── block-number-extension │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── src │ └── lib.rs └── Cargo.toml ├── environment ├── Cargo.toml └── src │ └── lib.rs ├── contracts ├── xc_regions │ ├── src │ │ ├── traits.rs │ │ ├── types.rs │ │ ├── tests.rs │ │ └── lib.rs │ └── Cargo.toml └── coretime_market │ ├── Cargo.toml │ └── src │ ├── tests.rs │ ├── types.rs │ └── lib.rs ├── package.json ├── .github └── workflows │ └── test.yaml ├── tests ├── common.ts └── market │ ├── updatePrice.test.ts │ ├── list.test.ts │ ├── unlist.test.ts │ └── purchase.test.ts └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | target 4 | types 5 | artifacts 6 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectFiles": ["contracts/**/*"], 3 | "typechainGeneratedPath": "types", 4 | "isWorkspace": true, 5 | "workspacePath": "./" 6 | } 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = [ "rustfmt", "clippy" ] 4 | targets = [ "wasm32-unknown-unknown"] 5 | profile = "minimal" 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "forceConsistentCasingInFileNames": true, 5 | "esModuleInterop": true, 6 | "types": ["mocha", "node"], 7 | "resolveJsonModule": true 8 | }, 9 | "include": ["tests/**/*.ts"], 10 | "exclude": ["node_modules/"] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts from the local tests sub-crate. 2 | /target/ 3 | 4 | # Ignore backup files creates by cargo fmt. 5 | **/*.rs.bk 6 | 7 | # Remove Cargo.lock when creating an executable, leave it for libraries 8 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 9 | Cargo.lock 10 | 11 | node_modules 12 | 13 | types/ 14 | artifacts/ 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | authors = ["RegionX "] 3 | edition = "2021" 4 | repository = "https://github.com/RegionX-Labs/RegionX.git" 5 | license = "GPL-3.0-only" 6 | 7 | [workspace] 8 | members = [ 9 | "contracts/xc_regions", 10 | "contracts/coretime_market", 11 | "environment", 12 | "primitives", 13 | "extension", 14 | "extension/uniques-extension", 15 | ] 16 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Basic 2 | edition = "2021" 3 | hard_tabs = true 4 | max_width = 100 5 | use_small_heuristics = "Max" 6 | # Imports 7 | imports_granularity = "Crate" 8 | reorder_imports = true 9 | # Consistency 10 | newline_style = "Unix" 11 | # Misc 12 | chain_width = 80 13 | spaces_around_ranges = false 14 | binop_separator = "Back" 15 | reorder_impl_items = false 16 | match_arm_leading_pipes = "Preserve" 17 | match_arm_blocks = false 18 | match_block_trailing_comma = true 19 | trailing_comma = "Vertical" 20 | trailing_semicolon = false 21 | use_field_init_shorthand = true 22 | # Format comments 23 | comment_width = 100 24 | wrap_comments = true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | RegionX 2 | Copyright (C) 2024 Master Union 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | #![cfg_attr(not(feature = "std"), no_std)] 17 | 18 | pub mod coretime; 19 | pub mod macros; 20 | pub mod uniques; 21 | 22 | /// Balance of an account. 23 | pub type Balance = u128; 24 | 25 | /// The type used for versioning metadata. 26 | pub type Version = u32; 27 | 28 | #[derive(scale::Encode, scale::Decode)] 29 | pub enum RuntimeCall { 30 | #[codec(index = 37)] 31 | Uniques(uniques::UniquesCall), 32 | } 33 | -------------------------------------------------------------------------------- /extension/uniques-extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uniques-extension" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | ink = { version = "4.2.1", default-features = false, optional = true } 13 | 14 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 15 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 16 | 17 | primitives = { path = "../../primitives", default-features = false } 18 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", branch = "develop", default-features = false } 19 | 20 | [lib] 21 | path = "src/lib.rs" 22 | 23 | [features] 24 | default = ["std"] 25 | ink = [ 26 | "dep:ink", 27 | ] 28 | std = [ 29 | "scale-info/std", 30 | "scale/std", 31 | ] 32 | substrate-std = [ 33 | "std", 34 | ] 35 | ink-std = [ 36 | "std", 37 | "ink", 38 | "ink/std", 39 | ] 40 | -------------------------------------------------------------------------------- /environment/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "environment" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | ink = { version = "4.2.1", default-features = false, optional = true } 13 | 14 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 15 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 16 | 17 | extension = { path = "../extension", default-features = false, features = ["ink"]} 18 | 19 | obce = { git = "https://github.com/727-Ventures/obce", default-features = false } 20 | 21 | [lib] 22 | path = "src/lib.rs" 23 | 24 | [features] 25 | default = ["std"] 26 | ink = [ 27 | "obce/ink", 28 | "dep:ink", 29 | ] 30 | std = [ 31 | "scale-info/std", 32 | "scale/std", 33 | "extension/ink-std", 34 | "obce/std", 35 | ] 36 | ink-std = [ 37 | "std", 38 | "ink", 39 | "ink/std", 40 | "obce/ink-std", 41 | ] 42 | -------------------------------------------------------------------------------- /extension/block-number-extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "block-number-extension" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | ink = { version = "4.2.1", default-features = false, optional = true } 13 | 14 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 15 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 16 | 17 | primitives = { path = "../../primitives", default-features = false } 18 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", branch = "develop", default-features = false } 19 | 20 | [lib] 21 | path = "src/lib.rs" 22 | 23 | [features] 24 | default = ["std"] 25 | ink = [ 26 | "dep:ink", 27 | ] 28 | std = [ 29 | "scale-info/std", 30 | "scale/std", 31 | ] 32 | substrate-std = [ 33 | "std", 34 | ] 35 | ink-std = [ 36 | "std", 37 | "ink", 38 | "ink/std", 39 | ] 40 | -------------------------------------------------------------------------------- /extension/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | #![cfg_attr(not(feature = "std"), no_std)] 17 | 18 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)] 19 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 20 | #[obce::ink_lang::extension] 21 | pub struct Extension; 22 | 23 | impl uniques_extension::UniquesExtension for Extension {} 24 | impl block_number_extension::BlockNumberProviderExtension for Extension {} 25 | -------------------------------------------------------------------------------- /extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extension" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | ink = { version = "4.2.1", default-features = false, optional = true } 13 | 14 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 15 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 16 | 17 | primitives = { path = "../primitives", default-features = false } 18 | uniques-extension = { path = "./uniques-extension", default-features = false, features = ["ink"] } 19 | block-number-extension = { path = "./block-number-extension", default-features = false, features = ["ink"] } 20 | 21 | obce = { git = "https://github.com/727-Ventures/obce", default-features = false } 22 | 23 | [lib] 24 | path = "src/lib.rs" 25 | 26 | [features] 27 | default = ["std"] 28 | ink = [ 29 | "obce/ink", 30 | "dep:ink", 31 | ] 32 | std = [ 33 | "scale-info/std", 34 | "scale/std", 35 | "obce/std" 36 | ] 37 | ink-std = [ 38 | "std", 39 | "ink", 40 | "ink/std", 41 | "obce/ink-std", 42 | ] 43 | -------------------------------------------------------------------------------- /contracts/xc_regions/src/traits.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | use crate::types::{VersionedRegion, XcRegionsError}; 17 | 18 | use openbrush::contracts::traits::psp34::Id; 19 | use primitives::coretime::Region; 20 | 21 | #[openbrush::wrapper] 22 | pub type RegionMetadataRef = dyn RegionMetadata; 23 | 24 | /// This is based on: `` 25 | #[openbrush::trait_definition] 26 | pub trait RegionMetadata { 27 | #[ink(message)] 28 | fn init(&mut self, id: Id, metadata: Region) -> Result<(), XcRegionsError>; 29 | 30 | #[ink(message)] 31 | fn get_metadata(&self, id: Id) -> Result; 32 | 33 | #[ink(message)] 34 | fn remove(&mut self, id: Id) -> Result<(), XcRegionsError>; 35 | } 36 | -------------------------------------------------------------------------------- /primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "primitives" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | ink = { version = "4.2.1", default-features = false } 13 | 14 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 15 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 16 | 17 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", branch = "develop", default-features = false } 18 | 19 | # Substrate 20 | # 21 | # We need to explicitly turn off some of the `sp-io` features, to avoid conflicts 22 | # (especially for global allocator). 23 | # 24 | # See also: https://substrate.stackexchange.com/questions/4733/error-when-compiling-a-contract-using-the-xcm-chain-extension. 25 | sp-io = { version = "23.0.0", default-features = false, features = ["disable_panic_handler", "disable_oom", "disable_allocator"] } 26 | sp-runtime = { version = "24.0.0", default-features = false } 27 | 28 | [lib] 29 | path = "src/lib.rs" 30 | 31 | [features] 32 | default = ["std"] 33 | std = [ 34 | "ink/std", 35 | "scale/std", 36 | "scale-info/std", 37 | "openbrush/std", 38 | "sp-runtime/std", 39 | "sp-io/std", 40 | ] 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regionx", 3 | "version": "1.0.0", 4 | "description": "Coretime marketplace & tooling", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "compile": "typechain-compiler --toolchain nightly", 11 | "prettier": "prettier --write tests", 12 | "test": "mocha --require ts-node/register --recursive ./tests --extension \".test.ts\" --exit --timeout 2000000" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/RegionX-Labs/RegionX.git" 17 | }, 18 | "keywords": [ 19 | "coretime" 20 | ], 21 | "author": "Master Union", 22 | "license": "GPL-3.0", 23 | "bugs": { 24 | "url": "https://github.com/RegionX-Labs/RegionX/issues" 25 | }, 26 | "homepage": "https://github.com/RegionX-Labs/RegionX#readme", 27 | "dependencies": { 28 | "@727-ventures/typechain-compiler": "^1.1.4", 29 | "@727-ventures/typechain-types": "^1.1.2", 30 | "@polkadot/api": "^10.11.2", 31 | "@typescript-eslint/eslint-plugin": "^6.19.1", 32 | "@typescript-eslint/parser": "^6.19.1", 33 | "chai": "^4.4.1", 34 | "chai-as-promised": "^7.1.1", 35 | "coretime-utils": "^0.2.3", 36 | "mocha": "^10.2.0", 37 | "ts-node": "^10.9.2" 38 | }, 39 | "devDependencies": { 40 | "@types/chai": "^4.3.11", 41 | "@types/chai-as-promised": "^7.1.8", 42 | "@types/jest": "^29.5.11", 43 | "@types/mocha": "^10.0.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/xc_regions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xc_regions" 3 | authors = ["RegionX "] 4 | version = "0.1.0" 5 | description = "Cross-chain Regions contracts." 6 | edition = "2021" 7 | 8 | [dependencies] 9 | ink = { version = "4.2.1", default-features = false, features = ["call-runtime"]} 10 | 11 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 12 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 13 | 14 | # OpenBrush dependency 15 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", branch = "develop", default-features = false, features=["psp34"] } 16 | 17 | environment = { path = "../../environment", default-features = false, features = ["ink"] } 18 | uniques-extension = { path = "../../extension/uniques-extension", default-features = false, features = ["ink"]} 19 | primitives = { path = "../../primitives", default-features = false } 20 | 21 | [dev-dependencies] 22 | ink_e2e = "4.2.1" 23 | obce = { git = "https://github.com/727-Ventures/obce", default-features = false, features = ["ink-std"] } 24 | 25 | [lib] 26 | path = "src/lib.rs" 27 | 28 | [features] 29 | default = ["std"] 30 | std = [ 31 | "ink/std", 32 | "scale/std", 33 | "scale-info/std", 34 | "openbrush/std", 35 | "environment/ink-std", 36 | "uniques-extension/ink-std", 37 | "primitives/std", 38 | ] 39 | ink-as-dependency = [] 40 | e2e-tests = [] 41 | -------------------------------------------------------------------------------- /environment/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 16 | 17 | use extension::Extension; 18 | use ink::env::{DefaultEnvironment, Environment}; 19 | 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 22 | pub enum ExtendedEnvironment {} 23 | 24 | impl Environment for ExtendedEnvironment { 25 | const MAX_EVENT_TOPICS: usize = ::MAX_EVENT_TOPICS; 26 | 27 | type AccountId = ::AccountId; 28 | type Balance = ::Balance; 29 | type Hash = ::Hash; 30 | type BlockNumber = ::BlockNumber; 31 | type Timestamp = ::Timestamp; 32 | 33 | type ChainExtension = Extension; 34 | } 35 | -------------------------------------------------------------------------------- /primitives/src/macros.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | #[macro_export] 17 | macro_rules! ensure { 18 | ( $x:expr, $y:expr $(,)? ) => {{ 19 | if !$x { 20 | return Err($y) 21 | } 22 | }}; 23 | } 24 | 25 | /// Panic if an expression doesn't evaluate to `Ok`. 26 | /// 27 | /// Used as `assert_ok!(expression_to_assert, expected_ok_expression)`, 28 | /// or `assert_ok!(expression_to_assert)` which would assert against `Ok(())`. 29 | #[macro_export] 30 | macro_rules! assert_ok { 31 | ( $x:expr $(,)? ) => { 32 | let is = $x; 33 | match is { 34 | Ok(_) => (), 35 | _ => assert!(false, "Expected Ok(_). Got {:#?}", is), 36 | } 37 | }; 38 | ( $x:expr, $y:expr $(,)? ) => { 39 | assert_eq!($x, Ok($y)); 40 | }; 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! address_of { 45 | ($account:ident) => { 46 | ink_e2e::account_id(ink_e2e::AccountKeyring::$account) 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /contracts/coretime_market/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coretime_market" 3 | authors = ["RegionX "] 4 | version = "0.1.0" 5 | description = "Secondary Coretime marketpalce contract." 6 | edition = "2021" 7 | 8 | [dependencies] 9 | ink = { version = "4.2.1", default-features = false } 10 | 11 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 12 | scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } 13 | sp-arithmetic = { version = "23.0.0", default-features = false } 14 | 15 | # OpenBrush dependency 16 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", branch = "develop", default-features = false, features=["psp34"] } 17 | 18 | environment = { path = "../../environment", default-features = false, features = ["ink"] } 19 | primitives = { path = "../../primitives", default-features = false } 20 | block-number-extension = { path = "../../extension/block-number-extension", default-features = false, features = ["ink"]} 21 | xc_regions = { path = "../xc_regions", default-features = false, features = ["ink-as-dependency"] } 22 | 23 | [dev-dependencies] 24 | ink_e2e = "4.2.1" 25 | 26 | [lib] 27 | path = "src/lib.rs" 28 | 29 | [features] 30 | default = ["std"] 31 | std = [ 32 | "ink/std", 33 | "primitives/std", 34 | "scale/std", 35 | "scale-info/std", 36 | "sp-arithmetic/std", 37 | "environment/ink-std", 38 | "openbrush/std", 39 | "xc_regions/std", 40 | ] 41 | ink-as-dependency = [] 42 | e2e-tests = [] 43 | 44 | [profile.release] 45 | overflow-checks = false 46 | -------------------------------------------------------------------------------- /extension/block-number-extension/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | #![cfg_attr(not(feature = "std"), no_std)] 16 | 17 | use openbrush::traits::BlockNumber; 18 | use scale::{Decode, Encode}; 19 | 20 | pub trait BlockNumberProviderExtension { 21 | /// The relay chain block number. Useful for determining the current timeslice. 22 | fn relay_chain_block_number(&self) -> Result { 23 | ::ink::env::chain_extension::ChainExtensionMethod::build(0x50001) 24 | .input::<()>() 25 | .output::, true>() 26 | .handle_error_code::() 27 | .call(&()) 28 | } 29 | } 30 | 31 | #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] 32 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 33 | pub enum BlockNumberProviderError { 34 | /// Origin Caller is not supported 35 | OriginCannotBeCaller = 98, 36 | /// Unknown error 37 | RuntimeError = 99, 38 | /// Unknow status code 39 | UnknownStatusCode, 40 | /// Encountered unexpected invalid SCALE encoding 41 | InvalidScaleEncoding, 42 | } 43 | 44 | impl ink::env::chain_extension::FromStatusCode for BlockNumberProviderError { 45 | fn from_status_code(status_code: u32) -> Result<(), Self> { 46 | match status_code { 47 | 0 => Ok(()), 48 | 98 => Err(Self::OriginCannotBeCaller), 49 | 99 => Err(Self::RuntimeError), 50 | _ => Err(Self::UnknownStatusCode), 51 | } 52 | } 53 | } 54 | 55 | impl From for BlockNumberProviderError { 56 | fn from(_: scale::Error) -> Self { 57 | BlockNumberProviderError::InvalidScaleEncoding 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contracts/xc_regions/src/types.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | use openbrush::contracts::psp34::PSP34Error; 17 | use primitives::{coretime::Region, Version}; 18 | 19 | #[derive(scale::Decode, scale::Encode, Debug, PartialEq, Eq)] 20 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 21 | pub enum XcRegionsError { 22 | 23 | /// The provided identifier is not a valid region id. 24 | InvalidRegionId, 25 | /// The metadata is either already initialized or the caller isn't the region owner. 26 | CannotInitialize, 27 | /// The region metadata cannot be removed as long as the underlying region continues to exist 28 | /// on this chain. 29 | CannotRemove, 30 | /// No metadata was found for the region. 31 | MetadataNotFound, 32 | /// The provided metadata doesn't match with the metadata extracted from the region id. 33 | InvalidMetadata, 34 | /// The associated metadata version was not found. 35 | VersionNotFound, 36 | /// An error occured in the underlying runtime. 37 | RuntimeError, 38 | /// An psp34 error occured. 39 | Psp34(PSP34Error), 40 | } 41 | 42 | impl core::fmt::Display for XcRegionsError { 43 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 44 | match self { 45 | XcRegionsError::InvalidRegionId => write!(f, "InvalidRegionId"), 46 | XcRegionsError::CannotInitialize => write!(f, "CannotInitialize"), 47 | XcRegionsError::CannotRemove => write!(f, "CannotRemove"), 48 | XcRegionsError::MetadataNotFound => write!(f, "MetadataNotFound"), 49 | XcRegionsError::InvalidMetadata => write!(f, "InvalidMetadata"), 50 | XcRegionsError::VersionNotFound => write!(f, "VersionNotFound"), 51 | XcRegionsError::RuntimeError => write!(f, "RuntimeError"), 52 | XcRegionsError::Psp34(err) => write!(f, "{:?}", err), 53 | } 54 | } 55 | } 56 | 57 | #[derive(scale::Decode, scale::Encode, Default, Clone, Debug, PartialEq, Eq)] 58 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 59 | pub struct VersionedRegion { 60 | pub version: Version, 61 | pub region: Region, 62 | } 63 | -------------------------------------------------------------------------------- /contracts/coretime_market/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::{coretime_market::CoretimeMarket, types::Listing}; 2 | use ink::env::{ 3 | test::{default_accounts, DefaultAccounts}, 4 | DefaultEnvironment, 5 | }; 6 | use openbrush::traits::BlockNumber; 7 | use primitives::coretime::{CoreMask, Region, Timeslice, TIMESLICE_PERIOD}; 8 | 9 | #[ink::test] 10 | fn calculate_region_price_works() { 11 | let DefaultAccounts:: { charlie, .. } = get_default_accounts(); 12 | 13 | 14 | let market = CoretimeMarket::new(charlie, 0, TIMESLICE_PERIOD); 15 | 16 | // Works for regions which haven't yet started. 17 | 18 | // complete coremask, so 80 active bits. 19 | assert_eq!( 20 | market.calculate_region_price( 21 | Region { begin: 2, end: 10, core: 0, mask: CoreMask::complete() }, 22 | Listing { 23 | seller: charlie, 24 | timeslice_price: 10, 25 | sale_recepient: charlie, 26 | metadata_version: 0, 27 | } 28 | ), 29 | Ok(80) // 8 * 10 30 | ); 31 | 32 | // 40 active bits, means the region only 'occupies' half of the core. 33 | assert_eq!( 34 | market.calculate_region_price( 35 | Region { begin: 2, end: 10, core: 0, mask: CoreMask::from_chunk(0, 40) }, 36 | Listing { 37 | seller: charlie, 38 | timeslice_price: 10, 39 | sale_recepient: charlie, 40 | metadata_version: 0, 41 | } 42 | ), 43 | Ok(40) // (10 / 2) * 8 44 | ); 45 | 46 | // Works for regions which started. 47 | advance_n_blocks(timeslice_to_block_number(4)); // the current timeslice will be 4. 48 | assert_eq!( 49 | market.calculate_region_price( 50 | Region { begin: 2, end: 10, core: 0, mask: CoreMask::complete() }, 51 | Listing { 52 | seller: charlie, 53 | timeslice_price: 10, 54 | sale_recepient: charlie, 55 | metadata_version: 0, 56 | } 57 | ), 58 | // 1/4th of the region is wasted, so the price is decreased proportionally. 59 | Ok(60) // 10 * 6 60 | ); 61 | 62 | // Expired region has no value: 63 | advance_n_blocks(timeslice_to_block_number(6)); // The current timeslice will be 10. 64 | assert_eq!( 65 | market.calculate_region_price( 66 | Region { begin: 2, end: 10, core: 0, mask: CoreMask::from_chunk(40, 80) }, 67 | Listing { 68 | seller: charlie, 69 | timeslice_price: 10, 70 | sale_recepient: charlie, 71 | metadata_version: 0, 72 | } 73 | ), 74 | Ok(0) 75 | ); 76 | } 77 | 78 | fn advance_n_blocks(n: u32) { 79 | for _ in 0..n { 80 | advance_block(); 81 | } 82 | } 83 | fn advance_block() { 84 | ink::env::test::advance_block::(); 85 | } 86 | 87 | fn timeslice_to_block_number(timeslice: Timeslice) -> BlockNumber { 88 | timeslice * TIMESLICE_PERIOD 89 | } 90 | 91 | fn get_default_accounts() -> DefaultAccounts { 92 | default_accounts::() 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: RegionX tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | install: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout the source code 16 | uses: actions/checkout@v3 17 | 18 | - name: Install & display rust toolchain 19 | run: | 20 | rustup show 21 | rustup toolchain install nightly 22 | rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu 23 | rustup component add clippy --toolchain nightly-x86_64-unknown-linux-gnu 24 | rustup show 25 | 26 | - name: Check targets are installed correctly 27 | run: rustup target list --installed 28 | 29 | - name: Cargo check 30 | run: cargo check 31 | 32 | - name: Check Clippy 33 | run: cargo clippy 34 | 35 | unittest: 36 | needs: install 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout the source code 40 | uses: actions/checkout@v3 41 | 42 | - name: Unit test 43 | run: cargo test 44 | 45 | format: 46 | needs: install 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Use cashed cargo 50 | uses: actions/cache@v3 51 | with: 52 | path: ~/.cargo 53 | key: ${{ runner.os }}-rust-${{ hashFiles('rust-toolchain.toml') }} 54 | 55 | - name: Checkout the source code 56 | uses: actions/checkout@v3 57 | 58 | - name: Ensure the rust code is formatted 59 | run: cargo fmt --all --check 60 | 61 | code_coverage: 62 | needs: install 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Checkout the source code 66 | uses: actions/checkout@v3 67 | 68 | - name: Run cargo-tarpaulin 69 | uses: actions-rs/tarpaulin@v0.1 70 | with: 71 | version: '0.21.0' 72 | 73 | - name: Upload to codecov.io 74 | uses: codecov/codecov-action@v3.1.0 75 | with: 76 | token: ${{secrets.CODECOV_TOKEN}} 77 | 78 | - name: Archive code coverage results 79 | uses: actions/upload-artifact@v3.1.0 80 | with: 81 | name: code-coverage-report 82 | path: cobertura.xml 83 | 84 | clippy: 85 | needs: install 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Use cashed cargo 89 | uses: actions/cache@v3 90 | with: 91 | path: ~/.cargo 92 | key: ${{ runner.os }}-rust-${{ hashFiles('rust-toolchain.toml') }} 93 | 94 | - name: Checkout the source code 95 | uses: actions/checkout@v3 96 | 97 | - name: Ensure clippy is happy 98 | run: cargo clippy -- -D warnings 99 | 100 | build: 101 | needs: install 102 | runs-on: ubuntu-latest 103 | steps: 104 | - name: Use cashed cargo 105 | uses: actions/cache@v3 106 | with: 107 | path: ~/.cargo 108 | key: ${{ runner.os }}-rust-${{ hashFiles('rust-toolchain.toml') }} 109 | 110 | - name: Checkout the source code 111 | uses: actions/checkout@v3 112 | 113 | - name: Ensure the project builds 114 | run: cargo build 115 | -------------------------------------------------------------------------------- /primitives/src/uniques.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | use crate::{coretime::RawRegionId, Balance}; 17 | use openbrush::traits::AccountId; 18 | use sp_runtime::MultiAddress; 19 | 20 | // The type used to identify collections in the underlying uniques pallet. 21 | pub type CollectionId = u32; 22 | 23 | #[derive(scale::Encode, scale::Decode)] 24 | pub enum UniquesCall { 25 | #[codec(index = 5)] 26 | Transfer { collection: CollectionId, item: RawRegionId, dest: MultiAddress }, 27 | #[codec(index = 13)] 28 | ApproveTransfer { 29 | collection: CollectionId, 30 | item: RawRegionId, 31 | delegate: MultiAddress, 32 | }, 33 | #[codec(index = 14)] 34 | CancelApproval { 35 | collection: CollectionId, 36 | item: RawRegionId, 37 | maybe_check_delegate: Option>, 38 | }, 39 | } 40 | 41 | #[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] 42 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 43 | pub struct CollectionDetails { 44 | /// Can change `owner`, `issuer`, `freezer` and `admin` accounts. 45 | pub owner: AccountId, 46 | /// Can mint tokens. 47 | pub issuer: AccountId, 48 | /// Can thaw tokens, force transfers and burn tokens from any account. 49 | pub admin: AccountId, 50 | /// Can freeze tokens. 51 | pub freezer: AccountId, 52 | /// The total balance deposited for the all storage associated with this collection. 53 | /// Used by `destroy`. 54 | pub total_deposit: Balance, 55 | /// If `true`, then no deposit is needed to hold items of this collection. 56 | pub free_holding: bool, 57 | /// The total number of outstanding items of this collection. 58 | pub items: u32, 59 | /// The total number of outstanding item metadata of this collection. 60 | pub item_metadatas: u32, 61 | /// The total number of attributes for this collection. 62 | pub attributes: u32, 63 | /// Whether the collection is frozen for non-admin transfers. 64 | pub is_frozen: bool, 65 | } 66 | 67 | /// Information concerning the ownership of a single unique item. 68 | #[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] 69 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 70 | pub struct ItemDetails { 71 | /// The owner of this item. 72 | pub owner: AccountId, 73 | /// The approved transferrer of this item, if one is set. 74 | pub approved: Option, 75 | /// Whether the item can be transferred or not. 76 | pub is_frozen: bool, 77 | /// The amount held in the pallet's default account for this item. Free-hold items will have 78 | /// this as zero. 79 | pub deposit: Balance, 80 | } 81 | -------------------------------------------------------------------------------- /extension/uniques-extension/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | #![cfg_attr(not(feature = "std"), no_std)] 16 | 17 | use openbrush::traits::AccountId; 18 | use primitives::{ 19 | coretime::RawRegionId, 20 | uniques::{CollectionDetails, CollectionId, ItemDetails}, 21 | }; 22 | use scale::{Decode, Encode}; 23 | 24 | /// These are only the functions that are essential for the xc-regions contract. However, the 25 | /// underlying chain extension is likely to implement many additional ones. 26 | /// 27 | /// We will utilize the chain extension solely for state reads, as extrinsics can be executed 28 | /// through `call_runtime`, which is more future-proof approach. 29 | /// 30 | /// Once WASM view functions are supported, there will no longer be a need for a chain extension. 31 | pub trait UniquesExtension { 32 | /// The owner of the specific item. 33 | fn owner( 34 | &self, 35 | collection_id: CollectionId, 36 | item_id: RawRegionId, 37 | ) -> Result, UniquesError> { 38 | ::ink::env::chain_extension::ChainExtensionMethod::build(0x40001) 39 | .input::<(CollectionId, RawRegionId)>() 40 | .output::, UniquesError>, true>() 41 | .handle_error_code::() 42 | .call(&(collection_id, item_id)) 43 | } 44 | 45 | /// Returns the details of a collection. 46 | fn collection( 47 | &self, 48 | collection_id: CollectionId, 49 | ) -> Result, UniquesError> { 50 | ::ink::env::chain_extension::ChainExtensionMethod::build(0x40006) 51 | .input::() 52 | .output::, UniquesError>, true>() 53 | .handle_error_code::() 54 | .call(&collection_id) 55 | } 56 | 57 | /// Returns the details of an item within a collection. 58 | fn item( 59 | &self, 60 | collection_id: CollectionId, 61 | item_id: RawRegionId, 62 | ) -> Result, UniquesError> { 63 | ::ink::env::chain_extension::ChainExtensionMethod::build(0x40007) 64 | .input::<(CollectionId, RawRegionId)>() 65 | .output::, UniquesError>, true>() 66 | .handle_error_code::() 67 | .call(&(collection_id, item_id)) 68 | } 69 | } 70 | 71 | #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] 72 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 73 | pub enum UniquesError { 74 | /// Origin Caller is not supported 75 | OriginCannotBeCaller = 98, 76 | /// Unknown error 77 | RuntimeError = 99, 78 | /// Unknow status code 79 | UnknownStatusCode, 80 | /// Encountered unexpected invalid SCALE encoding 81 | InvalidScaleEncoding, 82 | } 83 | 84 | impl ink::env::chain_extension::FromStatusCode for UniquesError { 85 | fn from_status_code(status_code: u32) -> Result<(), Self> { 86 | match status_code { 87 | 0 => Ok(()), 88 | 98 => Err(Self::OriginCannotBeCaller), 89 | 99 => Err(Self::RuntimeError), 90 | _ => Err(Self::UnknownStatusCode), 91 | } 92 | } 93 | } 94 | 95 | impl From for UniquesError { 96 | fn from(_: scale::Error) -> Self { 97 | UniquesError::InvalidScaleEncoding 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /primitives/src/coretime.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | use openbrush::traits::BlockNumber; 17 | 18 | /// The type used for identifying regions. 19 | /// 20 | /// This `u128` actually holds parts of the region metadata. 21 | pub type RawRegionId = u128; 22 | 23 | /// Relay chain block number. 24 | pub type Timeslice = u32; 25 | 26 | /// Index of a Polkadot Core. 27 | pub type CoreIndex = u16; 28 | 29 | /// Duration of a timeslice in rc blocks. 30 | pub const TIMESLICE_PERIOD: BlockNumber = 80; 31 | 32 | /// The bit length of a core mask. 33 | pub const CORE_MASK_BIT_LEN: usize = 80; 34 | 35 | /// All Regions are also associated with a Core Mask, an 80-bit bitmap, to denote the regularity at 36 | /// which it may be scheduled on the core. 37 | #[derive(scale::Decode, scale::Encode, Default, Clone, Debug, PartialEq, Eq)] 38 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 39 | pub struct CoreMask([u8; 10]); 40 | 41 | impl CoreMask { 42 | pub fn void() -> Self { 43 | Self([0u8; 10]) 44 | } 45 | pub fn complete() -> Self { 46 | Self([255u8; 10]) 47 | } 48 | pub fn count_zeros(&self) -> u32 { 49 | self.0.iter().map(|i| i.count_zeros()).sum() 50 | } 51 | pub fn count_ones(&self) -> u32 { 52 | self.0.iter().map(|i| i.count_ones()).sum() 53 | } 54 | pub fn from_chunk(from: u32, to: u32) -> Self { 55 | let mut v = [0u8; 10]; 56 | for i in (from.min(80) as usize)..(to.min(80) as usize) { 57 | v[i / 8] |= 128 >> (i % 8); 58 | } 59 | Self(v) 60 | } 61 | } 62 | 63 | impl From for CoreMask { 64 | fn from(x: u128) -> Self { 65 | let mut v = [0u8; 10]; 66 | v.iter_mut().rev().fold(x, |a, i| { 67 | *i = a as u8; 68 | a >> 8 69 | }); 70 | Self(v) 71 | } 72 | } 73 | impl From for u128 { 74 | fn from(x: CoreMask) -> Self { 75 | x.0.into_iter().fold(0u128, |a, i| a << 8 | i as u128) 76 | } 77 | } 78 | 79 | /// Self-describing identity for a Region of Bulk Coretime. 80 | #[derive(scale::Decode, scale::Encode, Default, Clone, Debug, PartialEq, Eq)] 81 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 82 | pub struct RegionId { 83 | /// The timeslice at which the region starts. 84 | pub begin: Timeslice, 85 | /// The index of the relay chain Core on which this Region will be scheduled. 86 | pub core: CoreIndex, 87 | /// The regularity parts in which this Region will be scheduled. 88 | pub mask: CoreMask, 89 | } 90 | 91 | impl From for RegionId { 92 | fn from(x: u128) -> Self { 93 | Self { begin: (x >> 96) as u32, core: (x >> 80) as u16, mask: x.into() } 94 | } 95 | } 96 | 97 | impl From for RawRegionId { 98 | fn from(x: RegionId) -> Self { 99 | (x.begin as u128) << 96 | (x.core as u128) << 80 | u128::from(x.mask) 100 | } 101 | } 102 | 103 | #[derive(scale::Decode, scale::Encode, Default, Clone, Debug, PartialEq, Eq)] 104 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 105 | pub struct Region { 106 | /// The timeslice at which the reigon starts. 107 | pub begin: Timeslice, 108 | /// The timeslice at which the region ends. 109 | pub end: Timeslice, 110 | /// The index of the relay chain Core on which this Region will be scheduled. 111 | pub core: CoreIndex, 112 | /// The regularity parts in which this Region will be scheduled. 113 | pub mask: CoreMask, 114 | } 115 | -------------------------------------------------------------------------------- /contracts/coretime_market/src/types.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | 17 | use openbrush::{ 18 | contracts::traits::psp34::PSP34Error, 19 | traits::{AccountId, BlockNumber}, 20 | }; 21 | use primitives::{Balance, Version}; 22 | use xc_regions::types::XcRegionsError; 23 | 24 | /// The configuration of the coretime market 25 | #[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] 26 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 27 | pub struct Config { 28 | /// The `AccountId` of the xc-regions contract. 29 | pub xc_regions_contract: AccountId, 30 | /// The deposit required to list a region on sale. 31 | pub listing_deposit: Balance, 32 | /// The duration of a timeslice in block numbers. 33 | pub timeslice_period: BlockNumber, 34 | } 35 | 36 | #[derive(scale::Decode, scale::Encode, Debug, PartialEq, Eq)] 37 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 38 | pub enum MarketError { 39 | /// An arithmetic error occured. 40 | ArithmeticError, 41 | /// The provided identifier is not a valid region id. 42 | InvalidRegionId, 43 | /// The specified region is expired. 44 | RegionExpired, 45 | /// The caller made the call without sending the required deposit amount. 46 | MissingDeposit, 47 | /// Caller tried to perform an action on a region that is not listed. 48 | RegionNotListed, 49 | /// The caller tried to purchase a region without sending enough tokens. 50 | InsufficientFunds, 51 | /// The metadata of the region doesn't match with what the caller expected. 52 | MetadataNotMatching, 53 | /// Failed to transfer the tokens to the seller. 54 | TransferFailed, 55 | 56 | /// The caller tried to perform an operation that they have no permission for. 57 | NotAllowed, 58 | 59 | /// An error occured when calling the xc-regions contract through the psp34 interface. 60 | XcRegionsPsp34Error(PSP34Error), 61 | /// An error occured when calling the xc-regions contract through the metadata interface. 62 | XcRegionsMetadataError(XcRegionsError), 63 | } 64 | 65 | impl core::fmt::Display for MarketError { 66 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 67 | match self { 68 | MarketError::ArithmeticError => write!(f, "ArithmeticError"), 69 | MarketError::InvalidRegionId => write!(f, "InvalidRegionId"), 70 | MarketError::RegionExpired => write!(f, "RegionExpired"), 71 | MarketError::MissingDeposit => write!(f, "MissingDeposit"), 72 | MarketError::RegionNotListed => write!(f, "RegionNotListed"), 73 | MarketError::InsufficientFunds => write!(f, "InsufficientFunds"), 74 | MarketError::MetadataNotMatching => write!(f, "MetadataNotMatching"), 75 | MarketError::TransferFailed => write!(f, "TransferFailed"), 76 | 77 | MarketError::NotAllowed => write!(f, "NotAllowed"), 78 | MarketError::XcRegionsPsp34Error(e) => write!(f, "{:?}", e), 79 | MarketError::XcRegionsMetadataError(e) => write!(f, "{}", e), 80 | } 81 | } 82 | } 83 | 84 | #[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] 85 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] 86 | pub struct Listing { 87 | /// The `AccountId` selling the specific region. 88 | pub seller: AccountId, 89 | /// The price per a single timeslice. 90 | pub timeslice_price: Balance, 91 | /// The `AccountId` receiving the payment from the sale. 92 | /// 93 | /// If not set specified otherwise this should be the `seller` account. 94 | pub sale_recepient: AccountId, 95 | /// The metadata version of the region listed on sale. Used to prevent front running attacks. 96 | pub metadata_version: Version, 97 | } 98 | -------------------------------------------------------------------------------- /tests/common.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise } from '@polkadot/api'; 2 | import { expect } from 'chai'; 3 | import { ReturnNumber } from '@727-ventures/typechain-types'; 4 | import { KeyringPair } from '@polkadot/keyring/types'; 5 | import XcRegions from '../types/contracts/xc_regions'; 6 | import Market from '../types/contracts/coretime_market'; 7 | import { Region } from 'coretime-utils'; 8 | 9 | const REGION_COLLECTION_ID = 42; 10 | 11 | export async function createRegionCollection(api: ApiPromise, caller: KeyringPair): Promise { 12 | 13 | const createCollectionCall = api.tx.uniques.create(REGION_COLLECTION_ID, caller.address); 14 | 15 | const callTx = async (resolve: () => void, reject: ({ reason }) => void) => { 16 | const unsub = await createCollectionCall.signAndSend(caller, ({ status, events }) => { 17 | if (status.isInBlock) { 18 | unsub(); 19 | events.forEach(({ event: { method, section } }) => { 20 | if (section == 'system' && method == 'ExtrinsicFailed') 21 | reject({ reason: 'Creating collection failed' }); 22 | }); 23 | resolve(); 24 | } 25 | }); 26 | }; 27 | 28 | return new Promise(callTx); 29 | } 30 | 31 | export async function initRegion( 32 | api: ApiPromise, 33 | xcRegions: XcRegions, 34 | caller: KeyringPair, 35 | region: Region, 36 | ) { 37 | await xcRegions.withSigner(caller).tx.init(region.getEncodedRegionId(api), { 38 | begin: region.getBegin(), 39 | core: region.getCore(), 40 | end: region.getEnd(), 41 | // @ts-ignore 42 | mask: region.getMask().getMask(), 43 | }); 44 | } 45 | 46 | export async function mintRegion( 47 | api: ApiPromise, 48 | caller: KeyringPair, 49 | region: Region, 50 | ): Promise { 51 | const rawRegionId = region.getEncodedRegionId(api); 52 | const mintCall = api.tx.uniques.mint(REGION_COLLECTION_ID, rawRegionId, caller.address); 53 | 54 | const callTx = async (resolve: () => void, reject: ({ reason }) => void) => { 55 | const unsub = await mintCall.signAndSend(caller, ({ status, events }) => { 56 | if (status.isInBlock) { 57 | unsub(); 58 | events.forEach(({ event: { method, section } }) => { 59 | if (section == 'system' && method == 'ExtrinsicFailed') 60 | reject({ reason: 'Minting failed' }); 61 | }); 62 | resolve(); 63 | } 64 | }); 65 | }; 66 | 67 | return new Promise(callTx); 68 | } 69 | 70 | export async function approveTransfer( 71 | api: ApiPromise, 72 | caller: KeyringPair, 73 | region: Region, 74 | delegate: string, 75 | ): Promise { 76 | const rawRegionId = region.getEncodedRegionId(api); 77 | const approveCall = api.tx.uniques.approveTransfer(REGION_COLLECTION_ID, rawRegionId, delegate); 78 | 79 | const callTx = async (resolve: () => void, reject: ({ reason }) => void) => { 80 | const unsub = await approveCall.signAndSend(caller, ({ status, events }) => { 81 | if (status.isInBlock) { 82 | unsub(); 83 | events.forEach(({ event: { method, section } }) => { 84 | if (section == 'system' && method == 'ExtrinsicFailed') 85 | reject({ reason: 'Approving region failed' }); 86 | }); 87 | resolve(); 88 | } 89 | }); 90 | }; 91 | 92 | return new Promise(callTx); 93 | } 94 | 95 | export async function expectOnSale(market: Market, id: any, seller: KeyringPair, bitPrice: number) { 96 | expect(market.query.listedRegions()).to.eventually.be.equal([id]); 97 | expect( 98 | BigInt((await market.query.listedRegion(id)).value.unwrap().ok.timeslicePrice.toString()), 99 | ).to.be.equal(BigInt(bitPrice)); 100 | expect((await market.query.listedRegion(id)).value.unwrap().ok.metadataVersion).to.be.equal(0); 101 | expect((await market.query.listedRegion(id)).value.unwrap().ok.seller).to.be.equal( 102 | seller.address, 103 | ); 104 | expect((await market.query.listedRegion(id)).value.unwrap().ok.saleRecepient).to.be.equal( 105 | seller.address, 106 | ); 107 | } 108 | 109 | // Helper function to parse Events 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 111 | export const expectEvent = (result: { events?: any }, name: string, args: any): void => { 112 | const event = result.events.find((event: { name: string }) => event.name === name); 113 | for (const key of Object.keys(event.args)) { 114 | if (event.args[key] instanceof ReturnNumber) { 115 | event.args[key] = BigInt(event.args[key]).toString(); 116 | } 117 | } 118 | expect(event.name).deep.eq(name); 119 | expect(JSON.stringify(event.args)).deep.eq(JSON.stringify(args)); 120 | }; 121 | 122 | export async function balanceOf(api: ApiPromise, acc: string): Promise { 123 | const account: any = (await api.query.system.account(acc)).toHuman(); 124 | return parseHNString(account.data.free); 125 | } 126 | 127 | 128 | export async function getBlockNumber(api: ApiPromise): Promise { 129 | const num = (await api.query.system.number()).toHuman(); 130 | return parseHNString(num.toString()); 131 | } 132 | 133 | export function parseHNString(str: string): number { 134 | return parseInt(str.replace(/,/g, '')); 135 | } 136 | 137 | export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RegionX 2 | 3 | ## 1. Introduction 4 | 5 | [RegionX](https://regionx.tech/) is a project dedicated to developing components for the new [Agile Coretime](https://github.com/polkadot-fellows/RFCs/blob/main/text/0001-agile-coretime.md) model. The goal of the project is to enable developer teams, researchers, and speculators to start trading, tracking, and analyzing the product Polkadot offers - Coretime. 6 | 7 | This repository is establishing the smart contract components for creating a secondary Coretime market. This infrastructure is meant to be leveraged by any end-user product built around Coretime. 8 | 9 | The repository currently contains two crucial components in the form of ink! contracts that are necessary for the creation of a flexible and decentralized Coretime market. 10 | 11 | ## 2. Core components 12 | 13 | ### 2.1 Cross-Chain Regions 14 | 15 | From a procurement perspective, regions can be seen as NFT tokens representing ownership of Coretime. Each region is characterized by a defined set of attributes that encompass all its properties. The following is the list of these attributes: 16 | 17 | - `begin`: Specifies the starting point of time from which a task assigned to the region can be scheduled on a core. 18 | - `end`: Specifies the deadline until a task assigned to the region can be scheduled on a core. 19 | - `length`: The duration of a region. Always equals to `end - begin`. 20 | - `core`: The core index to which the region belongs. 21 | - `mask`: The regularity parts in which this Region will be scheduled. 22 | - `owner`: The owner of the region. 23 | - `paid`: The payment for the region on the bulk market. Defined only for renewable regions. This is used to calculate the renewal price in the next bulk sale. 24 | 25 | The module containing all region-related logic is the [pallet-broker](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/broker). Deployed as part of the Coretime chain runtime, this pallet serves to handle all the Bulk Coretime procurement logic in Polkadot. 26 | 27 | Given that the Coretime chain does not have any kind of smart-contract support it is not possible to create a secondary market on the Coretime chain itself. 28 | For this reason we have to store all the secondary market logic on a separate parachain which supports contract deployment. 29 | Given that cross-chain NFT metadata transfer is still something that hasn't been resolved in the ecosystem we are implementing additional custom logic that will enable the transfer of regions acrross chains. 30 | 31 | An explanation of our solution for transferring the metadata of regions acrross chains can be found on the RegionX wiki: [Cross-Chain Regions](https://regionx.gitbook.io/wiki/advanced/cross-chain-regions). 32 | 33 | ### 2.2 Coretime Marketplace 34 | 35 | The RegionX Coretime market utilizes an order-book model and integrates directly with the XcRegions contract. For a region to be listed on the market, it must be represented within the XcRegion contract. 36 | 37 | The regions sold in the market are classified into two categories: 38 | - Active regions. The tasks that are assigned to active regions can currently be performed on a Polkadot core. 39 | - Inactive regions. These are the regions that will become active in the upcoming Bulk period. The regions purchased from the Coretime chain fall into this category until the start of the next Bulk period. 40 | 41 | #### Region pricing 42 | 43 | The formula used to calculate the price of a region listed for sale is as follows: 44 | 45 | $$ 46 | r_{price}=(r_{end}- t)*(tp * c_{occupancy}) 47 | $$ 48 | 49 | Where: 50 | - $r_{end}$: is the timeslice at which the region concludes 51 | - $t$: represents the current timeslice 52 | - $tp$: is the cost per timeslice defined by the seller upon listing the region on the market. 53 | - $c_{occupancy}$: represents the proportion of the Core that is occupied by the region. 54 | 55 | 56 | > The contract doesn't store the entire region's price; instead, it records the price of its timeslice, which is determined at the time of listing the region. 57 | 58 | ## 3. Develop 59 | 60 | 1. Make sure to have the latest [cargo contract](https://crates.io/crates/cargo-contract). 61 | 2. Clone the GitHub repository: 62 | 63 | ```sh 64 | git clone https://github.com/RegionX-Labs/RegionX.git 65 | ``` 66 | 67 | 3. Compile and run unit tests 68 | 69 | ```sh 70 | cd RegionX/ 71 | cargo build 72 | cargo test 73 | ``` 74 | 75 | 3. Build the contracts: 76 | 77 | ```sh 78 | # To build the xc-regions contract: 79 | cd contracts/xc-regions/ 80 | cargo contract build --release 81 | 82 | # To build the xc-regions contract: 83 | cd contracts/coretime-market/ 84 | cargo contract build --release 85 | ``` 86 | 87 | 4. Running e2e-tests 88 | 89 | 90 | Given that the xc-regions contract requires the underlying chain to implement the uniques pallet, specifying a custom contracts node is necessary when running e2e tests. For this purpose, we use the Astar local node from [Coretime-Mock](https://github.com/RegionX-Labs/Coretime-Mock) directory: 91 | 92 | 93 | ```sh 94 | export CONTRACTS_NODE="~/Coretime-Mock/bin/astar-collator" 95 | ``` 96 | 97 | 98 | Once that is configured, we can proceed to run the e2e tests: 99 | 100 | 101 | ```sh 102 | cargo test --features e2e-tests 103 | ``` 104 | 105 | 106 | Additionally, this repository contains e2e typescript tests that can be executed using the steps below: 107 | 108 | 109 | ```sh 110 | # in a separate terminal run a the astar-collator node from Coretime-Mock 111 | cd Coretime-Mock/ 112 | ./bin/astar-collator --dev 113 | ``` 114 | 115 | After the node is running in a separate terminal: 116 | ```sh 117 | # generate the artifacts 118 | npm run compile 119 | 120 | npm run test 121 | ``` 122 | 123 | ## 4. Deploy 124 | 125 | For the xc-regions contract to function correctly, the chain on which it is deployed must implement the uniques pallet. Given that the pallet index of the uniques pallet can vary across different chains, it's crucial to correctly configure this index before building and deploying the contract. To achieve this, the following steps should be taken: 126 | 127 | 1. Determine the index of the uniques pallet 128 | 2. Go to the `primitives/lib.rs` file: 129 | 3. Configure the index correctly: 130 | ```rust 131 | #[derive(scale::Encode, scale::Decode)] 132 | pub enum RuntimeCall { 133 | 134 | #[codec(index = )] 135 | Uniques(uniques::UniquesCall), 136 | } 137 | ``` 138 | 139 | Once this is correctly configured, the contract can then be deployed. 140 | -------------------------------------------------------------------------------- /tests/market/updatePrice.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; 2 | import { expect, use } from 'chai'; 3 | import { KeyringPair } from '@polkadot/keyring/types'; 4 | import XcRegions_Factory from '../../types/constructors/xc_regions'; 5 | import Market_Factory from '../../types/constructors/coretime_market'; 6 | import XcRegions from '../../types/contracts/xc_regions'; 7 | import Market from '../../types/contracts/coretime_market'; 8 | import chaiAsPromised from 'chai-as-promised'; 9 | import { CoreMask, Id, Region, RegionId, RegionRecord } from 'coretime-utils'; 10 | import { 11 | approveTransfer, 12 | balanceOf, 13 | createRegionCollection, 14 | expectEvent, 15 | expectOnSale, 16 | initRegion, 17 | mintRegion, 18 | } from '../common'; 19 | import { MarketErrorBuilder } from '../../types/types-returns/coretime_market'; 20 | 21 | use(chaiAsPromised); 22 | 23 | const REGION_COLLECTION_ID = 42; 24 | const LISTING_DEPOIST = 0; 25 | // In reality this is 80, however we use 8 for testing. 26 | const TIMESLICE_PERIOD = 8; 27 | 28 | const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 29 | // Create a keyring instance 30 | const keyring = new Keyring({ type: 'sr25519', ss58Format: 5 }); 31 | 32 | describe('Coretime market purchases', () => { 33 | let api: ApiPromise; 34 | let alice: KeyringPair; 35 | let bob: KeyringPair; 36 | let charlie: KeyringPair; 37 | 38 | let xcRegions: XcRegions; 39 | let market: Market; 40 | 41 | beforeEach(async function (): Promise { 42 | api = await ApiPromise.create({ provider: wsProvider, noInitWarn: true, types: { Id } }); 43 | 44 | alice = keyring.addFromUri('//Alice'); 45 | bob = keyring.addFromUri('//Bob'); 46 | charlie = keyring.addFromUri('//Charlie'); 47 | 48 | const xcRegionsFactory = new XcRegions_Factory(api, alice); 49 | xcRegions = new XcRegions((await xcRegionsFactory.new()).address, alice, api); 50 | 51 | const marketFactory = new Market_Factory(api, alice); 52 | market = new Market( 53 | (await marketFactory.new(xcRegions.address, LISTING_DEPOIST, TIMESLICE_PERIOD)).address, 54 | alice, 55 | api, 56 | ); 57 | 58 | if (!(await api.query.uniques.class(REGION_COLLECTION_ID)).toHuman()) { 59 | await createRegionCollection(api, alice); 60 | } 61 | }); 62 | 63 | it('Updating price works', async () => { 64 | const regionId: RegionId = { 65 | begin: 30, 66 | core: 40, 67 | mask: CoreMask.completeMask(), 68 | }; 69 | const regionRecord: RegionRecord = { 70 | end: 60, 71 | owner: alice.address, 72 | paid: null, 73 | }; 74 | const region = new Region(regionId, regionRecord); 75 | 76 | await mintRegion(api, alice, region); 77 | await approveTransfer(api, alice, region, xcRegions.address); 78 | 79 | await initRegion(api, xcRegions, alice, region); 80 | 81 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 82 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 83 | 84 | const timeslicePrice = 5 * Math.pow(10, 12); 85 | await market 86 | .withSigner(alice) 87 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 88 | 89 | await expectOnSale(market, id, alice, timeslicePrice); 90 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 91 | timeslicePrice * (region.getEnd() - region.getBegin()), 92 | ); 93 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 94 | 95 | const newTimeslicePrice = 6 * Math.pow(10, 12); 96 | 97 | const result = await market.withSigner(alice).tx.updateRegionPrice(id, newTimeslicePrice); 98 | expectEvent(result, 'RegionPriceUpdated', { 99 | regionId: id.toPrimitive().u128, 100 | newTimeslicePrice: newTimeslicePrice.toString(), 101 | }); 102 | await expectOnSale(market, id, alice, newTimeslicePrice); 103 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 104 | newTimeslicePrice * (region.getEnd() - region.getBegin()), 105 | ); 106 | }); 107 | 108 | it('Cannot update price for unlisted region', async () => { 109 | const regionId: RegionId = { 110 | begin: 30, 111 | core: 41, 112 | mask: CoreMask.completeMask(), 113 | }; 114 | const regionRecord: RegionRecord = { 115 | end: 60, 116 | owner: alice.address, 117 | paid: null, 118 | }; 119 | const region = new Region(regionId, regionRecord); 120 | 121 | await mintRegion(api, alice, region); 122 | await approveTransfer(api, alice, region, xcRegions.address); 123 | 124 | await initRegion(api, xcRegions, alice, region); 125 | 126 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 127 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 128 | 129 | const newTimeslicePrice = 6 * Math.pow(10, 12); 130 | 131 | const result = await market.withSigner(alice).query.updateRegionPrice(id, newTimeslicePrice); 132 | 133 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.RegionNotListed()); 134 | }); 135 | 136 | it('Only owner can update the price', async () => { 137 | const regionId: RegionId = { 138 | begin: 30, 139 | core: 42, 140 | mask: CoreMask.completeMask(), 141 | }; 142 | const regionRecord: RegionRecord = { 143 | end: 60, 144 | owner: alice.address, 145 | paid: null, 146 | }; 147 | const region = new Region(regionId, regionRecord); 148 | 149 | await mintRegion(api, alice, region); 150 | await approveTransfer(api, alice, region, xcRegions.address); 151 | 152 | await initRegion(api, xcRegions, alice, region); 153 | 154 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 155 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 156 | 157 | const timeslicePrice = 7 * Math.pow(10, 12); 158 | await market 159 | .withSigner(alice) 160 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 161 | 162 | await expectOnSale(market, id, alice, timeslicePrice); 163 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 164 | timeslicePrice * (region.getEnd() - region.getBegin()), 165 | ); 166 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 167 | 168 | const newTimeslicePrice = 6 * Math.pow(10, 12); 169 | 170 | const result = await market.withSigner(bob).query.updateRegionPrice(id, newTimeslicePrice); 171 | 172 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.NotAllowed()); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /tests/market/list.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; 2 | import { expect, use } from 'chai'; 3 | import { KeyringPair } from '@polkadot/keyring/types'; 4 | import XcRegions_Factory from '../../types/constructors/xc_regions'; 5 | import Market_Factory from '../../types/constructors/coretime_market'; 6 | import XcRegions from '../../types/contracts/xc_regions'; 7 | import Market from '../../types/contracts/coretime_market'; 8 | import chaiAsPromised from 'chai-as-promised'; 9 | import { CoreMask, Id, Region, RegionId, RegionRecord } from 'coretime-utils'; 10 | import { MarketErrorBuilder, PSP34ErrorBuilder } from '../../types/types-returns/coretime_market'; 11 | import { 12 | approveTransfer, 13 | 14 | balanceOf, 15 | createRegionCollection, 16 | expectEvent, 17 | expectOnSale, 18 | initRegion, 19 | mintRegion, 20 | wait, 21 | } from '../common'; 22 | 23 | use(chaiAsPromised); 24 | 25 | const REGION_COLLECTION_ID = 42; 26 | const LISTING_DEPOIST = 100; 27 | // In reality this is 80, however we use 8 for testing. 28 | const TIMESLICE_PERIOD = 8; 29 | 30 | const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 31 | // Create a keyring instance 32 | const keyring = new Keyring({ type: 'sr25519', ss58Format: 5 }); 33 | 34 | describe('Coretime market listing', () => { 35 | let api: ApiPromise; 36 | let alice: KeyringPair; 37 | 38 | let xcRegions: XcRegions; 39 | let market: Market; 40 | 41 | beforeEach(async function (): Promise { 42 | api = await ApiPromise.create({ provider: wsProvider, noInitWarn: true, types: { Id } }); 43 | 44 | alice = keyring.addFromUri('//Alice'); 45 | 46 | const xcRegionsFactory = new XcRegions_Factory(api, alice); 47 | xcRegions = new XcRegions((await xcRegionsFactory.new()).address, alice, api); 48 | 49 | const marketFactory = new Market_Factory(api, alice); 50 | market = new Market( 51 | (await marketFactory.new(xcRegions.address, LISTING_DEPOIST, TIMESLICE_PERIOD)).address, 52 | alice, 53 | api, 54 | ); 55 | 56 | if (!(await api.query.uniques.class(REGION_COLLECTION_ID)).toHuman()) { 57 | await createRegionCollection(api, alice); 58 | } 59 | }); 60 | 61 | it('Listing works', async () => { 62 | const regionId: RegionId = { 63 | begin: 30, 64 | core: 0, 65 | mask: CoreMask.completeMask(), 66 | }; 67 | const regionRecord: RegionRecord = { 68 | end: 60, 69 | owner: alice.address, 70 | paid: null, 71 | }; 72 | const region = new Region(regionId, regionRecord); 73 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 74 | 75 | await mintRegion(api, alice, region); 76 | await approveTransfer(api, alice, region, xcRegions.address); 77 | 78 | await initRegion(api, xcRegions, alice, region); 79 | 80 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 81 | 82 | const aliceBalance = await balanceOf(api, alice.address); 83 | 84 | const timeslicePrice = 50; 85 | const result = await market 86 | .withSigner(alice) 87 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 88 | expectEvent(result, 'RegionListed', { 89 | regionId: id.toPrimitive().u128, 90 | timeslicePrice: timeslicePrice.toString(), 91 | seller: alice.address, 92 | saleRecepient: alice.address.toString(), 93 | metadataVersion: 0, 94 | }); 95 | 96 | await expectOnSale(market, id, alice, timeslicePrice); 97 | 98 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 99 | timeslicePrice * (region.getEnd() - region.getBegin()), 100 | ); 101 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 102 | expect(await balanceOf(api, alice.address)).to.be.lessThan(aliceBalance - LISTING_DEPOIST); 103 | }); 104 | 105 | it('Listing requires listing deposit', async () => { 106 | const regionId: RegionId = { 107 | begin: 30, 108 | core: 1, 109 | mask: CoreMask.completeMask(), 110 | }; 111 | const regionRecord: RegionRecord = { 112 | end: 60, 113 | owner: alice.address, 114 | paid: null, 115 | }; 116 | const region = new Region(regionId, regionRecord); 117 | 118 | await mintRegion(api, alice, region); 119 | await approveTransfer(api, alice, region, xcRegions.address); 120 | 121 | await initRegion(api, xcRegions, alice, region); 122 | 123 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 124 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 125 | 126 | const timeslicePrice = 50; 127 | 128 | const result = await market 129 | .withSigner(alice) 130 | .query.listRegion(id, timeslicePrice, alice.address); 131 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.MissingDeposit()); 132 | 133 | }); 134 | 135 | it('Listing requires region to be approved to the market', async () => { 136 | const regionId: RegionId = { 137 | begin: 30, 138 | core: 2, 139 | mask: CoreMask.completeMask(), 140 | }; 141 | const regionRecord: RegionRecord = { 142 | end: 60, 143 | owner: alice.address, 144 | paid: null, 145 | }; 146 | const region = new Region(regionId, regionRecord); 147 | 148 | await mintRegion(api, alice, region); 149 | await approveTransfer(api, alice, region, xcRegions.address); 150 | 151 | await initRegion(api, xcRegions, alice, region); 152 | 153 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 154 | 155 | const timeslicePrice = 50; 156 | const result = await market 157 | .withSigner(alice) 158 | .query.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 159 | expect(result.value.unwrap().err).to.deep.equal( 160 | MarketErrorBuilder.XcRegionsPsp34Error(PSP34ErrorBuilder.NotApproved()), 161 | ); 162 | }); 163 | 164 | it('Listing expired region fails', async () => { 165 | const regionId: RegionId = { 166 | begin: 0, 167 | core: 3, 168 | mask: CoreMask.completeMask(), 169 | }; 170 | const regionRecord: RegionRecord = { 171 | end: 1, 172 | owner: alice.address, 173 | paid: null, 174 | }; 175 | const region = new Region(regionId, regionRecord); 176 | 177 | await mintRegion(api, alice, region); 178 | await approveTransfer(api, alice, region, xcRegions.address); 179 | 180 | await initRegion(api, xcRegions, alice, region); 181 | 182 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 183 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 184 | 185 | 186 | // Wait for the region to expire. 187 | await wait(2000 * TIMESLICE_PERIOD); 188 | 189 | const timeslicePrice = 50; 190 | const result = await market 191 | .withSigner(alice) 192 | .query.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 193 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.RegionExpired()); 194 | 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /tests/market/unlist.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; 2 | import { expect, use } from 'chai'; 3 | import { KeyringPair } from '@polkadot/keyring/types'; 4 | import XcRegions_Factory from '../../types/constructors/xc_regions'; 5 | import Market_Factory from '../../types/constructors/coretime_market'; 6 | import XcRegions from '../../types/contracts/xc_regions'; 7 | import Market from '../../types/contracts/coretime_market'; 8 | import chaiAsPromised from 'chai-as-promised'; 9 | import { CoreMask, Id, Region, RegionId, RegionRecord } from 'coretime-utils'; 10 | import { 11 | approveTransfer, 12 | balanceOf, 13 | createRegionCollection, 14 | expectEvent, 15 | expectOnSale, 16 | getBlockNumber, 17 | initRegion, 18 | mintRegion, 19 | wait, 20 | } from '../common'; 21 | import { MarketErrorBuilder } from '../../types/types-returns/coretime_market'; 22 | 23 | use(chaiAsPromised); 24 | 25 | const REGION_COLLECTION_ID = 42; 26 | const LISTING_DEPOIST = 5 * Math.pow(10, 15); 27 | // In reality this is 80, however we use 8 for testing. 28 | const TIMESLICE_PERIOD = 8; 29 | 30 | const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 31 | // Create a keyring instance 32 | const keyring = new Keyring({ type: 'sr25519', ss58Format: 5 }); 33 | 34 | describe('Coretime market unlisting', () => { 35 | let api: ApiPromise; 36 | let alice: KeyringPair; 37 | let bob: KeyringPair; 38 | 39 | let xcRegions: XcRegions; 40 | let market: Market; 41 | 42 | beforeEach(async function (): Promise { 43 | api = await ApiPromise.create({ provider: wsProvider, noInitWarn: true, types: { Id } }); 44 | 45 | alice = keyring.addFromUri('//Alice'); 46 | bob = keyring.addFromUri('//Bob'); 47 | 48 | const xcRegionsFactory = new XcRegions_Factory(api, alice); 49 | xcRegions = new XcRegions((await xcRegionsFactory.new()).address, alice, api); 50 | 51 | const marketFactory = new Market_Factory(api, alice); 52 | market = new Market( 53 | (await marketFactory.new(xcRegions.address, LISTING_DEPOIST, TIMESLICE_PERIOD)).address, 54 | alice, 55 | api, 56 | ); 57 | 58 | if (!(await api.query.uniques.class(REGION_COLLECTION_ID)).toHuman()) { 59 | await createRegionCollection(api, alice); 60 | } 61 | }); 62 | 63 | it('Unlisting works', async () => { 64 | const regionId: RegionId = { 65 | begin: 30, 66 | core: 20, 67 | mask: CoreMask.completeMask(), 68 | }; 69 | const regionRecord: RegionRecord = { 70 | end: 60, 71 | owner: alice.address, 72 | paid: null, 73 | }; 74 | const region = new Region(regionId, regionRecord); 75 | 76 | await mintRegion(api, alice, region); 77 | await approveTransfer(api, alice, region, xcRegions.address); 78 | 79 | await initRegion(api, xcRegions, alice, region); 80 | 81 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 82 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 83 | 84 | const aliceBalance = await balanceOf(api, alice.address); 85 | 86 | const timeslicePrice = 5 * Math.pow(10, 12); 87 | await market 88 | .withSigner(alice) 89 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 90 | 91 | await expectOnSale(market, id, alice, timeslicePrice); 92 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 93 | 94 | expect(await balanceOf(api, alice.address)).to.be.lessThan(aliceBalance - LISTING_DEPOIST); 95 | 96 | const result = await market.withSigner(alice).tx.unlistRegion(id); 97 | expectEvent(result, 'RegionUnlisted', { 98 | regionId: id.toPrimitive().u128, 99 | caller: alice.address, 100 | }); 101 | 102 | // Ensure the region is removed from sale: 103 | expect(market.query.listedRegions()).to.eventually.be.equal([]); 104 | expect((await market.query.listedRegion(id)).value.unwrap().ok).to.be.equal(null); 105 | 106 | // Alice receives the region back: 107 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.be.equal(alice.address); 108 | }); 109 | 110 | it('Unlisting not listed region fails', async () => { 111 | const regionId: RegionId = { 112 | begin: 30, 113 | core: 21, 114 | mask: CoreMask.completeMask(), 115 | }; 116 | const regionRecord: RegionRecord = { 117 | end: 60, 118 | owner: alice.address, 119 | paid: null, 120 | }; 121 | const region = new Region(regionId, regionRecord); 122 | 123 | await mintRegion(api, alice, region); 124 | await approveTransfer(api, alice, region, xcRegions.address); 125 | 126 | await initRegion(api, xcRegions, alice, region); 127 | 128 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 129 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 130 | 131 | const result = await market.withSigner(alice).query.unlistRegion(id); 132 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.RegionNotListed()); 133 | }); 134 | 135 | it('Only owner can unlist unexpired region', async () => { 136 | const regionId: RegionId = { 137 | begin: 30, 138 | core: 22, 139 | mask: CoreMask.completeMask(), 140 | }; 141 | const regionRecord: RegionRecord = { 142 | end: 60, 143 | owner: alice.address, 144 | paid: null, 145 | }; 146 | const region = new Region(regionId, regionRecord); 147 | 148 | await mintRegion(api, alice, region); 149 | await approveTransfer(api, alice, region, xcRegions.address); 150 | 151 | await initRegion(api, xcRegions, alice, region); 152 | 153 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 154 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 155 | 156 | const timeslicePrice = 5 * Math.pow(10, 12); 157 | await market 158 | .withSigner(alice) 159 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 160 | 161 | await expectOnSale(market, id, alice, timeslicePrice); 162 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 163 | 164 | const bobUnlistResult = await market.withSigner(bob).query.unlistRegion(id); 165 | expect(bobUnlistResult.value.unwrap().err).to.deep.equal(MarketErrorBuilder.NotAllowed()); 166 | 167 | const aliceUnlistResult = await market.withSigner(alice).tx.unlistRegion(id); 168 | expectEvent(aliceUnlistResult, 'RegionUnlisted', { 169 | regionId: id.toPrimitive().u128, 170 | caller: alice.address, 171 | }); 172 | 173 | // Ensure the region is removed from sale: 174 | expect(market.query.listedRegions()).to.eventually.be.equal([]); 175 | expect((await market.query.listedRegion(id)).value.unwrap().ok).to.be.equal(null); 176 | 177 | // Alice receives the region back: 178 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.be.equal(alice.address); 179 | }); 180 | 181 | /* TODO: Come up with a better way to test this. 182 | it('Anyone can unlist an expired region', async () => { 183 | const regionId: RegionId = { 184 | begin: 0, 185 | core: 23, 186 | mask: CoreMask.completeMask(), 187 | }; 188 | const regionRecord: RegionRecord = { 189 | end: 5, 190 | owner: alice.address, 191 | paid: null, 192 | }; 193 | const region = new Region(regionId, regionRecord); 194 | 195 | await mintRegion(api, alice, region); 196 | await approveTransfer(api, alice, region, xcRegions.address); 197 | 198 | await initRegion(api, xcRegions, alice, region); 199 | 200 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 201 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 202 | 203 | const timeslicePrice = 5 * Math.pow(10, 12); 204 | await market 205 | .withSigner(alice) 206 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 207 | 208 | await expectOnSale(market, id, alice, timeslicePrice); 209 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 210 | 211 | // Wait for the region to expire. 212 | await wait(2000 * region.getEnd() * TIMESLICE_PERIOD); 213 | 214 | const result = await market.withSigner(bob).tx.unlistRegion(id); 215 | expectEvent(result, 'RegionUnlisted', { 216 | regionId: id.toPrimitive().u128, 217 | caller: bob.address, 218 | }); 219 | 220 | // Ensure the region is removed from sale: 221 | expect(market.query.listedRegions()).to.eventually.be.equal([]); 222 | expect((await market.query.listedRegion(id)).value.unwrap().ok).to.be.equal(null); 223 | 224 | // Alice receives the region back: 225 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.be.equal(alice.address); 226 | 227 | // TODO: should ideally ensure that bob received the reward. 228 | }); 229 | */ 230 | }); 231 | -------------------------------------------------------------------------------- /tests/market/purchase.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; 2 | import { expect, use } from 'chai'; 3 | import { KeyringPair } from '@polkadot/keyring/types'; 4 | import XcRegions_Factory from '../../types/constructors/xc_regions'; 5 | import Market_Factory from '../../types/constructors/coretime_market'; 6 | import XcRegions from '../../types/contracts/xc_regions'; 7 | import Market from '../../types/contracts/coretime_market'; 8 | import chaiAsPromised from 'chai-as-promised'; 9 | import { CoreMask, Id, Region, RegionId, RegionRecord } from 'coretime-utils'; 10 | import { 11 | approveTransfer, 12 | balanceOf, 13 | createRegionCollection, 14 | expectEvent, 15 | expectOnSale, 16 | initRegion, 17 | mintRegion, 18 | } from '../common'; 19 | import { MarketErrorBuilder } from '../../types/types-returns/coretime_market'; 20 | 21 | use(chaiAsPromised); 22 | 23 | const REGION_COLLECTION_ID = 42; 24 | const LISTING_DEPOIST = 0; 25 | // In reality this is 80, however we use 8 for testing. 26 | const TIMESLICE_PERIOD = 8; 27 | 28 | const wsProvider = new WsProvider('ws://127.0.0.1:9944'); 29 | // Create a keyring instance 30 | const keyring = new Keyring({ type: 'sr25519', ss58Format: 5 }); 31 | 32 | describe('Coretime market purchases', () => { 33 | let api: ApiPromise; 34 | let alice: KeyringPair; 35 | let bob: KeyringPair; 36 | let charlie: KeyringPair; 37 | 38 | let xcRegions: XcRegions; 39 | let market: Market; 40 | 41 | beforeEach(async function (): Promise { 42 | api = await ApiPromise.create({ provider: wsProvider, noInitWarn: true, types: { Id } }); 43 | 44 | alice = keyring.addFromUri('//Alice'); 45 | bob = keyring.addFromUri('//Bob'); 46 | charlie = keyring.addFromUri('//Charlie'); 47 | 48 | const xcRegionsFactory = new XcRegions_Factory(api, alice); 49 | xcRegions = new XcRegions((await xcRegionsFactory.new()).address, alice, api); 50 | 51 | const marketFactory = new Market_Factory(api, alice); 52 | market = new Market( 53 | 54 | (await marketFactory.new(xcRegions.address, LISTING_DEPOIST, TIMESLICE_PERIOD)).address, 55 | 56 | alice, 57 | api, 58 | ); 59 | 60 | if (!(await api.query.uniques.class(REGION_COLLECTION_ID)).toHuman()) { 61 | await createRegionCollection(api, alice); 62 | } 63 | }); 64 | 65 | it('Purchasing works', async () => { 66 | const regionId: RegionId = { 67 | begin: 30, 68 | core: 10, 69 | mask: CoreMask.completeMask(), 70 | }; 71 | const regionRecord: RegionRecord = { 72 | end: 60, 73 | owner: alice.address, 74 | paid: null, 75 | }; 76 | const region = new Region(regionId, regionRecord); 77 | 78 | await mintRegion(api, alice, region); 79 | await approveTransfer(api, alice, region, xcRegions.address); 80 | 81 | await initRegion(api, xcRegions, alice, region); 82 | 83 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 84 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 85 | 86 | const timeslicePrice = 5 * Math.pow(10, 12); 87 | await market 88 | .withSigner(alice) 89 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 90 | 91 | await expectOnSale(market, id, alice, timeslicePrice); 92 | 93 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 94 | 95 | timeslicePrice * (region.getEnd() - region.getBegin()), 96 | ); 97 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 98 | 99 | const aliceBalance = await balanceOf(api, alice.address); 100 | const bobBalance = await balanceOf(api, bob.address); 101 | 102 | const result = await market 103 | .withSigner(bob) 104 | .tx.purchaseRegion(id, 0, { value: timeslicePrice * (region.getEnd() - region.getBegin()) }); 105 | expectEvent(result, 'RegionPurchased', { 106 | regionId: id.toPrimitive().u128, 107 | buyer: bob.address, 108 | totalPrice: (timeslicePrice * (region.getEnd() - region.getBegin())).toString(), 109 | }); 110 | 111 | // Bob receives the region: 112 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.be.equal(bob.address); 113 | 114 | // Bob's balance is reduced: 115 | expect(await balanceOf(api, bob.address)).to.be.lessThan( 116 | bobBalance - timeslicePrice * (region.getEnd() - region.getBegin()), 117 | ); 118 | // Alice's balance is increased. 119 | expect(await balanceOf(api, alice.address)).to.be.greaterThan(aliceBalance); 120 | 121 | 122 | // Ensure the region is removed from sale: 123 | expect(market.query.listedRegions()).to.eventually.be.equal([]); 124 | expect((await market.query.listedRegion(id)).value.unwrap().ok).to.be.equal(null); 125 | 126 | }); 127 | 128 | it('Purchasing fails when insufficient value is sent', async () => { 129 | const regionId: RegionId = { 130 | begin: 30, 131 | core: 11, 132 | mask: CoreMask.completeMask(), 133 | }; 134 | const regionRecord: RegionRecord = { 135 | end: 60, 136 | owner: alice.address, 137 | paid: null, 138 | }; 139 | const region = new Region(regionId, regionRecord); 140 | 141 | await mintRegion(api, alice, region); 142 | await approveTransfer(api, alice, region, xcRegions.address); 143 | 144 | await initRegion(api, xcRegions, alice, region); 145 | 146 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 147 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 148 | 149 | const timeslicePrice = 5 * Math.pow(10, 12); 150 | await market 151 | .withSigner(alice) 152 | .tx.listRegion(id, timeslicePrice, alice.address, { value: LISTING_DEPOIST }); 153 | 154 | await expectOnSale(market, id, alice, timeslicePrice); 155 | 156 | expect((await market.query.regionPrice(id)).value.unwrap().unwrap().toNumber()).to.be.equal( 157 | timeslicePrice * (region.getEnd() - region.getBegin()), 158 | ); 159 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 160 | 161 | // Sending less tokens than supposed: 162 | const result = await market.withSigner(bob).query.purchaseRegion(id, 0, { 163 | value: timeslicePrice * (region.getEnd() - region.getBegin() - 1), 164 | }); 165 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.InsufficientFunds()); 166 | }); 167 | 168 | it('Purchasing fails when region is not listed', async () => { 169 | const regionId: RegionId = { 170 | begin: 30, 171 | core: 12, 172 | mask: CoreMask.completeMask(), 173 | }; 174 | const regionRecord: RegionRecord = { 175 | end: 60, 176 | owner: alice.address, 177 | paid: null, 178 | }; 179 | const region = new Region(regionId, regionRecord); 180 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 181 | 182 | const timeslicePrice = 5 * Math.pow(10, 12); 183 | 184 | const result = await market.withSigner(bob).query.purchaseRegion(id, 0, { 185 | value: timeslicePrice * (region.getEnd() - region.getBegin()), 186 | }); 187 | expect(result.value.unwrap().err).to.deep.equal(MarketErrorBuilder.RegionNotListed()); 188 | }); 189 | 190 | it('Purchasing sends tokens to sale recepient instead of seller account', async () => { 191 | const regionId: RegionId = { 192 | begin: 30, 193 | core: 13, 194 | mask: CoreMask.completeMask(), 195 | }; 196 | const regionRecord: RegionRecord = { 197 | end: 60, 198 | owner: alice.address, 199 | paid: null, 200 | }; 201 | const region = new Region(regionId, regionRecord); 202 | 203 | await mintRegion(api, alice, region); 204 | await approveTransfer(api, alice, region, xcRegions.address); 205 | 206 | await initRegion(api, xcRegions, alice, region); 207 | 208 | const id: any = api.createType('Id', { U128: region.getEncodedRegionId(api) }); 209 | await xcRegions.withSigner(alice).tx.approve(market.address, id, true); 210 | 211 | const timeslicePrice = 5 * Math.pow(10, 12); 212 | await market 213 | .withSigner(alice) 214 | .tx.listRegion(id, timeslicePrice, charlie.address, { value: LISTING_DEPOIST }); 215 | 216 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.deep.equal(market.address); 217 | 218 | const charlieBalance = await balanceOf(api, charlie.address); 219 | const bobBalance = await balanceOf(api, bob.address); 220 | 221 | const result = await market 222 | .withSigner(bob) 223 | .tx.purchaseRegion(id, 0, { value: timeslicePrice * (region.getEnd() - region.getBegin()) }); 224 | expectEvent(result, 'RegionPurchased', { 225 | regionId: id.toPrimitive().u128, 226 | buyer: bob.address, 227 | totalPrice: (timeslicePrice * (region.getEnd() - region.getBegin())).toString(), 228 | }); 229 | 230 | // Bob receives the region: 231 | expect((await xcRegions.query.ownerOf(id)).value.unwrap()).to.be.equal(bob.address); 232 | 233 | // Bob's balance is reduced: 234 | expect(await balanceOf(api, bob.address)).to.be.lessThan( 235 | bobBalance - timeslicePrice * (region.getEnd() - region.getBegin()), 236 | ); 237 | // Charlie's balance is increased. 238 | expect(await balanceOf(api, charlie.address)).to.be.equal( 239 | charlieBalance + timeslicePrice * (region.getEnd() - region.getBegin()), 240 | ); 241 | 242 | 243 | // Ensure the region is removed from sale: 244 | expect(market.query.listedRegions()).to.eventually.be.equal([]); 245 | expect((await market.query.listedRegion(id)).value.unwrap().ok).to.be.equal(null); 246 | 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /contracts/xc_regions/src/tests.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | use crate::{ 17 | traits::RegionMetadata, 18 | types::{VersionedRegion, XcRegionsError}, 19 | xc_regions::{RegionInitialized, RegionRemoved, XcRegions}, 20 | REGIONS_COLLECTION_ID, 21 | }; 22 | use ink::env::{ 23 | test::{default_accounts, set_caller, DefaultAccounts}, 24 | DefaultEnvironment, 25 | }; 26 | use openbrush::contracts::psp34::{Id, PSP34}; 27 | use primitives::{ 28 | assert_ok, 29 | coretime::{RawRegionId, Region}, 30 | uniques::{CollectionId, ItemDetails}, 31 | Version, 32 | }; 33 | 34 | type Event = ::Type; 35 | 36 | #[ink::test] 37 | fn mock_environment_helper_functions_work() { 38 | let DefaultAccounts:: { charlie, .. } = get_default_accounts(); 39 | let mut xc_regions = XcRegions::new(); 40 | 41 | let region_id_0 = region_id(0); 42 | 43 | // State should be empty since we haven't yet minted any regions. 44 | assert!(xc_regions.items.get(region_id_0).is_none()); 45 | assert!(xc_regions.account.get(charlie).is_none()); 46 | 47 | // 1. Ensure mint works: 48 | assert_ok!(xc_regions.mint(region_id_0, charlie)); 49 | 50 | // Can't mint the same region twice: 51 | assert!(xc_regions.mint(region_id_0, charlie).is_err()); 52 | 53 | assert_eq!( 54 | xc_regions.items.get(region_id_0), 55 | Some(ItemDetails { 56 | owner: charlie, 57 | approved: None, 58 | is_frozen: false, 59 | deposit: Default::default() 60 | }) 61 | ); 62 | assert_eq!(xc_regions.account.get(charlie), Some(vec![region_id_0])); 63 | 64 | // 2. Ensure burn works: 65 | 66 | // Mint one more new region: 67 | let region_id_1 = region_id(1); 68 | assert_ok!(xc_regions.mint(region_id_1, charlie)); 69 | 70 | assert_ok!(xc_regions.burn(region_id_0)); 71 | assert!(xc_regions.items.get(region_id_0).is_none()); 72 | assert_eq!(xc_regions.account.get(charlie), Some(vec![region_id_1])); 73 | 74 | assert_ok!(xc_regions.burn(region_id_1)); 75 | assert!(xc_regions.items.get(region_id_1).is_none()); 76 | assert!(xc_regions.account.get(charlie).is_none()); 77 | 78 | assert!(xc_regions.burn(region_id_1).is_err()); 79 | } 80 | 81 | #[ink::test] 82 | fn init_works() { 83 | let DefaultAccounts:: { charlie, bob, .. } = get_default_accounts(); 84 | let mut xc_regions = XcRegions::new(); 85 | let contract = ink::env::account_id::(); 86 | 87 | // 1. Cannot initialize a region that doesn't exist: 88 | 89 | assert_eq!( 90 | xc_regions.init(Id::U128(0), Region::default()), 91 | Err(XcRegionsError::CannotInitialize) 92 | ); 93 | 94 | // 2. Cannot initialize a region that is not owned by the caller 95 | assert_ok!(xc_regions.mint(region_id(0), charlie)); 96 | 97 | set_caller::(bob); 98 | 99 | assert_eq!( 100 | xc_regions.init(Id::U128(0), Region::default()), 101 | Err(XcRegionsError::CannotInitialize) 102 | ); 103 | 104 | set_caller::(charlie); 105 | // 3. Initialization doesn't work with incorrect metadata: 106 | let invalid_metadata = Region { begin: 1, end: 2, core: 0, mask: Default::default() }; 107 | 108 | assert_eq!( 109 | xc_regions.init(Id::U128(0), invalid_metadata), 110 | Err(XcRegionsError::InvalidMetadata) 111 | ); 112 | 113 | // 4. Initialization works with correct metadata and the right caller: 114 | assert_ok!(xc_regions.init(Id::U128(0), Region::default())); 115 | 116 | 117 | // The region gets transferred to the contract: 118 | assert_eq!(xc_regions._uniques_owner(0), Some(contract)); 119 | 120 | // Charlie receives a wrapped region: 121 | assert_eq!(xc_regions.owner_of(Id::U128(0)), Some(charlie)); 122 | assert_eq!(xc_regions.balance_of(charlie), 1); 123 | 124 | assert_eq!(xc_regions.regions.get(0), Some(Region::default())); 125 | assert_eq!(xc_regions.metadata_versions.get(0), Some(0)); 126 | 127 | let emitted_events = ink::env::test::recorded_events().collect::>(); 128 | assert_init_event(&emitted_events.last().unwrap(), 0, Region::default(), 0); 129 | 130 | // 5. Calling init for an already initialized region will fail. 131 | 132 | assert_eq!( 133 | xc_regions.init(Id::U128(0), Region::default()), 134 | Err(XcRegionsError::CannotInitialize) 135 | ); 136 | } 137 | 138 | #[ink::test] 139 | fn remove_works() { 140 | let DefaultAccounts:: { bob, charlie, .. } = get_default_accounts(); 141 | let mut xc_regions = XcRegions::new(); 142 | set_caller::(charlie); 143 | 144 | let contract = ink::env::account_id::(); 145 | 146 | // Cannot remove a region that doesn't exist. 147 | 148 | assert_eq!(xc_regions.remove(Id::U128(0)), Err(XcRegionsError::CannotRemove)); 149 | 150 | // Minting and initializing a region: 151 | assert_ok!(xc_regions.mint(region_id(0), charlie)); 152 | assert_ok!(xc_regions.init(Id::U128(0), Region::default())); 153 | 154 | // The region gets transferred to the contract: 155 | assert_eq!(xc_regions._uniques_owner(0), Some(contract)); 156 | 157 | // Charlie receives a wrapped region: 158 | assert_eq!(xc_regions.owner_of(Id::U128(0)), Some(charlie)); 159 | assert_eq!(xc_regions.balance_of(charlie), 1); 160 | 161 | assert_eq!(xc_regions.regions.get(0), Some(Region::default())); 162 | assert_eq!(xc_regions.metadata_versions.get(0), Some(0)); 163 | 164 | // Only charlie can remove the region: 165 | set_caller::(bob); 166 | assert_eq!(xc_regions.remove(0), Err(XcRegionsError::CannotRemove)); 167 | 168 | set_caller::(charlie); 169 | // Removing a region works: 170 | 171 | assert_ok!(xc_regions.remove(Id::U128(0))); 172 | 173 | // The region gets transferred back to Charlie and the wrapped region gets burned. 174 | assert_eq!(xc_regions._uniques_owner(0), Some(charlie)); 175 | assert_eq!(xc_regions.owner_of(Id::U128(0)), None); 176 | assert_eq!(xc_regions.balance_of(charlie), 0); 177 | 178 | // The metadata should to be removed, however the metadata version should be retained in the 179 | // contract for this region. 180 | assert_eq!(xc_regions.regions.get(0), None); 181 | assert_eq!(xc_regions.metadata_versions.get(0), Some(0)); 182 | 183 | let emitted_events = ink::env::test::recorded_events().collect::>(); 184 | assert_removed_event(&emitted_events.last().unwrap(), 0); 185 | } 186 | 187 | #[ink::test] 188 | fn get_metadata_works() { 189 | let DefaultAccounts:: { charlie, .. } = get_default_accounts(); 190 | let mut xc_regions = XcRegions::new(); 191 | set_caller::(charlie); 192 | 193 | // Cannot get the metadata of a region that doesn't exist: 194 | 195 | assert_eq!(xc_regions.get_metadata(Id::U128(0)), Err(XcRegionsError::MetadataNotFound)); 196 | 197 | // Minting a region without initializing it. 198 | assert_ok!(xc_regions.mint(region_id(0), charlie)); 199 | assert_eq!(xc_regions.get_metadata(Id::U128(0)), Err(XcRegionsError::MetadataNotFound)); 200 | 201 | assert_ok!(xc_regions.init(Id::U128(0), Region::default())); 202 | assert_eq!( 203 | xc_regions.get_metadata(Id::U128(0)), 204 | 205 | Ok(VersionedRegion { version: 0, region: Region::default() }) 206 | ); 207 | } 208 | 209 | #[ink::test] 210 | fn metadata_version_gets_updated() { 211 | let DefaultAccounts:: { charlie, .. } = get_default_accounts(); 212 | let mut xc_regions = XcRegions::new(); 213 | set_caller::(charlie); 214 | 215 | assert_ok!(xc_regions.mint(region_id(0), charlie)); 216 | 217 | assert_ok!(xc_regions.init(Id::U128(0), Region::default())); 218 | assert_eq!( 219 | xc_regions.get_metadata(Id::U128(0)), 220 | Ok(VersionedRegion { version: 0, region: Region::default() }) 221 | ); 222 | 223 | assert_ok!(xc_regions.remove(Id::U128(0))); 224 | 225 | assert_ok!(xc_regions.init(Id::U128(0), Region::default())); 226 | assert_eq!( 227 | xc_regions.get_metadata(Id::U128(0)), 228 | Ok(VersionedRegion { version: 1, region: Region::default() }) 229 | ); 230 | } 231 | 232 | // Helper functions for test 233 | fn assert_init_event( 234 | event: &ink::env::test::EmittedEvent, 235 | expected_region_id: RawRegionId, 236 | expected_metadata: Region, 237 | expected_version: Version, 238 | ) { 239 | let decoded_event = ::decode(&mut &event.data[..]) 240 | .expect("encountered invalid contract event data buffer"); 241 | if let Event::RegionInitialized(RegionInitialized { region_id, metadata, version }) = 242 | decoded_event 243 | { 244 | assert_eq!( 245 | region_id, expected_region_id, 246 | "encountered invalid RegionInitialized.region_id" 247 | ); 248 | assert_eq!(metadata, expected_metadata, "encountered invalid RegionInitialized.metadata"); 249 | assert_eq!(version, expected_version, "encountered invalid RegionInitialized.version"); 250 | } else { 251 | panic!("encountered unexpected event kind: expected a RegionInitialized event") 252 | } 253 | } 254 | 255 | fn assert_removed_event(event: &ink::env::test::EmittedEvent, expected_region_id: RawRegionId) { 256 | let decoded_event = ::decode(&mut &event.data[..]) 257 | .expect("encountered invalid contract event data buffer"); 258 | if let Event::RegionRemoved(RegionRemoved { region_id }) = decoded_event { 259 | assert_eq!(region_id, expected_region_id, "encountered invalid RegionRemoved.region_id"); 260 | } else { 261 | panic!("encountered unexpected event kind: expected a RegionRemoved event") 262 | } 263 | } 264 | 265 | pub fn region_id(region_id: RawRegionId) -> (CollectionId, RawRegionId) { 266 | (REGIONS_COLLECTION_ID, region_id) 267 | } 268 | 269 | pub fn get_default_accounts() -> DefaultAccounts { 270 | default_accounts::() 271 | } 272 | -------------------------------------------------------------------------------- /contracts/coretime_market/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | //! Coretime market 17 | //! 18 | //! This is the contract implementation of a Coretime marketplace working on top of the `XcRegions` 19 | //! contract. 20 | //! 21 | //! The contract employs a timeslice-based pricing model that determines the price of regions on 22 | //! sale, based on the value of a single timeslice. This approach is useful as it allows us to 23 | //! emulate the expiring nature of Coretime. 24 | //! 25 | //! ## Terminology: 26 | //! 27 | //! - Expired region: A region that can no longer be assigned to any particular task. 28 | //! - Active region: A region which is currently able to perform a task. I.e. current timeslice > 29 | //! region.begin 30 | 31 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 32 | #![feature(min_specialization)] 33 | 34 | #[cfg(test)] 35 | mod tests; 36 | 37 | mod types; 38 | 39 | #[openbrush::contract(env = environment::ExtendedEnvironment)] 40 | pub mod coretime_market { 41 | 42 | use crate::types::{Config, Listing, MarketError}; 43 | 44 | use block_number_extension::BlockNumberProviderExtension; 45 | use environment::ExtendedEnvironment; 46 | use ink::{ 47 | codegen::{EmitEvent, Env}, 48 | prelude::vec::Vec, 49 | reflect::ContractEventBase, 50 | 51 | storage::Lazy, 52 | 53 | EnvAccess, 54 | }; 55 | use openbrush::{contracts::traits::psp34::Id, storage::Mapping, traits::Storage}; 56 | use primitives::{ 57 | 58 | coretime::{RawRegionId, Region, Timeslice, CORE_MASK_BIT_LEN}, 59 | 60 | ensure, Version, 61 | }; 62 | use sp_arithmetic::{traits::SaturatedConversion, FixedPointNumber, FixedU128}; 63 | use xc_regions::{traits::RegionMetadataRef, PSP34Ref}; 64 | 65 | #[ink(storage)] 66 | #[derive(Storage)] 67 | pub struct CoretimeMarket { 68 | 69 | /// A mapping that holds information about each region listed on sale. 70 | pub listings: Mapping, 71 | /// A vector containing all the regions listed on sale. 72 | pub listed_regions: Lazy>, 73 | /// The configuration of the market. Set on contract initialization. Can't be changed 74 | /// afterwards. 75 | pub config: Config, 76 | 77 | } 78 | 79 | #[ink(event)] 80 | pub struct RegionListed { 81 | /// The identifier of the region that got listed on sale. 82 | #[ink(topic)] 83 | pub(crate) region_id: RawRegionId, 84 | /// The per timeslice price of the listed region. 85 | pub(crate) timeslice_price: Balance, 86 | /// The seller of the region 87 | pub(crate) seller: AccountId, 88 | /// The sale revenue recipient. 89 | pub(crate) sale_recepient: AccountId, 90 | /// The metadata version of the region. 91 | pub(crate) metadata_version: Version, 92 | } 93 | 94 | #[ink(event)] 95 | 96 | pub struct RegionUnlisted { 97 | /// The identifier of the region that got unlisted from sale. 98 | #[ink(topic)] 99 | pub(crate) region_id: RawRegionId, 100 | /// The account that removed the region from sale. 101 | pub(crate) caller: AccountId, 102 | } 103 | 104 | #[ink(event)] 105 | pub struct RegionPurchased { 106 | /// The identifier of the region that got purchased. 107 | #[ink(topic)] 108 | pub(crate) region_id: RawRegionId, 109 | /// The buyer of the region 110 | pub(crate) buyer: AccountId, 111 | /// The total price paid for the listed region. 112 | pub(crate) total_price: Balance, 113 | } 114 | 115 | #[ink(event)] 116 | pub struct RegionPriceUpdated { 117 | /// The identifier of the region that got its price updated. 118 | #[ink(topic)] 119 | pub(crate) region_id: RawRegionId, 120 | /// The new per timeslice price. 121 | pub(crate) new_timeslice_price: Balance, 122 | } 123 | 124 | impl CoretimeMarket { 125 | #[ink(constructor)] 126 | pub fn new( 127 | xc_regions_contract: AccountId, 128 | listing_deposit: Balance, 129 | timeslice_period: BlockNumber, 130 | ) -> Self { 131 | Self { 132 | listings: Default::default(), 133 | listed_regions: Default::default(), 134 | config: Config { xc_regions_contract, listing_deposit, timeslice_period }, 135 | } 136 | } 137 | 138 | #[ink(message)] 139 | pub fn xc_regions_contract(&self) -> AccountId { 140 | 141 | self.config.xc_regions_contract 142 | } 143 | 144 | #[ink(message)] 145 | pub fn listed_regions(&self, maybe_who: Option) -> Vec { 146 | if let Some(who) = maybe_who { 147 | self.listed_regions 148 | .get_or_default() 149 | .into_iter() 150 | .filter(|region_id| { 151 | let Some(listing) = self.listings.get(region_id) else { return false }; 152 | listing.seller == who 153 | }) 154 | .collect() 155 | } else { 156 | self.listed_regions.get_or_default() 157 | } 158 | 159 | } 160 | 161 | #[ink(message)] 162 | pub fn listed_region(&self, id: Id) -> Result, MarketError> { 163 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 164 | Ok(self.listings.get(®ion_id)) 165 | } 166 | 167 | #[ink(message)] 168 | pub fn region_price(&self, id: Id) -> Result { 169 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 170 | 171 | let metadata = RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, id) 172 | 173 | .map_err(MarketError::XcRegionsMetadataError)?; 174 | let listing = self.listings.get(®ion_id).ok_or(MarketError::RegionNotListed)?; 175 | 176 | self.calculate_region_price(metadata.region, listing) 177 | } 178 | 179 | /// A function for listing a region on sale. 180 | /// 181 | /// ## Arguments: 182 | /// - `region_id`: The `u128` encoded identifier of the region that the caller intends to 183 | /// list for sale. 184 | /// - `timeslice_price`: The price per a single timeslice. 185 | /// - `sale_recepient`: The `AccountId` receiving the payment from the sale. If not 186 | /// specified this will be the caller. 187 | /// 188 | /// Before making this call, the caller must first approve their region to the market 189 | /// contract, as it will be transferred to the contract when listed for sale. 190 | /// 191 | /// This call is payable because listing a region requires a deposit from the user. This 192 | /// deposit will be returned upon unlisting the region from sale. The rationale behind this 193 | /// requirement is to prevent the contract state from becoming bloated with regions that 194 | /// have expired. 195 | #[ink(message, payable)] 196 | pub fn list_region( 197 | &mut self, 198 | id: Id, 199 | timeslice_price: Balance, 200 | sale_recepient: Option, 201 | ) -> Result<(), MarketError> { 202 | let caller = self.env().caller(); 203 | let market = self.env().account_id(); 204 | 205 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 206 | 207 | // Ensure that the region exists and its metadata is set. 208 | 209 | let metadata = 210 | RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, id.clone()) 211 | .map_err(MarketError::XcRegionsMetadataError)?; 212 | 213 | 214 | let current_timeslice = self.current_timeslice(); 215 | 216 | // It doesn't make sense to list a region that expired. 217 | ensure!(metadata.region.end > current_timeslice, MarketError::RegionExpired); 218 | 219 | ensure!( 220 | 221 | self.env().transferred_value() == self.config.listing_deposit, 222 | 223 | MarketError::MissingDeposit 224 | ); 225 | 226 | // Transfer the region to the market. 227 | PSP34Ref::transfer( 228 | &self.config.xc_regions_contract, 229 | market, 230 | id.clone(), 231 | Default::default(), 232 | ) 233 | .map_err(MarketError::XcRegionsPsp34Error)?; 234 | 235 | 236 | let sale_recepient = sale_recepient.unwrap_or(caller); 237 | 238 | self.listings.insert( 239 | ®ion_id, 240 | &Listing { 241 | seller: caller, 242 | timeslice_price, 243 | sale_recepient, 244 | metadata_version: metadata.version, 245 | }, 246 | ); 247 | 248 | 249 | let mut listed_regions = self.listed_regions.get_or_default(); 250 | listed_regions.push(region_id); 251 | self.listed_regions.set(&listed_regions); 252 | 253 | 254 | self.emit_event(RegionListed { 255 | region_id, 256 | timeslice_price, 257 | seller: caller, 258 | sale_recepient, 259 | metadata_version: metadata.version, 260 | }); 261 | 262 | Ok(()) 263 | } 264 | 265 | /// A function for unlisting a region on sale. 266 | /// 267 | /// ## Arguments: 268 | /// - `region_id`: The `u128` encoded identifier of the region that the caller intends to 269 | /// unlist from sale. 270 | 271 | /// 272 | /// In case the region is expired, this is callable by anyone and the caller will receive 273 | /// the listing deposit as a reward. 274 | #[ink(message)] 275 | pub fn unlist_region(&mut self, id: Id) -> Result<(), MarketError> { 276 | let caller = self.env().caller(); 277 | 278 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 279 | 280 | let listing = self.listings.get(®ion_id).ok_or(MarketError::RegionNotListed)?; 281 | let metadata = 282 | RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, id.clone()) 283 | .map_err(MarketError::XcRegionsMetadataError)?; 284 | 285 | let current_timeslice = self.current_timeslice(); 286 | 287 | // If the region is expired this is callable by anyone, otherwise only the seller can 288 | // unlist the region from the market. 289 | ensure!( 290 | caller == listing.seller || current_timeslice > metadata.region.end, 291 | MarketError::NotAllowed 292 | ); 293 | 294 | // Transfer the region to the seller. 295 | PSP34Ref::transfer( 296 | &self.config.xc_regions_contract, 297 | listing.seller, 298 | id.clone(), 299 | Default::default(), 300 | ) 301 | .map_err(MarketError::XcRegionsPsp34Error)?; 302 | 303 | // Remove the region from sale: 304 | self.remove_from_sale(region_id)?; 305 | 306 | // Reward the caller with listing deposit. 307 | self.env() 308 | .transfer(caller, self.config.listing_deposit) 309 | .map_err(|_| MarketError::TransferFailed)?; 310 | 311 | self.emit_event(RegionUnlisted { region_id, caller }); 312 | 313 | Ok(()) 314 | } 315 | 316 | /// A function for updating a listed region's price. 317 | 318 | /// 319 | /// ## Arguments: 320 | /// - `region_id`: The `u128` encoded identifier of the region being listed for sale. 321 | /// - `timeslice_price`: The new per timeslice price of the region. 322 | #[ink(message)] 323 | pub fn update_region_price( 324 | 325 | &mut self, 326 | id: Id, 327 | new_timeslice_price: Balance, 328 | ) -> Result<(), MarketError> { 329 | let caller = self.env().caller(); 330 | 331 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 332 | 333 | let mut listing = self.listings.get(®ion_id).ok_or(MarketError::RegionNotListed)?; 334 | 335 | ensure!(caller == listing.seller, MarketError::NotAllowed); 336 | 337 | listing.timeslice_price = new_timeslice_price; 338 | self.listings.insert(®ion_id, &listing); 339 | 340 | self.emit_event(RegionPriceUpdated { region_id, new_timeslice_price }); 341 | Ok(()) 342 | 343 | } 344 | 345 | /// A function for purchasing a region listed on sale. 346 | /// 347 | /// ## Arguments: 348 | /// - `region_id`: The `u128` encoded identifier of the region being listed for sale. 349 | /// - `metadata_version`: The required metadata version for the region. If the 350 | /// `metadata_version` does not match the current version stored in the xc-regions 351 | /// contract the purchase will fail. 352 | /// 353 | /// IMPORTANT NOTE: The client is responsible for ensuring that the metadata of the listed 354 | /// region is correct. 355 | #[ink(message, payable)] 356 | pub fn purchase_region( 357 | &mut self, 358 | id: Id, 359 | metadata_version: Version, 360 | ) -> Result<(), MarketError> { 361 | let caller = self.env().caller(); 362 | let transferred_value = self.env().transferred_value(); 363 | 364 | let Id::U128(region_id) = id else { return Err(MarketError::InvalidRegionId) }; 365 | let listing = self.listings.get(®ion_id).ok_or(MarketError::RegionNotListed)?; 366 | 367 | 368 | let metadata = 369 | RegionMetadataRef::get_metadata(&self.config.xc_regions_contract, id.clone()) 370 | .map_err(MarketError::XcRegionsMetadataError)?; 371 | 372 | 373 | let price = self.calculate_region_price(metadata.region, listing.clone())?; 374 | ensure!(transferred_value >= price, MarketError::InsufficientFunds); 375 | 376 | ensure!(listing.metadata_version == metadata_version, MarketError::MetadataNotMatching); 377 | 378 | // Transfer the region to the buyer. 379 | 380 | PSP34Ref::transfer( 381 | &self.config.xc_regions_contract, 382 | caller, 383 | id.clone(), 384 | Default::default(), 385 | ) 386 | .map_err(MarketError::XcRegionsPsp34Error)?; 387 | 388 | // Remove the region from sale: 389 | self.remove_from_sale(region_id)?; 390 | 391 | 392 | // Transfer the tokens to the sale recipient. 393 | self.env() 394 | .transfer(listing.sale_recepient, price) 395 | .map_err(|_| MarketError::TransferFailed)?; 396 | 397 | self.emit_event(RegionPurchased { region_id, buyer: caller, total_price: price }); 398 | 399 | Ok(()) 400 | } 401 | } 402 | 403 | // Internal functions: 404 | impl CoretimeMarket { 405 | pub(crate) fn calculate_region_price( 406 | &self, 407 | region: Region, 408 | listing: Listing, 409 | ) -> Result { 410 | let current_timeslice = self.current_timeslice(); 411 | 412 | let duration = region.end.saturating_sub(region.begin); 413 | 414 | let core_occupancy = 415 | FixedU128::checked_from_rational(region.mask.count_ones(), CORE_MASK_BIT_LEN) 416 | .ok_or(MarketError::ArithmeticError)?; 417 | 418 | let per_timeslice_price = (core_occupancy * listing.timeslice_price.into()) 419 | .into_inner() 420 | .saturating_div(FixedU128::accuracy()); 421 | 422 | if current_timeslice < region.begin { 423 | // The region didn't start yet, so there is no value lost. 424 | let price = per_timeslice_price.saturating_mul(duration.into()); 425 | 426 | return Ok(price); 427 | } 428 | 429 | let remaining_timeslices = region.end.saturating_sub(current_timeslice); 430 | let price = per_timeslice_price.saturating_mul(remaining_timeslices.into()); 431 | 432 | Ok(price) 433 | } 434 | 435 | // Remove a region from sale 436 | fn remove_from_sale(&mut self, region_id: RawRegionId) -> Result<(), MarketError> { 437 | let region_index = self 438 | .listed_regions 439 | .get_or_default() 440 | .iter() 441 | .position(|r| *r == region_id) 442 | .ok_or(MarketError::RegionNotListed)?; 443 | 444 | let mut listed_regions = self.listed_regions.get_or_default(); 445 | listed_regions.remove(region_index); 446 | self.listed_regions.set(&listed_regions); 447 | 448 | Ok(()) 449 | } 450 | 451 | 452 | #[cfg(not(test))] 453 | pub(crate) fn current_timeslice(&self) -> Timeslice { 454 | let latest_rc_block = 455 | self.env().extension().relay_chain_block_number().unwrap_or_default(); 456 | 457 | (latest_rc_block / self.config.timeslice_period).saturated_into() 458 | 459 | } 460 | 461 | #[cfg(test)] 462 | pub(crate) fn current_timeslice(&self) -> Timeslice { 463 | let latest_block = self.env().block_number(); 464 | 465 | (latest_block / self.config.timeslice_period).saturated_into() 466 | 467 | } 468 | 469 | fn emit_event::Type>>(&self, e: Event) { 470 | as EmitEvent>::emit_event::( 471 | self.env(), 472 | e, 473 | ); 474 | } 475 | } 476 | 477 | #[cfg(all(test, feature = "e2e-tests"))] 478 | pub mod tests { 479 | use super::*; 480 | use environment::ExtendedEnvironment; 481 | use ink_e2e::MessageBuilder; 482 | 483 | use primitives::coretime::TIMESLICE_PERIOD; 484 | 485 | use xc_regions::xc_regions::XcRegionsRef; 486 | 487 | type E2EResult = Result>; 488 | 489 | const REQUIRED_DEPOSIT: Balance = 1_000; 490 | 491 | #[ink_e2e::test(environment = ExtendedEnvironment)] 492 | async fn constructor_works(mut client: ink_e2e::Client) -> E2EResult<()> { 493 | let constructor = XcRegionsRef::new(); 494 | let xc_regions_acc_id = client 495 | .instantiate("xc-regions", &ink_e2e::alice(), constructor, 0, None) 496 | .await 497 | .expect("instantiate failed") 498 | .account_id; 499 | 500 | 501 | let constructor = 502 | CoretimeMarketRef::new(xc_regions_acc_id, REQUIRED_DEPOSIT, TIMESLICE_PERIOD); 503 | 504 | let market_acc_id = client 505 | .instantiate("coretime-market", &ink_e2e::alice(), constructor, 0, None) 506 | .await 507 | .expect("instantiate failed") 508 | .account_id; 509 | 510 | let xc_regions_contract = 511 | MessageBuilder::::from_account_id( 512 | market_acc_id.clone(), 513 | ) 514 | .call(|market| market.xc_regions_contract()); 515 | let xc_regions_contract = 516 | client.call_dry_run(&ink_e2e::alice(), &xc_regions_contract, 0, None).await; 517 | assert_eq!(xc_regions_contract.return_value(), xc_regions_acc_id); 518 | 519 | // There should be no regions listed on sale: 520 | let listed_regions = 521 | MessageBuilder::::from_account_id( 522 | market_acc_id.clone(), 523 | ) 524 | 525 | .call(|market| market.listed_regions(None)); 526 | 527 | let listed_regions = 528 | client.call_dry_run(&ink_e2e::alice(), &listed_regions, 0, None).await; 529 | assert_eq!(listed_regions.return_value(), vec![]); 530 | 531 | Ok(()) 532 | } 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /contracts/xc_regions/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of RegionX. 2 | // 3 | // RegionX is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // RegionX is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with RegionX. If not, see . 15 | 16 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 17 | #![feature(min_specialization)] 18 | 19 | pub mod traits; 20 | pub mod types; 21 | 22 | #[cfg(test)] 23 | mod tests; 24 | 25 | pub use crate::xc_regions::PSP34Ref; 26 | 27 | // NOTE: This should be the collection ID of the underlying region collection. 28 | pub const REGIONS_COLLECTION_ID: u32 = 42; 29 | 30 | #[openbrush::implementation(PSP34, PSP34Enumerable)] 31 | #[openbrush::contract(env = environment::ExtendedEnvironment)] 32 | pub mod xc_regions { 33 | use crate::{ 34 | traits::{regionmetadata_external, RegionMetadata}, 35 | types::{VersionedRegion, XcRegionsError}, 36 | REGIONS_COLLECTION_ID, 37 | }; 38 | use ink::{ 39 | codegen::{EmitEvent, Env}, 40 | storage::Mapping, 41 | }; 42 | use openbrush::traits::Storage; 43 | use primitives::{ 44 | coretime::{RawRegionId, Region, RegionId}, 45 | ensure, 46 | uniques::{ItemDetails, UniquesCall}, 47 | RuntimeCall, Version, 48 | }; 49 | use uniques_extension::UniquesExtension; 50 | 51 | #[cfg(test)] 52 | use primitives::uniques::CollectionId; 53 | 54 | #[openbrush::wrapper] 55 | pub type PSP34Ref = dyn PSP34 + PSP34Enumerable; 56 | 57 | #[ink(storage)] 58 | #[derive(Default, Storage)] 59 | pub struct XcRegions { 60 | #[storage_field] 61 | psp34: psp34::Data, 62 | #[storage_field] 63 | enumerable: enumerable::Data, 64 | /// A mapping that links RawRegionId to its corresponding region metadata. 65 | pub regions: Mapping, 66 | /// A mapping that keeps track of the metadata version for each region. 67 | /// 68 | /// This version gets incremented for a region each time it gets re-initialized. 69 | pub metadata_versions: Mapping, 70 | // Mock chain extension state only used for integration testing. 71 | #[cfg(test)] 72 | pub items: Mapping< 73 | (primitives::uniques::CollectionId, primitives::coretime::RawRegionId), 74 | ItemDetails, 75 | >, 76 | // Mock chain extension state only used for integration testing. 77 | #[cfg(test)] 78 | pub account: Mapping< 79 | AccountId, 80 | Vec<(primitives::uniques::CollectionId, primitives::coretime::RawRegionId)>, 81 | >, 82 | } 83 | 84 | #[ink(event)] 85 | pub struct RegionInitialized { 86 | /// The identifier of the region that got initialized. 87 | #[ink(topic)] 88 | pub(crate) region_id: RawRegionId, 89 | /// The associated metadata. 90 | pub(crate) metadata: Region, 91 | /// The version of the metadata. This is incremented by the contract each time the same 92 | /// region is initialized. 93 | pub(crate) version: Version, 94 | } 95 | 96 | #[ink(event)] 97 | pub struct RegionRemoved { 98 | /// The identifier of the region that got removed. 99 | #[ink(topic)] 100 | pub(crate) region_id: RawRegionId, 101 | } 102 | 103 | #[overrider(PSP34)] 104 | fn collection_id(&self) -> Id { 105 | Id::U32(REGIONS_COLLECTION_ID) 106 | } 107 | 108 | impl RegionMetadata for XcRegions { 109 | /// A function for minting a wrapped xcRegion and initializing the metadata of it. It can 110 | /// only be called if the specified region exists on this chain and the caller is the actual 111 | /// owner of the region. 112 | /// 113 | /// ## Arguments: 114 | /// - `raw_region_id` - The `u128` encoded region identifier. 115 | /// - `region` - The corresponding region metadata. 116 | /// 117 | /// This function conducts a sanity check to verify that the metadata derived from the 118 | /// `raw_region_id` aligns with the respective components of the metadata supplied through 119 | /// the region argument. 120 | /// 121 | /// If this is not the first time that this region is inititalized, the metadata version 122 | /// will get incremented. 123 | /// 124 | /// The underlying region will be transferred to this contract, and in response, a wrapped 125 | /// token will be minted for the caller. 126 | /// 127 | /// NOTE: Prior to invoking this ink message, the caller must grant approval to the contract 128 | /// for the region, enabling its transfer. 129 | /// 130 | /// ## Events: 131 | /// On success this ink message emits the `RegionInitialized` event. 132 | #[ink(message)] 133 | 134 | fn init(&mut self, id: Id, region: Region) -> Result<(), XcRegionsError> { 135 | let caller = self.env().caller(); 136 | 137 | let Id::U128(raw_region_id) = id else { return Err(XcRegionsError::InvalidRegionId) }; 138 | 139 | ensure!( 140 | Some(caller) == self._uniques_owner(raw_region_id), 141 | XcRegionsError::CannotInitialize 142 | ); 143 | 144 | // Cannot initialize a region that already has metadata stored. 145 | ensure!(self.regions.get(raw_region_id).is_none(), XcRegionsError::CannotInitialize); 146 | 147 | // Do a sanity check to ensure that the provided region metadata matches with the 148 | // metadata extracted from the region id. 149 | let region_id = RegionId::from(raw_region_id); 150 | ensure!(region_id.begin == region.begin, XcRegionsError::InvalidMetadata); 151 | ensure!(region_id.core == region.core, XcRegionsError::InvalidMetadata); 152 | ensure!(region_id.mask == region.mask, XcRegionsError::InvalidMetadata); 153 | 154 | // After passing all checks we will transfer the region to the contract and mint a 155 | // wrapped xcRegion token. 156 | let contract = self.env().account_id(); 157 | self._transfer(raw_region_id, contract)?; 158 | 159 | let new_version = if let Some(version) = self.metadata_versions.get(raw_region_id) { 160 | version.saturating_add(1) 161 | } else { 162 | Default::default() 163 | }; 164 | 165 | self.metadata_versions.insert(raw_region_id, &new_version); 166 | self.regions.insert(raw_region_id, ®ion); 167 | 168 | psp34::InternalImpl::_mint_to(self, caller, Id::U128(raw_region_id)) 169 | .map_err(XcRegionsError::Psp34)?; 170 | 171 | self.env().emit_event(RegionInitialized { 172 | region_id: raw_region_id, 173 | metadata: region, 174 | version: new_version, 175 | }); 176 | 177 | Ok(()) 178 | } 179 | 180 | /// A function to retrieve all metadata associated with a specific region. 181 | /// 182 | /// The function returns a `VersionedRegion`, encompassing the version of the retrieved 183 | /// metadata that is intended for client-side verification. 184 | /// 185 | /// ## Arguments: 186 | /// - `raw_region_id` - The `u128` encoded region identifier. 187 | #[ink(message)] 188 | 189 | fn get_metadata(&self, id: Id) -> Result { 190 | let Id::U128(region_id) = id else { return Err(XcRegionsError::InvalidRegionId) }; 191 | let Some(region) = self.regions.get(region_id) else { 192 | return Err(XcRegionsError::MetadataNotFound) 193 | }; 194 | 195 | let Some(version) = self.metadata_versions.get(region_id) else { 196 | // This should never really happen; if a region has its metadata stored, its version 197 | // should be stored as well. 198 | return Err(XcRegionsError::VersionNotFound) 199 | }; 200 | 201 | Ok(VersionedRegion { version, region }) 202 | } 203 | 204 | /// A function to return the region to its owner. 205 | /// 206 | /// This process involves burning the wrapped region and eliminating its associated 207 | /// metadata. 208 | /// 209 | /// Only the owner of the wrapped region can call this function. 210 | /// 211 | /// ## Arguments: 212 | /// - `raw_region_id` - The `u128` encoded region identifier. 213 | /// 214 | /// ## Events: 215 | /// On success this ink message emits the `RegionRemoved` event. 216 | #[ink(message)] 217 | 218 | fn remove(&mut self, id: Id) -> Result<(), XcRegionsError> { 219 | let Id::U128(region_id) = id else { return Err(XcRegionsError::InvalidRegionId) }; 220 | 221 | let owner = 222 | psp34::PSP34Impl::owner_of(self, id.clone()).ok_or(XcRegionsError::CannotRemove)?; 223 | 224 | ensure!(owner == self.env().caller(), XcRegionsError::CannotRemove); 225 | self.regions.remove(region_id); 226 | 227 | psp34::InternalImpl::_burn_from(self, owner, id).map_err(XcRegionsError::Psp34)?; 228 | self._transfer(region_id, owner)?; 229 | 230 | self.env().emit_event(RegionRemoved { region_id }); 231 | Ok(()) 232 | } 233 | } 234 | 235 | impl XcRegions { 236 | #[ink(constructor)] 237 | pub fn new() -> Self { 238 | Default::default() 239 | } 240 | } 241 | 242 | // Internal functions: 243 | #[cfg(not(test))] 244 | impl XcRegions { 245 | fn _transfer(&self, region_id: RawRegionId, dest: AccountId) -> Result<(), XcRegionsError> { 246 | self.env() 247 | .call_runtime(&RuntimeCall::Uniques(UniquesCall::Transfer { 248 | collection: REGIONS_COLLECTION_ID, 249 | item: region_id, 250 | dest: dest.into(), 251 | })) 252 | .map_err(|_| XcRegionsError::RuntimeError)?; 253 | 254 | Ok(()) 255 | } 256 | 257 | /// Returns whether the region exists on this chain or not. 258 | fn _uniques_exists(&self, region_id: RawRegionId) -> bool { 259 | self._uniques_item(region_id).is_some() 260 | } 261 | 262 | /// Returns the details of an item within a collection. 263 | fn _uniques_item(&self, item_id: RawRegionId) -> Option { 264 | self.env().extension().item(REGIONS_COLLECTION_ID, item_id).ok()? 265 | } 266 | 267 | /// The owner of the specific item. 268 | fn _uniques_owner(&self, region_id: RawRegionId) -> Option { 269 | self.env().extension().owner(REGIONS_COLLECTION_ID, region_id).ok()? 270 | } 271 | } 272 | 273 | // Implelementation of internal functions used only for integration tests. 274 | #[cfg(test)] 275 | impl XcRegions { 276 | fn _transfer( 277 | &mut self, 278 | region_id: RawRegionId, 279 | dest: AccountId, 280 | ) -> Result<(), XcRegionsError> { 281 | self.burn((REGIONS_COLLECTION_ID, region_id)).unwrap(); 282 | self.mint((REGIONS_COLLECTION_ID, region_id), dest).unwrap(); 283 | Ok(()) 284 | } 285 | 286 | /// Returns whether the region exists on this chain or not. 287 | pub fn _uniques_exists(&self, region_id: RawRegionId) -> bool { 288 | self._uniques_item(region_id).is_some() 289 | } 290 | 291 | /// Returns the details of an item within a collection. 292 | pub fn _uniques_item(&self, item_id: RawRegionId) -> Option { 293 | self.items.get((REGIONS_COLLECTION_ID, item_id)) 294 | } 295 | 296 | /// The owner of the specific item. 297 | pub fn _uniques_owner(&self, region_id: RawRegionId) -> Option { 298 | self.items.get((REGIONS_COLLECTION_ID, region_id)).map(|a| a.owner) 299 | } 300 | 301 | pub fn mint( 302 | &mut self, 303 | id: (CollectionId, RawRegionId), 304 | owner: AccountId, 305 | ) -> Result<(), &'static str> { 306 | ensure!(self.items.get((id.0, id.1)).is_none(), "Item already exists"); 307 | self.items.insert( 308 | (id.0, id.1), 309 | &ItemDetails { 310 | owner, 311 | approved: None, 312 | is_frozen: false, 313 | deposit: Default::default(), 314 | }, 315 | ); 316 | 317 | let mut owned = self.account.get(owner).map(|a| a).unwrap_or_default(); 318 | owned.push((id.0, id.1)); 319 | self.account.insert(owner, &owned); 320 | 321 | Ok(()) 322 | } 323 | 324 | pub fn burn(&mut self, id: (CollectionId, RawRegionId)) -> Result<(), &'static str> { 325 | let Some(owner) = self.items.get((id.0, id.1)).map(|a| a.owner) else { 326 | return Err("Item not found") 327 | }; 328 | 329 | let mut owned = self.account.get(owner).map(|a| a).unwrap_or_default(); 330 | owned.retain(|a| *a != (id.0, id.1)); 331 | 332 | if owned.is_empty() { 333 | self.account.remove(owner); 334 | } else { 335 | self.account.insert(owner, &owned); 336 | } 337 | 338 | self.items.remove((id.0, id.1)); 339 | 340 | Ok(()) 341 | } 342 | } 343 | 344 | #[cfg(all(test, feature = "e2e-tests"))] 345 | pub mod tests { 346 | use super::*; 347 | use crate::{ 348 | traits::regionmetadata_external::RegionMetadata, types::VersionedRegion, 349 | REGIONS_COLLECTION_ID, 350 | }; 351 | use environment::ExtendedEnvironment; 352 | use ink_e2e::{subxt::dynamic::Value, MessageBuilder}; 353 | use openbrush::contracts::psp34::psp34_external::PSP34; 354 | use primitives::address_of; 355 | 356 | type E2EResult = Result>; 357 | 358 | #[ink_e2e::test(environment = ExtendedEnvironment)] 359 | async fn init_non_existing_region_fails( 360 | mut client: ink_e2e::Client, 361 | ) -> E2EResult<()> { 362 | let constructor = XcRegionsRef::new(); 363 | let contract_acc_id = client 364 | .instantiate("xc-regions", &ink_e2e::alice(), constructor, 0, None) 365 | .await 366 | .expect("instantiate failed") 367 | .account_id; 368 | 369 | let raw_region_id = 0u128; 370 | let region = Region::default(); 371 | 372 | let init = MessageBuilder::::from_account_id( 373 | contract_acc_id.clone(), 374 | ) 375 | .call(|xc_regions| xc_regions.init(Id::U128(raw_region_id), region.clone())); 376 | let init_result = client.call_dry_run(&ink_e2e::alice(), &init, 0, None).await; 377 | assert_eq!(init_result.return_value(), Err(XcRegionsError::CannotInitialize)); 378 | 379 | Ok(()) 380 | } 381 | 382 | #[ink_e2e::test(environment = ExtendedEnvironment)] 383 | async fn init_works(mut client: E2EBackend) -> E2EResult<()> { 384 | let constructor = XcRegionsRef::new(); 385 | let contract_acc_id = client 386 | .instantiate("xc-regions", &ink_e2e::alice(), constructor, 0, None) 387 | .await 388 | .expect("instantiate failed") 389 | .account_id; 390 | 391 | let raw_region_id = 0u128; 392 | let region = Region::default(); 393 | 394 | // Create region: collection 395 | let call_data = vec![ 396 | Value::u128(REGIONS_COLLECTION_ID.into()), 397 | Value::unnamed_variant("Id", [Value::from_bytes(&address_of!(Alice))]), 398 | ]; 399 | client 400 | .runtime_call(&ink_e2e::alice(), "Uniques", "create", call_data) 401 | .await 402 | .expect("creating a collection failed"); 403 | 404 | // Mint region: 405 | let call_data = vec![ 406 | Value::u128(REGIONS_COLLECTION_ID.into()), 407 | Value::u128(raw_region_id.into()), 408 | Value::unnamed_variant("Id", [Value::from_bytes(&address_of!(Alice))]), 409 | ]; 410 | client 411 | .runtime_call(&ink_e2e::alice(), "Uniques", "mint", call_data) 412 | .await 413 | .expect("minting a region failed"); 414 | 415 | // Approve transfer region: 416 | let call_data = vec![ 417 | Value::u128(REGIONS_COLLECTION_ID.into()), 418 | Value::u128(raw_region_id.into()), 419 | Value::unnamed_variant("Id", [Value::from_bytes(contract_acc_id)]), 420 | ]; 421 | client 422 | .runtime_call(&ink_e2e::alice(), "Uniques", "approve_transfer", call_data) 423 | .await 424 | .expect("approving transfer failed"); 425 | 426 | let init = MessageBuilder::::from_account_id( 427 | contract_acc_id.clone(), 428 | ) 429 | .call(|xc_regions| xc_regions.init(Id::U128(raw_region_id), region.clone())); 430 | let init_result = client.call(&ink_e2e::alice(), init, 0, None).await; 431 | assert!(init_result.is_ok(), "Init should work"); 432 | 433 | // Ensure the state is properly updated: 434 | 435 | // Alice receives the wrapped region: 436 | let balance_of = MessageBuilder::::from_account_id( 437 | contract_acc_id.clone(), 438 | ) 439 | .call(|xc_regions| xc_regions.balance_of(address_of!(Alice))); 440 | let balance_of_res = client.call_dry_run(&ink_e2e::alice(), &balance_of, 0, None).await; 441 | assert_eq!(balance_of_res.return_value(), 1); 442 | 443 | let owner_of = MessageBuilder::::from_account_id( 444 | contract_acc_id.clone(), 445 | ) 446 | .call(|xc_regions| xc_regions.owner_of(Id::U128(0))); 447 | let owner_of_res = client.call_dry_run(&ink_e2e::alice(), &owner_of, 0, None).await; 448 | assert_eq!(owner_of_res.return_value(), Some(address_of!(Alice))); 449 | 450 | // The metadata is properly stored: 451 | let get_metadata = 452 | MessageBuilder::::from_account_id( 453 | contract_acc_id.clone(), 454 | ) 455 | .call(|xc_regions| xc_regions.get_metadata(Id::U128(raw_region_id))); 456 | let get_metadata_res = 457 | client.call_dry_run(&ink_e2e::alice(), &get_metadata, 0, None).await; 458 | 459 | assert_eq!(get_metadata_res.return_value(), Ok(VersionedRegion { version: 0, region })); 460 | 461 | Ok(()) 462 | } 463 | 464 | #[ink_e2e::test(environment = ExtendedEnvironment)] 465 | async fn remove_works(mut client: E2EBackend) -> E2EResult<()> { 466 | let constructor = XcRegionsRef::new(); 467 | let contract_acc_id = client 468 | .instantiate("xc-regions", &ink_e2e::alice(), constructor, 0, None) 469 | .await 470 | .expect("instantiate failed") 471 | .account_id; 472 | 473 | let raw_region_id = 0u128; 474 | let region = Region::default(); 475 | 476 | // Create region: collection 477 | let call_data = vec![ 478 | Value::u128(REGIONS_COLLECTION_ID.into()), 479 | Value::unnamed_variant("Id", [Value::from_bytes(&address_of!(Alice))]), 480 | ]; 481 | client 482 | .runtime_call(&ink_e2e::alice(), "Uniques", "create", call_data) 483 | .await 484 | .expect("creating a collection failed"); 485 | 486 | // Mint region: 487 | let call_data = vec![ 488 | Value::u128(REGIONS_COLLECTION_ID.into()), 489 | Value::u128(raw_region_id.into()), 490 | Value::unnamed_variant("Id", [Value::from_bytes(&address_of!(Alice))]), 491 | ]; 492 | client 493 | .runtime_call(&ink_e2e::alice(), "Uniques", "mint", call_data) 494 | .await 495 | .expect("minting a region failed"); 496 | 497 | // Approve transfer region: 498 | let call_data = vec![ 499 | Value::u128(REGIONS_COLLECTION_ID.into()), 500 | Value::u128(raw_region_id.into()), 501 | Value::unnamed_variant("Id", [Value::from_bytes(contract_acc_id)]), 502 | ]; 503 | client 504 | .runtime_call(&ink_e2e::alice(), "Uniques", "approve_transfer", call_data) 505 | .await 506 | .expect("approving transfer failed"); 507 | 508 | let init = MessageBuilder::::from_account_id( 509 | contract_acc_id.clone(), 510 | ) 511 | .call(|xc_regions| xc_regions.init(Id::U128(raw_region_id), region.clone())); 512 | let init_result = client.call(&ink_e2e::alice(), init, 0, None).await; 513 | assert!(init_result.is_ok(), "Init should succeed"); 514 | 515 | let remove = MessageBuilder::::from_account_id( 516 | contract_acc_id.clone(), 517 | ) 518 | .call(|xc_regions| xc_regions.remove(Id::U128(raw_region_id))); 519 | 520 | let remove_result = client.call(&ink_e2e::alice(), remove, 0, None).await; 521 | assert!(remove_result.is_ok(), "Remove should work"); 522 | 523 | // Ensure the state is properly updated: 524 | 525 | // Alice no longer holds the wrapped region: 526 | let balance_of = MessageBuilder::::from_account_id( 527 | contract_acc_id.clone(), 528 | ) 529 | .call(|xc_regions| xc_regions.balance_of(address_of!(Alice))); 530 | let balance_of_res = client.call_dry_run(&ink_e2e::alice(), &balance_of, 0, None).await; 531 | assert_eq!(balance_of_res.return_value(), 0); 532 | 533 | let owner_of = MessageBuilder::::from_account_id( 534 | contract_acc_id.clone(), 535 | ) 536 | .call(|xc_regions| xc_regions.owner_of(Id::U128(0))); 537 | let owner_of_res = client.call_dry_run(&ink_e2e::alice(), &owner_of, 0, None).await; 538 | assert_eq!(owner_of_res.return_value(), None); 539 | 540 | // The metadata should be removed: 541 | let get_metadata = 542 | MessageBuilder::::from_account_id( 543 | contract_acc_id.clone(), 544 | ) 545 | .call(|xc_regions| xc_regions.get_metadata(Id::U128(raw_region_id))); 546 | let get_metadata_res = 547 | client.call_dry_run(&ink_e2e::alice(), &get_metadata, 0, None).await; 548 | 549 | assert_eq!(get_metadata_res.return_value(), Err(XcRegionsError::MetadataNotFound)); 550 | 551 | Ok(()) 552 | } 553 | } 554 | } 555 | --------------------------------------------------------------------------------