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 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..e29da235 --- /dev/null +++ b/abi/IStatBlock.json @@ -0,0 +1,267 @@ +[ + { + "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" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "StatDescriptorUpdated", + "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" + }, + { + "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 new file mode 100644 index 00000000..ccc2af7f --- /dev/null +++ b/abi/StatBlock.json @@ -0,0 +1,314 @@ +[ + { + "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" + } + ], + "name": "StatCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "statID", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "descriptor", + "type": "string" + } + ], + "name": "StatDescriptorUpdated", + "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" + }, + { + "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/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/StatBlock.py b/cli/web3cli/StatBlock.py new file mode 100644 index 00000000..67756a1c --- /dev/null +++ b/cli/web3cli/StatBlock.py @@ -0,0 +1,538 @@ +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 + +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 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: + 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 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 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) + 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_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) + 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 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 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()) + 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) + + 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) + + 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) + + 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) + + 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 + + +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/cli/web3cli/statblock_events.py b/cli/web3cli/statblock_events.py new file mode 100644 index 00000000..3157cecb --- /dev/null +++ b/cli/web3cli/statblock_events.py @@ -0,0 +1,65 @@ +STAT_CREATED_ABI = { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "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", + "name": "descriptor", + "type": "string", + }, + ], + "name": "StatDescriptorUpdated", + "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 new file mode 100644 index 00000000..4bece0ff --- /dev/null +++ b/cli/web3cli/test_statblock.py @@ -0,0 +1,665 @@ +import unittest + +from brownie import accounts, network, web3 as web3_client +from brownie.exceptions import VirtualMachineError +from moonworm.watch import _fetch_events_chunk + +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_statblock(cls): + """ + Deploys the StatBlock contract being tested. + """ + 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.statblock = StatBlock.StatBlock(None) + cls.statblock.deploy( + cls.terminus.address, + cls.admin_terminus_pool_id, + cls.deployer_txconfig, + ) + + @classmethod + 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, + ) + + @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_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_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. + + Tests: + - createStat + - NumStats + - describeStat + """ + num_stats_0 = self.statblock.num_stats() + 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_1) + 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_1) + 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 + contract. + + Tests: + - createStat + - NumStats + """ + # 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) + + 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. + + Also tests that batchGetStats calls are consistent with return values of multiple getStats calls. + + 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 + 1 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)) + + # 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. + + 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 + 1 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)) + + 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 + 1 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 + 1 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() 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); diff --git a/contracts/stats/IStatBlock.sol b/contracts/stats/IStatBlock.sol new file mode 100644 index 00000000..95beb9c8 --- /dev/null +++ b/contracts/stats/IStatBlock.sol @@ -0,0 +1,64 @@ +// 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: 9a7d8aed +// +// 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 { + event StatCreated(uint256 statID); + event StatDescriptorUpdated(uint256 indexed statID, string descriptor); + event StatAssigned( + address indexed tokenAddress, + uint256 indexed tokenID, + uint256 indexed statID, + 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, + 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..18e268ab --- /dev/null +++ b/contracts/stats/StatBlock.sol @@ -0,0 +1,143 @@ +// 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 { + string public statBlockVersion = "0.0.1"; + address AdminTerminusAddress; + uint256 AdminTerminusPoolID; + // Stats are 0-indexed. + uint256 public 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; + 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 + ) external { + require( + isAdministrator(msg.sender), + "StatBlock.setStatDescriptor: msg.sender must be an administrator of the StatBlock" + ); + StatDescriptor[statID] = descriptor; + emit StatDescriptorUpdated(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]; + emit StatAssigned(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; + } +}