From 8e122386b6e81aedf93b2ecd40d55ff40ce8f6d2 Mon Sep 17 00:00:00 2001 From: six <51x@keemail.me> Date: Thu, 25 May 2023 15:58:35 +0200 Subject: [PATCH] Test deploy on Moonbase Alpha --- README.md | 8 +- contracts/CCTF_2022.sol | 138 +++++++ contracts/CCTF_by_Anonz.sol | 130 ++++++ frontend/abi.js | 798 ++++++++++++++++++++++++++++++++++++ frontend/cctf-icon.png | Bin 0 -> 6666 bytes frontend/cctf-logo.png | Bin 0 -> 6977 bytes frontend/index.html | 121 ++++++ frontend/metafaka.png | Bin 0 -> 1881 bytes frontend/metamask.svg | 1 + frontend/script.js | 330 +++++++++++++++ frontend/style.css | 360 ++++++++++++++++ 11 files changed, 1884 insertions(+), 2 deletions(-) create mode 100644 contracts/CCTF_2022.sol create mode 100644 contracts/CCTF_by_Anonz.sol create mode 100644 frontend/abi.js create mode 100644 frontend/cctf-icon.png create mode 100644 frontend/cctf-logo.png create mode 100644 frontend/index.html create mode 100644 frontend/metafaka.png create mode 100644 frontend/metamask.svg create mode 100644 frontend/script.js create mode 100644 frontend/style.css diff --git a/README.md b/README.md index fed67e5..0b3af49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# DCTF +# DCTF Beta -Decentralized CTF engine, used by CCTF \ No newline at end of file +Decentralized CTF engine, used by CCTF + +Testnet deployment on Moonbase Alpha: https://moonbase.moonscan.io/address/0x919f68cc35ce5d49a45c94dc44e7bf444f9a7531 + +Contributors: six, Silur, Anonz team, Metafaka team, BMEta team \ No newline at end of file diff --git a/contracts/CCTF_2022.sol b/contracts/CCTF_2022.sol new file mode 100644 index 0000000..018dca2 --- /dev/null +++ b/contracts/CCTF_2022.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: six and Silur +pragma solidity ^0.8.16; + +contract CCTF9 { + address public admin; + uint256 public volStart; + uint256 public volMaxPoints; + uint256 public powDiff; + bool public started; + + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint256 points; + } + + modifier onlyAdmin { + require(msg.sender == admin, "Not admin"); + _; + } + + modifier onlyActive { + require(started == true, "CCTF not started."); + _; + } + + struct Flag { + address signer; + bool onlyFirstSolver; + uint256 points; + string skill_name; + } + + mapping (address => Player) public players; + mapping (uint256 => Flag) public flags; + + event CCTFStarted(uint256 timestamp); + event FlagAdded(uint256 indexed flagId, address flagSigner); + event FlagRemoved(uint256 indexed flagId); + event FlagSolved(uint256 indexed flagId, address indexed solver); + + constructor(uint256 _volMaxPoints, uint256 _powDiff) { + admin = msg.sender; + volMaxPoints = _volMaxPoints; + powDiff = _powDiff; + started = false; + } + + function setAdmin(address _admin) external onlyAdmin { + require(_admin != address(0)); + admin = _admin; + } + + function setCCTFStatus(bool _started) external onlyAdmin { + started = _started; + } + + function setFlag(uint256 _flagId, address _flagSigner, bool _onlyFirstSolver, uint256 _points, string memory _skill) external onlyAdmin{ + flags[_flagId] = Flag(_flagSigner, _onlyFirstSolver, _points, _skill); + emit FlagAdded(_flagId, _flagSigner); + } + + function setPowDiff(uint256 _powDiff) external onlyAdmin { + powDiff = _powDiff; + } + + + function register(string memory _RTFM) external { + require(players[msg.sender].status == PlayerStatus.Unverified, 'Already registered or banned'); + //uint256 pow = uint256(keccak256(abi.encodePacked("CCTF", msg.sender,"registration", nonce))); + //require(pow < powDiff, "invalid pow"); + require(keccak256(abi.encodePacked('I_read_it')) == keccak256(abi.encodePacked(_RTFM))); // PoW can be used for harder challenges, this is Entry! + players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(address player, PlayerStatus status) external onlyAdmin { + players[player].status = status; + } + + +////////// Submit flags + mapping(bytes32 => bool) usedNs; // Against replay attack (we only check message signer) + mapping (address => mapping (uint256 => bool)) Solves; // address -> challenge ID -> solved/not + uint256 public submission_success_count = 0; // For statistics + + function SubmitFlag(bytes32 _message, bytes memory signature, uint256 _submitFor) external onlyActive { + require(players[msg.sender].status == PlayerStatus.Verified, "You are not even playing"); + require(bytes32(_message).length <= 256, "Too long message."); + require(!usedNs[_message]); + usedNs[_message] = true; + require(recoverSigner(_message, signature) == flags[_submitFor].signer, "Not signed with the correct key."); + require(Solves[msg.sender][_submitFor] == false); + + Solves[msg.sender][_submitFor] = true; + players[msg.sender].points += flags[_submitFor].points; + players[msg.sender].points = players[msg.sender].points < volMaxPoints ? players[msg.sender].points : volMaxPoints; + + if (flags[_submitFor].onlyFirstSolver) { + flags[_submitFor].points = 0; + } + + submission_success_count = submission_success_count + 1; + emit FlagSolved(_submitFor, msg.sender); + } + + function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v){ + require(sig.length == 65, "Invalid signature length"); + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + +////////// Check status, scores, etc + function getPlayerStatus(address _player) external view returns (PlayerStatus) { + return players[_player].status; + } + + function getPlayerPoints(address _player) external view returns (uint256) { + return players[_player].points < volMaxPoints ? players[_player].points : volMaxPoints; + } + + function getSuccessfulSubmissionCount() external view returns (uint256){ + return submission_success_count; + } +} diff --git a/contracts/CCTF_by_Anonz.sol b/contracts/CCTF_by_Anonz.sol new file mode 100644 index 0000000..58d93df --- /dev/null +++ b/contracts/CCTF_by_Anonz.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Authors: Anonz team, developed at Polkadot Metaverse Championship, as part of their CCTF track solution. +// Based on Six's and Silur's CCTF 2022 code. +pragma solidity ^0.8.16; + +contract CryptoCTFX { + enum PlayerStatus { + Unverified, + Verified, + Banned + } + + struct Player { + PlayerStatus status; + uint score; + } + + modifier onlyExistingContest(uint contestID) { + require(contests[contestID].admin != address(0), "Unknown contest ID"); + _; + } + + modifier onlyAdmin(uint contestID) { + require(msg.sender == contests[contestID].admin, "You are not the admin of this contest"); + _; + } + + modifier onlyOpen(uint contestID) { + require(contests[contestID].submissionsOpen, "Submissions are not open for this contest at this time"); + _; + } + + modifier onlyExistingChallenge(uint contestID, uint challengeID) { + require(contests[contestID].challenges[challengeID].obscuredFlag != address(0), "Unknown challenge ID"); + _; + } + + struct Challenge { + address obscuredFlag; // public key of the flag + uint worth; + uint256 descriptionFingerprint; + bool onlyFirstSolver; + string skill; + } + + struct Contest { + address admin; + mapping (uint => Challenge) challenges; + mapping (address => Player) players; + bool submissionsOpen; + mapping (address => mapping (uint => bool)) solves; // address -> challengeID -> solved/not + mapping (uint => bool) anySolves; // challengeID -> solved/not + } + + mapping (uint => Contest) public contests; + + event ChallengeAddedOrUpdated(uint contestID, uint indexed challengeID); + event ChallengeSolved(uint contestID, uint indexed challengeID, address indexed solver); + + function createContest(uint contestID) external { + require(contests[contestID].admin == address(0), "This contest ID has already been registered"); + contests[contestID].admin = msg.sender; + contests[contestID].submissionsOpen = false; + } + + function setAdmin(uint contestID, address newAdmin) external onlyExistingContest(contestID) onlyAdmin(contestID) { + require(newAdmin != address(0)); + contests[contestID].admin = newAdmin; + } + + function setSubmissionsStatus(uint contestID, bool open) external onlyExistingContest(contestID) onlyAdmin(contestID) { + contests[contestID].submissionsOpen = open; + } + + function addOrUpdateChallenge(uint contestID, uint challengeID, address obscuredFlag, uint worth, uint256 descriptionFingerprint, bool onlyFirstSolver, string memory skill) external onlyExistingContest(contestID) onlyAdmin(contestID) { + require(obscuredFlag != address(0), "The obscured flag value must not be 0"); + contests[contestID].challenges[challengeID] = Challenge(obscuredFlag, worth, descriptionFingerprint, onlyFirstSolver, skill); + emit ChallengeAddedOrUpdated(contestID, challengeID); + } + + function register(uint contestID, string memory password) external onlyExistingContest(contestID) { + require(contests[contestID].players[msg.sender].status == PlayerStatus.Unverified, "You are already registered or banned in this contest"); + require(keccak256(abi.encodePacked("I_read_it")) == keccak256(abi.encodePacked(password))); + contests[contestID].players[msg.sender].status = PlayerStatus.Verified; + } + + function setPlayerStatus(uint contestID, address player, PlayerStatus status) external onlyExistingContest(contestID) onlyAdmin(contestID) { + contests[contestID].players[player].status = status; + } + + function submitFlag(uint contestID, uint challengeID, bytes memory signature) external onlyExistingContest(contestID) onlyExistingChallenge(contestID, challengeID) onlyOpen(contestID) { + require(contests[contestID].players[msg.sender].status == PlayerStatus.Verified, "You are unverified or banned in this contest"); + // the correct signature is an ECDSA signature where (1) the message (hash) is the sender address and (2) the private key is the flag; + // (2) is checked by testing against the public key, which can then be public information + address recoveredSigner = recoverSigner(bytes32(abi.encodePacked(msg.sender)), signature); + require(recoveredSigner != address(0), "Invalid signature"); + require(recoveredSigner == contests[contestID].challenges[challengeID].obscuredFlag, "Wrong answer"); + require(!contests[contestID].solves[msg.sender][challengeID], "You have already solved this challenge of this contest"); + + if (!contests[contestID].anySolves[challengeID] || !contests[contestID].challenges[challengeID].onlyFirstSolver) { + contests[contestID].players[msg.sender].score += contests[contestID].challenges[challengeID].worth; + } + contests[contestID].solves[msg.sender][challengeID] = true; + contests[contestID].anySolves[challengeID] = true; + + emit ChallengeSolved(contestID, challengeID, msg.sender); + } + + function recoverSigner(bytes32 messageHash, bytes memory signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); + return ecrecover(messageHash, v, r, s); + } + + function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) { + require(sig.length == 65, "Invalid signature length"); + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + + function getPlayerStatus(uint contestID, address player) external view onlyExistingContest(contestID) returns (PlayerStatus) { + return contests[contestID].players[player].status; + } + + function getPlayerScore(uint contestID, address player) external view onlyExistingContest(contestID) returns (uint) { + return contests[contestID].players[player].score; + } +} diff --git a/frontend/abi.js b/frontend/abi.js new file mode 100644 index 0000000..c703f66 --- /dev/null +++ b/frontend/abi.js @@ -0,0 +1,798 @@ +myABI = [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_volMaxPoints", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "CCTFStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "flagSigner", + "type": "address" + } + ], + "name": "FlagAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + } + ], + "name": "FlagRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "flagId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "FlagSolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum CCTF9.PlayerStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "PlayerStatusChanged", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_message", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "_submitFor", + "type": "uint256" + } + ], + "name": "SubmitFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "TIME_DECAY_MAX", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "endTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "flagCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flagList", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "flags", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFlags", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "solveCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentPoints", + "type": "uint256" + }, + { + "internalType": "string", + "name": "skill_name", + "type": "string" + } + ], + "internalType": "struct CCTF9.Flag[]", + "name": "flagListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_player", + "type": "address" + } + ], + "name": "getPlayerStatus", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPlayers", + "outputs": [ + { + "components": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256[2][]", + "name": "pointsPerFlag", + "type": "uint256[2][]" + } + ], + "internalType": "struct CCTF9.Player[]", + "name": "playerListRet", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSuccessfulSubmissionCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "playerCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "playerList", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "players", + "outputs": [ + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "powDiff", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_ethSignedMessageHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "recoverSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_RTFM", + "type": "string" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + } + ], + "name": "register", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_admin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_endTime", + "type": "uint256" + } + ], + "name": "setCCTFEndTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_paused", + "type": "bool" + } + ], + "name": "setCCTFPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_startTime", + "type": "uint256" + } + ], + "name": "setCCTFStartTime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_flagId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_flagSigner", + "type": "address" + }, + { + "internalType": "enum CCTF9.FlagType", + "name": "_flagType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_points", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_skill", + "type": "string" + } + ], + "name": "setFlag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "internalType": "enum CCTF9.PlayerStatus", + "name": "status", + "type": "uint8" + } + ], + "name": "setPlayerStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_powDiff", + "type": "uint256" + } + ], + "name": "setPowDiff", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "sig", + "type": "bytes" + } + ], + "name": "splitSignature", + "outputs": [ + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "startTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "started", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "submission_success_count", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volMaxPoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "volStart", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/frontend/cctf-icon.png b/frontend/cctf-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ae4a6efe77bc80e5d026547c28145f8a092d369 GIT binary patch literal 6666 zcmZu$cT`i&zNH5#p-K%w2?!`i3y_3fgNO)76QxEGqy-4Q2!aFz61s>;7c3~^M=ufx z5Cy@|ks62;=^Yf2m+yY}-TVG{Yn?fBX3kk>X7=pg-s}8Ml7%^vlTCz;j*gD=iix2W z9UVRH@50JR>#3jMH>Xvt7G~B)w3>?x2;>6t@No0-@t&TZde+F(I-?42gyEgNLY-Ya z?E~XoBXS)A0|KmXn(G_uI^MCBkT@TiZyQx&5s~ePj&&Iw9rXxweE06H-vgKM45#Mi zX5>}FrluyihWeFjMpv&R&2N~7q&N!*oHa7hM|t1W($=uFHPhACzUzA{GVi9cilUvP zrI;A#maDac%MG7MXH0@iM0j{`vP)!{c~GK@l$4~iyIn-N*~Lq;!Ie4%1>| zaz9B?X>(=9Sef6TgTv#=t^E`*9i7nO6+=Di(22EapUgo;erC!W+!fcGFS172B0SW#} z^yhoyoIGzVzAPQ~i~K~h*xfjA%H#}C;t5qMr~Ui_iNzl}Qu-Fex$hy5RMDE~9WcQQ z1VRTGK{r`&a>=jn2*YOFgh4kC$~{5X_Hti_@af+(>?kX$FXYOanPAhU@ z`6adss)+@bB zb!lt_&4kg0<5mQ7F~MjU2zH=l@KIE_=B|uUwj6HXwK%#@Dc^ER9dvI+h}4K^`J||6 ziyc$mP<>8R;Og5pHSJY;!VB6PTu9qt0|{|p*VGG$m+*wSY$Ro?2Z9b=;DNR5&>;(M z;m#ze^z8a)YXXo3n}pao0nk4D+R|1xb=21?SEHFnBHUtZENd&k)b3&6)#E0$0-`j& zdf=StP2xgPyecaV5k9Os_(XgE%@?I}w;Karyn!s$#1KXC)E^yZr!R*?6G}zFeRIq4 z$T(ONWk3=K423=}JpuR4d$b~nus?;Qo{x7gVFEQedV^&ec0cqyj_8H`c|w|UBHOR# zuntr~G5FyfuuWoX_>0wN^G4Is)(feMH%Cq??}AQ0qac_fdf_#SE!op$be&sQ0%udVS52EJL-s`QU z1;lMz)82t6gJ|wRP;`0S3lLI^f%Mt4?7x^r?j%w}Y}|-EC}hP=9E!+8y47oWA^_XU zte#6mp*!6hY400H5!E}Y)}tS#utY*M?b?#gkM*8O@OsP?a78PkUw;z1abD!F5R)fDL;P{Wr8)_f`52CeUSXd~ZfIuHgf;MLH-i%AjD7iUn?m5`WgZwA7GBss6h1ZbgbUdMo#8j5jYUkwgA;S zGJC7d1M-MCX5H#XYew?IT6!`Gb2bG`z^WZdbZGYMj!byNz5AlD$MfS56&$WrsXsPB{D<_`H;@xsA=U!(E#52h>4^A4=Uy!g-T z&}O`da4?n;{NPKmX16hM&H|i1aBd-8h|>y;D)rAIhI2G;scms-h9^#5#Dj~$mp9+H zK*TtnEDMb`N%&H=*E)sKQ$o_5PlJ?(vc8< z<%`etr0GmFv6uZYxPfyzu>KkZ;w#?hIPm-jjrk@iEVB4d9-sw_f1yt;rGR14*^J;N zjFS{G-nBF@Y0`R(+J`qk@>pSCir1L9@`D2j;Dfc8QnoI}65r4mLSxIn8%O<0h7HYq zqtsrEV#A4auSZ2W3iMQW23=_9Q!;t=^#|)zolnL37hl~+MV3*W$`Y9HE@Z6^=G%X1 zRvu@yIjg+L9FhXE5>S%&abpZ1vJ0M>4N5511RE{}*@2(FgxfxRf`#72IZ=*&x@YV{ zZLs#`NpnRQFWBV~r7l>e=N*``T$>fvs&%+-q z@kAvP@}^TvsV31x7&n$vD%TgMY}%!CnG<>Qa$i6Rx9NxV8qDLkc+S*@)_0SwaeM+( zYQaHfe9f=sKo-BMPH!~}mmqIMhYV4JO%dHnd_kimxYPC-1uuw_-$u)#(D`K@pO?W$b8rldEJk$v!Hbkj10Ozn?YMjI$Pbe|y<`iV(pZby=xhdc zF9PZoc^DLq*kvRli%>FX2mOi{Dj=Hib)Z2JWem`9R1v$TyG-XZ-tMEMK!^p+7Kzz| z1kZ{v3>H;Z7@jV9)ZolJ0`eGpEkz+DgI!yO?Y{Bl@9SR>=O8ildIbsnSm(G>a#pru z%a}9o$RI=J-Io8eP1xuL*0p^uT5fb9_o^qntc3yF;twyY0i|cOJ5Eo7hGV{LM3!~5D{6eyv^d2Y3zZ}Vf6 z+-;n;!q+4g3L)o>XJ7jKdiV~z$TUrGP##lV^jsbUMv=>a1zUr#mN=G~_ef)d2+6jh zf%7=vh5rZBe*o_C!ug4prn!XBgG;&ReCsw^iE8SH%{QV`It=9n8NyaS2F?0AH_U2O z7xN8?;Zl!2 zvG3u^F+@jgDaZM9J^7nbI!^*f^^(3XZtoynVhMdG2|N6dz$r@kC(5%;sX|elkh;+f zmFg!gMbnF_wdb6ELyFu9T=c;XGCp}19y@S0MSpmg@K;b2)bzn8yu z&q4_45;A3LX5DhSsElm1=Zt?`y)4x%jz3P znLkTpbBaEm?H$6(GZ*jF581D2DntLqeWJwI|=>4*_A zHxlyAP20*u6v$rZTU`!}Fu>?`6q$cUyvv|1^UHM;R-IRIQBeL>7xAx&lsHnk`~7)u z$si%|r{n*-V%pLVzaxmF7uMDvVEgFQ4)H^mn1AKZ?mT6OTVifYEU9uKZ*QSH^M|C) zm_d;)k(&Md7$NabQS+TF7E&{^^o(l;)xa)9N$Z~yh$ka$STj9<+Oz6-n3AX1_CsU| zwZc*!@OIsR?bce&Vqb0=_h4!xa5{bXRoqJ|79v`5sVdG%55S8dJ7E%Wz7PcB(eg5X zN$HJ5oVjZdf7enGqY?h`JnE{Yg$a4isWity5${|eFrj0rTrRrX6S@`1#9pv1BxHGR zX)iXQfcJ;xjugxG8)oq+jbP=)y`-Unc!>C;zx2A1ETW!b-6SR&175m|mQcrph8h;0 zxvlBWZ^W_Bvw}??o@4#z=n!9&V62*Ksr!sU<}Y7kc{icSv7UPEW}R{ue;460A3yHI=br^y1++Z2x}H<^(e)R}v9jMr|D>7b~;?9Y~0p8GUSjE3gJ4 z=Aa?QziNMf9mBm^}@rU_8EMj|uvdAG@=49r4klZ*}B>VwD_ZVak5#|(K}3%YtZIJt>1-N75hp{ZsK^A zsb5P=Ppe&%IYRs|IfGI-+sq;*eRSj6v_s1}{QcVzzC@UTyEh?b+tNQa!q)zRw%e3E z@!q6--LVhSiFDAjN*D`JKiw5s=^tQ{(YwH~3ROr}mwv zmm5bf*10Bf80UX}AF?jWi~D&X^hp0z1d6CL>FWScJCrfUY!-(2MHb`*zXH*uGCBRh zMICjuEM#P4pZU$SBH~eQf@NHSX%2n0%L zHQP{~{S6y_UX1k!a#_*Qch=U1?2mO@I(^QDc)fBC!Mn!)Y58G=(C~$V%R+S}x?MjS zPi`l;UAz2J?3zJ@SqLjBkZ1GN_h9@V7KMU8HckcQv{e#VfVy(|sae3|_AfMITTmxa z5=SwqCV*_6d(?akStZ5{)R;I{W+y#KZNRh6A0*{x;&u)G!-apu``^S^Zon-i+|g&E zQV%$rWQ^4s=Q-K>O$^Ly=9r}eY!U7~TM1kS4GVh$$QeYpHa;bF*_Z$A32L`Mi6Z_% z=P=C88(os>Os?RAUlMy9RA%lNLw^)nQpBh>PC}R|?d8*F%TiH2ydvhJPUsK#zXsEF zb`d*Vwls15(pTFb=quKDHvM}xLGWe*PHsY?O-74kxW}cyo@lHQGxh-ie$ej zKr0x*t8wq=+3#dMyGqCe{8lk5g6-{dphLJA2K)Vx{1`|R6+#oI+T3?nAzQjs+I6Re5_~11YidY> zLl+9Qana?hsCR8}HK;g;kPx%}kZKP&>m5g7R7OF2J1iQM}Bw ztb1W(g<)jdcDfr4e3{W=L*%c6xgf{M=J~qZ5mhzRXl*9p3>9)HQ&Gn$W&#(RPMx)I z3=`YuN2C>%`WhmwNP4S9sEYl88GGeUM8Zb=R%avScO@HQxo5S=jvUJ*RwF1@Vt`5f z7rdm+f%GLKa{7|^Tb$;Dv5X}evA@GHk^-H4WzWdb=d?P7e=K8(XVoyQ2FwOlxcnhN z-lOXHzuj-_aoFjn3Q;G;f7u^+J_Qr$x#=4*kx(o{=d9Fc{>(;R{qn77ZL7}7f)MMSvWViz!AK^e&j>Q;jcVTH@1gs-(0j8ANjb}9;?0V0HiMe0 zj*-8wC@y$70!nBH$7u)tu44J)QD~)Us8?S9 zIF6jNa{T%4+m;I+}`pc-#g4qD1F z?(Z!S0C?Pfnm#Qa`hiV*u(Wf-EtDN1OXeV_L zSbuAKSpfibQoTTOFhze#npfP!{k;o??Xi0PpNiXguKZ93crgKQS59p7m+Q?2u?dFcWEQpN!+EaLlj-qVY5U&CKRP(=Y*%6I^cvaB%t(!zGV3le< zb?X*U6?fylS(+A6d~z>fE18-lQmt#O={=10iY_;NeJt!Jy^{4)iYpyq=@j1SZF`AF{1b?yv%CEeni&h z8wD$#|KMv6Z>rXhUX7;K!S@BF&LVYzduGG{+(*yxz5xx+H}GE-t|~|_Rk=m99#s@D z;$cFf2O;6N$ui7K&Av*0@0fPVjye6lGJo*TATo3EQpoZNx~u02^r_uMJ)ktyLyZxj z-la6RPs?oeNpx*L9TV=Z4g~pnSknVY-*ai1e+OD9Ya>3-r5G!~rUsT7u?8Yq6!aD1 z17HQTWeII+@bXU6bcplFd$oG8@aefMV`);h2k$e`iy3e;aW2PL8lYL^iuT~b7Vxj+ v8uMMB9pDsE^Jn6G27731Vq#LYD~isK&nhXUxY7QJ&|NVyH+-k>^6-BE+9r8$ literal 0 HcmV?d00001 diff --git a/frontend/cctf-logo.png b/frontend/cctf-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..036bc8d0ce0422b3c99f22f6ab29878e046f802d GIT binary patch literal 6977 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 TV6I~l00000NkvXXu0mjf*mBb` literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..99a0374 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,121 @@ + + + + CCTF Frontend by Metafaka + + + + + + +
+ + + +
+
+ +
+
+ PLAYERS
+ loading... +
+
+ +
+
+ FOUND FLAGS
+ loading... +
+
+ +
+
+ --:--:-- +
+
+ +
+
+ + +
+ + Getting Started +
+
+
+
+ +
+
+

My Account

+
+ +
+ + +
+
+ +
+
Loading...
+

Leaderboard

+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/frontend/metafaka.png b/frontend/metafaka.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e12f530a450eb5041a454dcdb7cded1e083444 GIT binary patch literal 1881 zcma)-`8OMg8io_GheRx`s-~gTh}ttcR4_`_mZoDT%G6dmsXP|2Yh*0jFY0ghCBcOP;|cT z@ErgEwEJUaL0>Kh7%%dd8o;`FI(})fSd2g*_2!KXNQkkq@yg0dVq)Ux=%|yEQ(Rn}NF*YW zNMJA+hr@YzcvxCm?(XiYs;V9y9(HwgrKhLo=jTJAP&S+0(b1u!qf=jBk4B?;Jl@2_ zL|z#CO*Cy**y|l&MP$bnz?Hh83w}|U?fHy_9_2;Fu49u&IgXALO z$I-LTHxjw^2M;BnJ0iFBJBmrMl;2$Ny6c=5>+VK@51qWb5$CJo`IGEYSkiScApv>F z#`Km)d+{n276wCZWkueej}AT0(C-)fSe)Q2t+wt`ot4to1Fr7Yq>upldZJ(NXg!X` z99`*q7p8-{a8HZ;rt`5|uI#m-J}o{}uF=XNc&Ne}!MwJiB8h^zn9fF=RzAN4eA*Bx z2i-5E)fH=+5OP=Qlr>Et$3(~5Af7h-l$@gPHQb#%g`}+M4+J>{Uz(Opj<8Rv$yN)~ znFpS>>!K>3GI>O9Ny(F+E*OWhnlTp6KswB}PErb|l`|z=k{m&h;L0laJm0Z3XF2u3 zYU0_^Y{_-4R}^JslPAnEZP{t1Zidz3sznxYZ_!8UR#j2s+x8{-VVf-G75ydZTq7xW ztKnD-!t~JzN#^t+1HqLeBxiXN0g6G2(R^&G!LJ4WF1(xOTY@CyCa&jMfmjn8d1jF_ z5zw8P8jS0GGw>c)DFuc1%2*d?M6I{XLqZ^QqcDzs*H?YE{CoWF_sb8G?uSaIM^v5> z6JPS_Pu!Bsl>%;2dYboTe&%E1N$0J*75E!QyFl>wyvx>^r|Xrw#3sAIHWT_lp~h^Q ziXNC*kjB!TJ+nwXyXrm|&ukn-*U0&75<~VA;37U=Bqu@1YyEmwv!GQMgnS`EF++g<=bZEs$76PW&^b}G zqd)kUF~N==Ztw6X)tEGbp1xOUT7`S-;*Iu7uvt<-ZGHSDgopkbk-|GiqaIC_BpwXK zq<#MCljMXERI?oUehJie7ufq~o#|Qf%VU4fk}e_NI~)OXU1%1x3)tw$+^eHzkUii{ z&%wG^BA#dB%o{S0)c!l>Ipkr$D+ib6g{s33z-?04IS^dXk$;yp*TXT-8&hWq8^ASX zm(}>}jhh`V-82!&8>yCg`!$st_3Su31yb|z=aQB;%`GoWfz||Brun`@?LBzPr~p)?}F_F>OX}$L&zMI;b|{ct)evKZjS*A6W&;~PrJ&a?)bQ>UYD$pwR6L?25;PR&Y-&VrTR$`B z__+!&ruWd~uAYiV-<3^*cTQKDhzgssIXcfVQRyLxka&WD{jv(Q-K9<4#aP;MCQp zLl=iOzMIPIJrgwJ+Eh3?S4eVX$m2t7lkS;+1N#%*QibW~?RjciQyEAFsG756w--twJ1%LrUX zaz3}JFtGQt_6(^rr=vVB<8Y4pqKJlu&XvR_!@j)0zaWDPy?<$mJqaDj{lhvtVjO<7 H56=2G2pLhW literal 0 HcmV?d00001 diff --git a/frontend/metamask.svg b/frontend/metamask.svg new file mode 100644 index 0000000..963c73e --- /dev/null +++ b/frontend/metamask.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..c0cc5db --- /dev/null +++ b/frontend/script.js @@ -0,0 +1,330 @@ +"use strict"; + +document.addEventListener('DOMContentLoaded', function () { + + const contractAddress = '0xc8bd0A15E35Ddc1692D907583696443e2970779b'; + const nullAddress = '0x0000000000000000000000000000000000000000'; + var userAccount = nullAddress; + + var btnRegister = document.getElementById("register"); + var btnAccountEnable = document.getElementById("account_enable"); + + btnRegister.addEventListener('click', register); + btnAccountEnable.addEventListener('click', accountEnable); + + var funcType; + var mark; + var expirationTime; + var timerIsRunning = false; + var players = []; + + // Modern dapp browsers... + if(window.ethereum) { + console.log("Modern dapp browser detected"); + window.web3 = new Web3(window.ethereum); + checkNetworkType(); + ethereum.on('accountsChanged', (accounts) => accountChange(accounts[0])); + ethereum.on('chainChanged', (_chainId) => window.location.reload()); + } + // Legacy dapp browsers... + else if(window.web3) { + console.log("Legacy dapp browser detected"); + window.web3 = new Web3(web3.currentProvider); + // Acccounts always exposed + checkNetworkType(); + } + // Non-dapp browsers... + else { + console.log("Non-Ethereum browser detected. You should consider trying MetaMask!"); + window.web3 = new Web3(new Web3.providers.WebsocketProvider('wss://goerli.infura.io/ws/v3/d82552d5ad5841e5acbbe7a8417a23ad')); + } + + function switchToGoerli() { + const iconUrl = ""; + const goerli = { + chainId: "0x5", + chainName: "Goerli test network", + rpcUrls: ["https://goerli.infura.io/v3/"], + nativeCurrency: { + name: "GoerliETH", + symbol: "ETH", + decimals: 18, + }, + blockExplorerUrls: ["https://goerli.etherscan.io"], + iconUrls: [iconUrl], + }; + + const { request } = window.ethereum; + request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: goerli.chainId }], + }).catch(e => { + // This error code indicates that the chain has not been added to MetaMask. + if (e.code === 4902) { + return request({ + method: "wallet_addEthereumChain", + params: [goerli], + }); + } else { + throw e; + } + }); + } + + function checkNetworkType() { + web3.eth.net.getId() + .then(function(result) { + if(result != 5) { /* Goerli Mainnet */ + alert("Please select Goerli test network on your Metamask!"); + console.log('switchToGoerli'); + switchToGoerli(); + } + console.log('Network Id:', result); + }); + } + + function displayShortAddress(address) { + return(address.slice(0, 6) + "..." + address.slice(-4)) + } + + function setUserAccount() { + var x = userAccount.toString(); + document.getElementById("user").textContent = displayShortAddress(x); + if(userAccount != nullAddress) { + document.getElementById("account_locked").style.display = "none"; + document.getElementById("account_unlocked").style.display = "block"; + } + else { + document.getElementById("account_locked").style.display = "block"; + document.getElementById("account_unlocked").style.display = "none"; + } + } + + function getPlayerStatus() { + myContract.methods.getPlayerStatus(userAccount).call() + .then(function(result) { + console.log("Player status: " + result); + if(result == 0) { + document.getElementById("unverified").style.display = "block"; + document.getElementById("verified").style.display = "none"; + document.getElementById("banned").style.display = "none"; + } + else if(result == 1) { + document.getElementById("unverified").style.display = "none"; + document.getElementById("verified").style.display = "block"; + document.getElementById("banned").style.display = "none"; + } + else { + document.getElementById("unverified").style.display = "none"; + document.getElementById("verified").style.display = "none"; + document.getElementById("banned").style.display = "block"; + } + }); + } + + function accountChange(newAccount) { + if(web3.utils.isAddress(newAccount)) { + userAccount = web3.utils.toChecksumAddress(newAccount); + } + else userAccount = nullAddress; + console.log('ACCOUNT: ' + userAccount); + console.log('CheckNetworkType'); + checkNetworkType(); + setUserAccount(); + getPlayerStatus(); + } + + async function getAccount(type) { + if (window.ethereum) { // for modern DApps browser + try { + await window.ethereum.request({ method: 'eth_requestAccounts' }); + } catch (error) { + console.error(error); + } + } + if (window.web3){ + await window.web3.eth.getAccounts().then(it => { + accountChange(it[0]); + if (type == "REGISTER") registerPlayer(); + }); + } + else { + alert('No Metamask or Ethereum wallet found. Read "Getting Started" for more information'); + console.log('No Metamask or Ethereum wallet found. Read "Getting Started" for more information'); + } + } + + function register() { + funcType = "REGISTER"; + getAccount(funcType); + } + + function accountEnable() { + funcType = "NONE"; + getAccount(funcType); + } + + function setPlayers(data) { + document.getElementById("players").textContent = data; + } + + function setSubmissions(data) { + document.getElementById("submissions").textContent = data; + } + + function setExpiration(newExpiration) { + if(! timerIsRunning) startTimer(); + expirationTime = newExpiration; + updateClock(); + } + + function setRank(data) { + var text = ""; + text += "PlayerStatusNameFlags foundPoints"; + + data.forEach(item => { + console.log(item[2]); + text += "" + item[0] + "" + item[2] + "" + item[3] + "" + item[1] + ""; + }); + + document.getElementById("loading").style.display = "none"; + document.getElementById("rank_list").innerHTML = text; + } + + function registerPlayer() { + var rtfm = 'I_read_it'; // todo: Checkbox check removed for easier testing + var playerName = document.getElementById("playerName").value; + myContract.methods.register(rtfm, playerName).send({from: userAccount}) + .then(function() { + console.log("register()"); + rank(); + getPlayerStatus(); + getPlayers(); + }); + } + + function isGameGoing() { + myContract.methods.started().call() + .then(function(result) { + console.log("is going: " + result); + return(result); + }); + } + + function startTimer() { + mark = setInterval(updateClock, 1000); + timerIsRunning = true; + } + + function stopTimer() { + clearInterval(mark); + timerIsRunning = false; + } + + function updateClock() { + const remainingTime = expirationTime - Math.floor((new Date()).getTime() / 1000); + + if(remainingTime > 0) { + //var hour = Math.floor(remainingTime / (60 * 60)) % 24; + var hour = Math.floor(remainingTime / (60 * 60)); + var min = Math.floor(remainingTime / 60) % 60; + var sec = remainingTime % 60; + if(hour < 10) hour = '0' + hour; + if(min < 10) min = '0' + min; + if(sec < 10) sec = '0' + sec; + document.getElementById("remaining_time").textContent = hour + ':' + min + ':' + sec; + } + else gameOver(); + } + + function gameOver() { + stopTimer(); + document.getElementById("remaining_time").textContent = "--:--:--"; + if(isGameGoing()) { + document.getElementById("remaining_time").textContent = "Game Over"; + } + else { + document.getElementById("remaining_time").textContent = "Not Started"; + } + } + + function expiration() { + myContract.methods.endTime().call() + .then(function(result) { + console.log("expiration: " + result); + setExpiration(result); + }); + } + + function getPlayers() { + myContract.methods.playerCount().call() + .then(function(result) { + console.log("playerCount: " + result); + setPlayers(result); + }); + } + + function submissions() { + myContract.methods.getSuccessfulSubmissionCount().call() + .then(function(result) { + console.log("SuccessfulSubmissionCount: " + result); + setSubmissions(result); + }); + } + + function rank() { + myContract.methods.getPlayers().call() + .then(function(result) { + console.log("Players: " + result); + setRank(result); + }); + } + + //=============================================================================================== + // Get the modal + var modal = document.getElementById('myModal'); + + // Get the button that opens the modal + var btn = document.getElementById("myBtn"); + + // Get the element that closes the modal + var span = document.getElementsByClassName("close")[0]; + + // When the user clicks the button, open the modal + btn.onclick = function() { + modal.style.display = "block"; + } + + // When the user clicks on (x), close the modal + span.onclick = function() { + modal.style.display = "none"; + } + + // When the user clicks anywhere outside of the modal, close it + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } + + //=============================================================================================== + var myContract; + myContract = new web3.eth.Contract(myABI, contractAddress); + + myContract.events.PlayerStatusChanged() + .on("data", function(event) { + var data = event.returnValues; + console.log("PlayerStatusChanged: " + data); + rank(); + }) + .on("error", function(error) { + var text = error; + console.log("PlayerStatusChanged: " + text); + }) + .on("error", console.error); + + expiration(); + getPlayers(); + submissions(); + rank(); +}); diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..90e2584 --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,360 @@ +body { + margin: 0; + overflow: hidden; + font-family: 'Roboto Mono', monospace; + color: #FFFFFF; +} + +.top { + background: #45484d; + background: linear-gradient(to right, #000000 220px,#2D3436 50%,#2D3436 calc(50% + 220px),#000000 100%); + position: absolute; + display: flex; + width: 100%; + height: 70px; +} + +.top_logo { + width: 220px; + align-items: center; + display: flex; + font-size: 32px; + justify-content: center; +} + +.top_left { + width: calc(50% - 205px); + align-items: center; + display: flex; +} + +.top_center { + width: 190px; + align-items: center; + display: flex; + justify-content: center; +} + +.top_right { + width: calc(50% - 205px); + align-items: center; + display: flex; + justify-content: flex-end; +} + +.left { + background-color: #000000; + position: absolute; + left: 0; top: 70px; bottom: 0; + width: 220px; + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ +} + +.left::-webkit-scrollbar { + display: none; +} + +.main { + background: #bcbfbe; + background: radial-gradient(ellipse at center, #2D3436 0%,#000000 100%); + position: absolute; + left: 220px; top: 70px; right: 0; bottom: 0; +} + +.left .activated { + border: 1px solid #2FA232; + background: #FFFFFF; + color: #2FA232; +} + +.topitem { + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +#playerName { + border: 0; + border-radius: 0.25rem; + font-family: 'Roboto Mono', monospace; + line-height: 1.5; + white-space: nowrap; + text-decoration: none; + margin: 10px 0; + padding: 0.25rem 0.5rem; + width: 93%; +} + +#register { + border: 0; + border-radius: 0.25rem; + color: #FFFFFF; + background: #2FA232; + background: linear-gradient(225deg, #166D3B 0%, #2FA232 100%); + font-family: 'Roboto Mono', monospace; + font-size: 1rem; + line-height: 1.5; + white-space: nowrap; + text-decoration: none; + margin: 10px 0; + padding: 0.25rem 0.5rem; + width: 100%; + cursor: pointer; +} + +#register:hover { + background: #2FA232; + background: linear-gradient(45deg, #166D3B 0%, #2FA232 100%); +} + +.left-content { + margin: 14px; +} + +#header_uppercase { + font-size: 13px; + color: #CCCCCC; + font-family: monospace; + white-space: nowrap; +} + +#account_enable { + border: 1px solid #dcdcdc; + border-radius: 0.25rem; + background: #f4f6f7; + font-family: 'Roboto Mono', monospace; + font-size: 1rem; + white-space: nowrap; + text-decoration: none; + padding: 0.25rem 0.5rem; + margin: 10px 0; + width: 100%; + cursor: pointer; +} + +#account_enable:hover { + background: #fafafb; + border-color: #2FA232; + color: #2FA232; +} + +#metamask { + width: 20px; + vertical-align: text-top; + padding-right: 10px; +} + + + + +/* The Modal (background) */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.modal-content { + position: relative; + background-color: #000000; + margin: 70px auto; + padding: 0; + max-width: 800px; + border: 3px solid #2FA232; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.4s; + animation-name: animatetop; + animation-duration: 0.4s +} + +/* Add Animation */ +@-webkit-keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +@keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +/* The Close Button */ +.close { + float: right; + font-size: 28px; +} + +.close:hover, +.close:focus { + color: #BBBBBB; + text-decoration: none; + cursor: pointer; +} + +.modal-header { + padding: 1px 12px 1px 24px; +} + +.modal-body {padding: 2px 16px;} + +.modal-footer { + padding: 16px; +} + +.top a:link, .top a:visited, .top a:hover, .top a:active { + color: white; + text-decoration: none; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #d8dadb; + margin-top: 1em; + padding: 0; +} +.buttons { + text-align: center; +} + +.buttons i { + font-size: 24px; + margin: 2px 0; + background-color: #2FA232; + background-image: linear-gradient(45deg, #8929AD 0%, #2FA232 50%, #43B7B8 100%); + background-size: 100%; + -webkit-background-clip: text; + -moz-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-text-fill-color: transparent; +} + +li { + padding-bottom: 10px; +} + +.form-switch { + display: inline-block; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.form-switch i { + position: relative; + display: inline-block; + width: 46px; + height: 26px; + background-color: #e6e6e6; + border-radius: 23px; + vertical-align: text-bottom; + transition: all 0.3s linear; +} + +.form-switch i::before { + content: ""; + position: absolute; + left: 0; + width: 42px; + height: 22px; + background-color: #fff; + border-radius: 11px; + transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1); + transition: all 0.25s linear; +} + +.form-switch i::after { + content: ""; + position: absolute; + left: 0; + width: 22px; + height: 22px; + background-color: #fff; + border-radius: 11px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6); + transform: translate3d(2px, 2px, 0); + transition: all 0.2s ease-in-out; +} + +.form-switch:active i::after { + width: 28px; + transform: translate3d(2px, 2px, 0); +} + +.form-switch:active input:checked + i::after { transform: translate3d(16px, 2px, 0); } + +.form-switch input { display: none; } + +.form-switch input:checked + i { background-color: #2FA232; } + +.form-switch input:checked + i::before { transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0); } + +.form-switch input:checked + i::after { transform: translate3d(22px, 2px, 0); } + +h1 { + font-size: 16px; + margin-top: 1em; + margin-bottom: 0; + margin-left: 0; + margin-right: 0; + color: #2FA232; +} + +#loading { + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + text-align: center; + text-shadow: 2px 2px 4px #FFFFFF; +} + +.dropdown-p { + text-align: center; +} + +#rank_list { + width: 90%; + border-collapse: collapse; + border-style: hidden; + text-align: center; + margin-left: auto; + margin-right: auto; +} + +#rank_list th { + border: 1px solid #5f5f5f; + padding: 8px; + min-width: 40px; +} + +#rank_list td { + border: 1px solid #5f5f5f; + padding: 8px; + min-width: 40px; +} + +.center { + display: block; + margin-left: auto; + margin-right: auto; + width: 20%; + } + +#rules { + color: #cccccc; + font-size: 14px; +}