From 4eeb7ccb6e10a50fb7be4f97f085afc20bb6a4a0 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 16:36:44 -0700 Subject: [PATCH 01/16] First implementation of `StatBlock` --- cli/web3cli/StatBlock.py | 476 +++++++++++++++++++++++++++++++++ cli/web3cli/cli.py | 5 + contracts/stats/IStatBlock.sol | 48 ++++ contracts/stats/StatBlock.sol | 124 +++++++++ 4 files changed, 653 insertions(+) create mode 100644 cli/web3cli/StatBlock.py create mode 100644 contracts/stats/IStatBlock.sol create mode 100644 contracts/stats/StatBlock.sol diff --git a/cli/web3cli/StatBlock.py b/cli/web3cli/StatBlock.py new file mode 100644 index 00000000..53740c30 --- /dev/null +++ b/cli/web3cli/StatBlock.py @@ -0,0 +1,476 @@ +# Code generated by moonworm : https://github.com/bugout-dev/moonworm +# Moonworm version : 0.7.0 + +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 StatBlock: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "StatBlock" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("StatBlock") + if self.address is not None: + self.contract: Optional[Contract] = Contract.from_abi( + self.contract_name, self.address, self.abi + ) + + def deploy( + self, + admin_terminus_address: ChecksumAddress, + admin_terminus_pool_id: int, + transaction_config, + ): + contract_class = contract_from_build(self.contract_name) + deployed_contract = contract_class.deploy( + admin_terminus_address, admin_terminus_pool_id, 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 assign_stats( + self, + token_address: ChecksumAddress, + token_id: int, + stat_i_ds: List, + values: List, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.assignStats( + token_address, token_id, stat_i_ds, values, transaction_config + ) + + def batch_assign_stats( + self, + token_addresses: List, + token_i_ds: List, + stat_i_ds: Any, + values: Any, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.batchAssignStats( + token_addresses, token_i_ds, stat_i_ds, values, transaction_config + ) + + def batch_get_stats( + self, + token_addresses: List, + token_i_ds: List, + stat_i_ds: List, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.batchGetStats.call( + token_addresses, token_i_ds, stat_i_ds, block_identifier=block_number + ) + + def create_stat(self, descriptor: str, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createStat(descriptor, transaction_config) + + def describe_stat( + self, stat_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.describeStat.call(stat_id, block_identifier=block_number) + + def get_stats( + self, + token_address: ChecksumAddress, + token_id: int, + stat_i_ds: List, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getStats.call( + token_address, token_id, stat_i_ds, block_identifier=block_number + ) + + def is_administrator( + self, + account: ChecksumAddress, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.isAdministrator.call( + account, block_identifier=block_number + ) + + +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 = StatBlock(None) + result = contract.deploy( + admin_terminus_address=args.admin_terminus_address, + admin_terminus_pool_id=args.admin_terminus_pool_id, + 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 = StatBlock(args.address) + result = contract.verify_contract() + print(result) + + +def handle_admin_terminus_info(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.admin_terminus_info(block_number=args.block_number) + print(result) + + +def handle_assign_stats(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + transaction_config = get_transaction_config(args) + result = contract.assign_stats( + token_address=args.token_address, + token_id=args.token_id, + stat_i_ds=args.stat_i_ds, + values=args.values, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_batch_assign_stats(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + transaction_config = get_transaction_config(args) + result = contract.batch_assign_stats( + token_addresses=args.token_addresses, + token_i_ds=args.token_i_ds, + stat_i_ds=args.stat_i_ds, + values=args.values, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_batch_get_stats(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.batch_get_stats( + token_addresses=args.token_addresses, + token_i_ds=args.token_i_ds, + stat_i_ds=args.stat_i_ds, + block_number=args.block_number, + ) + print(result) + + +def handle_create_stat(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_stat( + descriptor=args.descriptor, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_describe_stat(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.describe_stat( + stat_id=args.stat_id, block_number=args.block_number + ) + print(result) + + +def handle_get_stats(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.get_stats( + token_address=args.token_address, + token_id=args.token_id, + stat_i_ds=args.stat_i_ds, + block_number=args.block_number, + ) + print(result) + + +def handle_is_administrator(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.is_administrator( + account=args.account, block_number=args.block_number + ) + print(result) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for StatBlock") + 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.add_argument( + "--admin-terminus-address", required=True, help="Type: address" + ) + deploy_parser.add_argument( + "--admin-terminus-pool-id", required=True, help="Type: uint256", type=int + ) + 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) + + assign_stats_parser = subcommands.add_parser("assign-stats") + add_default_arguments(assign_stats_parser, True) + assign_stats_parser.add_argument( + "--token-address", required=True, help="Type: address" + ) + assign_stats_parser.add_argument( + "--token-id", required=True, help="Type: uint256", type=int + ) + assign_stats_parser.add_argument( + "--stat-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + assign_stats_parser.add_argument( + "--values", required=True, help="Type: uint256[]", nargs="+" + ) + assign_stats_parser.set_defaults(func=handle_assign_stats) + + batch_assign_stats_parser = subcommands.add_parser("batch-assign-stats") + add_default_arguments(batch_assign_stats_parser, True) + batch_assign_stats_parser.add_argument( + "--token-addresses", required=True, help="Type: address[]", nargs="+" + ) + batch_assign_stats_parser.add_argument( + "--token-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + batch_assign_stats_parser.add_argument( + "--stat-i-ds", required=True, help="Type: uint256[][]", type=eval + ) + batch_assign_stats_parser.add_argument( + "--values", required=True, help="Type: uint256[][]", type=eval + ) + batch_assign_stats_parser.set_defaults(func=handle_batch_assign_stats) + + batch_get_stats_parser = subcommands.add_parser("batch-get-stats") + add_default_arguments(batch_get_stats_parser, False) + batch_get_stats_parser.add_argument( + "--token-addresses", required=True, help="Type: address[]", nargs="+" + ) + batch_get_stats_parser.add_argument( + "--token-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + batch_get_stats_parser.add_argument( + "--stat-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + batch_get_stats_parser.set_defaults(func=handle_batch_get_stats) + + create_stat_parser = subcommands.add_parser("create-stat") + add_default_arguments(create_stat_parser, True) + create_stat_parser.add_argument( + "--descriptor", required=True, help="Type: string", type=str + ) + create_stat_parser.set_defaults(func=handle_create_stat) + + describe_stat_parser = subcommands.add_parser("describe-stat") + add_default_arguments(describe_stat_parser, False) + describe_stat_parser.add_argument( + "--stat-id", required=True, help="Type: uint256", type=int + ) + describe_stat_parser.set_defaults(func=handle_describe_stat) + + get_stats_parser = subcommands.add_parser("get-stats") + add_default_arguments(get_stats_parser, False) + get_stats_parser.add_argument( + "--token-address", required=True, help="Type: address" + ) + get_stats_parser.add_argument( + "--token-id", required=True, help="Type: uint256", type=int + ) + get_stats_parser.add_argument( + "--stat-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + get_stats_parser.set_defaults(func=handle_get_stats) + + is_administrator_parser = subcommands.add_parser("is-administrator") + add_default_arguments(is_administrator_parser, False) + is_administrator_parser.add_argument( + "--account", required=True, help="Type: address" + ) + is_administrator_parser.set_defaults(func=handle_is_administrator) + + return parser + + +def main() -> None: + parser = generate_cli() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/cli/web3cli/cli.py b/cli/web3cli/cli.py index 3684a50f..f957fb08 100644 --- a/cli/web3cli/cli.py +++ b/cli/web3cli/cli.py @@ -18,6 +18,7 @@ GOFPFacet, GOFPPredicates, InventoryFacet, + StatBlock, ) @@ -79,9 +80,13 @@ def main() -> None: predicates_subparsers.add_parser( "gofp", parents=[gofp_predicates_parser], add_help=False ) + inventory_parser = InventoryFacet.generate_cli() subparsers.add_parser("inventory", parents=[inventory_parser], add_help=False) + statblock_parser = StatBlock.generate_cli() + subparsers.add_parser("statblock", parents=[statblock_parser], add_help=False) + args = parser.parse_args() args.func(args) diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol new file mode 100644 index 00000000..2f4b3f55 --- /dev/null +++ b/contracts/stats/IStatBlock.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/** + * Authors: Moonstream Engineering (engineering - at - moonstream.to) + * GitHub: https://github.com/moonstream-to/web3 + */ + +// Interface ID: 591ac2e1 +// +// Calculated by solface: https://github.com/moonstream-to/solface +// solface version: 0.1.0 +// +// To recalculate from root directory of this repo: +// $ jq .abi build/contracts/IStatBlock.json | solface -name IStatBlock -annotations | grep "Interface ID:" +interface IStatBlock { + function isAdministrator(address account) external view returns (bool); + + function createStat(string memory descriptor) external returns (uint256); + + function describeStat(uint256) external view returns (string memory); + + function assignStats( + address tokenAddress, + uint256 tokenID, + uint256[] memory statIDs, + uint256[] memory values + ) external; + + function batchAssignStats( + address[] memory tokenAddresses, + uint256[] memory tokenIDs, + uint256[][] memory statIDs, + uint256[][] memory values + ) external; + + function getStats( + address tokenAddress, + uint256 tokenID, + uint256[] memory statIDs + ) external view returns (uint256[] memory); + + function batchGetStats( + address[] memory tokenAddresses, + uint256[] memory tokenIDs, + uint256[] memory statIDs + ) external view returns (uint256[][] memory); +} diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol new file mode 100644 index 00000000..e1034c9a --- /dev/null +++ b/contracts/stats/StatBlock.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/** + * Authors: Moonstream Engineering (engineering - at - moonstream.to) + * GitHub: https://github.com/moonstream-to/web3 + */ + +import {IERC1155} from "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; +import {IStatBlock} from "./IStatBlock.sol"; + +contract StatBlock is IStatBlock { + address AdminTerminusAddress; + uint256 AdminTerminusPoolID; + // Stats are 0-indexed. + uint256 NumStats; + mapping(uint256 => string) StatDescriptor; + mapping(address => mapping(uint256 => mapping(uint256 => uint256))) Stat; + + constructor(address adminTerminusAddress, uint256 adminTerminusPoolID) { + AdminTerminusAddress = adminTerminusAddress; + AdminTerminusPoolID = adminTerminusPoolID; + } + + function adminTerminusInfo() external view returns (address, uint256) { + return (AdminTerminusAddress, AdminTerminusPoolID); + } + + function isAdministrator(address account) public view returns (bool) { + IERC1155 terminus = IERC1155(AdminTerminusAddress); + return terminus.balanceOf(account, AdminTerminusPoolID) > 0; + } + + function createStat( + string memory descriptor + ) external returns (uint256 statID) { + require( + isAdministrator(msg.sender), + "StatBlock.createStat: msg.sender must be an administrator of the StatBlock" + ); + statID = NumStats++; + StatDescriptor[statID] = descriptor; + } + + function describeStat( + uint256 statID + ) external view returns (string memory) { + return StatDescriptor[statID]; + } + + function assignStats( + address tokenAddress, + uint256 tokenID, + uint256[] memory statIDs, + uint256[] memory values + ) public { + require( + isAdministrator(msg.sender), + "StatBlock.assignStats: msg.sender must be an administrator of the StatBlock" + ); + require( + statIDs.length == values.length, + "StatBlock.assignStats: statIDs and values must be the same length" + ); + for (uint256 i = 0; i < statIDs.length; i++) { + Stat[tokenAddress][tokenID][statIDs[i]] = values[i]; + } + } + + function batchAssignStats( + address[] memory tokenAddresses, + uint256[] memory tokenIDs, + uint256[][] memory statIDs, + uint256[][] memory values + ) external { + require( + isAdministrator(msg.sender), + "StatBlock.batchAssignStats: msg.sender must be an administrator of the StatBlock" + ); + require( + tokenAddresses.length == tokenIDs.length, + "StatBlock.batchAssignStats: tokenAddresses and tokenIDs must be the same length" + ); + require( + tokenAddresses.length == statIDs.length, + "StatBlock.batchAssignStats: tokenAddresses and statIDs must be the same length" + ); + require( + tokenAddresses.length == values.length, + "StatBlock.batchAssignStats: tokenAddresses and values must be the same length" + ); + for (uint256 i = 0; i < tokenAddresses.length; i++) { + assignStats(tokenAddresses[i], tokenIDs[i], statIDs[i], values[i]); + } + } + + function getStats( + address tokenAddress, + uint256 tokenID, + uint256[] memory statIDs + ) public view returns (uint256[] memory) { + uint256[] memory values = new uint256[](statIDs.length); + for (uint256 i = 0; i < statIDs.length; i++) { + values[i] = Stat[tokenAddress][tokenID][statIDs[i]]; + } + return values; + } + + function batchGetStats( + address[] memory tokenAddresses, + uint256[] memory tokenIDs, + uint256[] memory statIDs + ) external view returns (uint256[][] memory) { + require( + tokenAddresses.length == tokenIDs.length, + "StatBlock.batchGetStats: tokenAddresses and tokenIDs must be the same length" + ); + uint256[][] memory values = new uint256[][](tokenAddresses.length); + for (uint256 i = 0; i < tokenAddresses.length; i++) { + values[i] = getStats(tokenAddresses[i], tokenIDs[i], statIDs); + } + return values; + } +} From 0df9b0e2a5ff8916366320be3f0400df5640666c Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 16:57:30 -0700 Subject: [PATCH 02/16] Started writing StatBlock tests --- cli/web3cli/StatBlock.py | 15 +++++++ cli/web3cli/test_statblock.py | 82 +++++++++++++++++++++++++++++++++++ contracts/stats/StatBlock.sol | 2 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 cli/web3cli/test_statblock.py diff --git a/cli/web3cli/StatBlock.py b/cli/web3cli/StatBlock.py index 53740c30..cc28bb6a 100644 --- a/cli/web3cli/StatBlock.py +++ b/cli/web3cli/StatBlock.py @@ -103,6 +103,10 @@ def verify_contract(self): contract_class = contract_from_build(self.contract_name) contract_class.publish_source(self.contract) + def num_stats(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.NumStats.call(block_identifier=block_number) + def admin_terminus_info( self, block_number: Optional[Union[str, int]] = "latest" ) -> Any: @@ -268,6 +272,13 @@ def handle_verify_contract(args: argparse.Namespace) -> None: print(result) +def handle_num_stats(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.num_stats(block_number=args.block_number) + print(result) + + def handle_admin_terminus_info(args: argparse.Namespace) -> None: network.connect(args.network) contract = StatBlock(args.address) @@ -380,6 +391,10 @@ def generate_cli() -> argparse.ArgumentParser: add_default_arguments(verify_contract_parser, False) verify_contract_parser.set_defaults(func=handle_verify_contract) + num_stats_parser = subcommands.add_parser("num-stats") + add_default_arguments(num_stats_parser, False) + num_stats_parser.set_defaults(func=handle_num_stats) + 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) diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py new file mode 100644 index 00000000..7986afe8 --- /dev/null +++ b/cli/web3cli/test_statblock.py @@ -0,0 +1,82 @@ +import unittest + +from brownie import accounts, network +from brownie.exceptions import VirtualMachineError + +from . import MockErc20, MockTerminus, StatBlock + +MAX_UINT = 2**256 - 1 + + +class StatBlockTests(unittest.TestCase): + @classmethod + def setup_permissions(cls): + cls.terminus = MockTerminus.MockTerminus(None) + cls.terminus.deploy(cls.deployer_txconfig) + + cls.payment_token = MockErc20.MockErc20(None) + cls.payment_token.deploy("lol", "lol", cls.deployer_txconfig) + + cls.terminus.set_payment_token(cls.payment_token.address, cls.deployer_txconfig) + cls.terminus.set_pool_base_price(1, cls.deployer_txconfig) + + cls.terminus.set_payment_token(cls.payment_token.address, cls.deployer_txconfig) + cls.terminus.set_pool_base_price(1, cls.deployer_txconfig) + + cls.payment_token.mint(cls.deployer.address, 999999, cls.deployer_txconfig) + cls.payment_token.approve(cls.terminus.address, MAX_UINT, cls.deployer_txconfig) + + cls.terminus.create_pool_v1(1, False, True, cls.deployer_txconfig) + cls.admin_terminus_pool_id = cls.terminus.total_pools() + + cls.terminus.mint( + cls.administrator.address, + cls.admin_terminus_pool_id, + 1, + "", + cls.deployer_txconfig, + ) + + @classmethod + def setup_statblock(cls): + cls.statblock = StatBlock.StatBlock(None) + cls.statblock.deploy( + cls.terminus.address, + cls.admin_terminus_pool_id, + cls.deployer_txconfig, + ) + + @classmethod + def setUpClass(cls): + try: + network.connect() + except: + pass + + cls.deployer = accounts[0] + cls.deployer_txconfig = {"from": cls.deployer} + + cls.administrator = accounts[1] + cls.player = accounts[2] + cls.random_person = accounts[3] + + cls.setup_permissions() + cls.setup_statblock() + + def test_admin_can_create_stat(self): + """ + Tests that an administrator can create stats on a StatBlock contract. + """ + num_stats_0 = self.statblock.num_stats() + stat_name = f"stat_{num_stats_0}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + num_stats_1 = self.statblock.num_stats() + + self.assertEqual(num_stats_1, num_stats_0 + 1) + + stat_description = self.statblock.describe_stat(num_stats_0) + self.assertEqual(stat_description, stat_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol index e1034c9a..03952c1d 100644 --- a/contracts/stats/StatBlock.sol +++ b/contracts/stats/StatBlock.sol @@ -13,7 +13,7 @@ contract StatBlock is IStatBlock { address AdminTerminusAddress; uint256 AdminTerminusPoolID; // Stats are 0-indexed. - uint256 NumStats; + uint256 public NumStats; mapping(uint256 => string) StatDescriptor; mapping(address => mapping(uint256 => mapping(uint256 => uint256))) Stat; From 80cd0f0f406e1c41737c7b6c0fd9dcd7cd7fb9aa Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 16:59:52 -0700 Subject: [PATCH 03/16] Update IStatBlock to include events And emitting events in `StatBlock`. --- contracts/stats/IStatBlock.sol | 10 +++++++++- contracts/stats/StatBlock.sol | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol index 2f4b3f55..7cef50e5 100644 --- a/contracts/stats/IStatBlock.sol +++ b/contracts/stats/IStatBlock.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.0; * GitHub: https://github.com/moonstream-to/web3 */ -// Interface ID: 591ac2e1 +// Interface ID: 458bb0c0 // // Calculated by solface: https://github.com/moonstream-to/solface // solface version: 0.1.0 @@ -14,6 +14,14 @@ pragma solidity ^0.8.0; // To recalculate from root directory of this repo: // $ jq .abi build/contracts/IStatBlock.json | solface -name IStatBlock -annotations | grep "Interface ID:" interface IStatBlock { + event StatCreated(uint256 statID, string descriptor); + event StatAssigned( + address indexed tokenAddress, + uint256 indexed tokenID, + uint256 indexed statID, + uint256 value + ); + function isAdministrator(address account) external view returns (bool); function createStat(string memory descriptor) external returns (uint256); diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol index 03952c1d..3f458176 100644 --- a/contracts/stats/StatBlock.sol +++ b/contracts/stats/StatBlock.sol @@ -40,6 +40,7 @@ contract StatBlock is IStatBlock { ); statID = NumStats++; StatDescriptor[statID] = descriptor; + emit StatCreated(statID, descriptor); } function describeStat( @@ -64,6 +65,7 @@ contract StatBlock is IStatBlock { ); for (uint256 i = 0; i < statIDs.length; i++) { Stat[tokenAddress][tokenID][statIDs[i]] = values[i]; + emit StatAssigned(tokenAddress, tokenID, statIDs[i], values[i]); } } From 17ec145082d4ffbc12b2331261bf8826479f09ba Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 17:07:18 -0700 Subject: [PATCH 04/16] Added test for StatCreated event --- cli/web3cli/statblock_events.py | 51 +++++++++++++++++++++++++++++++++ cli/web3cli/test_statblock.py | 21 ++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 cli/web3cli/statblock_events.py diff --git a/cli/web3cli/statblock_events.py b/cli/web3cli/statblock_events.py new file mode 100644 index 00000000..d75f65a6 --- /dev/null +++ b/cli/web3cli/statblock_events.py @@ -0,0 +1,51 @@ +STAT_CREATED_ABI = { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "statID", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "string", + "name": "descriptor", + "type": "string", + }, + ], + "name": "StatCreated", + "type": "event", +} + +STAT_ASSIGNED_ABI = { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "address", + "name": "tokenAddress", + "type": "address", + }, + { + "indexed": True, + "internalType": "uint256", + "name": "tokenID", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "uint256", + "name": "statID", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "value", + "type": "uint256", + }, + ], + "name": "StatAssigned", + "type": "event", +} diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index 7986afe8..05b5a8b1 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -1,9 +1,10 @@ import unittest -from brownie import accounts, network +from brownie import accounts, network, web3 as web3_client from brownie.exceptions import VirtualMachineError +from moonworm.watch import _fetch_events_chunk -from . import MockErc20, MockTerminus, StatBlock +from . import MockErc20, MockTerminus, StatBlock, statblock_events MAX_UINT = 2**256 - 1 @@ -69,7 +70,7 @@ def test_admin_can_create_stat(self): """ num_stats_0 = self.statblock.num_stats() stat_name = f"stat_{num_stats_0}" - self.statblock.create_stat(stat_name, {"from": self.administrator}) + tx_receipt = self.statblock.create_stat(stat_name, {"from": self.administrator}) num_stats_1 = self.statblock.num_stats() self.assertEqual(num_stats_1, num_stats_0 + 1) @@ -77,6 +78,20 @@ def test_admin_can_create_stat(self): stat_description = self.statblock.describe_stat(num_stats_0) self.assertEqual(stat_description, stat_name) + stat_created_events = _fetch_events_chunk( + web3_client, + statblock_events.STAT_CREATED_ABI, + tx_receipt.block_number, + tx_receipt.block_number, + ) + self.assertEqual(len(stat_created_events), 1) + + event = stat_created_events[0] + self.assertEqual(event["event"], "StatCreated") + self.assertEqual(event["args"]["statID"], num_stats_0) + self.assertEqual(event["args"]["descriptor"], stat_name) + self.assertEqual(event["address"], self.statblock.address) + if __name__ == "__main__": unittest.main() From e6759f2bba83261f8d914ee12772fd2753ab47e8 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 17:09:39 -0700 Subject: [PATCH 05/16] Added test that non-adminstrator cannot create stats --- cli/web3cli/test_statblock.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index 05b5a8b1..f143cb83 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -92,6 +92,24 @@ def test_admin_can_create_stat(self): self.assertEqual(event["args"]["descriptor"], stat_name) self.assertEqual(event["address"], self.statblock.address) + def test_nonadmin_cannot_create_stat(self): + """ + Tests that an account which is not a StatBlock administrator cannot create a stat on the StatBlock + contract. + """ + # Test that player account does not own administrator badges. + self.assertEqual( + self.terminus.balance_of(self.player.address, self.admin_terminus_pool_id), + 0, + ) + + num_stats_0 = self.statblock.num_stats() + stat_name = f"stat_{num_stats_0}" + with self.assertRaises(VirtualMachineError): + self.statblock.create_stat(stat_name, {"from": self.player}) + num_stats_1 = self.statblock.num_stats() + self.assertEqual(num_stats_1, num_stats_0) + if __name__ == "__main__": unittest.main() From a12e4ec68d1a218488fcc23b95e7dd904dbb4fcb Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 8 Aug 2023 21:51:42 -0700 Subject: [PATCH 06/16] assignStats test for administrator --- cli/test.sh | 3 +- cli/web3cli/test_statblock.py | 199 ++++++++++++++++++++++++++++++++-- contracts/mock/MockERC721.sol | 2 +- 3 files changed, 191 insertions(+), 13 deletions(-) diff --git a/cli/test.sh b/cli/test.sh index 7196e8dc..75550a51 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -26,7 +26,8 @@ fi TEST_COMMAND=${@:-discover} -cd .. +SCRIPT_DIR=$(realpath $(dirname $0)) +cd $SCRIPT_DIR/.. brownie compile cd - set -x diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index f143cb83..b3f632b9 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -4,14 +4,31 @@ from brownie.exceptions import VirtualMachineError from moonworm.watch import _fetch_events_chunk -from . import MockErc20, MockTerminus, StatBlock, statblock_events +from . import ( + MockErc20, + MockERC721, + MockERC1155, + MockTerminus, + StatBlock, + statblock_events, +) MAX_UINT = 2**256 - 1 class StatBlockTests(unittest.TestCase): + """ + StatBlockTests is the full suite of tests for the reference implementation of StatBlock. + + To test a custom StatBlock implementation, inherit from this class and modify the setup_permissions + and setup_statblock methods to deploy that StatBlock implementation. + """ + @classmethod - def setup_permissions(cls): + def setup_statblock(cls): + """ + Deploys the StatBlock contract being tested. + """ cls.terminus = MockTerminus.MockTerminus(None) cls.terminus.deploy(cls.deployer_txconfig) @@ -30,20 +47,23 @@ def setup_permissions(cls): cls.terminus.create_pool_v1(1, False, True, cls.deployer_txconfig) cls.admin_terminus_pool_id = cls.terminus.total_pools() - cls.terminus.mint( - cls.administrator.address, + cls.statblock = StatBlock.StatBlock(None) + cls.statblock.deploy( + cls.terminus.address, cls.admin_terminus_pool_id, - 1, - "", cls.deployer_txconfig, ) @classmethod - def setup_statblock(cls): - cls.statblock = StatBlock.StatBlock(None) - cls.statblock.deploy( - cls.terminus.address, + def setup_permissions(cls): + """ + Grants administrator permissions to the administrator account. + """ + cls.terminus.mint( + cls.administrator.address, cls.admin_terminus_pool_id, + 1, + "", cls.deployer_txconfig, ) @@ -61,12 +81,26 @@ def setUpClass(cls): cls.player = accounts[2] cls.random_person = accounts[3] - cls.setup_permissions() cls.setup_statblock() + cls.setup_permissions() + + cls.erc20_contract = MockErc20.MockErc20(None) + cls.erc20_contract.deploy("ERC20 Token", "ERC20", cls.deployer_txconfig) + + cls.erc721_contract = MockERC721.MockERC721(None) + cls.erc721_contract.deploy(cls.deployer_txconfig) + + cls.erc1155_contract = MockERC1155.MockERC1155(None) + cls.erc1155_contract.deploy(cls.deployer_txconfig) def test_admin_can_create_stat(self): """ Tests that an administrator can create stats on a StatBlock contract. + + Tests: + - createStat + - NumStats + - describeStat """ num_stats_0 = self.statblock.num_stats() stat_name = f"stat_{num_stats_0}" @@ -96,6 +130,10 @@ def test_nonadmin_cannot_create_stat(self): """ Tests that an account which is not a StatBlock administrator cannot create a stat on the StatBlock contract. + + Tests: + - createStat + - NumStats """ # Test that player account does not own administrator badges. self.assertEqual( @@ -110,6 +148,145 @@ def test_nonadmin_cannot_create_stat(self): num_stats_1 = self.statblock.num_stats() self.assertEqual(num_stats_1, num_stats_0) + def test_admin_can_assign_stats(self): + """ + Tests that administrator can assign a set of stats to a token + + Tests: + - createStat + - NumStats + - assignStats + - getStats + - batchGetStats + """ + # Setup phase: create the stats that we will assign to. + num_assignable_stats = 3 + num_stats_0 = self.statblock.num_stats() + + stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + + for i in stat_ids: + stat_name = f"stat_{i}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + + num_stats_1 = self.statblock.num_stats() + self.assertEqual(num_stats_1, num_stats_0 + num_assignable_stats) + + # Assign stats to ERC20 token. This is done by using 0 as the token_id. + expected_erc20_stats = [20 + i for i in stat_ids] + tx_receipt_0 = self.statblock.assign_stats( + self.erc20_contract.address, + 0, + stat_ids, + expected_erc20_stats, + {"from": self.administrator}, + ) + + # Assign stats to ERC721 token. The token need not yet be minted. + erc721_token_id = 42 + expected_erc721_stats = [721 + 42 + i for i in stat_ids] + tx_receipt_1 = self.statblock.assign_stats( + self.erc721_contract.address, + erc721_token_id, + stat_ids, + expected_erc721_stats, + {"from": self.administrator}, + ) + + # Assign stats to ERC1155 tokens by token_id. + erc1155_token_id = 1337 + expected_erc1155_stats = [1155 + 1337 + i for i in stat_ids] + tx_receipt_2 = self.statblock.assign_stats( + self.erc1155_contract.address, + erc1155_token_id, + stat_ids, + expected_erc1155_stats, + {"from": self.administrator}, + ) + + # Check for StatAssigned event emissions. + stat_assigned_events = _fetch_events_chunk( + web3_client, + statblock_events.STAT_ASSIGNED_ABI, + tx_receipt_0.block_number, + tx_receipt_2.block_number, + ) + + self.assertEqual(len(stat_assigned_events), 3 * num_assignable_stats) + + for i in range(num_assignable_stats): + self.assertEqual(stat_assigned_events[i]["event"], "StatAssigned") + self.assertEqual( + stat_assigned_events[i]["args"]["tokenAddress"], + self.erc20_contract.address, + ) + self.assertEqual(stat_assigned_events[i]["args"]["tokenID"], 0) + self.assertEqual(stat_assigned_events[i]["args"]["statID"], stat_ids[i]) + self.assertEqual( + stat_assigned_events[i]["args"]["value"], expected_erc20_stats[i] + ) + + for i in range(num_assignable_stats): + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["event"], "StatAssigned" + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["tokenAddress"], + self.erc721_contract.address, + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["tokenID"], + erc721_token_id, + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["statID"], + stat_ids[i], + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["value"], + expected_erc721_stats[i], + ) + + for i in range(num_assignable_stats): + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["event"], + "StatAssigned", + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"][ + "tokenAddress" + ], + self.erc1155_contract.address, + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["tokenID"], + erc1155_token_id, + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["statID"], + stat_ids[i], + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["value"], + expected_erc1155_stats[i], + ) + + # Get stats and make sure they are correct + actual_erc20_stats = self.statblock.get_stats( + self.erc20_contract.address, 0, stat_ids + ) + self.assertEqual(actual_erc20_stats, tuple(expected_erc20_stats)) + + actual_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + self.assertEqual(actual_erc721_stats, tuple(expected_erc721_stats)) + + actual_erc1155_stats = self.statblock.get_stats( + self.erc1155_contract.address, erc1155_token_id, stat_ids + ) + self.assertEqual(actual_erc1155_stats, tuple(expected_erc1155_stats)) + if __name__ == "__main__": unittest.main() diff --git a/contracts/mock/MockERC721.sol b/contracts/mock/MockERC721.sol index 0ab790d9..e5e146cc 100644 --- a/contracts/mock/MockERC721.sol +++ b/contracts/mock/MockERC721.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "@openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; contract MockERC721 is ERC721Enumerable { - constructor() ERC721("Mock Crypto Unicorns", "MOCKUNICORNS") {} + constructor() ERC721("Mock ERC721", "MOCKERC721") {} function mint(address to, uint256 tokenId) external { _mint(to, tokenId); From 04977f9de6d25cba7c7fdd57d3bd9de6c1cd8b04 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Fri, 11 Aug 2023 09:44:58 -0700 Subject: [PATCH 07/16] Updated ABIs --- abi/GOFPFacet.json | 458 +++++++++++++++++- abi/GOFPPredicates.json | 46 ++ abi/IGOFP.json | 1005 +++++++++++++++++++++++++++++++++++++++ abi/IStatBlock.json | 223 +++++++++ abi/StatBlock.json | 270 +++++++++++ 5 files changed, 2001 insertions(+), 1 deletion(-) create mode 100644 abi/GOFPPredicates.json create mode 100644 abi/IGOFP.json create mode 100644 abi/IStatBlock.json create mode 100644 abi/StatBlock.json diff --git a/abi/GOFPFacet.json b/abi/GOFPFacet.json index 19433558..a737ce0b 100644 --- a/abi/GOFPFacet.json +++ b/abi/GOFPFacet.json @@ -1,4 +1,47 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "path", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "name": "PathChoicePredicateSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -55,6 +98,49 @@ "name": "PathRegistered", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "path", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "name": "PathRewardChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -198,6 +284,37 @@ "name": "StageRewardChanged", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "name": "StakingPredicateSet", + "type": "event" + }, { "inputs": [], "name": "adminTerminusInfo", @@ -216,6 +333,81 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stageNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pathNumber", + "type": "uint256" + } + ], + "internalType": "struct PathDetails", + "name": "path", + "type": "tuple" + }, + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "callPathChoicePredicate", + "outputs": [ + { + "internalType": "bool", + "name": "valid", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "callSessionStakingPredicate", + "outputs": [ + { + "internalType": "bool", + "name": "valid", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -354,6 +546,105 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stageNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pathNumber", + "type": "uint256" + } + ], + "internalType": "struct PathDetails", + "name": "path", + "type": "tuple" + } + ], + "name": "getPathChoicePredicate", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "internalType": "struct Predicate", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "path", + "type": "uint256" + } + ], + "name": "getPathReward", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "internalType": "struct Reward", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -415,6 +706,42 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + } + ], + "name": "getSessionStakingPredicate", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "internalType": "struct Predicate", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -472,7 +799,7 @@ "type": "uint256" } ], - "internalType": "struct StageReward", + "internalType": "struct Reward", "name": "", "type": "tuple" } @@ -509,6 +836,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "gofpVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + }, + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [ { @@ -704,6 +1049,89 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stageNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pathNumber", + "type": "uint256" + } + ], + "internalType": "struct PathDetails", + "name": "path", + "type": "tuple" + }, + { + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "name": "setPathChoicePredicate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "stages", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "paths", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "terminusAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "terminusPoolIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "rewardAmounts", + "type": "uint256[]" + } + ], + "name": "setPathRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -740,6 +1168,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + }, + { + "internalType": "address", + "name": "predicateAddress", + "type": "address" + }, + { + "internalType": "bytes", + "name": "initialArguments", + "type": "bytes" + } + ], + "name": "setSessionStakingPredicate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/GOFPPredicates.json b/abi/GOFPPredicates.json new file mode 100644 index 00000000..d6bfb7e4 --- /dev/null +++ b/abi/GOFPPredicates.json @@ -0,0 +1,46 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxStakable", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gofpAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "address", + "name": "nftAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "doesNotExceedMaxTokensInSession", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/IGOFP.json b/abi/IGOFP.json new file mode 100644 index 00000000..8682bb12 --- /dev/null +++ b/abi/IGOFP.json @@ -0,0 +1,1005 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "path", + "type": "uint256" + } + ], + "name": "PathChosen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "path", + "type": "uint256" + } + ], + "name": "PathRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "path", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "name": "PathRewardChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "name": "SessionActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isChoosingActive", + "type": "bool" + } + ], + "name": "SessionChoosingActivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "playerTokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "paymentTokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paymentAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "indexed": false, + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isForgiving", + "type": "bool" + } + ], + "name": "SessionCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "uri", + "type": "string" + } + ], + "name": "SessionUriChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "name": "StageRewardChanged", + "type": "event" + }, + { + "inputs": [], + "name": "adminTerminusInfo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "paths", + "type": "uint256[]" + } + ], + "name": "chooseCurrentStagePaths", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "playerTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "paymentTokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "paymentAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "internalType": "uint256[]", + "name": "stages", + "type": "uint256[]" + }, + { + "internalType": "bool", + "name": "isForgiving", + "type": "bool" + } + ], + "name": "createSession", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + } + ], + "name": "getCorrectPathForStage", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + } + ], + "name": "getCurrentStage", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + } + ], + "name": "getPathChoice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "path", + "type": "uint256" + } + ], + "name": "getPathReward", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "internalType": "struct IGOFP.Compound0", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + } + ], + "name": "getSession", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "playerTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "paymentTokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "paymentAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isChoosingActive", + "type": "bool" + }, + { + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "internalType": "uint256[]", + "name": "stages", + "type": "uint256[]" + }, + { + "internalType": "bool", + "name": "isForgiving", + "type": "bool" + } + ], + "internalType": "struct IGOFP.Compound1", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getSessionTokenStakeGuard", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + } + ], + "name": "getStageReward", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "terminusAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "terminusPoolId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardAmount", + "type": "uint256" + } + ], + "internalType": "struct IGOFP.Compound2", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getStakedTokenInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "adminTerminusAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "adminTerminusPoolID", + "type": "uint256" + } + ], + "name": "init", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "numSessions", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "staker", + "type": "address" + } + ], + "name": "numTokensStakedIntoSession", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC1155BatchReceived", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC1155Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "path", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "setIsChoosingActive", + "type": "bool" + } + ], + "name": "setCorrectPathForStage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "stages", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "paths", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "terminusAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "terminusPoolIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "rewardAmounts", + "type": "uint256[]" + } + ], + "name": "setPathRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "name": "setSessionActive", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isChoosingActive", + "type": "bool" + } + ], + "name": "setSessionChoosingActive", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "uri", + "type": "string" + } + ], + "name": "setSessionUri", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "stages", + "type": "uint256[]" + }, + { + "internalType": "address[]", + "name": "terminusAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "terminusPoolIds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "rewardAmounts", + "type": "uint256[]" + } + ], + "name": "setStageRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + } + ], + "name": "stakeTokensIntoSession", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfStakerInSessionByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "sessionId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "tokenIds", + "type": "uint256[]" + } + ], + "name": "unstakeTokensFromSession", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/IStatBlock.json b/abi/IStatBlock.json new file mode 100644 index 00000000..41b7f5d5 --- /dev/null +++ b/abi/IStatBlock.json @@ -0,0 +1,223 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "StatAssigned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "assignStats", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "tokenAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "tokenIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[][]", + "name": "statIDs", + "type": "uint256[][]" + }, + { + "internalType": "uint256[][]", + "name": "values", + "type": "uint256[][]" + } + ], + "name": "batchAssignStats", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "tokenAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "tokenIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + } + ], + "name": "batchGetStats", + "outputs": [ + { + "internalType": "uint256[][]", + "name": "", + "type": "uint256[][]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "createStat", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "describeStat", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + } + ], + "name": "getStats", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "isAdministrator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/StatBlock.json b/abi/StatBlock.json new file mode 100644 index 00000000..f1162478 --- /dev/null +++ b/abi/StatBlock.json @@ -0,0 +1,270 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "adminTerminusAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "adminTerminusPoolID", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "StatAssigned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "inputs": [], + "name": "NumStats", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "adminTerminusInfo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "assignStats", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "tokenAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "tokenIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[][]", + "name": "statIDs", + "type": "uint256[][]" + }, + { + "internalType": "uint256[][]", + "name": "values", + "type": "uint256[][]" + } + ], + "name": "batchAssignStats", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "tokenAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "tokenIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + } + ], + "name": "batchGetStats", + "outputs": [ + { + "internalType": "uint256[][]", + "name": "", + "type": "uint256[][]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "createStat", + "outputs": [ + { + "internalType": "uint256", + "name": "statID", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "statID", + "type": "uint256" + } + ], + "name": "describeStat", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "statIDs", + "type": "uint256[]" + } + ], + "name": "getStats", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "isAdministrator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file From f6bd8e7b6c4f571b59dc968f030a232ea11386de Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Fri, 11 Aug 2023 10:08:41 -0700 Subject: [PATCH 08/16] Added test for admin calls to batchAssignStats --- cli/web3cli/test_statblock.py | 130 +++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index b3f632b9..b95f94af 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -150,7 +150,9 @@ def test_nonadmin_cannot_create_stat(self): def test_admin_can_assign_stats(self): """ - Tests that administrator can assign a set of stats to a token + Tests that administrator can assign a set of stats to a token. + + Also tests that batchGetStats calls are consistent with return values of multiple getStats calls. Tests: - createStat @@ -287,6 +289,132 @@ def test_admin_can_assign_stats(self): ) self.assertEqual(actual_erc1155_stats, tuple(expected_erc1155_stats)) + def test_admin_can_batch_assign_stats(self): + """ + Tests that administrator can assign a set of stats to multiple tokens in a single transaction. + + Tests: + - createStat + - NumStats + - batchAssignStats + - getStats + """ + # Setup phase: create the stats that we will assign to. + num_assignable_stats = 3 + num_stats_0 = self.statblock.num_stats() + + stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + + for i in stat_ids: + stat_name = f"stat_{i}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + + num_stats_1 = self.statblock.num_stats() + self.assertEqual(num_stats_1, num_stats_0 + num_assignable_stats) + + # Assign stats to ERC20, ERC721, and ERC1155 tokens all in one transaction. + erc721_token_id = 42 + erc1155_token_id = 1337 + + expected_erc20_stats = [20 + i for i in stat_ids] + expected_erc721_stats = [721 + 42 + i for i in stat_ids] + expected_erc1155_stats = [1155 + 1337 + i for i in stat_ids] + + tx_receipt = self.statblock.batch_assign_stats( + [ + self.erc20_contract.address, + self.erc721_contract.address, + self.erc1155_contract.address, + ], + [0, erc721_token_id, erc1155_token_id], + [stat_ids, stat_ids, stat_ids], + [expected_erc20_stats, expected_erc721_stats, expected_erc1155_stats], + {"from": self.administrator}, + ) + + # Check for StatAssigned event emissions. + stat_assigned_events = _fetch_events_chunk( + web3_client, + statblock_events.STAT_ASSIGNED_ABI, + tx_receipt.block_number, + tx_receipt.block_number, + ) + + self.assertEqual(len(stat_assigned_events), 3 * num_assignable_stats) + + for i in range(num_assignable_stats): + self.assertEqual(stat_assigned_events[i]["event"], "StatAssigned") + self.assertEqual( + stat_assigned_events[i]["args"]["tokenAddress"], + self.erc20_contract.address, + ) + self.assertEqual(stat_assigned_events[i]["args"]["tokenID"], 0) + self.assertEqual(stat_assigned_events[i]["args"]["statID"], stat_ids[i]) + self.assertEqual( + stat_assigned_events[i]["args"]["value"], expected_erc20_stats[i] + ) + + for i in range(num_assignable_stats): + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["event"], "StatAssigned" + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["tokenAddress"], + self.erc721_contract.address, + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["tokenID"], + erc721_token_id, + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["statID"], + stat_ids[i], + ) + self.assertEqual( + stat_assigned_events[num_assignable_stats + i]["args"]["value"], + expected_erc721_stats[i], + ) + + for i in range(num_assignable_stats): + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["event"], + "StatAssigned", + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"][ + "tokenAddress" + ], + self.erc1155_contract.address, + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["tokenID"], + erc1155_token_id, + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["statID"], + stat_ids[i], + ) + self.assertEqual( + stat_assigned_events[2 * num_assignable_stats + i]["args"]["value"], + expected_erc1155_stats[i], + ) + + # Get stats and make sure they are correct + actual_erc20_stats = self.statblock.get_stats( + self.erc20_contract.address, 0, stat_ids + ) + self.assertEqual(actual_erc20_stats, tuple(expected_erc20_stats)) + + actual_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + self.assertEqual(actual_erc721_stats, tuple(expected_erc721_stats)) + + actual_erc1155_stats = self.statblock.get_stats( + self.erc1155_contract.address, erc1155_token_id, stat_ids + ) + self.assertEqual(actual_erc1155_stats, tuple(expected_erc1155_stats)) + if __name__ == "__main__": unittest.main() From e3b2bffd8e1394182cc2673dee661189baae4542 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Fri, 11 Aug 2023 10:11:36 -0700 Subject: [PATCH 09/16] Added consistency check for... `batchGetStats` with `getStats` --- cli/web3cli/test_statblock.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index b95f94af..1031b032 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -289,6 +289,21 @@ def test_admin_can_assign_stats(self): ) self.assertEqual(actual_erc1155_stats, tuple(expected_erc1155_stats)) + # Test getting stats in a batch + stats_batch = self.statblock.batch_get_stats( + [ + self.erc20_contract.address, + self.erc721_contract.address, + self.erc1155_contract.address, + ], + [0, erc721_token_id, erc1155_token_id], + stat_ids, + ) + # Check for consistency with outputs of `getStats` for each `(tokenAddress, tokenID)` pair. + self.assertEqual(stats_batch[0], tuple(actual_erc20_stats)) + self.assertEqual(stats_batch[1], tuple(actual_erc721_stats)) + self.assertEqual(stats_batch[2], tuple(actual_erc1155_stats)) + def test_admin_can_batch_assign_stats(self): """ Tests that administrator can assign a set of stats to multiple tokens in a single transaction. From fd94d27185f9c6bd506c97620854e25c85a3c00a Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Fri, 11 Aug 2023 10:18:46 -0700 Subject: [PATCH 10/16] Tests for non-administrator failure to assign stats... using both `assignStats` and `batchAssignStats`. --- cli/web3cli/test_statblock.py | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index 1031b032..d45cd331 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -430,6 +430,94 @@ def test_admin_can_batch_assign_stats(self): ) self.assertEqual(actual_erc1155_stats, tuple(expected_erc1155_stats)) + def test_nonadmin_cannot_assign_stats(self): + """ + Tests that a non-administrator cannot assign stats to tokens. + + Tests: + - assignStats + + Uses: + - createStat + - getStats + """ + num_assignable_stats = 3 + num_stats_0 = self.statblock.num_stats() + + stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + + for i in stat_ids: + stat_name = f"stat_{i}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + + num_stats_1 = self.statblock.num_stats() + self.assertEqual(num_stats_1, num_stats_0 + num_assignable_stats) + + erc721_token_id = 43 + expected_erc721_stats = [721 + erc721_token_id + i for i in stat_ids] + + expected_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + + with self.assertRaises(VirtualMachineError): + self.statblock.assign_stats( + self.erc721_contract.address, + erc721_token_id, + stat_ids, + expected_erc721_stats, + {"from": self.player}, + ) + + actual_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + self.assertEqual(actual_erc721_stats, expected_erc721_stats) + + def test_nonadmin_cannot_batch_assign_stats(self): + """ + Tests that a non-administrator cannot assign stats to tokens in a batch. + + Tests: + - batchAssignStats + + Uses: + - createStat + - getStats + """ + num_assignable_stats = 3 + num_stats_0 = self.statblock.num_stats() + + stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + + for i in stat_ids: + stat_name = f"stat_{i}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + + num_stats_1 = self.statblock.num_stats() + self.assertEqual(num_stats_1, num_stats_0 + num_assignable_stats) + + erc721_token_id = 44 + expected_erc721_stats = [721 + erc721_token_id + i for i in stat_ids] + + expected_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + + with self.assertRaises(VirtualMachineError): + self.statblock.batch_assign_stats( + [self.erc721_contract.address], + [erc721_token_id], + [stat_ids], + [expected_erc721_stats], + {"from": self.player}, + ) + + actual_erc721_stats = self.statblock.get_stats( + self.erc721_contract.address, erc721_token_id, stat_ids + ) + self.assertEqual(actual_erc721_stats, expected_erc721_stats) + if __name__ == "__main__": unittest.main() From 796a3beba6df5cf1349efd267e1d18926e962da6 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Fri, 11 Aug 2023 10:22:02 -0700 Subject: [PATCH 11/16] Added GitHub Action to test StatBlock --- .github/workflows/statblock.yml | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/statblock.yml diff --git a/.github/workflows/statblock.yml b/.github/workflows/statblock.yml new file mode 100644 index 00000000..d1beb05d --- /dev/null +++ b/.github/workflows/statblock.yml @@ -0,0 +1,42 @@ +name: StatBlock tests + +on: + pull_request: + paths: + - "contracts/stats/**" + - "cli/web3cli/test_statblock.py" + - "cli/web3cli/StatBlock.py" + - "cli/web3cli/IStatBlock.py" + - "cli/web3cli/statblock_events.py" + - ".github/workflows/statblock.yml" + branches: + - main +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "16" + - uses: actions/setup-python@v2 + with: + python-version: "3.9" + - name: Install ganache + run: npm install -g ganache-cli + - name: Upgrade pip + env: + BROWNIE_LIB: 1 + run: pip install -U pip + - name: Install additional dev dependencies + run: | + pip install black moonworm + - name: Install dependencies for CLI + working-directory: cli/ + env: + BROWNIE_LIB: 1 + run: | + pip install -e . + - name: Run tests + working-directory: cli/ + run: bash test.sh web3cli.test_statblock From 47497ec308ffaf5027e01b8dfdaf2cc1e956d833 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Sun, 20 Aug 2023 16:50:51 -0700 Subject: [PATCH 12/16] 1-indexing of stat IDs --- cli/web3cli/StatBlock.py | 4 ++-- cli/web3cli/test_statblock.py | 22 +++++++++++++++------- contracts/stats/StatBlock.sol | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cli/web3cli/StatBlock.py b/cli/web3cli/StatBlock.py index cc28bb6a..d9971ba9 100644 --- a/cli/web3cli/StatBlock.py +++ b/cli/web3cli/StatBlock.py @@ -1,5 +1,5 @@ -# Code generated by moonworm : https://github.com/bugout-dev/moonworm -# Moonworm version : 0.7.0 +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 import argparse import json diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index d45cd331..8aac597a 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -103,13 +103,13 @@ def test_admin_can_create_stat(self): - describeStat """ num_stats_0 = self.statblock.num_stats() - stat_name = f"stat_{num_stats_0}" + stat_name = f"stat_{num_stats_0 + 1}" tx_receipt = self.statblock.create_stat(stat_name, {"from": self.administrator}) num_stats_1 = self.statblock.num_stats() self.assertEqual(num_stats_1, num_stats_0 + 1) - stat_description = self.statblock.describe_stat(num_stats_0) + stat_description = self.statblock.describe_stat(num_stats_1) self.assertEqual(stat_description, stat_name) stat_created_events = _fetch_events_chunk( @@ -122,7 +122,7 @@ def test_admin_can_create_stat(self): event = stat_created_events[0] self.assertEqual(event["event"], "StatCreated") - self.assertEqual(event["args"]["statID"], num_stats_0) + self.assertEqual(event["args"]["statID"], num_stats_1) self.assertEqual(event["args"]["descriptor"], stat_name) self.assertEqual(event["address"], self.statblock.address) @@ -165,7 +165,9 @@ def test_admin_can_assign_stats(self): num_assignable_stats = 3 num_stats_0 = self.statblock.num_stats() - stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + stat_ids = [ + i + 1 for i in range(num_stats_0, num_stats_0 + num_assignable_stats) + ] for i in stat_ids: stat_name = f"stat_{i}" @@ -318,7 +320,9 @@ def test_admin_can_batch_assign_stats(self): num_assignable_stats = 3 num_stats_0 = self.statblock.num_stats() - stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + stat_ids = [ + i + 1 for i in range(num_stats_0, num_stats_0 + num_assignable_stats) + ] for i in stat_ids: stat_name = f"stat_{i}" @@ -444,7 +448,9 @@ def test_nonadmin_cannot_assign_stats(self): num_assignable_stats = 3 num_stats_0 = self.statblock.num_stats() - stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + stat_ids = [ + i + 1 for i in range(num_stats_0, num_stats_0 + num_assignable_stats) + ] for i in stat_ids: stat_name = f"stat_{i}" @@ -488,7 +494,9 @@ def test_nonadmin_cannot_batch_assign_stats(self): num_assignable_stats = 3 num_stats_0 = self.statblock.num_stats() - stat_ids = [i for i in range(num_stats_0, num_stats_0 + num_assignable_stats)] + stat_ids = [ + i + 1 for i in range(num_stats_0, num_stats_0 + num_assignable_stats) + ] for i in stat_ids: stat_name = f"stat_{i}" diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol index 3f458176..8552f4f9 100644 --- a/contracts/stats/StatBlock.sol +++ b/contracts/stats/StatBlock.sol @@ -38,7 +38,7 @@ contract StatBlock is IStatBlock { isAdministrator(msg.sender), "StatBlock.createStat: msg.sender must be an administrator of the StatBlock" ); - statID = NumStats++; + statID = ++NumStats; StatDescriptor[statID] = descriptor; emit StatCreated(statID, descriptor); } From 007de939d8bb870e894e0d90be4f0f0523075fce Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Sun, 20 Aug 2023 17:13:11 -0700 Subject: [PATCH 13/16] Added `setStatDescriptor` method `StatCreated` event now only reports `statID`, no longer contains `descriptor` argument. Added `StatDescriptorUpdated` argument which reports `statID` and new `descriptor`. Added tests for access control. --- cli/web3cli/StatBlock.py | 30 +++++++ cli/web3cli/statblock_events.py | 16 +++- cli/web3cli/test_statblock.py | 133 +++++++++++++++++++++++++++++++- contracts/stats/IStatBlock.sol | 3 +- contracts/stats/StatBlock.sol | 18 ++++- 5 files changed, 196 insertions(+), 4 deletions(-) diff --git a/cli/web3cli/StatBlock.py b/cli/web3cli/StatBlock.py index d9971ba9..79bff74d 100644 --- a/cli/web3cli/StatBlock.py +++ b/cli/web3cli/StatBlock.py @@ -183,6 +183,12 @@ def is_administrator( account, block_identifier=block_number ) + def set_stat_descriptor( + self, stat_id: int, descriptor: str, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setStatDescriptor(stat_id, descriptor, transaction_config) + def get_transaction_config(args: argparse.Namespace) -> Dict[str, Any]: signer = network.accounts.load(args.sender, args.password) @@ -372,6 +378,20 @@ def handle_is_administrator(args: argparse.Namespace) -> None: print(result) +def handle_set_stat_descriptor(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_stat_descriptor( + stat_id=args.stat_id, + descriptor=args.descriptor, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + def generate_cli() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="CLI for StatBlock") parser.set_defaults(func=lambda _: parser.print_help()) @@ -478,6 +498,16 @@ def generate_cli() -> argparse.ArgumentParser: ) is_administrator_parser.set_defaults(func=handle_is_administrator) + set_stat_descriptor_parser = subcommands.add_parser("set-stat-descriptor") + add_default_arguments(set_stat_descriptor_parser, True) + set_stat_descriptor_parser.add_argument( + "--stat-id", required=True, help="Type: uint256", type=int + ) + set_stat_descriptor_parser.add_argument( + "--descriptor", required=True, help="Type: string", type=str + ) + set_stat_descriptor_parser.set_defaults(func=handle_set_stat_descriptor) + return parser diff --git a/cli/web3cli/statblock_events.py b/cli/web3cli/statblock_events.py index d75f65a6..3157cecb 100644 --- a/cli/web3cli/statblock_events.py +++ b/cli/web3cli/statblock_events.py @@ -7,6 +7,20 @@ "name": "statID", "type": "uint256", }, + ], + "name": "StatCreated", + "type": "event", +} + +STAT_DESCRIPTOR_UPDATED_ABI = { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "uint256", + "name": "statID", + "type": "uint256", + }, { "indexed": False, "internalType": "string", @@ -14,7 +28,7 @@ "type": "string", }, ], - "name": "StatCreated", + "name": "StatDescriptorUpdated", "type": "event", } diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index 8aac597a..686d2f07 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -123,9 +123,26 @@ def test_admin_can_create_stat(self): event = stat_created_events[0] self.assertEqual(event["event"], "StatCreated") self.assertEqual(event["args"]["statID"], num_stats_1) - self.assertEqual(event["args"]["descriptor"], stat_name) self.assertEqual(event["address"], self.statblock.address) + stat_descriptor_updated_events = _fetch_events_chunk( + web3_client, + statblock_events.STAT_DESCRIPTOR_UPDATED_ABI, + tx_receipt.block_number, + tx_receipt.block_number, + ) + self.assertEqual(len(stat_descriptor_updated_events), 1) + + stat_descriptor_updated_event = stat_descriptor_updated_events[0] + self.assertEqual( + stat_descriptor_updated_event["event"], "StatDescriptorUpdated" + ) + self.assertEqual(stat_descriptor_updated_event["args"]["statID"], num_stats_1) + self.assertEqual(stat_descriptor_updated_event["args"]["descriptor"], stat_name) + self.assertEqual( + stat_descriptor_updated_event["address"], self.statblock.address + ) + def test_nonadmin_cannot_create_stat(self): """ Tests that an account which is not a StatBlock administrator cannot create a stat on the StatBlock @@ -148,6 +165,120 @@ def test_nonadmin_cannot_create_stat(self): num_stats_1 = self.statblock.num_stats() self.assertEqual(num_stats_1, num_stats_0) + def test_admin_can_set_stat_descriptor(self): + """ + Tests that an administrator can modify stat descriptors. + + Note that since the stat does not have to be created before its descriptor is set, this test + works with a stat that does not yet exist. + + It then creates the stat and checks that the stat descriptor was updated from the createStat + call, too. + + Tests: + - setStatDescriptor + - createStat + - describeStat + """ + num_stats = self.statblock.num_stats() + nonexistent_stat_id = num_stats + 1 + + expected_stat_descriptor = "nonexistent_stat" + + tx_receipt_0 = self.statblock.set_stat_descriptor( + nonexistent_stat_id, expected_stat_descriptor, {"from": self.administrator} + ) + + actual_stat_descriptor = self.statblock.describe_stat(nonexistent_stat_id) + self.assertEqual(actual_stat_descriptor, expected_stat_descriptor) + + stat_descriptor_updated_events_0 = _fetch_events_chunk( + web3_client, + statblock_events.STAT_DESCRIPTOR_UPDATED_ABI, + tx_receipt_0.block_number, + tx_receipt_0.block_number, + ) + self.assertEqual(len(stat_descriptor_updated_events_0), 1) + + stat_descriptor_updated_event_0 = stat_descriptor_updated_events_0[0] + self.assertEqual( + stat_descriptor_updated_event_0["event"], "StatDescriptorUpdated" + ) + self.assertEqual( + stat_descriptor_updated_event_0["args"]["statID"], nonexistent_stat_id + ) + self.assertEqual( + stat_descriptor_updated_event_0["args"]["descriptor"], + expected_stat_descriptor, + ) + self.assertEqual( + stat_descriptor_updated_event_0["address"], self.statblock.address + ) + + expected_new_descriptor = f"stat_{nonexistent_stat_id}" + + tx_receipt_1 = self.statblock.create_stat( + expected_new_descriptor, {"from": self.administrator} + ) + + actual_new_descriptor = self.statblock.describe_stat(nonexistent_stat_id) + self.assertEqual(actual_new_descriptor, expected_new_descriptor) + + stat_descriptor_updated_events_1 = _fetch_events_chunk( + web3_client, + statblock_events.STAT_DESCRIPTOR_UPDATED_ABI, + tx_receipt_1.block_number, + tx_receipt_1.block_number, + ) + self.assertEqual(len(stat_descriptor_updated_events_1), 1) + + stat_descriptor_updated_event_1 = stat_descriptor_updated_events_1[0] + self.assertEqual( + stat_descriptor_updated_event_1["event"], "StatDescriptorUpdated" + ) + self.assertEqual( + stat_descriptor_updated_event_1["args"]["statID"], nonexistent_stat_id + ) + self.assertEqual( + stat_descriptor_updated_event_1["args"]["descriptor"], + expected_new_descriptor, + ) + self.assertEqual( + stat_descriptor_updated_event_1["address"], self.statblock.address + ) + + def test_nonadmin_cannot_set_stat_descriptor(self): + """ + Tests that a non-administrator cannot modify stat descriptors. + + Checks with both non-existent and existent stats. + + Tests: + - setStatDescriptor + """ + num_stats = self.statblock.num_stats() + nonexistent_stat_id = num_stats + 1 + + attempted_stat_descriptor = "nonexistent_stat" + + with self.assertRaises(VirtualMachineError): + self.statblock.set_stat_descriptor( + nonexistent_stat_id, attempted_stat_descriptor, {"from": self.player} + ) + + actual_stat_descriptor = self.statblock.describe_stat(nonexistent_stat_id) + self.assertEqual(actual_stat_descriptor, "") + + stat_name = f"stat_{nonexistent_stat_id}" + self.statblock.create_stat(stat_name, {"from": self.administrator}) + actual_new_descriptor = self.statblock.describe_stat(nonexistent_stat_id) + self.assertEqual(actual_new_descriptor, stat_name) + + with self.assertRaises(VirtualMachineError): + self.statblock.set_stat_descriptor( + nonexistent_stat_id, attempted_stat_descriptor, {"from": self.player} + ) + def test_admin_can_assign_stats(self): """ Tests that administrator can assign a set of stats to a token. diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol index 7cef50e5..17110f0f 100644 --- a/contracts/stats/IStatBlock.sol +++ b/contracts/stats/IStatBlock.sol @@ -14,7 +14,8 @@ pragma solidity ^0.8.0; // To recalculate from root directory of this repo: // $ jq .abi build/contracts/IStatBlock.json | solface -name IStatBlock -annotations | grep "Interface ID:" interface IStatBlock { - event StatCreated(uint256 statID, string descriptor); + event StatCreated(uint256 statID); + event StatDescriptorUpdated(uint256 indexed statID, string descriptor); event StatAssigned( address indexed tokenAddress, uint256 indexed tokenID, diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol index 8552f4f9..4a261d62 100644 --- a/contracts/stats/StatBlock.sol +++ b/contracts/stats/StatBlock.sol @@ -40,7 +40,23 @@ contract StatBlock is IStatBlock { ); statID = ++NumStats; StatDescriptor[statID] = descriptor; - emit StatCreated(statID, descriptor); + emit StatCreated(statID); + emit StatDescriptorUpdated(statID, descriptor); + } + + // NOTE: This method does not check that the statID has already been created. That check is + // unnecessary, but it means that checking if a description exists is not a correct way to check + // if the stat with the corresponding statID exists. + function setStatDescriptor( + uint256 statID, + string memory descriptor + ) public { + require( + isAdministrator(msg.sender), + "StatBlock.setStatDescriptor: msg.sender must be an administrator of the StatBlock" + ); + StatDescriptor[statID] = descriptor; + emit StatDescriptorUpdated(statID, descriptor); } function describeStat( From 84788db2bfd364ace2a70b2a507432670656026b Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Mon, 21 Aug 2023 00:01:30 -0700 Subject: [PATCH 14/16] Added `statBlockVersion()` view method to `IStatBlock` ... and implemented it as a public string on `StatBlock`. Also added `setStatDescriptor` to `IStatBlock`. This was already implemented previously on `StatBlock`. --- cli/web3cli/StatBlock.py | 17 +++++++++++++++++ cli/web3cli/test_statblock.py | 3 +++ contracts/stats/IStatBlock.sol | 7 +++++++ contracts/stats/StatBlock.sol | 3 ++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/cli/web3cli/StatBlock.py b/cli/web3cli/StatBlock.py index 79bff74d..67756a1c 100644 --- a/cli/web3cli/StatBlock.py +++ b/cli/web3cli/StatBlock.py @@ -189,6 +189,12 @@ def set_stat_descriptor( self.assert_contract_is_instantiated() return self.contract.setStatDescriptor(stat_id, descriptor, transaction_config) + def stat_block_version( + self, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.statBlockVersion.call(block_identifier=block_number) + def get_transaction_config(args: argparse.Namespace) -> Dict[str, Any]: signer = network.accounts.load(args.sender, args.password) @@ -392,6 +398,13 @@ def handle_set_stat_descriptor(args: argparse.Namespace) -> None: print(result.info()) +def handle_stat_block_version(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = StatBlock(args.address) + result = contract.stat_block_version(block_number=args.block_number) + print(result) + + def generate_cli() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="CLI for StatBlock") parser.set_defaults(func=lambda _: parser.print_help()) @@ -508,6 +521,10 @@ def generate_cli() -> argparse.ArgumentParser: ) set_stat_descriptor_parser.set_defaults(func=handle_set_stat_descriptor) + stat_block_version_parser = subcommands.add_parser("stat-block-version") + add_default_arguments(stat_block_version_parser, False) + stat_block_version_parser.set_defaults(func=handle_stat_block_version) + return parser diff --git a/cli/web3cli/test_statblock.py b/cli/web3cli/test_statblock.py index 686d2f07..4bece0ff 100644 --- a/cli/web3cli/test_statblock.py +++ b/cli/web3cli/test_statblock.py @@ -93,6 +93,9 @@ def setUpClass(cls): cls.erc1155_contract = MockERC1155.MockERC1155(None) cls.erc1155_contract.deploy(cls.deployer_txconfig) + def test_stat_block_version(self): + self.assertEqual(self.statblock.stat_block_version(), "0.0.1") + def test_admin_can_create_stat(self): """ Tests that an administrator can create stats on a StatBlock contract. diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol index 17110f0f..ce9ebc4f 100644 --- a/contracts/stats/IStatBlock.sol +++ b/contracts/stats/IStatBlock.sol @@ -23,12 +23,19 @@ interface IStatBlock { uint256 value ); + function statBlockVersion() external view returns (string memory); + function isAdministrator(address account) external view returns (bool); function createStat(string memory descriptor) external returns (uint256); function describeStat(uint256) external view returns (string memory); + function setStatDescriptor( + uint256 statID, + string memory descriptor + ) external; + function assignStats( address tokenAddress, uint256 tokenID, diff --git a/contracts/stats/StatBlock.sol b/contracts/stats/StatBlock.sol index 4a261d62..18e268ab 100644 --- a/contracts/stats/StatBlock.sol +++ b/contracts/stats/StatBlock.sol @@ -10,6 +10,7 @@ import {IERC1155} from "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155 import {IStatBlock} from "./IStatBlock.sol"; contract StatBlock is IStatBlock { + string public statBlockVersion = "0.0.1"; address AdminTerminusAddress; uint256 AdminTerminusPoolID; // Stats are 0-indexed. @@ -50,7 +51,7 @@ contract StatBlock is IStatBlock { function setStatDescriptor( uint256 statID, string memory descriptor - ) public { + ) external { require( isAdministrator(msg.sender), "StatBlock.setStatDescriptor: msg.sender must be an administrator of the StatBlock" From d974f9b7ce7a1f04793b4cfc97dbc3c6f7a56af0 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Mon, 21 Aug 2023 00:03:03 -0700 Subject: [PATCH 15/16] Updated `IStatBlock` interface ID --- contracts/stats/IStatBlock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol index ce9ebc4f..95beb9c8 100644 --- a/contracts/stats/IStatBlock.sol +++ b/contracts/stats/IStatBlock.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.0; * GitHub: https://github.com/moonstream-to/web3 */ -// Interface ID: 458bb0c0 +// Interface ID: 9a7d8aed // // Calculated by solface: https://github.com/moonstream-to/solface // solface version: 0.1.0 From 892b53d5a68f6ffa15a235dfee3bb5c413baf977 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Mon, 21 Aug 2023 00:04:13 -0700 Subject: [PATCH 16/16] Extracted ABIs --- abi/IStatBlock.json | 46 ++++++++++++++++++++++++++++++++++++++++++++- abi/StatBlock.json | 46 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/abi/IStatBlock.json b/abi/IStatBlock.json index 41b7f5d5..e29da235 100644 --- a/abi/IStatBlock.json +++ b/abi/IStatBlock.json @@ -38,6 +38,19 @@ "internalType": "uint256", "name": "statID", "type": "uint256" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" }, { "indexed": false, @@ -46,7 +59,7 @@ "type": "string" } ], - "name": "StatCreated", + "name": "StatDescriptorUpdated", "type": "event" }, { @@ -219,5 +232,36 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "setStatDescriptor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "statBlockVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/abi/StatBlock.json b/abi/StatBlock.json index f1162478..ccc2af7f 100644 --- a/abi/StatBlock.json +++ b/abi/StatBlock.json @@ -54,6 +54,19 @@ "internalType": "uint256", "name": "statID", "type": "uint256" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" }, { "indexed": false, @@ -62,7 +75,7 @@ "type": "string" } ], - "name": "StatCreated", + "name": "StatDescriptorUpdated", "type": "event" }, { @@ -266,5 +279,36 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "setStatDescriptor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "statBlockVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file