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 }