Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cha0s/dfd 82 Write an NFT contract. #3

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
RPC_URL=http://localhost:8545
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# From nft.storage account
NFT_STORAGE_KEY=12345
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ cache/
out/
node_modules/
broadcast/
.env
.env
yarn-error.log
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/Openzeppelin/openzeppelin-contracts
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[default]
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
Expand Down
Binary file added images/cutie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/gold_trophy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at dc14c7
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at 9cf142
8 changes: 8 additions & 0 deletions metadata/grand_prix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
name: "Wallbreaker",
description: "Winners of the Eternal Grand Prix",
attributes: [
{ trait_type: "Grand Prix", value: "Cumbersome Scrawny" },
{ display_type: "number", trait_type: "Rank", value: 1 },
],
};
47 changes: 47 additions & 0 deletions nft.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NFTStorage, File, Blob } from 'nft.storage'
import fs from 'fs'
import mime from 'mime'
import path from 'path'
import 'dotenv/config';
import meta from './metadata/grand_prix.js';

// read the API key from an environment variable. You'll need to set this before running the example!
const API_KEY = process.env.NFT_STORAGE_KEY

async function fileFromPath(filePath) {
const content = await fs.promises.readFile(filePath)
const type = mime.getType(filePath)
return new File([content], path.basename(filePath), { type })
}


async function storeExampleNFT() {
const args = process.argv.slice(2)

if (args.length !== 1) {
console.error(`usage: ${process.argv[0]} ${process.argv[1]} <image-path>`)
process.exit(1)
}
const [imagePath] = args

const image = await fileFromPath(imagePath)
const nft = {
image, // use image Blob as `image` field
...meta
}
console.log(image instanceof Blob);

if(!API_KEY) throw new Error("no NFT STORAGE key found");
const client = new NFTStorage({ token: API_KEY })
console.log(`nft`, nft);
const metadata = await client.store(nft)
console.log('NFT data stored!')
console.log('Metadata URI: ', metadata.url)
}

storeExampleNFT()
.catch(err => {
console.error(err)
process.exit(1)
})

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@dfdao/gp-registry",
"type": "module",
"version": "1.0.0",
"license": "GPL-3.0",
"files": [
Expand Down Expand Up @@ -35,6 +36,11 @@
"start:node": "anvil -m 'test test test test test test test test test test test junk'"
},
"dependencies": {
"cross-fetch": "^3.1.5",
"dotenv": "^16.0.1",
"mime": "^3.0.0",
"nft.storage": "^7.0.0",
"node-fetch": "^3.2.10",
"run-pty": "^3.0.0",
"wait-on": "^6.0.1"
}
Expand Down
4 changes: 4 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ds-test/=lib/solmate/lib/ds-test/src/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
solmate/=lib/solmate/src/
81 changes: 81 additions & 0 deletions src/NFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;

import "solmate/tokens/ERC721.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";

error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();
error InvalidURI();

contract NFT is ERC721, Ownable {
using Strings for uint256;
string public baseURI;
uint256 public currentTokenId;
mapping(uint256 => string) tokenURIs;

constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) {
baseURI = _baseURI;
}

function mintTo(address recipient, string memory _tokenURI)
public
payable
onlyOwner
returns (uint256)
{
if (bytes(baseURI).length == 0) revert InvalidURI();

uint256 newTokenId = ++currentTokenId;
tokenURIs[newTokenId] = _tokenURI;
_safeMint(recipient, newTokenId);
return newTokenId;
}

function bulkMintTo(address[] memory recipients, string memory _tokenURI)
public
payable
onlyOwner
returns (uint256[] memory)
{
uint256[] memory tokenIds = new uint[](recipients.length);

if (bytes(baseURI).length == 0) revert InvalidURI();
for(uint256 i; i < recipients.length; i++) {
uint256 newTokenId = ++currentTokenId;
tokenURIs[newTokenId] = _tokenURI;
_safeMint(recipients[i], newTokenId);
tokenIds[i] = newTokenId;
}

return tokenIds;
}

function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
if (ownerOf(tokenId) == address(0)) {
revert NonExistentTokenURI();
}
return bytes(baseURI).length > 0 ? tokenURIs[tokenId] : "";
}

function withdrawPayments(address payable payee) external onlyOwner {
uint256 balance = address(this).balance;
(bool transferTx, ) = payee.call{value: balance}("");
if (!transferTx) {
revert WithdrawTransfer();
}
}
}
100 changes: 100 additions & 0 deletions test/NFT.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/NFT.sol";

contract NFTTest is Test {
using stdStorage for StdStorage;

NFT private nft;
string private tokenURI = "wee";

function setUp() public {
// Deploy NFT contract

nft = new NFT("NFT_tutorial", "TUT", "/Users/0x/darkforest/dfdao/arena/gp-registry/images/cutie.png");
}

function testURI() public {
uint256 tokenId = nft.mintTo{value: 0.08 ether}(address(1),tokenURI);
console.log("tokenUri", nft.tokenURI(tokenId));
}

function testMintNoPrice() public {
nft.mintTo(address(1),"");
}

function testFailMintToZeroAddress() public {
nft.mintTo{value: 0.08 ether}(address(0),tokenURI);
}

function testNewMintOwnerRegistered() public {
nft.mintTo{value: 0.08 ether}(address(1),tokenURI);
uint256 slotOfNewOwner = stdstore
.target(address(nft))
.sig(nft.ownerOf.selector)
.with_key(1)
.find();

uint160 ownerOfTokenIdOne = uint160(
uint256(
(vm.load(address(nft), bytes32(abi.encode(slotOfNewOwner))))
)
);
assertEq(address(ownerOfTokenIdOne), address(1),tokenURI);
}

function testBalanceIncremented() public {
nft.mintTo{value: 0.08 ether}(address(1),tokenURI);
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(1))
.find();

uint256 balanceFirstMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceFirstMint, 1);

nft.mintTo{value: 0.08 ether}(address(1),tokenURI);
uint256 balanceSecondMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceSecondMint, 2);
}

function testSafeContractReceiver() public {
Receiver receiver = new Receiver();
nft.mintTo{value: 0.08 ether}(address(receiver),tokenURI);
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(receiver))
.find();

uint256 balance = uint256(vm.load(address(nft), bytes32(slotBalance)));
assertEq(balance, 1);
}

function testFailUnSafeContractReceiver() public {
vm.etch(address(1), bytes("mock code"));
nft.mintTo{value: 0.08 ether}(address(1),tokenURI);
}


}

contract Receiver is ERC721TokenReceiver {
function onERC721Received(
address operator,
address from,
uint256 id,
bytes calldata data
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
}

Loading