/ contracts / src / Bloxy.sol
Bloxy.sol
  1  // SPDX-License-Identifier: UNLICENSED
  2  pragma solidity ^0.8.13;
  3  
  4  import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
  5  import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  6  import "@openzeppelin/contracts/access/Ownable.sol";
  7  import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
  8  import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
  9  import "solidity-stringutils/strings.sol";
 10  import "./AggregatorV3Interface.sol";
 11  
 12  contract Bloxy is ERC721Enumerable, Ownable, ReentrancyGuard {
 13      using strings for *;
 14      using Strings for *;
 15  
 16  
 17      error BloxyNameTaken(string);
 18      error InvalidName(string);
 19      error NotReclaimable(uint256);
 20      error PaymentFailed(uint256);
 21      error NotEnoughETH(uint256, uint256);
 22      error FailedToReturnETH();
 23  
 24      event NewEntry(uint256 indexed tokenId, uint256 indexed expiry, string name);
 25      event RemovedEntry(uint256 indexed tokenId);
 26      event ExpiryExtended(uint256 indexed tokenId, uint256 indexed expiry);
 27      event ReclaimedEntry(uint256 indexed tokenId, string name);
 28  
 29      uint256 constant MONTH = 30 days; //30* days
 30      string constant IMAGE = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy1TVkctMjAwMTA5MDQvRFREL3N2ZzEwLmR0ZCI+CjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiB3aWR0aD0iMTAyNC4wMDAwMDBwdCIgaGVpZ2h0PSIxMDI0LjAwMDAwMHB0IiB2aWV3Qm94PSIwIDAgMTAyNC4wMDAwMDAgMTAyNC4wMDAwMDAiCiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0Ij4KCjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMDAwMDAwLDEwMjQuMDAwMDAwKSBzY2FsZSgwLjEwMDAwMCwtMC4xMDAwMDApIgpmaWxsPSIjMDAwMDAwIiBzdHJva2U9Im5vbmUiPgo8cGF0aCBkPSJNNDkzMCA4OTA2IGMtOTEgLTUxIC0zNDUgLTE5MiAtNTY2IC0zMTQgLTU2MSAtMzEwIC02OTggLTM4NyAtNzAxCi0zOTggLTIgLTcgNDk2IC0yOTQgNTg3IC0zMzcgMTMgLTcgMTIwIDUwIDQzNiAyMjkgMjMxIDEzMCA0MjQgMjM3IDQyOSAyMzcgNgowIDEyOSAtNjcgMjc1IC0xNTAgMTQ2IC04MyAzMzMgLTE4OSA0MTUgLTIzNiBsMTUwIC04NSA0MCAyMyBjMjIgMTIgMTU3IDg5CjMwMCAxNjkgMTQzIDgxIDI2MSAxNTAgMjYyIDE1NCAxIDQgLTQ2IDM0IC0xMDUgNjYgLTU5IDMzIC0zODEgMjEyIC03MTcgMzk4Ci0zMzUgMTg2IC02MTYgMzM4IC02MjUgMzM3IC04IDAgLTg5IC00MiAtMTgwIC05M3oiLz4KPHBhdGggZD0iTTUwNTAgODI2NiBjLTMwIC0xOCAtMTg1IC0xMDYgLTM0NSAtMTk2IC0xNTkgLTkwIC0zMjAgLTE4MSAtMzU3Ci0yMDMgbC02OCAtMzkgMCAtMzg1IGMwIC0yMTEgMyAtMzgyIDggLTM4MCAxMCA2IDYxNiAzNDggNzMzIDQxMyBsOTYgNTQgMzc5Ci0yMTYgYzIwOCAtMTE5IDM5NiAtMjI1IDQxNyAtMjM1IGwzNyAtMjAgLTIgMzg4IC0zIDM4NyAtNjUgMzcgYy02MTkgMzQ5Ci03NjEgNDI5IC03NjcgNDI5IC01IDAgLTMzIC0xNSAtNjMgLTM0eiIvPgo8cGF0aCBkPSJNNjQ4OSA3ODk3IGMtMTU4IC04OSAtMjg3IC0xNjYgLTI4OSAtMTcxIC0yIC04IDI2MiAtMTU5IDc2OCAtNDM4CjExMSAtNjEgMjAxIC0xMTYgMTk5IC0xMjEgLTEgLTUgLTIwNiAtMTI0IC00NTUgLTI2NSAtMjQ4IC0xNDIgLTQ1MiAtMjYwCi00NTIgLTI2MyAwIC03IDU3NyAtMzI5IDU4OSAtMzI5IDUgMCAyMTMgMTE2IDQ2MiAyNTggMjUwIDE0MiA1OTIgMzM3IDc2MQo0MzMgMTY5IDk2IDMwOCAxNzYgMzA4IDE3OSAwIDMgLTI0OSAxNDIgLTU1MyAzMTAgLTgzMiA0NTkgLTEwMzUgNTcwIC0xMDQ0CjU2OSAtNCAwIC0xMzcgLTczIC0yOTQgLTE2MnoiLz4KPHBhdGggZD0iTTI2NTAgNzYyNCBjLTQyMSAtMjM0IC03NzYgLTQzMSAtNzg4IC00MzggLTI2IC0xNCAtNjggMTIgOTQzIC01NTkKMTI3IC03MiAzMDggLTE3NSA0MDQgLTIyOSBsMTc0IC0xMDAgNDIgMjQgYzQyNSAyMzYgNTQ0IDMwNCA1NDUgMzA5IDAgNSAtODAyCjQ2NSAtODc4IDUwNCAtMTggOSAtMzIgMjEgLTMwIDI2IDIgNiAyMTkgMTMxIDQ4MyAyNzkgMjY0IDE0NyA0ODEgMjcyIDQ4MgoyNzYgMyAxMSAtNTc3IDMzNCAtNTk4IDMzNCAtOCAtMSAtMzU4IC0xOTIgLTc3OSAtNDI2eiIvPgo8cGF0aCBkPSJNMzY2MCA3NDc5IGMtMjE3IC0xMjEgLTQzMiAtMjQxIC00NzcgLTI2NyAtNDYgLTI1IC04MyAtNDggLTgzIC01MQowIC0zIDEzNCAtODEgMjk4IC0xNzQgMTYzIC05MiAzMTcgLTE3OSAzNDMgLTE5NCBsNDUgLTI2IDEzNyA3OSAxMzcgNzkgMCAzODgKYzAgMjEzIC0xIDM4NyAtMiAzODcgLTIgMCAtMTgxIC0xMDAgLTM5OCAtMjIxeiIvPgo8cGF0aCBkPSJNNjE3MCA3MzEzIGwxIC0zODggMTI3IC03MiBjNzEgLTQwIDEzNCAtNzMgMTQyIC03MyAxNiAwIDY5MCAzODAKNjkwIDM4OSAtMSA2IC05NDMgNTMxIC05NTQgNTMxIC0zIDAgLTYgLTE3NCAtNiAtMzg3eiIvPgo8cGF0aCBkPSJNNzk1NSA2OTA5IGMtMjQyIC0xMzcgLTU4NyAtMzMzIC03NjcgLTQzNiBsLTMyOCAtMTg2IDAgLTM4NCAwIC0zODQKNTggMzIgYzcwIDQwIDcxNyA0MzAgNzkwIDQ3NiAyOSAxOCA1NyAzMyA2MiAzMyA2IDAgMTEgLTE0MSAxMiAtMzc0IGwzIC0zNzUKMzEwIDE3OCAzMTAgMTc4IDMgNzI1IGMxIDM5OSAtMSA3MzUgLTYgNzQ3IC03IDE5IC00MSAxIC00NDcgLTIzMHoiLz4KPHBhdGggZD0iTTE4MjAgNjQwMyBjMCAtNDEyIDQgLTc1MyA4IC03NTkgNCAtNSAxMzEgLTgwIDI4MiAtMTY2IDE1MSAtODUgMjkyCi0xNjUgMzEzIC0xNzggbDM3IC0yMiAwIDM4MSBjMCAyMTAgMyAzODEgNiAzODEgMyAwIDE2NiAtOTYgMzYyIC0yMTQgMzgxCi0yMjcgNTMxIC0zMTYgNTM4IC0zMTYgMiAwIDQgMTc0IDQgMzg4IGwtMSAzODcgLTEwNyA1OSBjLTU5IDMzIC0zODEgMjE1Ci03MTcgNDA1IC03MzkgNDE4IC03MDggNDAxIC03MTcgNDAxIC01IDAgLTggLTMzNiAtOCAtNzQ3eiIvPgo8cGF0aCBkPSJNNTk5NSA2NDkwIGMtMTYgLTEwIC0yMTAgLTEyMCAtNDMwIC0yNDUgLTIyMCAtMTI1IC00MTEgLTIzNCAtNDI1Ci0yNDIgLTI1IC0xNCAtMzQgLTEwIC0yNzAgMTI0IC02ODggMzkyIC02NTkgMzc2IC02ODggMzYzIC0yNiAtMTEgLTQ2OSAtMjYwCi01NDAgLTMwMyBsLTM0IC0yMCA2OSAtNDAgYzM3IC0yMSAxODcgLTEwOCAzMzMgLTE5MiA1ODMgLTMzNSA5OTggLTU3NCAxMDQ5Ci02MDQgbDUzIC0zMiAxODcgMTA4IGMxMDIgNTkgNDIyIDI0NCA3MTEgNDEyIDI4OSAxNjcgNTQ1IDMxNiA1NjkgMzMwIDQyIDIzCjQzIDI1IDI1IDM4IC0xNyAxMyAtNTc3IDMyNCAtNTc5IDMyMiAwIDAgLTEzIC05IC0zMCAtMTl6Ii8+CjxwYXRoIGQ9Ik0zNTkyIDU3NTkgbDMgLTM5MCA0MzAgLTI0OSA0MzAgLTI0OSA1IC0zNzggNSAtMzc4IDMxNSAtMTgyIGMxNzMKLTEwMCAzMTggLTE4MiAzMjMgLTE4MiA0IC0xIDcgMzQwIDcgNzU3IDAgNzEwIC0xIDc2MCAtMTcgNzc0IC0xMCA4IC0xMDggNjcKLTIxOCAxMzAgLTI5NSAxNzAgLTc2MiA0MzkgLTk5NSA1NzMgLTExMyA2NCAtMjIyIDEyOCAtMjQyIDE0MSAtMjEgMTMgLTQwIDI0Ci00MyAyNCAtMyAwIC00IC0xNzYgLTMgLTM5MXoiLz4KPHBhdGggZD0iTTY0MzAgNjAzNiBjLTQxMSAtMjM4IC0xMTA5IC02NDIgLTEyMDIgLTY5NSBsLTk4IC01NiAwIC03NjcgMCAtNzY2CjE5OCAxMTUgYzEwOCA2MyAyNTIgMTQ3IDMyMCAxODYgbDEyMiA3MiAwIDM3NSAwIDM3NiA0MyAyMiBjMjMgMTEgMjE4IDEyNAo0MzIgMjUwIGwzOTAgMjI5IDMgMzg2IGMxIDIxMyAtMSAzODcgLTUgMzg3IC01IDAgLTk2IC01MSAtMjAzIC0xMTR6Ii8+CjxwYXRoIGQ9Ik03NDUwIDU4NDQgYy0xNTcgLTk2IC0yODYgLTE3OCAtMjg4IC0xODMgLTIgLTUgMzIgLTI5IDc1IC01MyA0MwotMjUgMTY4IC05NiAyNzggLTE2MCAxMTAgLTYzIDIxMCAtMTIwIDIyMyAtMTI3IGwyMiAtMTIgMCAzNTYgYzAgMjc4IC0zIDM1NQotMTIgMzU0IC03IDAgLTE0MSAtNzkgLTI5OCAtMTc1eiIvPgo8cGF0aCBkPSJNMjQ4MCA1NjQ2IGMwIC0xOTYgMiAtMzU2IDQgLTM1NiAyIDAgNTMgMjggMTEzIDYzIDU5IDM1IDE4OSAxMTAKMjg4IDE2NyA5OSA1NyAxODUgMTA3IDE5MiAxMTMgOSA2IC03NyA2MiAtMjgwIDE4NCAtMTYxIDk2IC0yOTggMTc3IC0zMDQgMTgwCi0xMCA0IC0xMyAtNzAgLTEzIC0zNTF6Ii8+CjxwYXRoIGQ9Ik03MTM4IDQ3MDYgbDIgLTY5NCAtMTgyIC0xMDkgYy0xMDEgLTYwIC0zMDUgLTE4MCAtNDU1IC0yNjggLTE1MAotODggLTI3MSAtMTY0IC0yNzAgLTE2OSAyIC02IDE0NCAtOTIgMzE1IC0xOTMgbDMxMiAtMTgzIDM0MyAyMDkgYzE4OCAxMTUKMzg5IDIzOCA0NDcgMjczIGwxMDUgNjUgMCA3MDggMCA3MDggLTMwMCAxNjkgYy0xNjUgOTQgLTMwNCAxNzIgLTMxMCAxNzQgLTcKMyAtOSAtMjMwIC03IC02OTB6Ii8+CjxwYXRoIGQ9Ik04MDgzIDUyMjcgbC0zMDMgLTE3MiAwIC03MTUgMCAtNzE1IC0yNzEgLTE2NSBjLTE1MCAtOTEgLTM1MyAtMjE1Ci00NTMgLTI3NiBsLTE4MSAtMTEyIC0zIC0zNzUgLTIgLTM3NiA1MTIgMzEzIGMyODIgMTcyIDYyNyAzODIgNzY1IDQ2NyBsMjUzCjE1NCAwIDEwNzMgYzAgNTg5IC0zIDEwNzIgLTcgMTA3MiAtNSAwIC0xNDQgLTc4IC0zMTAgLTE3M3oiLz4KPHBhdGggZD0iTTI4ODUgNTI2NCBjLTExMCAtNjMgLTI0NiAtMTQzIC0zMDMgLTE3NiBsLTEwMiAtNjAgMiAtNzA1IDMgLTcwNgo0NDQgLTI2OCA0NDUgLTI2OCAzMCAxOSBjMTcgMTAgMTYwIDk1IDMxOCAxODggMjEwIDEyNCAyODQgMTcyIDI3NSAxODAgLTEwIDgKLTMzMCAxOTcgLTc5OSA0NzAgbC05OCA1NyAwIDY5MyBjMCAzODAgLTMgNjkyIC03IDY5MiAtNSAwIC05OCAtNTIgLTIwOCAtMTE2eiIvPgo8cGF0aCBkPSJNMTgzMCA0Mjk3IGwwIC0xMDcyIDY4IC00MSBjMzcgLTIyIDE5NSAtMTE5IDM1MiAtMjE0IDg4OCAtNTM5IDExMDUKLTY3MCAxMTE0IC02NzAgMyAwIDYgMTcwIDYgMzc4IGwwIDM3OSAtNDEyIDI0OSBjLTIyNyAxMzYgLTQzMiAyNjAgLTQ1NSAyNzQKbC00MiAyNSAtMyA3MTAgLTMgNzA5IC0xMzUgNzYgYy03NCA0MiAtMjEyIDEyMCAtMzA2IDE3MyAtOTUgNTMgLTE3NSA5NyAtMTc4Cjk3IC0zIDAgLTYgLTQ4MyAtNiAtMTA3M3oiLz4KPHBhdGggZD0iTTU0NDggMzY4MiBsLTMxOCAtMTg3IDAgLTExMjMgMCAtMTEyNCAzMyAxOSBjNjEgMzcgMTI4MiA3ODUgMTM4Mgo4NDggbDEwMCA2MiAzIDM3NyBjMSAyMDggLTEgMzc1IC01IDM3MyAtNSAtMiAtMTk4IC0xMjAgLTQzMCAtMjYxIC0yMzIgLTE0MQotNDI2IC0yNTYgLTQzMiAtMjU2IC04IDAgLTEyIDIxNiAtMTMgNzI5IGwtMyA3MzAgLTMxNyAtMTg3eiIvPgo8cGF0aCBkPSJNNDQ2MCAzMTMxIGMwIC00MDIgLTIgLTczMSAtNCAtNzMxIC0zIDAgLTEwNyA2MiAtMjMzIDEzOSAtMTI1IDc2Ci0zMTggMTkzIC00MjggMjYwIGwtMjAwIDEyMSAtMyAtMzgyIGMtMiAtMzU2IC0xIC0zODQgMTUgLTM5NiAxMyAtMTAgMTQyMQotODU2IDE0OTYgLTg5OSA0IC0yIDcgNTAyIDcgMTEyMCBsMCAxMTI1IC0zMTcgMTgyIGMtMTc1IDEwMSAtMzIxIDE4NSAtMzI1CjE4NyAtNSAyIC04IC0zMjUgLTggLTcyNnoiLz4KPHBhdGggZD0iTTU4OTAgMzI2MSBjLTUyIC0zMiAtOTQgLTYxIC05MiAtNjQgMSAtNCAyIC0xNzUgMiAtMzgxIGwwIC0zNzQgMjk4CjE4MCBjMzkyIDIzOCA1MjAgMzE3IDUyNyAzMjcgNSA4IC02MTUgMzcyIC02MzEgMzcwIC01IDAgLTUyIC0yNyAtMTA0IC01OHoiLz4KPHBhdGggZD0iTTM5MTQgMzEzMSBjLTE2NSAtOTcgLTMwMyAtMTc5IC0zMDUgLTE4NCAtMiAtNCAyMSAtMjIgNTEgLTQxIDQyNgotMjU5IDc3MSAtNDY2IDc3NSAtNDY2IDMgMCA1IDE3MCA1IDM3OCBsMCAzNzcgLTk5IDU4IGMtNTUgMzEgLTEwNiA1NyAtMTEzCjU2IC03IC0xIC0xNDggLTgwIC0zMTQgLTE3OHoiLz4KPC9nPgo8L3N2Zz4K";
 31  
 32      string public BloxyIPAddress;
 33      string public AkashWalletAddress;
 34  
 35      struct Entry {
 36          string name;
 37          string domain;
 38          string target;
 39          uint256 expiry;
 40      }
 41      Entry[] public entries; 
 42      mapping(bytes32 => bool) public hashTaken;
 43  
 44      IERC20 public paymentToken;
 45      uint256 paymentTokenDecimals;
 46      uint256 basePrice = 10; // price * 10 to have more precision
 47  
 48      AggregatorV3Interface internal dataFeed;
 49  
 50  
 51      constructor(address _paymentToken, uint256 _paymentTokenDecimals, address _datafeed) ERC721("Bloxy", "BLX") Ownable(msg.sender){
 52          paymentToken = IERC20(_paymentToken);
 53          paymentTokenDecimals = _paymentTokenDecimals - 1; //compensate for basePrice precision
 54          dataFeed = AggregatorV3Interface(_datafeed);
 55      }
 56  
 57      function newEntry(Entry memory entry) public payable nonReentrant() {
 58          uint256 _nextId = totalSupply();
 59          
 60          bytes32 _hash = _getHash(entry.name);
 61          if (!_validate(entry.name)) {
 62              revert InvalidName(entry.name);
 63          }
 64  
 65          if (hashTaken[_hash]) {
 66              revert BloxyNameTaken(entry.name);
 67          }
 68  
 69          if (balanceOf(msg.sender) != 0) { //Only the first one has unlimited expiry
 70              entry.expiry = block.timestamp + MONTH;
 71              _takeCost(MONTH);
 72          } else {
 73              entry.expiry = 0;
 74          }
 75  
 76          entries.push(entry);
 77          hashTaken[_hash] = true;
 78          _mint(msg.sender, _nextId);
 79          
 80  
 81          emit NewEntry(_nextId, entry.expiry, entry.name);
 82      }
 83  
 84      function setEntry(uint256 tokenId, Entry memory entry) public nonReentrant() {
 85          address _tokenOwner = ownerOf(tokenId);
 86          _checkAuthorized(_tokenOwner, msg.sender, tokenId);
 87  
 88          Entry memory _current = entries[tokenId];
 89  
 90          bytes32 _hash = _getHash(entry.name);
 91          bytes32 _currentHash = _getHash(_current.name);
 92  
 93          if (_currentHash != _hash) {
 94              if (!_validate(entry.name)) {
 95                  revert InvalidName(entry.name);
 96              }
 97  
 98              if (hashTaken[_hash]) {
 99                  revert BloxyNameTaken(entry.name);
100              }
101  
102              hashTaken[_currentHash] = false;
103              hashTaken[_hash] = true;
104          }
105    
106          entry.expiry = _current.expiry; //Make sure users cannot overwrite the expiry
107          entries[tokenId] = entry;
108      }
109  
110      function removeEntry(uint256 tokenId) public nonReentrant() {
111          address _tokenOwner = ownerOf(tokenId);
112          _checkAuthorized(_tokenOwner, msg.sender, tokenId);
113  
114          bytes32 _hash = _getHash(entries[tokenId].name);
115  
116          uint256 lastEntryId = entries.length - 1;
117          Entry memory lastEntry = entries[lastEntryId];
118          entries[tokenId] = lastEntry;
119  
120          _burn(tokenId);
121          hashTaken[_hash] = false;
122          delete entries[lastEntryId];
123          entries.pop();
124  
125          emit RemovedEntry(tokenId);
126      }
127  
128      function reclaim(uint256 tokenId, Entry memory entry) public payable {
129          Entry storage _entry = entries[tokenId];
130  
131          if (_entry.expiry < block.timestamp + 60 days) {
132              _entry.name = "";
133              bytes32 _hash = _getHash(_entry.name);
134              hashTaken[_hash] = false;
135  
136              newEntry(entry);
137          } else {
138              revert NotReclaimable(tokenId);
139          }
140  
141          emit ReclaimedEntry(tokenId, _entry.name);
142      }
143  
144      function extend(uint256 tokenId, uint256 newExpiry) public payable nonReentrant() {
145          address _tokenOwner = ownerOf(tokenId);
146          _checkAuthorized(_tokenOwner, msg.sender, tokenId);
147  
148          Entry storage _entry = entries[tokenId];
149  
150          uint256 extendBy = newExpiry - block.timestamp;
151  
152          if (_entry.expiry > block.timestamp) {
153              extendBy = newExpiry - _entry.expiry;
154          }
155          
156          _entry.expiry = newExpiry;
157          _takeCost(extendBy);
158  
159          emit ExpiryExtended(tokenId, newExpiry);
160      }
161  
162      function getPrice(uint256 duration) public view returns(uint256) {
163          return duration == 0 ? 0 : (basePrice*10**paymentTokenDecimals * ((duration * 100) / MONTH)) / 100;
164      }
165  
166      function getPriceETH(uint256 duration) public view returns(uint256) {
167  
168          (
169              /* uint80 roundID */,
170              int answer,
171              /*uint startedAt*/,
172              /*uint timeStamp*/,
173              /*uint80 answeredInRound*/
174          ) = dataFeed.latestRoundData();
175          return getPrice(duration) * 10**(18 + uint256(dataFeed.decimals())) / (uint256(answer) * 10**(paymentTokenDecimals+1));
176      }
177  
178      function _takeCost(uint256 duration) private {
179          if (msg.value > 0) {
180              uint256 priceETH = getPriceETH(duration);
181              if (msg.value < priceETH) {
182                  revert NotEnoughETH(msg.value, priceETH);
183              }
184              if (msg.value > priceETH) {
185                  uint256 returnETH = msg.value - priceETH;
186                  (bool sent,) = payable(msg.sender).call{value: returnETH}("");
187                  if (!sent) {
188                      revert FailedToReturnETH();
189                  }
190              }
191          } else {
192              uint256 price = getPrice(duration);
193  
194              if (!paymentToken.transferFrom(msg.sender, address(this), price)) {
195                  revert PaymentFailed(price);
196              }
197          }
198      }
199  
200      function getAll() public view returns(Entry[] memory) {
201          return entries;
202      }
203  
204      function tokenURI(uint256 tokenId) public view override returns(string memory) {
205          Entry storage _entry = entries[tokenId];
206          bool valid = _isValid(_entry.expiry); 
207  
208          string memory attributes = string(abi.encodePacked(
209              '"attributes" : [',
210                      '{',
211                          '"trait_type": "Valid",',
212                          '"value": ', valid ? "true" : "false", '',
213                      '},',
214                      '{',
215                          '"trait_type": "Expires",',
216                          '"value": ', _entry.expiry.toString(), '',
217                      '}',
218                  ']'
219          ));
220  
221          string memory result = string(abi.encodePacked(
222              '{',
223                  '"name": "Bloxy #', tokenId.toString(), '",',
224                  '"description": "Bloxy is a simple http(s) proxy where all the entries are permissionlessly managed on-chain",',
225                  '"image": "', IMAGE, '",',
226                  attributes,
227              '}'
228          ));
229          return result;
230      }
231  
232      function withdraw() public onlyOwner() {
233          uint256 ethBalance = address(this).balance;
234          uint256 tokenBalance = paymentToken.balanceOf(address(this));
235  
236          (bool sent,) = payable(owner()).call{value: ethBalance}("");
237          paymentToken.transfer(address(owner()), tokenBalance);
238      }
239  
240      function getValid() public view returns(Entry[] memory) {
241          Entry[] memory valid = new Entry[](_countValid());
242          Entry memory tmp;
243          uint256 el = entries.length; 
244          uint256 j;
245          for (uint256 i; i<el; i++) {
246              tmp = entries[i];
247              if (_isValid(entries[i].expiry)) {
248                  valid[j] = entries[i];
249                  j++;
250              }
251          }
252          return valid;
253      }
254  
255      function _countValid() private view returns(uint256) {
256          uint256 l = 0;
257  
258          uint256 el = entries.length; 
259          for (uint256 i; i<el; i++) {
260              if (_isValid(entries[i].expiry)) {
261                  l++;
262              }
263          }
264  
265          return l;
266      }
267  
268      function _isValid(uint256 expiry) private view returns(bool) {
269          return expiry == 0 || expiry >= block.timestamp;
270      }
271  
272      function _getHash(string memory input) private view returns(bytes32) {
273          return keccak256(bytes(input));
274      }
275  
276      function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
277          address previousOwner = super._update(to, tokenId, auth);
278  
279          Entry memory entry = entries[tokenId];
280  
281          if (auth != address(0) && to != address(0) && balanceOf(to) > 1 && entry.expiry == 0) { //If this was not the first entry and the expiry is unlimited
282              entries[tokenId].expiry = block.timestamp + 2 days; //Set expiry to 2 days
283          }
284  
285          if (auth != address(0) && to != address(0) && balanceOf(to) == 1) { //If this was not the first entry and the expiry is unlimited
286              entries[tokenId].expiry = 0;
287          }
288  
289  
290          return previousOwner;
291      }
292  
293      function _validate(string memory name) private view returns(bool) {
294          strings.slice memory nameSlice = name.toSlice();
295          return !nameSlice.contains(".".toSlice()) && !nameSlice.contains("*".toSlice());
296      }
297  
298     
299  }