From e6a942de570c3000012ac28784db6081e6336bb8 Mon Sep 17 00:00:00 2001 From: Kellan Wampler Date: Tue, 5 Mar 2024 10:25:07 -0500 Subject: [PATCH] First pass atAchievements contract. --- cli/web3cli/AchievementFacet.py | 940 +++++++++++++++++++++++ cli/web3cli/core.py | 185 ++++- cli/web3cli/test_achievement.py | 194 +++++ contracts/inventory/AchievementFacet.sol | 243 ++++++ contracts/inventory/InventoryFacet.sol | 4 +- 5 files changed, 1563 insertions(+), 3 deletions(-) create mode 100644 cli/web3cli/AchievementFacet.py create mode 100644 cli/web3cli/test_achievement.py create mode 100644 contracts/inventory/AchievementFacet.sol diff --git a/cli/web3cli/AchievementFacet.py b/cli/web3cli/AchievementFacet.py new file mode 100644 index 00000000..d3ac6172 --- /dev/null +++ b/cli/web3cli/AchievementFacet.py @@ -0,0 +1,940 @@ +# Code generated by moonworm : https://github.com/bugout-dev/moonworm +# Moonworm version : 0.6.2 + +import argparse +import json +import os +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from brownie import Contract, network, project +from brownie.network.contract import ContractContainer +from eth_typing.evm import ChecksumAddress + + +PROJECT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +BUILD_DIRECTORY = os.path.join(PROJECT_DIRECTORY, "build", "contracts") + + +def boolean_argument_type(raw_value: str) -> bool: + TRUE_VALUES = ["1", "t", "y", "true", "yes"] + FALSE_VALUES = ["0", "f", "n", "false", "no"] + + if raw_value.lower() in TRUE_VALUES: + return True + elif raw_value.lower() in FALSE_VALUES: + return False + + raise ValueError( + f"Invalid boolean argument: {raw_value}. Value must be one of: {','.join(TRUE_VALUES + FALSE_VALUES)}" + ) + + +def bytes_argument_type(raw_value: str) -> str: + return raw_value + + +def get_abi_json(abi_name: str) -> List[Dict[str, Any]]: + abi_full_path = os.path.join(BUILD_DIRECTORY, f"{abi_name}.json") + if not os.path.isfile(abi_full_path): + raise IOError( + f"File does not exist: {abi_full_path}. Maybe you have to compile the smart contracts?" + ) + + with open(abi_full_path, "r") as ifp: + build = json.load(ifp) + + abi_json = build.get("abi") + if abi_json is None: + raise ValueError(f"Could not find ABI definition in: {abi_full_path}") + + return abi_json + + +def contract_from_build(abi_name: str) -> ContractContainer: + # This is workaround because brownie currently doesn't support loading the same project multiple + # times. This causes problems when using multiple contracts from the same project in the same + # python project. + PROJECT = project.main.Project("moonworm", Path(PROJECT_DIRECTORY)) + + abi_full_path = os.path.join(BUILD_DIRECTORY, f"{abi_name}.json") + if not os.path.isfile(abi_full_path): + raise IOError( + f"File does not exist: {abi_full_path}. Maybe you have to compile the smart contracts?" + ) + + with open(abi_full_path, "r") as ifp: + build = json.load(ifp) + + return ContractContainer(PROJECT, build) + + +class AchievementFacet: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "AchievementFacet" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("AchievementFacet") + if self.address is not None: + self.contract: Optional[Contract] = Contract.from_abi( + self.contract_name, self.address, self.abi + ) + + def deploy(self, transaction_config): + contract_class = contract_from_build(self.contract_name) + deployed_contract = contract_class.deploy(transaction_config) + self.address = deployed_contract.address + self.contract = deployed_contract + return deployed_contract.tx + + def assert_contract_is_instantiated(self) -> None: + if self.contract is None: + raise Exception("contract has not been instantiated") + + def verify_contract(self): + self.assert_contract_is_instantiated() + contract_class = contract_from_build(self.contract_name) + contract_class.publish_source(self.contract) + + def admin_terminus_info( + self, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.adminTerminusInfo.call(block_identifier=block_number) + + def create_achievement_slot(self, metadata_uri: str, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createAchievementSlot(metadata_uri, transaction_config) + + def create_slot(self, persistent: bool, slot_uri: str, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createSlot(persistent, slot_uri, transaction_config) + + def equip( + self, + subject_token_id: int, + slot: int, + item_type: int, + item_address: ChecksumAddress, + item_token_id: int, + amount: int, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.equip( + subject_token_id, + slot, + item_type, + item_address, + item_token_id, + amount, + transaction_config, + ) + + def get_equipped_item( + self, + subject_token_id: int, + slot: int, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getEquippedItem.call( + subject_token_id, slot, block_identifier=block_number + ) + + def get_slot_by_id( + self, slot_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getSlotById.call(slot_id, block_identifier=block_number) + + def get_slot_uri( + self, slot_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getSlotURI.call(slot_id, block_identifier=block_number) + + def grant_admin_privilege(self, admin: ChecksumAddress, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.grantAdminPrivilege(admin, transaction_config) + + def init( + self, + admin_terminus_address: ChecksumAddress, + admin_terminus_pool_id: int, + contract_address: ChecksumAddress, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.init( + admin_terminus_address, + admin_terminus_pool_id, + contract_address, + transaction_config, + ) + + def initialize(self, subject_address: ChecksumAddress, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.initialize(subject_address, transaction_config) + + def mark_item_as_equippable_in_slot( + self, + slot: int, + item_type: int, + item_address: ChecksumAddress, + item_pool_id: int, + max_amount: int, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.markItemAsEquippableInSlot( + slot, item_type, item_address, item_pool_id, max_amount, transaction_config + ) + + def max_amount_of_item_in_slot( + self, + slot: int, + item_type: int, + item_address: ChecksumAddress, + item_pool_id: int, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.maxAmountOfItemInSlot.call( + slot, item_type, item_address, item_pool_id, block_identifier=block_number + ) + + def num_slots(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.numSlots.call(block_identifier=block_number) + + def on_erc1155_batch_received( + self, + arg1: ChecksumAddress, + arg2: ChecksumAddress, + arg3: List, + arg4: List, + arg5: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.onERC1155BatchReceived( + arg1, arg2, arg3, arg4, arg5, transaction_config + ) + + def on_erc1155_received( + self, + arg1: ChecksumAddress, + arg2: ChecksumAddress, + arg3: int, + arg4: int, + arg5: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.onERC1155Received( + arg1, arg2, arg3, arg4, arg5, transaction_config + ) + + def on_erc721_received( + self, + arg1: ChecksumAddress, + arg2: ChecksumAddress, + arg3: int, + arg4: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.onERC721Received( + arg1, arg2, arg3, arg4, transaction_config + ) + + def revoke_admin_privilege(self, admin: ChecksumAddress, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.revokeAdminPrivilege(admin, transaction_config) + + def set_slot_persistent( + self, slot_id: int, persistent: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setSlotPersistent(slot_id, persistent, transaction_config) + + def set_slot_uri(self, new_slot_uri: str, slot_id: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setSlotURI(new_slot_uri, slot_id, transaction_config) + + def slot_is_persistent( + self, slot_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.slotIsPersistent.call( + slot_id, block_identifier=block_number + ) + + def subject(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.subject.call(block_identifier=block_number) + + def supports_interface( + self, interface_id: bytes, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.supportsInterface.call( + interface_id, block_identifier=block_number + ) + + def unequip( + self, + subject_token_id: int, + slot: int, + unequip_all: bool, + amount: int, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.unequip( + subject_token_id, slot, unequip_all, amount, transaction_config + ) + + +def get_transaction_config(args: argparse.Namespace) -> Dict[str, Any]: + signer = network.accounts.load(args.sender, args.password) + transaction_config: Dict[str, Any] = {"from": signer} + if args.gas_price is not None: + transaction_config["gas_price"] = args.gas_price + if args.max_fee_per_gas is not None: + transaction_config["max_fee"] = args.max_fee_per_gas + if args.max_priority_fee_per_gas is not None: + transaction_config["priority_fee"] = args.max_priority_fee_per_gas + if args.confirmations is not None: + transaction_config["required_confs"] = args.confirmations + if args.nonce is not None: + transaction_config["nonce"] = args.nonce + return transaction_config + + +def add_default_arguments(parser: argparse.ArgumentParser, transact: bool) -> None: + parser.add_argument( + "--network", required=True, help="Name of brownie network to connect to" + ) + parser.add_argument( + "--address", required=False, help="Address of deployed contract to connect to" + ) + if not transact: + parser.add_argument( + "--block-number", + required=False, + type=int, + help="Call at the given block number, defaults to latest", + ) + return + parser.add_argument( + "--sender", required=True, help="Path to keystore file for transaction sender" + ) + parser.add_argument( + "--password", + required=False, + help="Password to keystore file (if you do not provide it, you will be prompted for it)", + ) + parser.add_argument( + "--gas-price", default=None, help="Gas price at which to submit transaction" + ) + parser.add_argument( + "--max-fee-per-gas", + default=None, + help="Max fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--max-priority-fee-per-gas", + default=None, + help="Max priority fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--confirmations", + type=int, + default=None, + help="Number of confirmations to await before considering a transaction completed", + ) + parser.add_argument( + "--nonce", type=int, default=None, help="Nonce for the transaction (optional)" + ) + parser.add_argument( + "--value", default=None, help="Value of the transaction in wei(optional)" + ) + parser.add_argument("--verbose", action="store_true", help="Print verbose output") + + +def handle_deploy(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = get_transaction_config(args) + contract = AchievementFacet(None) + result = contract.deploy(transaction_config=transaction_config) + print(result) + if args.verbose: + print(result.info()) + + +def handle_verify_contract(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.verify_contract() + print(result) + + +def handle_admin_terminus_info(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.admin_terminus_info(block_number=args.block_number) + print(result) + + +def handle_create_achievement_slot(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_achievement_slot( + metadata_uri=args.metadata_uri, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_create_slot(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_slot( + persistent=args.persistent, + slot_uri=args.slot_uri, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_equip(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.equip( + subject_token_id=args.subject_token_id, + slot=args.slot, + item_type=args.item_type, + item_address=args.item_address, + item_token_id=args.item_token_id, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_get_equipped_item(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.get_equipped_item( + subject_token_id=args.subject_token_id, + slot=args.slot, + block_number=args.block_number, + ) + print(result) + + +def handle_get_slot_by_id(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.get_slot_by_id( + slot_id=args.slot_id, block_number=args.block_number + ) + print(result) + + +def handle_get_slot_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.get_slot_uri(slot_id=args.slot_id, block_number=args.block_number) + print(result) + + +def handle_grant_admin_privilege(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.grant_admin_privilege( + admin=args.admin, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_init(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.init( + admin_terminus_address=args.admin_terminus_address, + admin_terminus_pool_id=args.admin_terminus_pool_id, + contract_address=args.contract_address, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_initialize(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.initialize( + subject_address=args.subject_address, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_mark_item_as_equippable_in_slot(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.mark_item_as_equippable_in_slot( + slot=args.slot, + item_type=args.item_type, + item_address=args.item_address, + item_pool_id=args.item_pool_id, + max_amount=args.max_amount, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_max_amount_of_item_in_slot(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.max_amount_of_item_in_slot( + slot=args.slot, + item_type=args.item_type, + item_address=args.item_address, + item_pool_id=args.item_pool_id, + block_number=args.block_number, + ) + print(result) + + +def handle_num_slots(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.num_slots(block_number=args.block_number) + print(result) + + +def handle_on_erc1155_batch_received(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.on_erc1155_batch_received( + arg1=args.arg1, + arg2=args.arg2, + arg3=args.arg3, + arg4=args.arg4, + arg5=args.arg5, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_on_erc1155_received(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.on_erc1155_received( + arg1=args.arg1, + arg2=args.arg2, + arg3=args.arg3, + arg4=args.arg4, + arg5=args.arg5, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_on_erc721_received(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.on_erc721_received( + arg1=args.arg1, + arg2=args.arg2, + arg3=args.arg3, + arg4=args.arg4, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_revoke_admin_privilege(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.revoke_admin_privilege( + admin=args.admin, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_slot_persistent(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_slot_persistent( + slot_id=args.slot_id, + persistent=args.persistent, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_slot_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_slot_uri( + new_slot_uri=args.new_slot_uri, + slot_id=args.slot_id, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_slot_is_persistent(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.slot_is_persistent( + slot_id=args.slot_id, block_number=args.block_number + ) + print(result) + + +def handle_subject(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.subject(block_number=args.block_number) + print(result) + + +def handle_supports_interface(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + result = contract.supports_interface( + interface_id=args.interface_id, block_number=args.block_number + ) + print(result) + + +def handle_unequip(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = AchievementFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.unequip( + subject_token_id=args.subject_token_id, + slot=args.slot, + unequip_all=args.unequip_all, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for AchievementFacet") + parser.set_defaults(func=lambda _: parser.print_help()) + subcommands = parser.add_subparsers() + + deploy_parser = subcommands.add_parser("deploy") + add_default_arguments(deploy_parser, True) + deploy_parser.set_defaults(func=handle_deploy) + + verify_contract_parser = subcommands.add_parser("verify-contract") + add_default_arguments(verify_contract_parser, False) + verify_contract_parser.set_defaults(func=handle_verify_contract) + + admin_terminus_info_parser = subcommands.add_parser("admin-terminus-info") + add_default_arguments(admin_terminus_info_parser, False) + admin_terminus_info_parser.set_defaults(func=handle_admin_terminus_info) + + create_achievement_slot_parser = subcommands.add_parser("create-achievement-slot") + add_default_arguments(create_achievement_slot_parser, True) + create_achievement_slot_parser.add_argument( + "--metadata-uri", required=True, help="Type: string", type=str + ) + create_achievement_slot_parser.set_defaults(func=handle_create_achievement_slot) + + create_slot_parser = subcommands.add_parser("create-slot") + add_default_arguments(create_slot_parser, True) + create_slot_parser.add_argument( + "--persistent", required=True, help="Type: bool", type=boolean_argument_type + ) + create_slot_parser.add_argument( + "--slot-uri", required=True, help="Type: string", type=str + ) + create_slot_parser.set_defaults(func=handle_create_slot) + + equip_parser = subcommands.add_parser("equip") + add_default_arguments(equip_parser, True) + equip_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + equip_parser.add_argument("--slot", required=True, help="Type: uint256", type=int) + equip_parser.add_argument( + "--item-type", required=True, help="Type: uint256", type=int + ) + equip_parser.add_argument("--item-address", required=True, help="Type: address") + equip_parser.add_argument( + "--item-token-id", required=True, help="Type: uint256", type=int + ) + equip_parser.add_argument("--amount", required=True, help="Type: uint256", type=int) + equip_parser.set_defaults(func=handle_equip) + + get_equipped_item_parser = subcommands.add_parser("get-equipped-item") + add_default_arguments(get_equipped_item_parser, False) + get_equipped_item_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + get_equipped_item_parser.add_argument( + "--slot", required=True, help="Type: uint256", type=int + ) + get_equipped_item_parser.set_defaults(func=handle_get_equipped_item) + + get_slot_by_id_parser = subcommands.add_parser("get-slot-by-id") + add_default_arguments(get_slot_by_id_parser, False) + get_slot_by_id_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int + ) + get_slot_by_id_parser.set_defaults(func=handle_get_slot_by_id) + + get_slot_uri_parser = subcommands.add_parser("get-slot-uri") + add_default_arguments(get_slot_uri_parser, False) + get_slot_uri_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int + ) + get_slot_uri_parser.set_defaults(func=handle_get_slot_uri) + + grant_admin_privilege_parser = subcommands.add_parser("grant-admin-privilege") + add_default_arguments(grant_admin_privilege_parser, True) + grant_admin_privilege_parser.add_argument( + "--admin", required=True, help="Type: address" + ) + grant_admin_privilege_parser.set_defaults(func=handle_grant_admin_privilege) + + init_parser = subcommands.add_parser("init") + add_default_arguments(init_parser, True) + init_parser.add_argument( + "--admin-terminus-address", required=True, help="Type: address" + ) + init_parser.add_argument( + "--admin-terminus-pool-id", required=True, help="Type: uint256", type=int + ) + init_parser.add_argument("--contract-address", required=True, help="Type: address") + init_parser.set_defaults(func=handle_init) + + initialize_parser = subcommands.add_parser("initialize") + add_default_arguments(initialize_parser, True) + initialize_parser.add_argument( + "--subject-address", required=True, help="Type: address" + ) + initialize_parser.set_defaults(func=handle_initialize) + + mark_item_as_equippable_in_slot_parser = subcommands.add_parser( + "mark-item-as-equippable-in-slot" + ) + add_default_arguments(mark_item_as_equippable_in_slot_parser, True) + mark_item_as_equippable_in_slot_parser.add_argument( + "--slot", required=True, help="Type: uint256", type=int + ) + mark_item_as_equippable_in_slot_parser.add_argument( + "--item-type", required=True, help="Type: uint256", type=int + ) + mark_item_as_equippable_in_slot_parser.add_argument( + "--item-address", required=True, help="Type: address" + ) + mark_item_as_equippable_in_slot_parser.add_argument( + "--item-pool-id", required=True, help="Type: uint256", type=int + ) + mark_item_as_equippable_in_slot_parser.add_argument( + "--max-amount", required=True, help="Type: uint256", type=int + ) + mark_item_as_equippable_in_slot_parser.set_defaults( + func=handle_mark_item_as_equippable_in_slot + ) + + max_amount_of_item_in_slot_parser = subcommands.add_parser( + "max-amount-of-item-in-slot" + ) + add_default_arguments(max_amount_of_item_in_slot_parser, False) + max_amount_of_item_in_slot_parser.add_argument( + "--slot", required=True, help="Type: uint256", type=int + ) + max_amount_of_item_in_slot_parser.add_argument( + "--item-type", required=True, help="Type: uint256", type=int + ) + max_amount_of_item_in_slot_parser.add_argument( + "--item-address", required=True, help="Type: address" + ) + max_amount_of_item_in_slot_parser.add_argument( + "--item-pool-id", required=True, help="Type: uint256", type=int + ) + max_amount_of_item_in_slot_parser.set_defaults( + func=handle_max_amount_of_item_in_slot + ) + + num_slots_parser = subcommands.add_parser("num-slots") + add_default_arguments(num_slots_parser, False) + num_slots_parser.set_defaults(func=handle_num_slots) + + on_erc1155_batch_received_parser = subcommands.add_parser( + "on-erc1155-batch-received" + ) + add_default_arguments(on_erc1155_batch_received_parser, True) + on_erc1155_batch_received_parser.add_argument( + "--arg1", required=True, help="Type: address" + ) + on_erc1155_batch_received_parser.add_argument( + "--arg2", required=True, help="Type: address" + ) + on_erc1155_batch_received_parser.add_argument( + "--arg3", required=True, help="Type: uint256[]", nargs="+" + ) + on_erc1155_batch_received_parser.add_argument( + "--arg4", required=True, help="Type: uint256[]", nargs="+" + ) + on_erc1155_batch_received_parser.add_argument( + "--arg5", required=True, help="Type: bytes", type=bytes_argument_type + ) + on_erc1155_batch_received_parser.set_defaults(func=handle_on_erc1155_batch_received) + + on_erc1155_received_parser = subcommands.add_parser("on-erc1155-received") + add_default_arguments(on_erc1155_received_parser, True) + on_erc1155_received_parser.add_argument( + "--arg1", required=True, help="Type: address" + ) + on_erc1155_received_parser.add_argument( + "--arg2", required=True, help="Type: address" + ) + on_erc1155_received_parser.add_argument( + "--arg3", required=True, help="Type: uint256", type=int + ) + on_erc1155_received_parser.add_argument( + "--arg4", required=True, help="Type: uint256", type=int + ) + on_erc1155_received_parser.add_argument( + "--arg5", required=True, help="Type: bytes", type=bytes_argument_type + ) + on_erc1155_received_parser.set_defaults(func=handle_on_erc1155_received) + + on_erc721_received_parser = subcommands.add_parser("on-erc721-received") + add_default_arguments(on_erc721_received_parser, True) + on_erc721_received_parser.add_argument( + "--arg1", required=True, help="Type: address" + ) + on_erc721_received_parser.add_argument( + "--arg2", required=True, help="Type: address" + ) + on_erc721_received_parser.add_argument( + "--arg3", required=True, help="Type: uint256", type=int + ) + on_erc721_received_parser.add_argument( + "--arg4", required=True, help="Type: bytes", type=bytes_argument_type + ) + on_erc721_received_parser.set_defaults(func=handle_on_erc721_received) + + revoke_admin_privilege_parser = subcommands.add_parser("revoke-admin-privilege") + add_default_arguments(revoke_admin_privilege_parser, True) + revoke_admin_privilege_parser.add_argument( + "--admin", required=True, help="Type: address" + ) + revoke_admin_privilege_parser.set_defaults(func=handle_revoke_admin_privilege) + + set_slot_persistent_parser = subcommands.add_parser("set-slot-persistent") + add_default_arguments(set_slot_persistent_parser, True) + set_slot_persistent_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int + ) + set_slot_persistent_parser.add_argument( + "--persistent", required=True, help="Type: bool", type=boolean_argument_type + ) + set_slot_persistent_parser.set_defaults(func=handle_set_slot_persistent) + + set_slot_uri_parser = subcommands.add_parser("set-slot-uri") + add_default_arguments(set_slot_uri_parser, True) + set_slot_uri_parser.add_argument( + "--new-slot-uri", required=True, help="Type: string", type=str + ) + set_slot_uri_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int + ) + set_slot_uri_parser.set_defaults(func=handle_set_slot_uri) + + slot_is_persistent_parser = subcommands.add_parser("slot-is-persistent") + add_default_arguments(slot_is_persistent_parser, False) + slot_is_persistent_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int + ) + slot_is_persistent_parser.set_defaults(func=handle_slot_is_persistent) + + subject_parser = subcommands.add_parser("subject") + add_default_arguments(subject_parser, False) + subject_parser.set_defaults(func=handle_subject) + + supports_interface_parser = subcommands.add_parser("supports-interface") + add_default_arguments(supports_interface_parser, False) + supports_interface_parser.add_argument( + "--interface-id", required=True, help="Type: bytes4", type=bytes_argument_type + ) + supports_interface_parser.set_defaults(func=handle_supports_interface) + + unequip_parser = subcommands.add_parser("unequip") + add_default_arguments(unequip_parser, True) + unequip_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + unequip_parser.add_argument("--slot", required=True, help="Type: uint256", type=int) + unequip_parser.add_argument( + "--unequip-all", required=True, help="Type: bool", type=boolean_argument_type + ) + unequip_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + unequip_parser.set_defaults(func=handle_unequip) + + return parser + + +def main() -> None: + parser = generate_cli() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/cli/web3cli/core.py b/cli/web3cli/core.py index 4de58ca6..a903c77b 100644 --- a/cli/web3cli/core.py +++ b/cli/web3cli/core.py @@ -14,6 +14,7 @@ abi, Lootbox, MockTerminus, + AchievementFacet, CraftingFacet, Diamond, DiamondCutFacet, @@ -24,6 +25,7 @@ CraftingFacet, GOFPFacet, InventoryFacet, + ITerminus, TerminusFacet, TerminusInitializer, ) @@ -36,6 +38,7 @@ "ReentrancyExploitable": ReentrancyExploitable, "CraftingFacet": CraftingFacet, "GOFPFacet": GOFPFacet, + "AchievementFacet": AchievementFacet, "InventoryFacet": InventoryFacet, "TerminusFacet": TerminusFacet, } @@ -47,6 +50,9 @@ "GOFPFacet": lambda address, *args: GOFPFacet.GOFPFacet( address ).contract.init.encode_input(*args), + "AchievementFacet": lambda address, *args: AchievementFacet.AchievementFacet( + address + ).contract.initialize.encode_input(*args), "InventoryFacet": lambda address, *args: InventoryFacet.InventoryFacet( address ).contract.init.encode_input(*args), @@ -71,6 +77,7 @@ class EngineFeatures(Enum): DROPPER = "DropperFacet" GOFP = "GOFPFacet" + ACHIEVEMNT="AchievementFacet" INVENTORY = "InventoryFacet" TERMINUS = "TerminusFacet" @@ -85,6 +92,7 @@ def feature_from_facet_name(facet_name: str) -> Optional[EngineFeatures]: FEATURE_FACETS: Dict[EngineFeatures, List[str]] = { EngineFeatures.DROPPER: ["DropperFacet"], EngineFeatures.GOFP: ["GOFPFacet"], + EngineFeatures.ACHIEVEMNT: ["AchievementFacet"], EngineFeatures.INVENTORY: ["InventoryFacet"], EngineFeatures.TERMINUS: ["TerminusFacet"], } @@ -92,6 +100,7 @@ def feature_from_facet_name(facet_name: str) -> Optional[EngineFeatures]: FEATURE_IGNORES: Dict[EngineFeatures, List[str]] = { EngineFeatures.DROPPER: {"methods": ["init"], "selectors": []}, EngineFeatures.GOFP: {"methods": ["init"], "selectors": []}, + EngineFeatures.ACHIEVEMNT: {"methods": ["initialize"], "selectors": []}, EngineFeatures.INVENTORY: {"methods": ["init"], "selectors": []}, EngineFeatures.TERMINUS: {"methods": [], "selectors": []}, } @@ -657,6 +666,94 @@ def gofp_gogogo( return deployment_info +def achievement_gogogo( + subject_erc721_address: str, + transaction_config: Dict[str, Any], + diamond_cut_address: Optional[str] = None, + diamond_address: Optional[str] = None, + diamond_loupe_address: Optional[str] = None, + ownership_address: Optional[str] = None, + achievement_facet_address: Optional[str] = None, + terminus_facet_address: Optional[str] = None, + terminus_initializer_address: Optional[str] = None, + verify_contracts: Optional[bool] = False, +) -> Dict[str, Any]: + """ + Deploys an EIP2535 Diamond contract and an InventoryFacet and mounts the InventoryFacet onto the Diamond contract. + + Returns the addresses and attachments. + """ + deployment_info = diamond_gogogo( + owner_address=transaction_config["from"].address, + transaction_config=transaction_config, + diamond_cut_address=diamond_cut_address, + diamond_address=diamond_address, + diamond_loupe_address=diamond_loupe_address, + ownership_address=ownership_address, + verify_contracts=verify_contracts, + ) + + if achievement_facet_address is None: + achievement_facet = AchievementFacet.AchievementFacet(None) + achievement_facet.deploy(transaction_config=transaction_config) + else: + achievement_facet = AchievementFacet.AchievementFacet(achievement_facet_address) + + deployment_info["contracts"]["AchievementFacet"] = achievement_facet.address + + if terminus_facet_address is None: + terminus_facet = TerminusFacet.TerminusFacet(None) + terminus_facet.deploy(transaction_config=transaction_config) + else: + terminus_facet = TerminusFacet.TerminusFacet(terminus_facet_address) + + deployment_info["contracts"]["TerminusFacet"] = terminus_facet.address + + if terminus_initializer_address is None: + terminus_initializer = TerminusInitializer.TerminusInitializer(None) + terminus_initializer.deploy(transaction_config=transaction_config) + terminus_initializer_address = terminus_initializer.address + + if verify_contracts: + try: + achievement_facet.verify_contract() + deployment_info["verified"].append("AchiveemntFacet") + except Exception as e: + deployment_info["verification_errors"].append(repr(e)) + + facet_cut( + deployment_info["contracts"]["Diamond"], + "TerminusFacet", + terminus_facet.address, + "add", + transaction_config, + initializer_address=terminus_initializer_address, + feature=EngineFeatures.TERMINUS, + initializer_args=[], + ) + deployment_info["attached"].append("TerminusFacet") + + terminus = ITerminus.ITerminus(deployment_info["contracts"]["Diamond"]) + terminus.set_controller(terminus.address, transaction_config) + + print("Set controller to be the achievement facet.") + + facet_cut( + deployment_info["contracts"]["Diamond"], + "AchievementFacet", + achievement_facet.address, + "add", + transaction_config, + initializer_address=achievement_facet.address, + feature=EngineFeatures.ACHIEVEMNT, + initializer_args=[ + subject_erc721_address, + ], + ) + deployment_info["attached"].append("AchievementFacet") + + return deployment_info + def inventory_gogogo( admin_terminus_address: str, @@ -761,7 +858,7 @@ def terminus_gogogo( if verify_contracts: try: terminus_facet.verify_contract() - deployment_info["verified"].append("InventoryFacet") + deployment_info["verified"].append("TerminusFacet") except Exception as e: deployment_info["verification_errors"].append(repr(e)) @@ -893,6 +990,24 @@ def handle_terminus_gogogo(args: argparse.Namespace) -> None: json.dump(result, args.outfile) json.dump(result, sys.stdout, indent=4) +def handle_achievement_gogogo(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = TerminusFacet.get_transaction_config(args) + result = achievement_gogogo( + subject_erc721_address=args.subject_erc721_address, + transaction_config=transaction_config, + diamond_cut_address=args.diamond_cut_address, + diamond_address=args.diamond_address, + diamond_loupe_address=args.diamond_loupe_address, + ownership_address=args.ownership_address, + terminus_facet_address=args.terminus_facet_address, + terminus_initializer_address=args.terminus_initializer_address, + verify_contracts=args.verify_contracts, + ) + if args.outfile is not None: + with args.outfile: + json.dump(result, args.outfile) + json.dump(result, sys.stdout, indent=4) def handle_crafting_gogogo(args: argparse.Namespace) -> None: network.connect(args.network) @@ -1068,8 +1183,76 @@ def generate_cli(): ) gofp_gogogo_parser.set_defaults(func=handle_gofp_gogogo) + achievement_gogogo_parser = subcommands.add_parser( + "achievement-gogogo", + help="Deploy Achievement diamond contract", + description="Deploy Achievement diamond contract", + ) + Diamond.add_default_arguments(achievement_gogogo_parser, transact=True) + achievement_gogogo_parser.add_argument( + "--verify-contracts", + action="store_true", + help="Verify contracts", + ) + achievement_gogogo_parser.add_argument( + "--subject-erc721-address", + required=True, + help="Address of ERC721 contract that the Inventory modifies", + ) + achievement_gogogo_parser.add_argument( + "--diamond-cut-address", + required=False, + default=None, + help="Address to deployed DiamondCutFacet. If provided, this command skips deployment of a new DiamondCutFacet.", + ) + achievement_gogogo_parser.add_argument( + "--diamond-address", + required=False, + default=None, + help="Address to deployed Diamond contract. If provided, this command skips deployment of a new Diamond contract and simply mounts the required facets onto the existing Diamond contract. Assumes that there is no collision of selectors.", + ) + achievement_gogogo_parser.add_argument( + "--diamond-loupe-address", + required=False, + default=None, + help="Address to deployed DiamondLoupeFacet. If provided, this command skips deployment of a new DiamondLoupeFacet. It mounts the existing DiamondLoupeFacet onto the Diamond.", + ) + achievement_gogogo_parser.add_argument( + "--ownership-address", + required=False, + default=None, + help="Address to deployed OwnershipFacet. If provided, this command skips deployment of a new OwnershipFacet. It mounts the existing OwnershipFacet onto the Diamond.", + ) + achievement_gogogo_parser.add_argument( + "--terminus-facet-address", + required=False, + default=None, + help="Address to deployed TerminusFacet. If provided, this command skips deployment of a new TerminusFacet. It mounts the existing TerminusFacet onto the Diamond.", + ) + achievement_gogogo_parser.add_argument( + "--terminus-initializer-address", + required=False, + default=None, + help="Address to deployed TerminusInitializer. If provided, this command skips deployment of a new TerminusInitializer. It uses the given TerminusInitializer to initialize the diamond upon the mounting of the TerminusFacet.", + ) + achievement_gogogo_parser.add_argument( + "--achievement-facet-address", + required=False, + default=None, + help="Address to deployed AchievementFacet. If provided, this command skips deployment of a new AchievementFacet. It mounts the existing AchievementFacet onto the Diamond.", + ) + achievement_gogogo_parser.add_argument( + "-o", + "--outfile", + type=argparse.FileType("w"), + default=None, + help="(Optional) file to write deployed addresses to", + ) + achievement_gogogo_parser.set_defaults(func=handle_achievement_gogogo) + inventory_gogogo_parser = subcommands.add_parser( "inventory-gogogo", + help="Deploy Inventory diamond contract", description="Deploy Inventory diamond contract", ) Diamond.add_default_arguments(inventory_gogogo_parser, transact=True) diff --git a/cli/web3cli/test_achievement.py b/cli/web3cli/test_achievement.py new file mode 100644 index 00000000..5437331d --- /dev/null +++ b/cli/web3cli/test_achievement.py @@ -0,0 +1,194 @@ +import unittest + +from brownie import accounts, network, web3 as web3_client, ZERO_ADDRESS +from brownie.exceptions import VirtualMachineError +from brownie.network import chain +from moonworm.watch import _fetch_events_chunk + +from . import AchievementFacet, MockERC721, MockTerminus, inventory_events +from .core import achievement_gogogo + +MAX_UINT = 2**256 - 1 + + +class AchievementTestCase(unittest.TestCase): + @classmethod + def deploy_achievement(cls) -> str: + """ + Deploys an Achiement contract and returns the address. + """ + deployed_contracts = achievement_gogogo( + cls.nft.address, + cls.owner_tx_config, + ) + return deployed_contracts["contracts"]["Diamond"] + + @classmethod + def setUpClass(cls) -> None: + try: + network.connect() + except: + pass + + cls.owner = accounts[0] + cls.owner_tx_config = {"from": cls.owner} + + cls.admin = accounts[1] + cls.admin_tx_config = {"from": cls.admin} + + cls.player = accounts[2] + cls.player_tx_config = {"from": cls.player} + + cls.random_person = accounts[3] + cls.random_person_tx_config = {"from": cls.random_person} + + cls.nft = MockERC721.MockERC721(None) + cls.nft.deploy(cls.owner_tx_config) + + cls.item_nft = MockERC721.MockERC721(None) + cls.item_nft.deploy(cls.owner_tx_config) + + cls.predeployment_block = len(chain) + achievement_address = cls.deploy_achievement() + cls.achievement = AchievementFacet.AchievementFacet(achievement_address) + cls.postdeployment_block = len(chain) + + cls.terminus = MockTerminus.MockTerminus(achievement_address) + + cls.achievement.grant_admin_privilege(cls.admin.address, cls.owner_tx_config) + + +class AchievementSetupTests(AchievementTestCase): + + def test_admin_terminus_info(self): + terminus_info = self.achievement.admin_terminus_info() + self.assertEqual(terminus_info[0], self.achievement.address) + + def test_administrator_designated_event(self): + terminus_info = self.achievement.admin_terminus_info() + + administrator_designated_events = _fetch_events_chunk( + web3_client, + inventory_events.ADMINISTRATOR_DESIGNATED_ABI, + self.predeployment_block, + self.postdeployment_block, + ) + self.assertEqual(len(administrator_designated_events), 1) + + self.assertEqual( + administrator_designated_events[0]["args"]["adminTerminusAddress"], + terminus_info[0], + ) + self.assertEqual( + administrator_designated_events[0]["args"]["adminTerminusPoolId"], + terminus_info[1], + ) + + def test_subject_erc721_address(self): + self.assertEqual(self.achievement.subject(), self.nft.address) + + def test_contract_address_designated_event(self): + contract_address_designated_events = _fetch_events_chunk( + web3_client, + inventory_events.NEW_SUBJECT_ADDRESS_ABI, + self.predeployment_block, + self.postdeployment_block, + ) + self.assertEqual(len(contract_address_designated_events), 1) + + self.assertEqual( + contract_address_designated_events[0]["args"]["contractAddress"], + self.nft.address, + ) + + def test_grant_and_revoke_admin_privilege(self): + (admin_terminus_addres, admin_terminus_pool_id) = self.achievement.admin_terminus_info() + + self.assertEqual( + admin_terminus_addres, + self.terminus.address + ) + + balance = self.terminus.balance_of(self.random_person.address, admin_terminus_pool_id) + self.assertEqual( + balance, + 0 + ) + + self.achievement.grant_admin_privilege(self.random_person.address, self.owner_tx_config) + + balance = self.terminus.balance_of(self.random_person.address, admin_terminus_pool_id) + self.assertEqual( + balance, + 1 + ) + + self.achievement.revoke_admin_privilege(self.random_person.address, self.owner_tx_config) + + balance = self.terminus.balance_of(self.random_person.address, admin_terminus_pool_id) + self.assertEqual( + balance, + 0 + ) + + +class TestAdminFlow(AchievementTestCase): + + def test_admin_can_create_achievement_slot(self): + + metadata_uri = "http://www.example.com/test_achievement_1" + + initial_num_pools = self.terminus.total_pools() + initial_num_slots = self.achievement.num_slots() + + + self.assertEqual(initial_num_pools, initial_num_slots) + + self.achievement.create_achievement_slot(metadata_uri, self.admin_tx_config) + + final_num_pools = self.terminus.total_pools() + final_num_slots = self.achievement.num_slots() + + self.assertEqual( + final_num_pools, + initial_num_pools + 1, + ) + self.assertEqual( + final_num_slots, + initial_num_slots + 1, + ) + self.assertEqual(final_num_pools, final_num_slots) + self.assertEqual( + self.terminus.uri(final_num_pools), + metadata_uri + ) + self.assertEqual( + self.achievement.get_slot_uri(final_num_slots), + metadata_uri + ) + + def test_non_admin_cannot_create_achievement_slot(self): + + metadata_uri = "http://www.example.com/test_achievement_1" + + initial_num_pools = self.terminus.total_pools() + initial_num_slots = self.achievement.num_slots() + + + self.assertEqual(initial_num_pools, initial_num_slots) + + with self.assertRaises(VirtualMachineError): + self.achievement.create_achievement_slot(metadata_uri, self.random_person_tx_config) + + final_num_pools = self.terminus.total_pools() + final_num_slots = self.achievement.num_slots() + + self.assertEqual( + final_num_pools, + initial_num_pools, + ) + self.assertEqual( + final_num_slots, + initial_num_slots, + ) + self.assertEqual(final_num_pools, final_num_slots) \ No newline at end of file diff --git a/contracts/inventory/AchievementFacet.sol b/contracts/inventory/AchievementFacet.sol new file mode 100644 index 00000000..e64f5087 --- /dev/null +++ b/contracts/inventory/AchievementFacet.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {InventoryFacet, LibInventory} from "./InventoryFacet.sol"; +import {Slot, EquippedItem} from "../interfaces/IInventory.sol"; +import {ITerminus} from "../interfaces/ITerminus.sol"; +import {LibTerminus} from "../terminus/LibTerminus.sol"; +import {LibDiamondMoonstream} from "../diamond/libraries/LibDiamondMoonstream.sol"; + +import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; +import {ERC1155Receiver} from "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Receiver.sol"; + +library LibAchievement { + bytes32 constant STORAGE_POSITION = + keccak256("moonstreamdao.eth.storage.Achievement"); + + struct AchievementStorage { + // Execution permissions + uint256 adminTerminusPoolID; + } + + function achievementStorage() + internal + pure + returns (AchievementStorage storage acs) + { + bytes32 position = STORAGE_POSITION; + assembly { + acs.slot := position + } + } +} + +contract AchievementFacet is InventoryFacet { + event AdminPrivilegeGranted(address indexed admin); + event AdminPrivilegeRevoked(address indexed admin); + + function _enforceIsAdmin() internal view { + ITerminus terminus = ITerminus(address(this)); + require( + terminus.balanceOf( + msg.sender, + LibAchievement.achievementStorage().adminTerminusPoolID + ) > 0, + "AchievementFacet._enforceIsAdmin: not admin" + ); + } + + function _checkIsAdmin(address maybeAdmin) internal view returns (bool) { + ITerminus terminus = ITerminus(address(this)); + return + terminus.balanceOf( + maybeAdmin, + LibAchievement.achievementStorage().adminTerminusPoolID + ) > 0; + } + + function initialize(address subjectAddress) external { + LibDiamondMoonstream.enforceIsContractOwner(); + + LibAchievement.AchievementStorage storage acs = LibAchievement + .achievementStorage(); + + // Set up executor pool + ITerminus terminus = ITerminus(address(this)); + acs.adminTerminusPoolID = terminus.createPoolV1( + type(uint256).max, + false, + true + ); + terminus.setPaymentToken(address(0)); + terminus.setPoolBasePrice(0); + + // Copies `init` method from InventoryFacet - that is declared as an external method so cannot be + // called directly here. + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + istore.AdminTerminusAddress = address(this); + istore.AdminTerminusPoolId = acs.adminTerminusPoolID; + istore.ContractERC721Address = subjectAddress; + + // Slot 1 is unusable so that slot id will match pool id. + istore.NumSlots += 1; + uint256 newSlot = istore.NumSlots; + // save the slot type! + istore.SlotData[newSlot] = Slot({SlotURI: "", SlotIsPersistent: true}); + + emit AdministratorDesignated(address(this), acs.adminTerminusPoolID); + emit NewSubjectAddress(subjectAddress); + } + + // Grants an account the role of admin. + function grantAdminPrivilege(address admin) external { + LibDiamondMoonstream.enforceIsContractOwner(); + + LibAchievement.AchievementStorage storage acs = LibAchievement + .achievementStorage(); + + ITerminus terminus = ITerminus(address(this)); + + if (terminus.balanceOf(admin, acs.adminTerminusPoolID) == 0) { + terminus.mint(admin, acs.adminTerminusPoolID, 1, ""); + } + + emit AdminPrivilegeGranted(admin); + } + + // Revokes the admin role from an account. + function revokeAdminPrivilege(address admin) external { + LibDiamondMoonstream.enforceIsContractOwner(); + + LibAchievement.AchievementStorage storage acs = LibAchievement + .achievementStorage(); + + ITerminus terminus = ITerminus(address(this)); + + uint256 balance = terminus.balanceOf(admin, acs.adminTerminusPoolID); + if (balance > 0) { + terminus.burn(admin, acs.adminTerminusPoolID, balance); + } + + emit AdminPrivilegeRevoked(admin); + } + + function createSlot(bool, string memory) public override returns (uint256) { + revert("This method is disabled"); + } + + function markItemAsEquippableInSlot( + uint256, + uint256, + address, + uint256, + uint256 + ) public override { + revert("This method is disabled"); + } + + function createAchievementSlot( + string memory metadataURI + ) public returns (uint256) { + _enforceIsAdmin(); + + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + + // Slots are 1-indexed! + istore.NumSlots += 1; + uint256 newSlot = istore.NumSlots; + // save the slot type! + istore.SlotData[newSlot] = Slot({ + SlotURI: metadataURI, + SlotIsPersistent: true + }); + + // create terminus token + ITerminus terminus = ITerminus(address(this)); + uint256 poolId = terminus.createPoolV2( + type(uint256).max, + false, + true, + metadataURI + ); + + require( + newSlot == poolId, + "AchievementFacet.createAchievementSlot: Slot id and pool id are mismatched." + ); + + // mark token equippable in slot + istore.SlotEligibleItems[newSlot][LibInventory.ERC1155_ITEM_TYPE][ + address(this) + ][poolId] = 1; + + emit ItemMarkedAsEquippableInSlot( + newSlot, + LibInventory.ERC1155_ITEM_TYPE, + address(this), + poolId, + 1 + ); + + emit SlotCreated(msg.sender, newSlot); + emit NewSlotURI(newSlot); + emit NewSlotPersistence(newSlot, true); + + return newSlot; + } + + function _mintToInventory(uint256 subjectTokenId, uint256 poolId) internal { + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + + // We could also revert the transaction and say the subject already has this achievement. Or we + // could do nothing and assume the achievement is correctly appplied already. + if ( + istore + .EquippedItems[istore.ContractERC721Address][subjectTokenId][poolId] + .ItemType != 0 + ) { + _unequip(subjectTokenId, poolId, true, 0); + } + + ITerminus terminus = ITerminus(address(this)); + terminus.mint(address(this), poolId, 1, ""); + + istore.EquippedItems[istore.ContractERC721Address][subjectTokenId][ + poolId + ] = EquippedItem({ + ItemType: LibInventory.ERC1155_ITEM_TYPE, + ItemAddress: address(this), + ItemTokenId: poolId, + Amount: 1 + }); + + emit ItemEquipped( + subjectTokenId, + poolId, + LibInventory.ERC1155_ITEM_TYPE, + address(this), + poolId, + 1, + msg.sender + ); + } + + function adminBatchMintToInventory( + uint256[] memory subjectTokenIds, + uint256[] memory poolIds + ) public { + _enforceIsAdmin(); + + require( + subjectTokenIds.length == poolIds.length, + "AchievementFacet.adminBatchMintToInventory: subjectTokenIds and poolIds length mismatch" + ); + + uint256 i = 0; + for (i = 0; i < subjectTokenIds.length; i++) { + _mintToInventory(subjectTokenIds[i], poolIds[i]); + } + } +} diff --git a/contracts/inventory/InventoryFacet.sol b/contracts/inventory/InventoryFacet.sol index 25b70933..f4d9baf6 100644 --- a/contracts/inventory/InventoryFacet.sol +++ b/contracts/inventory/InventoryFacet.sol @@ -160,7 +160,7 @@ contract InventoryFacet is function createSlot( bool persistent, string memory slotURI - ) public onlyAdmin returns (uint256) { + ) public virtual onlyAdmin returns (uint256) { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); @@ -234,7 +234,7 @@ contract InventoryFacet is address itemAddress, uint256 itemPoolId, uint256 maxAmount - ) public onlyAdmin { + ) public virtual onlyAdmin { require( itemType == LibInventory.ERC20_ITEM_TYPE || itemType == LibInventory.ERC721_ITEM_TYPE ||