diff --git a/Cargo.toml b/Cargo.toml index 93e74297..05451ba3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,4 +67,6 @@ members = [ "contracts/seed-nft-minter/meta", "contracts/token-release", "contracts/token-release/meta", + "contracts/mvx-game-sc", + "contracts/mvx-game-sc/meta" ] diff --git a/contracts/mvx-game-sc/.gitignore b/contracts/mvx-game-sc/.gitignore new file mode 100644 index 00000000..dd49a952 --- /dev/null +++ b/contracts/mvx-game-sc/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +*/target/ + +# The mxpy output +/output*/ + +# Mandos test trace +trace*.scen.json diff --git a/contracts/mvx-game-sc/Cargo.toml b/contracts/mvx-game-sc/Cargo.toml new file mode 100644 index 00000000..94c08f4b --- /dev/null +++ b/contracts/mvx-game-sc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mvx-game-sc" +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.43.4" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.43.4" diff --git a/contracts/mvx-game-sc/README.md b/contracts/mvx-game-sc/README.md new file mode 100644 index 00000000..064dcfce --- /dev/null +++ b/contracts/mvx-game-sc/README.md @@ -0,0 +1,3 @@ +# lib + +`lib` is a simple Smart Contract. diff --git a/contracts/mvx-game-sc/meta/Cargo.toml b/contracts/mvx-game-sc/meta/Cargo.toml new file mode 100644 index 00000000..77f0a821 --- /dev/null +++ b/contracts/mvx-game-sc/meta/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mvx-game-sc-meta" +version = "0.0.0" +edition = "2018" +publish = false +authors = [ "you",] + +[dev-dependencies] + +[dependencies.mvx-game-sc] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.43.4" diff --git a/contracts/mvx-game-sc/meta/src/main.rs b/contracts/mvx-game-sc/meta/src/main.rs new file mode 100644 index 00000000..22154128 --- /dev/null +++ b/contracts/mvx-game-sc/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/contracts/mvx-game-sc/multiversx.json b/contracts/mvx-game-sc/multiversx.json new file mode 100644 index 00000000..73655396 --- /dev/null +++ b/contracts/mvx-game-sc/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/mvx-game-sc/mxpy-up.py b/contracts/mvx-game-sc/mxpy-up.py new file mode 100644 index 00000000..8cfacc3e --- /dev/null +++ b/contracts/mvx-game-sc/mxpy-up.py @@ -0,0 +1,379 @@ +import logging +import os +import os.path +import shutil +import stat +import subprocess +import sys +from argparse import ArgumentParser +from pathlib import Path +from typing import List, Tuple + +logger = logging.getLogger("installer") + +MIN_REQUIRED_PYTHON_VERSION = (3, 8, 0) +sdk_path = Path("~/multiversx-sdk").expanduser().resolve() + + +def main(): + parser = ArgumentParser() + parser.add_argument("--exact-version", help="the exact version of mxpy to install") + parser.add_argument("--from-branch", help="use a branch of multiversx/mx-sdk-py-cli") + parser.add_argument("--not-interactive", action="store_true", default=False) + parser.add_argument("--verbose", action="store_true", default=False) + parser.set_defaults(modify_path=True) + args = parser.parse_args() + + exact_version = args.exact_version + from_branch = args.from_branch + interactive = not args.not_interactive + verbose = args.verbose + + log_level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig(level=log_level) + + if get_operating_system() == "windows": + print(""" +IMPORTANT NOTE +============== + +Windows support is limited and experimental. +""") + confirm_continuation(interactive) + + guard_non_root_user() + guard_python_version() + migrate_v6(interactive) + + # In case of a fresh install: + sdk_path.mkdir(parents=True, exist_ok=True) + create_venv() + logger.info("Installing the necessary dependencies...") + install_mxpy(exact_version, from_branch, verbose) + + run_post_install_checks() + + if interactive: + guide_system_path_integration() + + +def guard_non_root_user(): + logger.debug("Checking user (should not be root).") + + operating_system = get_operating_system() + + if operating_system == "windows": + return + if os.getuid() == 0: + raise InstallError("You should not install mxpy as root.") + + +def guard_python_version(): + python_version = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro) + + logger.debug("Checking Python version.") + logger.debug(f"Python version: {format_version(python_version)}") + if python_version < MIN_REQUIRED_PYTHON_VERSION: + raise InstallError(f"You need Python {format_version(MIN_REQUIRED_PYTHON_VERSION)} or later.") + + +def format_version(version: Tuple[int, int, int]) -> str: + major, minor, patch = version + return f"{major}.{minor}.{patch}" + + +def get_operating_system(): + aliases = { + "linux": "linux", + "linux1": "linux", + "linux2": "linux", + "darwin": "osx", + "win32": "windows", + "cygwin": "windows", + "msys": "windows" + } + + operating_system = aliases.get(sys.platform) + if operating_system is None: + raise InstallError(f"Unknown platform: {sys.platform}") + + return operating_system + + +def migrate_v6(interactive: bool): + nodejs_folder = sdk_path / "nodejs" + + if nodejs_folder.exists(): + print(f""" +In previous versions of the SDK, the "wasm-opt" tool was installed in the "nodejs" folder. + +This is no longer the case - now, "wasm-opt" is a separate module. + +The following folder will be removed: {nodejs_folder}. + +You may need to reinstall wasm-opt using `mxpy deps install wasm-opt`. +""") + confirm_continuation(interactive) + + shutil.rmtree(nodejs_folder) + + global_testnet_toml = sdk_path / "testnet.toml" + if global_testnet_toml.exists(): + global_testnet_toml.unlink() + + +def create_venv(): + require_python_venv_tools() + venv_folder = get_mxpy_venv_path() + venv_folder.mkdir(parents=True, exist_ok=True) + + logger.debug(f"Creating virtual environment in: {venv_folder}.") + import venv + builder = venv.EnvBuilder(with_pip=True, symlinks=True) + builder.clear_directory(venv_folder) + builder.create(venv_folder) + + logger.debug(f"Virtual environment has been created in: {venv_folder}.") + + +def require_python_venv_tools(): + operating_system = get_operating_system() + + try: + import ensurepip + import venv + logger.debug(f"Packages found: {ensurepip}, {venv}.") + except ModuleNotFoundError: + if operating_system == "linux": + python_venv = f"python{sys.version_info.major}.{sys.version_info.minor}-venv" + raise InstallError(f'Packages [venv] or [ensurepip] not found. Please run "sudo apt install {python_venv}" and then run mxpy-up again.') + else: + raise InstallError("Packages [venv] or [ensurepip] not found, please install them first. See https://docs.python.org/3/tutorial/venv.html.") + + +def get_mxpy_venv_path(): + return sdk_path / "mxpy-venv" + + +def install_mxpy(exact_version: str, from_branch: str, verbose: bool): + logger.debug("Installing mxpy in virtual environment...") + + if from_branch: + package_to_install = f"https://github.com/multiversx/mx-sdk-py-cli/archive/refs/heads/{from_branch}.zip" + else: + package_to_install = "multiversx_sdk_cli" if not exact_version else f"multiversx_sdk_cli=={exact_version}" + + venv_path = get_mxpy_venv_path() + + return_code = run_in_venv(["python3", "-m", "pip", "install", "--upgrade", "pip"], venv_path, verbose) + if return_code != 0: + raise InstallError("Could not upgrade pip.") + return_code = run_in_venv(["pip3", "install", "--no-cache-dir", package_to_install], venv_path, verbose) + if return_code != 0: + raise InstallError("Could not install mxpy.") + + logger.debug("Creating mxpy shortcut...") + + shortcut_path = sdk_path / "mxpy" + + try: + shortcut_path.unlink() + logger.debug(f"Removed existing shortcut: {shortcut_path}") + except FileNotFoundError: + logger.debug(f"Shortcut does not exist yet: {shortcut_path}") + pass + + shortcut_content = get_mxpy_shortcut_content() + shortcut_path.write_text(shortcut_content) + + st = os.stat(shortcut_path) + os.chmod(shortcut_path, st.st_mode | stat.S_IEXEC) + + logger.info("You have successfully installed mxpy.") + + +def get_mxpy_shortcut_content(): + operating_system = get_operating_system() + venv_path = get_mxpy_venv_path() + + if operating_system == "windows": + return f"""#!/bin/sh +. "{venv_path / 'Scripts' / 'activate'}" && python3 -m multiversx_sdk_cli.cli "$@" && deactivate +""" + + return f"""#!/bin/sh +. "{venv_path / 'bin' / 'activate'}" && python3 -m multiversx_sdk_cli.cli "$@" && deactivate +""" + + +def run_in_venv(args: List[str], venv_path: Path, verbose: bool): + env = os.environ.copy() + + if "PYTHONHOME" in env: + del env["PYTHONHOME"] + + env["PATH"] = str(venv_path / "bin") + ":" + env["PATH"] + env["VIRTUAL_ENV"] = str(venv_path) + + if verbose: + process = subprocess.Popen(args, env=env) + else: + process = subprocess.Popen(args, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + + return process.wait() + + +def run_post_install_checks(): + multiversx_sdk_path = Path("~/multiversx-sdk").expanduser() + + logger.debug("Running post-install checks...") + + if multiversx_sdk_path.exists(): + logger.debug("~/multiversx-sdk exists OK") + else: + logger.warning("~/multiversx-sdk exists NOK") + + if (multiversx_sdk_path / "mxpy").exists(): + logger.debug("~/multiversx-sdk/mxpy shortcut created OK") + else: + logger.warning("~/multiversx-sdk/mxpy shortcut created NOK") + + +def guide_system_path_integration(): + interactive = True + operating_system = get_operating_system() + + if operating_system == "windows": + print(f""" +############################################################################### +On Windows, for the "mxpy" command shortcut to be available, you MUST ADD the directory "{sdk_path}" to the system PATH. + +You can do this by following these steps: + +https://superuser.com/questions/949560/how-do-i-set-system-environment-variables-in-windows-10 + +############################################################################### +Do you UNDERSTAND the above? +############################################################################### +""") + confirm_continuation(interactive) + return + + old_export_directive = f'export PATH="{Path("~/elrondsdk").expanduser()}:$PATH"\t# elrond-sdk' + new_export_directive = f'export PATH="${{HOME}}/multiversx-sdk:$PATH"\t# multiversx-sdk' + + profile_files = get_profile_files() + + if not profile_files: + print(f""" +############################################################################### +No shell profile files have been found. + +The "mxpy" command shortcut will not be available until you add the directory "{sdk_path}" to the system PATH. +############################################################################### +Do you UNDERSTAND the above? +""") + confirm_continuation(interactive) + return + + profile_files_formatted = "\n".join(f" - {file}" for file in profile_files) + profile_files_contents = [profile_file.read_text() for profile_file in profile_files] + any_old_export_directive = any(old_export_directive in content for content in profile_files_contents) + any_new_export_directive = any(new_export_directive in content for content in profile_files_contents) + + if any_old_export_directive: + # We don't perform the removal automatically (a bit risky) + print(f""" +############################################################################### +It seems that the old path "~/elrondsdk" is still configured in shell profile. + +Please MANUALLY remove it from the shell profile (now or after the installer script ends). + +Your shell profile files: +{profile_files_formatted} + +The entry (entries) to remove: + {old_export_directive} +############################################################################### +Make sure you UNDERSTAND the above before proceeding further. +############################################################################### +""") + confirm_continuation(interactive) + + if any_new_export_directive: + # Note: in some (rare) cases, here we'll have false positives (e.g. if the export directive is commented out). + print(f""" +############################################################################### +It seems that the path "~/multiversx-sdk" is already configured in shell profile. + +To confirm this, CHECK the shell profile (now or after the installer script ends). + +Your shell profile files: +{profile_files_formatted} + +The entry to check (it should be present): + {new_export_directive}. +############################################################################### +Make sure you UNDERSTAND the above before proceeding further. +############################################################################### +""") + confirm_continuation(interactive) + return + + print(f""" +############################################################################### +In order to use the "mxpy" command shortcut, you have to manually extend the PATH variable to include "~/multiversx-sdk". + +In order to manually extend the PATH variable, add the following line to your shell profile file upon installation: + + export PATH="${{HOME}}/multiversx-sdk:${{PATH}}" + +Your shell profile files: +{profile_files_formatted} + +Upon editing the shell profile file, you may have to RESTART THE USER SESSION for the changes to take effect. +""") + confirm_continuation(interactive) + + +def get_profile_files() -> List[Path]: + files = [ + Path("~/.profile").expanduser().resolve(), + Path("~/.bashrc").expanduser().resolve(), + Path("~/.bash_profile").expanduser().resolve(), + Path("~/.zshrc").expanduser().resolve() + ] + + return [file for file in files if file.exists()] + + +class InstallError(Exception): + def __init__(self, message: str): + super().__init__(message) + + +def confirm_continuation(interactive: bool): + if not interactive: + return + + answer = input("Continue? (y/n)") + if answer.lower() not in ["y", "yes"]: + print("Confirmation not given. Will stop.") + exit(1) + + +if __name__ == "__main__": + try: + main() + except Exception as err: + logger.fatal(err) + sys.exit(1) + + print(""" +############################################################################### +Installer script finished successfully. +############################################################################### +For more information go to https://docs.multiversx.com. +For support, please contact us at http://discord.gg/MultiversXBuilders. +############################################################################### +""") diff --git a/contracts/mvx-game-sc/mxsc-template.toml b/contracts/mvx-game-sc/mxsc-template.toml new file mode 100644 index 00000000..7c73decd --- /dev/null +++ b/contracts/mvx-game-sc/mxsc-template.toml @@ -0,0 +1,19 @@ +name = "lib" +contract_trait = "lib" +src_file = "lib.rs" +rename_pairs = [ + [ + "blockchain.set_current_dir_from_workspace(\"contracts/examples/lib\");", + "// blockchain.set_current_dir_from_workspace(\"relative path to your workspace, if applicable\");", + ], +] +files_include = [ + "meta", + "scenarios", + "src", + "tests", + "wasm/Cargo.toml", + "Cargo.toml", + "README.md", + "multiversx.json", +] diff --git a/contracts/mvx-game-sc/scenarios/adder.scen.json b/contracts/mvx-game-sc/scenarios/adder.scen.json new file mode 100644 index 00000000..b031d1e5 --- /dev/null +++ b/contracts/mvx-game-sc/scenarios/adder.scen.json @@ -0,0 +1,99 @@ +{ + "name": "mvx-game-sc", + "comment": "add then check", + "gasSchedule": "v3", + "steps": [ + { + "step": "setState", + "accounts": { + "address:owner": { + "nonce": "1", + "balance": "0" + } + }, + "newAddresses": [ + { + "creatorAddress": "address:owner", + "creatorNonce": "1", + "newAddress": "sc:lib" + } + ] + }, + { + "step": "scDeploy", + "id": "1", + "tx": { + "from": "address:owner", + "contractCode": "file:../output/mvx-game-sc.wasm", + "arguments": [ + "5" + ], + "gasLimit": "5,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "", + "logs": "*", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scQuery", + "id": "2", + "tx": { + "to": "sc:lib", + "function": "getSum", + "arguments": [] + }, + "expect": { + "out": [ + "5" + ], + "status": "", + "logs": [] + } + }, + { + "step": "scCall", + "id": "3", + "tx": { + "from": "address:owner", + "to": "sc:lib", + "function": "add", + "arguments": [ + "3" + ], + "gasLimit": "5,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "", + "logs": "*", + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "*", + "balance": "0", + "storage": {}, + "code": "" + }, + "sc:lib": { + "nonce": "0", + "balance": "0", + "storage": { + "str:sum": "8" + }, + "code": "file:../output/mvx-game-sc.wasm" + } + } + } + ] +} \ No newline at end of file diff --git a/contracts/mvx-game-sc/scenarios/interactor_trace.scen.json b/contracts/mvx-game-sc/scenarios/interactor_trace.scen.json new file mode 100644 index 00000000..3337e6f8 --- /dev/null +++ b/contracts/mvx-game-sc/scenarios/interactor_trace.scen.json @@ -0,0 +1,81 @@ +{ + "steps": [ + { + "step": "setState", + "accounts": { + "0xe32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60": { + "nonce": "481", + "balance": "106274669842530000003", + "esdt": { + "str:CAN-14dc0a": "1000", + "str:CAN-2abf4b": "1000", + "str:CAN-6d39e6": "1000", + "str:CAN-ac1592": "1000" + }, + "username": "" + } + } + }, + { + "step": "setState", + "newAddresses": [ + { + "creatorAddress": "0xe32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", + "creatorNonce": "481", + "newAddress": "0x0000000000000000050028600ceb73ac22ec0b6f257aff7bed74dffa3ebfed60" + } + ] + }, + { + "step": "scDeploy", + "id": "", + "tx": { + "from": "0xe32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", + "contractCode": "file:../output/mvx-game-sc.wasm", + "arguments": [ + "0x00" + ], + "gasLimit": "70,000,000", + "gasPrice": "" + }, + "expect": { + "status": "0" + } + }, + { + "step": "scCall", + "id": "", + "tx": { + "from": "0xe32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", + "to": "0x0000000000000000050028600ceb73ac22ec0b6f257aff7bed74dffa3ebfed60", + "function": "add", + "arguments": [ + "0x07" + ], + "gasLimit": "70,000,000", + "gasPrice": "" + }, + "expect": { + "status": "0" + } + }, + { + "step": "scCall", + "id": "", + "tx": { + "from": "0xe32afedc904fe1939746ad973beb383563cf63642ba669b3040f9b9428a5ed60", + "to": "0x0000000000000000050028600ceb73ac22ec0b6f257aff7bed74dffa3ebfed60", + "function": "add", + "arguments": [ + "0x05" + ], + "gasLimit": "70,000,000", + "gasPrice": "" + }, + "expect": { + "status": "0" + } + } + ], + "name": "" +} \ No newline at end of file diff --git a/contracts/mvx-game-sc/src/lib.rs b/contracts/mvx-game-sc/src/lib.rs new file mode 100644 index 00000000..d5699b0c --- /dev/null +++ b/contracts/mvx-game-sc/src/lib.rs @@ -0,0 +1,87 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +pub mod owner; +pub mod private; +pub mod storage; +pub mod types; + +#[multiversx_sc::contract] +pub trait MvxGameSc: storage::StorageModule + owner::OwnerModule + private::PrivateModule { + #[init] + fn init( + &self, + enabled_opt: OptionalValue, + game_start_fee_opt: OptionalValue, + token_id_opt: OptionalValue, + ) { + match enabled_opt { + OptionalValue::Some(_) => self.enabled().set(true), + OptionalValue::None => {} + } + + match game_start_fee_opt { + OptionalValue::Some(val) => self.game_start_fee().set(val), + OptionalValue::None => { + require!(!self.game_start_fee().is_empty(), "game start fee not set") + } + } + + match token_id_opt { + OptionalValue::Some(val) => self.token_id().set(val), + OptionalValue::None => require!(!self.token_id().is_empty(), "fee token id not set"), + } + } + + #[payable("*")] + #[endpoint(createGame)] + fn create_game( + &self, + waiting_time: u64, + number_of_players_min: u64, + number_of_players_max: u64, + wager: BigUint, + ) { + self.require_enabled(); + + let (token_id, amount) = self.call_value().single_fungible_esdt(); + self.validate_create_game_payment(&token_id, &amount, &wager, waiting_time); + + let (min, max) = self.get_min_max(number_of_players_min, number_of_players_max); + + let caller = self.blockchain().get_caller(); + self.create_new_game(caller, waiting_time, min, max, wager); + } + + #[payable("*")] + #[endpoint(joinGame)] + fn join_game(&self, game_id: u64) { + self.require_enabled(); + + let (token_id, amount) = self.call_value().single_fungible_esdt(); + let now = self.blockchain().get_block_timestamp(); + let caller = self.blockchain().get_caller(); + + let game_settings = self.validate_join_game(&caller, now, &token_id, &amount, game_id); + + self.add_player(caller, game_id); + + self.refresh_game_status(game_id, game_settings); + } + + //manually claim back wager if the game is invalid + #[endpoint(claimBackWager)] + fn claim_back_wager(&self, game_id: u64) { + self.require_enabled(); + + let caller = self.blockchain().get_caller(); + let wager = self.validate_claim_wager(&caller, game_id); + + let token_id = self.token_id().get(); + self.send().direct(&caller, &token_id, 0u64, &wager); + + self.remove_player(caller, game_id); + } +} diff --git a/contracts/mvx-game-sc/src/owner.rs b/contracts/mvx-game-sc/src/owner.rs new file mode 100644 index 00000000..6d0f69eb --- /dev/null +++ b/contracts/mvx-game-sc/src/owner.rs @@ -0,0 +1,75 @@ +use crate::types::Status; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[multiversx_sc::module] +pub trait OwnerModule: crate::private::PrivateModule + crate::storage::StorageModule { + //u64 is percentage * 100 + //function called by the owner when the winners have been decided + #[only_owner] + #[endpoint(sendReward)] + fn send_reward( + &self, + game_id: u64, + winners: OptionalValue>, + ) { + self.require_enabled(); + + let game_settings = self.validate_send_reward(game_id); + let token_id = self.token_id().get(); + + match game_settings.status { + Status::Invalid => { + self.send_back_wager(game_id, &game_settings.wager, &token_id); + + let game_creation_fee = self.game_start_fee().get(); + self.send() + .direct(&game_settings.creator, &token_id, 0u64, &game_creation_fee); + } + Status::Valid => { + match winners { + OptionalValue::Some(val) => { + let len = self.players(game_id).len(); + let total_wager = &BigUint::from(len) * &game_settings.wager; + + for (winner, percentage) in val.into_iter() { + let reward_per_winner = + &BigUint::from(percentage) * &total_wager / &BigUint::from(100u64); + self.send() + .direct(&winner, &token_id, 0u64, &reward_per_winner); + } + } + //tie/draw + OptionalValue::None => { + self.send_back_wager(game_id, &game_settings.wager, &token_id); + } + } + } + } + } + + #[only_owner] + #[endpoint(enableSC)] + fn enable_sc(&self) { + self.enabled().set(true) + } + + #[only_owner] + #[endpoint(disableSC)] + fn disable_sc(&self) { + self.enabled().clear() + } + + #[only_owner] + #[endpoint(setTokenId)] + fn set_token_id(&self, token_id: EgldOrEsdtTokenIdentifier) { + self.token_id().set(token_id) + } + + #[only_owner] + #[endpoint(setGameStartFee)] + fn set_game_start_fee(&self, amount: BigUint) { + self.game_start_fee().set(amount) + } +} diff --git a/contracts/mvx-game-sc/src/private.rs b/contracts/mvx-game-sc/src/private.rs new file mode 100644 index 00000000..d68e3b0e --- /dev/null +++ b/contracts/mvx-game-sc/src/private.rs @@ -0,0 +1,177 @@ +use crate::types::{GameSettings, Status}; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[multiversx_sc::module] +pub trait PrivateModule: crate::storage::StorageModule { + //game + fn create_new_game( + &self, + caller: ManagedAddress, + waiting_time: u64, + min: u64, + max: u64, + wager: BigUint, + ) { + let new_id = self.get_new_game_id(); + self.last_game_id().set(new_id); + let now = self.blockchain().get_block_timestamp(); + + let time_limit = now + waiting_time; + let game_settings = GameSettings { + time_limit, + number_of_players_min: min, + number_of_players_max: max, + wager, + creator: caller, + status: Status::Invalid, + }; + self.game_settings(new_id).set(game_settings); + } + + fn add_player(&self, caller: ManagedAddress, game_id: u64) { + self.games_per_user(&caller).insert(game_id); + self.players(game_id).insert(caller); + } + + fn remove_player(&self, caller: ManagedAddress, game_id: u64) { + self.games_per_user(&caller).swap_remove(&game_id); + self.players(game_id).swap_remove(&caller); + } + + fn refresh_game_status(&self, game_id: u64, game_settings: GameSettings) { + let len = self.players(game_id).len() as u64; + if game_settings.number_of_players_min <= len { + self.game_settings(game_id) + .update(|val| val.status = Status::Valid); + } + } + + fn send_back_wager(&self, game_id: u64, wager: &BigUint, token_id: &EgldOrEsdtTokenIdentifier) { + for player in self.players(game_id).iter() { + self.send().direct(&player, token_id, 0u64, wager); + } + } + + //requires + fn validate_create_game_payment( + &self, + token_id: &TokenIdentifier, + amount: &BigUint, + wager: &BigUint, + waiting_time: u64, + ) { + require!(wager > &BigUint::zero(), "wager can't be 0"); + require!(waiting_time > 0u64, "waiting time can't be 0"); + + let approved_token_id = self.token_id().get(); + let start_fee = self.game_start_fee().get(); + + require!( + token_id == &approved_token_id && amount == &start_fee, + "start game payment not right" + ); + } + + fn validate_join_game( + &self, + caller: &ManagedAddress, + now: u64, + token_id: &TokenIdentifier, + amount: &BigUint, + game_id: u64, + ) -> GameSettings { + require!( + !self.game_settings(game_id).is_empty(), + "no settings for game id" + ); + let game_settings = self.game_settings(game_id).get(); + let accepted_token_id = self.token_id().get(); + + require!( + !self.games_per_user(caller).contains(&game_id), + "user already joined this game" + ); + + require!(now <= game_settings.time_limit, "waiting time has passed"); + + let len = self.players(game_id).len() as u64; + require!( + len < game_settings.number_of_players_max, + "max number of players reached" + ); + + require!(token_id == &accepted_token_id, "wrong token sent"); + require!(amount == &game_settings.wager, "wrong amount paid"); + + game_settings + } + + fn validate_claim_wager(&self, caller: &ManagedAddress, game_id: u64) -> BigUint { + require!( + !self.game_settings(game_id).is_empty(), + "no settings for game id" + ); + + require!( + self.games_per_user(caller).contains(&game_id), + "caller has not joined the game" + ); + + let game_settings = self.game_settings(game_id).get(); + let now = self.blockchain().get_block_timestamp(); + + require!( + now > game_settings.time_limit, + "waiting time is not over yet" + ); + + require!( + game_settings.status == Status::Invalid, + "can manually claim back wager only if the game is invalid" + ); + + game_settings.wager + } + + fn validate_send_reward(&self, game_id: u64) -> GameSettings { + require!( + !self.game_settings(game_id).is_empty(), + "no settings for game id" + ); + + let game_settings = self.game_settings(game_id).get(); + let now = self.blockchain().get_block_timestamp(); + + require!( + now > game_settings.time_limit, + "waiting time is not over yet" + ); + + game_settings + } + + fn require_enabled(&self) { + require!(!self.enabled().is_empty(), "maintenance") + } + + //helper + fn get_new_game_id(&self) -> u64 { + if self.last_game_id().is_empty() { + return 1u64; + } + let last_id = self.last_game_id().get(); + last_id + 1u64 + } + + fn get_min_max(&self, a: u64, b: u64) -> (u64, u64) { + require!(a != 0u64 && b != 0u64, "number of players cannot be 0"); + + if a > b { + return (b, a); + } + + (a, b) + } +} diff --git a/contracts/mvx-game-sc/src/storage.rs b/contracts/mvx-game-sc/src/storage.rs new file mode 100644 index 00000000..35383377 --- /dev/null +++ b/contracts/mvx-game-sc/src/storage.rs @@ -0,0 +1,38 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use crate::types::GameSettings; + +#[multiversx_sc::module] +pub trait StorageModule { + //GENERAL SETTINGS + #[view(getTokenId)] + #[storage_mapper("tokenId")] + fn token_id(&self) -> SingleValueMapper; + + #[view(getGameStartFee)] + #[storage_mapper("gameStartFee")] + fn game_start_fee(&self) -> SingleValueMapper; + + #[view(getEnabled)] + #[storage_mapper("enabled")] + fn enabled(&self) -> SingleValueMapper; + + //GAME + #[view(getLastGameId)] + #[storage_mapper("lastGameId")] + fn last_game_id(&self) -> SingleValueMapper; + + #[view(getGameSettings)] + #[storage_mapper("gameSettings")] + fn game_settings(&self, game_id: u64) -> SingleValueMapper>; + + #[view(getPlayers)] + #[storage_mapper("players")] + fn players(&self, game_id: u64) -> UnorderedSetMapper; + + //USERS + #[view(getGamesPerUser)] + #[storage_mapper("gamesPerUser")] + fn games_per_user(&self, user: &ManagedAddress) -> UnorderedSetMapper; +} diff --git a/contracts/mvx-game-sc/src/types.rs b/contracts/mvx-game-sc/src/types.rs new file mode 100644 index 00000000..e4e61392 --- /dev/null +++ b/contracts/mvx-game-sc/src/types.rs @@ -0,0 +1,39 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + + +#[derive( + TopEncode, + TopDecode, + TypeAbi, + NestedEncode, + NestedDecode, + Clone, + ManagedVecItem, + Debug, + PartialEq, +)] +pub enum Status { + Valid, + Invalid, +} + +#[derive( + TopEncode, + TopDecode, + TypeAbi, + NestedEncode, + NestedDecode, + Clone, + ManagedVecItem, + Debug, + PartialEq, +)] +pub struct GameSettings { + pub time_limit: u64, //start_time + waiting time + pub number_of_players_min: u64, //min and max + pub number_of_players_max: u64, + pub wager: BigUint, + pub creator: ManagedAddress, + pub status: Status +} diff --git a/contracts/mvx-game-sc/wasm/Cargo.lock b/contracts/mvx-game-sc/wasm/Cargo.lock new file mode 100644 index 00000000..abc2a424 --- /dev/null +++ b/contracts/mvx-game-sc/wasm/Cargo.lock @@ -0,0 +1,219 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "multiversx-sc" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406939660d0c79dd191c6677f4b048df873a95f4531d8abafc9cdbe282bf1725" +dependencies = [ + "bitflags", + "hashbrown", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e15b46c17b87c0c7cdd79b041a4abd7f3a2b45f3c993f6ce38c0f233e82b6" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7bc0762cd6d88f8bc54805bc652b042a61cd7fbc2d0a325010f088b78fb2ac" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e006240993963b482fe0682ae49b2d07255495e3c86706925d119137376cdfc" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e721d1bc80de2ede4099a9040519486c3c1139cb0287d8fc4f9fc3e8a3f19e" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "mvx-game-sc" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "mvx-game-sc-wasm" +version = "0.0.0" +dependencies = [ + "multiversx-sc-wasm-adapter", + "mvx-game-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/contracts/mvx-game-sc/wasm/Cargo.toml b/contracts/mvx-game-sc/wasm/Cargo.toml new file mode 100644 index 00000000..bc6eb69b --- /dev/null +++ b/contracts/mvx-game-sc/wasm/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mvx-game-sc-wasm" +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[lib] +crate-type = [ "cdylib",] + +[workspace] +members = [ ".",] + +[dev-dependencies] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" + +[dependencies.mvx-game-sc] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.43.4" diff --git a/contracts/mvx-game-sc/wasm/src/lib.rs b/contracts/mvx-game-sc/wasm/src/lib.rs new file mode 100644 index 00000000..23cdd65d --- /dev/null +++ b/contracts/mvx-game-sc/wasm/src/lib.rs @@ -0,0 +1,43 @@ +// Code generated by the multiversx-sc multi-contract system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 15 +// Async Callback (empty): 1 +// Total number of exported functions: 17 + +#![no_std] + +// Configuration that works with rustc < 1.73.0. +// TODO: Recommended rustc version: 1.73.0 or newer. +#![feature(lang_items)] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + mvx_game_sc + ( + init => init + createGame => create_game + joinGame => join_game + claimBackWager => claim_back_wager + getTokenId => token_id + getGameStartFee => game_start_fee + getEnabled => enabled + getLastGameId => last_game_id + getGameSettings => game_settings + getPlayers => players + getGamesPerUser => games_per_user + sendReward => send_reward + enableSC => enable_sc + disableSC => disable_sc + setTokenId => set_token_id + setGameStartFee => set_game_start_fee + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {}