From 7e521c1f556a38fffd5c949c4b14f53c696fbc34 Mon Sep 17 00:00:00 2001 From: Kai Kang Date: Tue, 7 May 2024 17:23:02 +0800 Subject: [PATCH] ADD: tests for aime factory beta --- contracts/aime/AIMeNFTBeta.sol | 283 ++++++++++++++++++++ hardhat.config.ts | 4 +- scripts/aime/deploy_aime.ts | 14 +- test/AIMeFactoryBeta.spec.ts | 469 ++++++++++++++++++++++++++++++++- 4 files changed, 760 insertions(+), 10 deletions(-) create mode 100644 contracts/aime/AIMeNFTBeta.sol diff --git a/contracts/aime/AIMeNFTBeta.sol b/contracts/aime/AIMeNFTBeta.sol new file mode 100644 index 0000000..450fbe8 --- /dev/null +++ b/contracts/aime/AIMeNFTBeta.sol @@ -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; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index f7cac9c..f4ea6a1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -120,7 +120,9 @@ const config = { accounts: [ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", - "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", + "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" ] }, hardhat: {}, diff --git a/scripts/aime/deploy_aime.ts b/scripts/aime/deploy_aime.ts index cb5ba35..e6e50b1 100644 --- a/scripts/aime/deploy_aime.ts +++ b/scripts/aime/deploy_aime.ts @@ -25,7 +25,7 @@ async function deployAIMeContractsAll() { console.log("aimeCreator contract deployed to", aimeCreator.address); } -async function deployAIMeFactoryLauncher() { +async function deployAIMeFactory() { const contractFactory = await ethers.getContractFactory("AIMeFactoryV4"); const instance = await contractFactory.deploy(); await instance.deployed(); @@ -33,6 +33,14 @@ async function deployAIMeFactoryLauncher() { console.log("AIMeFactory V4 contract deployed to", instance.address); } +async function deployAIMeFactoryBeta() { + const contractFactory = await ethers.getContractFactory("AIMeFactoryBeta"); + const instance = await contractFactory.deploy(); + await instance.deployed(); + + console.log("AIMeFactory [Beta] contract deployed to", instance.address); +} + async function deployAIMePowerLauncher() { const powerLauncherContractFactory = await ethers.getContractFactory( "AIMePowerLauncher" @@ -53,7 +61,7 @@ async function deployAIMePowerLauncherSepolia() { console.log("powerLauncher sepolia contract deployed to", powerLauncher.address); } -async function deployAIMeCreatorLauncher() { +async function deployAIMeCreator() { const aimeCreatorContractFactory = await ethers.getContractFactory( "AIMeCreator" ); @@ -65,7 +73,7 @@ async function deployAIMeCreatorLauncher() { // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. -deployAIMePowerLauncherSepolia().catch((error) => { +deployAIMeCreator().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/test/AIMeFactoryBeta.spec.ts b/test/AIMeFactoryBeta.spec.ts index c0c63b8..6d94a61 100644 --- a/test/AIMeFactoryBeta.spec.ts +++ b/test/AIMeFactoryBeta.spec.ts @@ -1,5 +1,5 @@ import { ethers } from "hardhat"; -import { AIMeFactoryBeta, AIMeNFTV4, AIMePower } from "../typechain"; +import { AIMeFactoryBeta, AIMePower, AIMeNFTV4 } from "../typechain"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { BigNumberish } from "ethers"; const { expect } = require("chai"); @@ -97,12 +97,13 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { let owner: SignerWithAddress; let aimeSigner: SignerWithAddress; let user1: SignerWithAddress; + let user2: SignerWithAddress; const protocolFeeEthValue = "0.0001"; const protocolFeeCreateEthValue = "0.001"; const ownerPowerBalance = "10000"; beforeEach("init", async () => { - [owner, aimeSigner, user1] = await ethers.getSigners(); + [owner, aimeSigner, user1, user2] = await ethers.getSigners(); const aimeContractFactory = await ethers.getContractFactory( "AIMeFactoryBeta" @@ -117,6 +118,10 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { user1.address, ethers.utils.parseEther(ownerPowerBalance) ); + await aimePowerContract.mint( + user2.address, + ethers.utils.parseEther(ownerPowerBalance) + ); }); describe("Contract deploy", () => { @@ -195,7 +200,7 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { ).to.be.revertedWith("Insufficient payment"); }); - it("can create aime with fee", async () => { + it("can create aime with fee (without signer)", async () => { const tx = await aimeFactoryContract.createAIMe( createAIMeParams, ethers.constants.AddressZero, @@ -209,6 +214,46 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { expect(aimeAddress).not.eq(ethers.constants.AddressZero); }); + it("can not create aime with signer but has no sig", async () => { + const nonce = await aimeFactoryContract.addressNonce(user1.address); + const sig = await genSig( + aimeSigner, + user1.address, + name, + avatar, + bio, + image, + "fake_social_key", + "fake_social_id", + minPrice, + priceIncrement, + nonce + ); + expect( + aimeFactoryContract.createAIMe( + createAIMeParams, + ethers.constants.AddressZero, + aimeSigner.address, + sig, + { value: ethers.utils.parseEther(protocolFeeCreateEthValue) } + ) + ).to.be.revertedWith("Invalid signature"); + }); + + it("social info requires signer", async () => { + createAIMeParams.socialId = "twitter"; + createAIMeParams.socialKey = "some_twitter_handle"; + expect( + aimeFactoryContract.createAIMe( + createAIMeParams, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + "0x", + { value: ethers.utils.parseEther(protocolFeeCreateEthValue) } + ) + ).to.be.revertedWith("Invalid signature"); + }); + it("can create aime with signer", async () => { const socialId = "twitter"; const socialKey = "some_twitter_handle"; @@ -390,12 +435,13 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { let avatar = "avatar_url"; let socialKey = "twitter"; let socialId = "some_twitter_handle"; - let minPrice = 20; + let minPrice = ethers.utils.parseEther("10"); let priceIncrement = 10; let createAIMeParams: any; let aimeNftContract: AIMeNFTV4; + let aimeNftNoPowerContract: AIMeNFTV4; - beforeEach("create one aime", async () => { + beforeEach("create two aimes", async () => { createAIMeParams = { creator: user1.address, name, @@ -443,6 +489,30 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { const nftName = await aimeNftContract.name(); expect(nftName).eq(name); + + // also create one aime with no power + const noPowerAimeName = "no_power_aime"; + createAIMeParams.name = noPowerAimeName; + createAIMeParams.socialKey = ""; + createAIMeParams.socialId = ""; + const tx2 = await aimeFactoryContract + .connect(user1) + .createAIMe( + createAIMeParams, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + "0x", + { value: ethers.utils.parseEther(protocolFeeCreateEthValue) } + ); + await tx2.wait(); + const aimeAddressNoPower = await aimeFactoryContract.aimeAddresses( + noPowerAimeName + ); + aimeNftNoPowerContract = await nftContractFactory.attach( + aimeAddressNoPower + ); + const nftNoPowerName = await aimeNftNoPowerContract.name(); + expect(nftNoPowerName).eq(noPowerAimeName); }); it("aime created with social and power", async () => { @@ -454,7 +524,44 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { expect(nftContent.data).eq(socialId); }); - it("can mint nft", async () => { + it("aime with no power cannot mint nft", async () => { + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("2"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + expect( + aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ) + ).to.be.revertedWith("No AIME Power"); + }); + + it("cannot mint without enough fee", async () => { const key = "new_key"; const type = "editable"; const data = "new_data_for_new_key"; @@ -480,6 +587,180 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { .approve(aimeNftContract.address, nftPrice); await approveTx.wait(); + expect( + aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther("0.0000001") } + ) + ).to.be.revertedWith("Insufficient payment"); + }); + + it("cannot mint nft without enough power", async () => { + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("200000"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, nftPrice); + await approveTx.wait(); + + expect( + aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ) + ).to.be.revertedWith("balance not enough"); + }); + + it("can not mint if allowance too low", async () => { + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("20"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, ethers.utils.parseEther("10")); + await approveTx.wait(); + + expect( + aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ) + ).to.be.revertedWith("allowance not enough"); + }); + + it("can not mint if price too low", async () => { + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("10"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, ethers.utils.parseEther("10")); + await approveTx.wait(); + + expect( + aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ) + ).to.be.revertedWith("price too low"); + }); + + it("can mint nft", async () => { + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("200"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, nftPrice); + await approveTx.wait(); + + const userPowerBalanceBefore = await aimePowerContract.balanceOf( + user1.address + ); + const contractPowerBalanceBefore = await aimePowerContract.balanceOf( + aimeNftContract.address + ); const tx = await aimeFactoryContract .connect(user1) .mintAIMeNFT( @@ -501,6 +782,182 @@ describe("AIMe Factory and NFT [Beta] Contract", () => { const nftContent = await aimeNftContract.tokenContents(tokenId); expect(nftContent.key).eq(key); expect(nftContent.data).eq(data); + + const userPowerBalanceAfter = await aimePowerContract.balanceOf( + user1.address + ); + const contractPowerBalanceAfter = await aimePowerContract.balanceOf( + aimeNftContract.address + ); + expect(userPowerBalanceAfter).eq(userPowerBalanceBefore.sub(nftPrice)); + expect(contractPowerBalanceAfter).eq( + contractPowerBalanceBefore.add(nftPrice) + ); + }); + + // sell nft to contract & buy back from contract + it("sell and buy back from contract", async () => { + // first mint nft + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("200"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, nftPrice); + await approveTx.wait(); + const tx = await aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ); + await tx.wait(); + const tokenId = 2; + const nftOwner = await aimeNftContract.ownerOf(tokenId); + expect(nftOwner).eq(user1.address); + + // sell nft to contract + const userPowerBalanceBeforeSell = await aimePowerContract.balanceOf( + user1.address + ); + const txSell = await aimeNftContract.connect(user1).sellNFT(tokenId); + await txSell.wait(); + const nftOwnerAfterSell = await aimeNftContract.ownerOf(tokenId); + expect(nftOwnerAfterSell).eq(aimeNftContract.address); + const userPowerBalanceAfterSell = await aimePowerContract.balanceOf( + user1.address + ); + expect(userPowerBalanceAfterSell).eq( + userPowerBalanceBeforeSell.add(nftPrice) + ); + + // buy nft from contract + const userPowerBalanceBeforeBuy = await aimePowerContract.balanceOf( + user1.address + ); + // cannot buy before approve + expect(aimeNftContract.connect(user1).buyNFT(tokenId)).to.be.revertedWith( + "allowance not enough" + ); + + const approveTxForBuy = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, nftPrice); + await approveTxForBuy.wait(); + + const txBuy = await aimeNftContract.connect(user1).buyNFT(tokenId); + await txBuy.wait(); + const nftOwnerAfterBuy = await aimeNftContract.ownerOf(tokenId); + expect(nftOwnerAfterBuy).eq(user1.address); + const userPowerBalanceAfterBuy = await aimePowerContract.balanceOf( + user1.address + ); + expect(userPowerBalanceAfterBuy).eq( + userPowerBalanceBeforeBuy.sub(nftPrice) + ); + }); + + // buy nft from another user + it("buy nft from another user", async () => { + // first mint nft + // first mint nft + const key = "new_key"; + const type = "editable"; + const data = "new_data_for_new_key"; + const image = "new_image_url"; + const nftPrice = ethers.utils.parseEther("200"); + const currentNonce = await aimeFactoryContract.addressNonce( + user1.address + ); + const sig = await genMintNftSig( + aimeSigner, + user1.address, + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + currentNonce + ); + + const approveTx = await aimePowerContract + .connect(user1) + .approve(aimeNftContract.address, nftPrice); + await approveTx.wait(); + const tx = await aimeFactoryContract + .connect(user1) + .mintAIMeNFT( + aimeNftContract.address, + key, + type, + data, + image, + nftPrice, + sig, + { value: ethers.utils.parseEther(protocolFeeEthValue) } + ); + await tx.wait(); + const tokenId = 2; + const nftOwner = await aimeNftContract.ownerOf(tokenId); + expect(nftOwner).eq(user1.address); + + // user2 buy nft from user1 + expect(aimeNftContract.connect(user2).buyNFT(tokenId)).to.be.revertedWith( + "allowance not enough" + ); + const nftBuyPirce = nftPrice.add(nftPrice.div(10)); + const approveTxForBuy = await aimePowerContract + .connect(user2) + .approve(aimeNftContract.address, nftBuyPirce); + await approveTxForBuy.wait(); + + const user1PowerBalanceBeforeBuy = await aimePowerContract.balanceOf( + user1.address + ); + const user2PowerBalanceBeforeBuy = await aimePowerContract.balanceOf( + user2.address + ); + + const txBuy = await aimeNftContract.connect(user2).buyNFT(tokenId); + await txBuy.wait(); + const nftOwnerAfterBuy = await aimeNftContract.ownerOf(tokenId); + expect(nftOwnerAfterBuy).eq(user2.address); + const user1PowerBalanceAfterBuy = await aimePowerContract.balanceOf( + user1.address + ); + const user2PowerBalanceAfterBuy = await aimePowerContract.balanceOf( + user2.address + ); + expect(user1PowerBalanceAfterBuy).eq( + user1PowerBalanceBeforeBuy.add(nftBuyPirce) + ); + expect(user2PowerBalanceAfterBuy).eq( + user2PowerBalanceBeforeBuy.sub(nftBuyPirce) + ); }); }); });