-
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.
- Loading branch information
Showing
4 changed files
with
760 additions
and
10 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@openzeppelin/contracts/utils/Base64.sol"; | ||
import "@openzeppelin/contracts/utils/Strings.sol"; | ||
|
||
contract AIMeNFTBeta is ERC721, Ownable, ERC721Holder { | ||
using Strings for uint256; | ||
IERC20 public aimePower; | ||
uint256 public constant NFT_HOLDING_PERIOD = 30 days; | ||
uint256 public aimePowerReserved; | ||
uint256 public minPrice; | ||
uint256 public nftPriceIncrement; | ||
string public avatar; | ||
address private _factory; | ||
uint256 private _nextTokenId; | ||
mapping(uint256 => AIMeInfo) public tokenContents; | ||
|
||
error AIMeNFTUnauthorizedAccount(address account); | ||
|
||
event AIMePowerSet(address aimePowerAddress); | ||
event AIMeNFTMinted( | ||
address creator, | ||
address aimeAddress, | ||
uint256 tokenId, | ||
string key, | ||
string infoType, | ||
string data, | ||
uint256 amount | ||
); | ||
event AIMeNFTUpdated( | ||
uint256 tokenId, | ||
string data | ||
); | ||
|
||
event TradeNFT(address from, address to, uint256 tokenId, uint256 price); | ||
|
||
modifier onlyFactory() { | ||
if (factory() != _msgSender()) { | ||
revert AIMeNFTUnauthorizedAccount(_msgSender()); | ||
} | ||
_; | ||
} | ||
|
||
struct AIMeInfo { | ||
string key; | ||
string dataType; | ||
string data; | ||
string image; | ||
uint256 amount; | ||
uint256 currentAmount; | ||
uint256 timestamp; | ||
} | ||
|
||
constructor( | ||
string memory name_, | ||
string memory symbol_, | ||
string memory avatar_, | ||
string memory bio_, | ||
string memory image_, | ||
uint256 minPrice_, | ||
uint256 nftPriceIncrement_ | ||
// address aimePowerAddress | ||
) ERC721(name_, symbol_) { | ||
_factory = _msgSender(); | ||
// aimePower = IERC20(aimePowerAddress); | ||
avatar = avatar_; | ||
minPrice = minPrice_; | ||
nftPriceIncrement = nftPriceIncrement_; | ||
|
||
// mint initial nft | ||
safeMint(address(this), "basic_prompt", "static", bio_, image_, 0); | ||
} | ||
|
||
function factory() public view virtual returns (address) { | ||
return _factory; | ||
} | ||
|
||
function setAIMePower(address aimePowerAddress) public onlyFactory { | ||
require(address(aimePower) == address(0), "AIME Power already set"); | ||
aimePower = IERC20(aimePowerAddress); | ||
emit AIMePowerSet(aimePowerAddress); | ||
} | ||
|
||
function addAIMePowerReserved(uint256 amount) public { | ||
require(address(aimePower) != address(0), "No AIME Power"); | ||
require( | ||
aimePower.balanceOf(msg.sender) >= amount, | ||
"balance not enough" | ||
); | ||
require( | ||
aimePower.allowance(msg.sender, address(this)) >= amount, | ||
"allowance not enough" | ||
); | ||
aimePower.transferFrom(msg.sender, address(this), amount); | ||
aimePowerReserved += amount; | ||
} | ||
|
||
function safeMint( | ||
address to, | ||
string memory key, | ||
string memory dataType, | ||
string memory data, | ||
string memory image, | ||
uint256 amount | ||
) public onlyFactory returns (uint256) { | ||
uint256 timestamp = 0; | ||
if (to != address(this)) { | ||
require(address(aimePower) != address(0), "No AIME Power"); | ||
require(amount >= minPrice, "price too low"); | ||
if ( | ||
aimePowerReserved >= amount && | ||
aimePower.balanceOf(address(this)) >= amount | ||
) { | ||
aimePowerReserved -= amount; | ||
timestamp = block.timestamp; | ||
} else { | ||
require( | ||
aimePower.balanceOf(to) >= amount, | ||
"balance not enough" | ||
); | ||
require( | ||
aimePower.allowance(to, address(this)) >= amount, | ||
"allowance not enough" | ||
); | ||
aimePower.transferFrom(to, address(this), amount); | ||
} | ||
} | ||
|
||
uint256 tokenId = _nextTokenId++; | ||
_safeMint(to, tokenId); | ||
tokenContents[tokenId] = AIMeInfo( | ||
key, | ||
dataType, | ||
data, | ||
image, | ||
amount, | ||
amount, | ||
timestamp | ||
); | ||
|
||
emit AIMeNFTMinted( | ||
tx.origin, | ||
address(this), | ||
tokenId, | ||
key, | ||
dataType, | ||
data, | ||
amount | ||
); | ||
|
||
return tokenId; | ||
} | ||
|
||
function getSellNFTPrice(uint256 tokenId) public view returns (uint256) { | ||
uint256 duration = block.timestamp - tokenContents[tokenId].timestamp; | ||
if (duration < NFT_HOLDING_PERIOD) { | ||
return tokenContents[tokenId].amount * duration / NFT_HOLDING_PERIOD; | ||
} else { | ||
return tokenContents[tokenId].amount; | ||
} | ||
} | ||
|
||
function sellNFT(uint256 tokenId) external { | ||
_safeTransfer(msg.sender, address(this), tokenId, ""); | ||
uint256 amount = getSellNFTPrice(tokenId); | ||
aimePower.transfer(msg.sender, amount); | ||
emit TradeNFT(msg.sender, address(this), tokenId, amount); | ||
} | ||
|
||
function buyNFT(uint256 tokenId) external { | ||
require(address(aimePower) != address(0), "No AIME Power"); | ||
address owner = ownerOf(tokenId); | ||
require(owner != address(0), "Invalid token owner"); | ||
uint256 amount; | ||
if (owner == address(this)) { | ||
amount = tokenContents[tokenId].amount; | ||
} else { | ||
amount = | ||
(tokenContents[tokenId].currentAmount * (nftPriceIncrement + 100)) / | ||
100; | ||
tokenContents[tokenId].currentAmount = amount; | ||
} | ||
|
||
require(amount > 0, "Not for sale"); | ||
require( | ||
aimePower.balanceOf(msg.sender) >= amount, | ||
"balance not enough" | ||
); | ||
require( | ||
aimePower.allowance(msg.sender, address(this)) >= amount, | ||
"allowance not enough" | ||
); | ||
aimePower.transferFrom(msg.sender, owner, amount); | ||
_safeTransfer(owner, msg.sender, tokenId, ""); | ||
emit TradeNFT(owner, msg.sender, tokenId, amount); | ||
} | ||
|
||
function updateAIMeInfo( | ||
uint256 tokenId, | ||
address owner, | ||
string memory data, | ||
string memory image | ||
) public onlyFactory { | ||
address tokenOwner = ownerOf(tokenId); | ||
require( | ||
tokenOwner == owner && owner != address(0), | ||
"Invalid token owner" | ||
); | ||
tokenContents[tokenId].data = data; | ||
tokenContents[tokenId].image = image; | ||
emit AIMeNFTUpdated(tokenId, data); | ||
} | ||
|
||
function tokenURI( | ||
uint256 tokenId | ||
) public view virtual override returns (string memory) { | ||
require( | ||
_exists(tokenId), | ||
"ERC721Metadata: URI query for nonexistent token" | ||
); | ||
|
||
string memory tokenName = string( | ||
abi.encodePacked(name(), " #", tokenId.toString()) | ||
); | ||
string memory imageUrl = tokenContents[tokenId].image; | ||
string memory tradeUrl = string( | ||
abi.encodePacked( | ||
"https://app-dev.aime.bot/nft/", | ||
Strings.toHexString(uint256(uint160(address(this))), 20), | ||
"/", | ||
tokenId.toString() | ||
) | ||
); | ||
|
||
string memory attributes = string( | ||
abi.encodePacked( | ||
"[", | ||
'{"trait_type": "type","value": "', | ||
tokenContents[tokenId].dataType, | ||
'"},', | ||
'{"trait_type": "key","value": "', | ||
tokenContents[tokenId].key, | ||
'"},', | ||
'{"trait_type": "data","value": "', | ||
tokenContents[tokenId].data, | ||
'"}', | ||
"]" | ||
) | ||
); | ||
|
||
string memory json = Base64.encode( | ||
bytes( | ||
string( | ||
abi.encodePacked( | ||
'{"name": "', | ||
tokenName, | ||
'", "description": "A block of content of ', | ||
name(), | ||
". Trade at: ", | ||
tradeUrl, | ||
'", "attributes":', | ||
attributes, | ||
', "amount": "', | ||
tokenContents[tokenId].amount.toString(), | ||
'", "image": "', | ||
imageUrl, | ||
'"}' | ||
) | ||
) | ||
) | ||
); | ||
string memory output = string( | ||
abi.encodePacked("data:application/json;base64,", json) | ||
); | ||
|
||
return output; | ||
} | ||
} |
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.