-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from RSSNext/feat/achievementNft
Feat/achievement nft
- Loading branch information
Showing
10 changed files
with
475 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.22; | ||
|
||
import {AchievementDetails} from "../../src/libraries/AchievementDataTypes.sol"; | ||
|
||
interface IAchievement { | ||
/** | ||
* @notice Initializes the contract. Setup token name, symbol and account with APP_ADMIN_ROLE. | ||
* @param name_ The name of the token. | ||
* @param symbol_ The symbol of the token. | ||
* @param admin_ The account to be granted with APP_ADMIN_ROLE. | ||
* @param powerToken_ The address of the power token. | ||
*/ | ||
function initialize( | ||
string calldata name_, | ||
string calldata symbol_, | ||
address admin_, | ||
address powerToken_ | ||
) external; | ||
|
||
/** | ||
* @notice Only the APP_ADMIN_ROLE can set the achievement details. | ||
* @param name Name of the achievement. | ||
* @param description Description of the achievement. | ||
* @param imageURL Image URL of the achievement. | ||
*/ | ||
function setAchievement( | ||
string calldata name, | ||
string calldata description, | ||
string calldata imageURL | ||
) external; | ||
|
||
/** | ||
* @notice Mints a token to `account`. | ||
* @param achievement Name of the achievement to mint. | ||
* @return tokenId The new minted token id. | ||
*/ | ||
function mint(string calldata achievement) external returns (uint256 tokenId); | ||
|
||
/** | ||
* @notice Returns total supply of tokens. | ||
* @return Total supply of tokens. | ||
*/ | ||
function totalSupply() external view returns (uint256); | ||
|
||
/** | ||
* @notice Returns the achievement details of all achievements. | ||
* @return Achievement details. | ||
*/ | ||
function getAllAchievements() external view returns (AchievementDetails[] memory); | ||
|
||
/** | ||
* @notice Returns whether the account has the achievement. | ||
* @param account The address of the account. | ||
* @param achievementName The name of the achievement. | ||
* @return Whether the account has the achievement. | ||
*/ | ||
function hasAchievement(address account, string calldata achievementName) | ||
external | ||
view | ||
returns (bool); | ||
|
||
/** | ||
* @notice Returns the address of the PowerToken contract. | ||
* @return Address of the PowerToken contract. | ||
*/ | ||
function powerToken() external view returns (address); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.22; | ||
|
||
interface IErrors { | ||
/// @dev receiver is empty. | ||
error Unauthorized(); | ||
|
||
/// @dev achievement is not set. | ||
error AchievementNotSet(); | ||
|
||
/// @dev achievement is already mint. | ||
error AlreadyMinted(string achievementName); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.22; | ||
|
||
interface IEvents { | ||
event AchievementSet(string indexed name, string description, string imageURL); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.22; | ||
|
||
struct AchievementDetails { | ||
string name; | ||
string description; | ||
string imageURL; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
// SPDX-License-Identifier: MIT | ||
// solhint-disable quotes | ||
pragma solidity 0.8.22; | ||
|
||
import {AccessControlEnumerableUpgradeable} from | ||
"@openzeppelin-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; | ||
import {ERC721Upgradeable} from "@openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol"; | ||
|
||
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | ||
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; | ||
|
||
import {IAchievement} from "../interfaces/IAchievement.sol"; | ||
import {IErrors} from "../interfaces/IAchievementErrors.sol"; | ||
import {IEvents} from "../interfaces/IAchievementEvents.sol"; | ||
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; | ||
|
||
import {AchievementDetails} from "../libraries/AchievementDataTypes.sol"; | ||
|
||
contract Achievement is | ||
IERC721, | ||
AccessControlEnumerableUpgradeable, | ||
ERC721Upgradeable, | ||
IErrors, | ||
IEvents, | ||
IAchievement | ||
{ | ||
using EnumerableSet for EnumerableSet.Bytes32Set; | ||
|
||
bytes32 public constant APP_ADMIN_ROLE = keccak256("APP_ADMIN_ROLE"); | ||
|
||
bytes32 public constant APP_USER_ROLE = keccak256("APP_USER_ROLE"); | ||
|
||
uint256 internal _counter; | ||
uint256 internal _totalSupply; | ||
|
||
address internal _powerToken; | ||
|
||
// achievement name => achievement details | ||
EnumerableSet.Bytes32Set internal _achievements; | ||
|
||
// mapping from bytes32(hash of achievement name) to AchievementDetails | ||
mapping(bytes32 => AchievementDetails) internal _achievementDetails; | ||
|
||
// tokenId => achievement name | ||
mapping(uint256 => bytes32) internal _tokenIdToAchievements; | ||
|
||
// if user has minted this type of achievement | ||
mapping(address => mapping(bytes32 => bool)) internal _userAchievements; | ||
|
||
/// @inheritdoc IAchievement | ||
function initialize( | ||
string calldata name_, | ||
string calldata symbol_, | ||
address admin_, | ||
address powerToken_ | ||
) external override initializer { | ||
__ERC721_init(name_, symbol_); | ||
|
||
if (admin_ != address(0)) { | ||
_grantRole(DEFAULT_ADMIN_ROLE, admin_); | ||
_grantRole(APP_ADMIN_ROLE, admin_); | ||
} | ||
|
||
_powerToken = powerToken_; | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function setAchievement( | ||
string calldata name, | ||
string calldata description, | ||
string calldata imageURL | ||
) external override onlyRole(APP_ADMIN_ROLE) { | ||
bytes32 nameHash = _getNameHash(name); | ||
_achievements.add(nameHash); | ||
_achievementDetails[nameHash] = AchievementDetails(name, description, imageURL); | ||
emit AchievementSet(name, description, imageURL); | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function mint(string calldata achievementName) external override returns (uint256 tokenId) { | ||
address account = msg.sender; | ||
|
||
if (!AccessControlEnumerableUpgradeable(_powerToken).hasRole(APP_USER_ROLE, account)) { | ||
revert Unauthorized(); | ||
} | ||
|
||
bytes32 nameHash = _getNameHash(achievementName); | ||
if (bytes(_achievementDetails[nameHash].imageURL).length == 0) { | ||
revert AchievementNotSet(); | ||
} | ||
|
||
// TODO: check if the user has already minted this type of achievement | ||
if (_userAchievements[account][nameHash]) { | ||
revert AlreadyMinted(achievementName); | ||
} | ||
|
||
_userAchievements[account][nameHash] = true; | ||
|
||
tokenId = ++_counter; | ||
_mint(account, tokenId); | ||
|
||
_tokenIdToAchievements[tokenId] = nameHash; | ||
|
||
++_totalSupply; | ||
} | ||
|
||
/// @inheritdoc ERC721Upgradeable | ||
function tokenURI(uint256 id) public view override returns (string memory) { | ||
AchievementDetails memory details = _achievementDetails[_tokenIdToAchievements[id]]; | ||
string memory json = string.concat( | ||
'{"name": "Achievement: ', | ||
details.name, | ||
'", "description": "', | ||
details.description, | ||
'", "image": "', | ||
details.imageURL, | ||
'"}' | ||
); | ||
|
||
return json; | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function totalSupply() external view override returns (uint256) { | ||
return _totalSupply; | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function getAllAchievements() external view override returns (AchievementDetails[] memory) { | ||
uint256 length = _achievements.length(); | ||
AchievementDetails[] memory result = new AchievementDetails[](length); | ||
|
||
for (uint256 i = 0; i < length; i++) { | ||
bytes32 nameHash = _achievements.at(i); | ||
result[i] = _achievementDetails[nameHash]; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function hasAchievement(address account, string calldata achievementName) | ||
external | ||
view | ||
override | ||
returns (bool) | ||
{ | ||
return _userAchievements[account][_getNameHash(achievementName)]; | ||
} | ||
|
||
/// @inheritdoc IAchievement | ||
function powerToken() external view override returns (address) { | ||
return _powerToken; | ||
} | ||
|
||
function supportsInterface(bytes4 interfaceId) | ||
public | ||
view | ||
virtual | ||
override(AccessControlEnumerableUpgradeable, ERC721Upgradeable, IERC165) | ||
returns (bool) | ||
{ | ||
return super.supportsInterface(interfaceId); | ||
} | ||
|
||
function _getNameHash(string calldata name) internal pure returns (bytes32) { | ||
return keccak256(abi.encodePacked(name)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.