非同质化通证(Non-Fungible Token,NFT)是一种架构在区块链技术上的,不可复制、篡改、分割的加密数字权益证明,可以理解为 一种去中心化的“虚拟资产或实物资产的数字所有权证书”。 从技术层面来看,NFT以智能合约的形式发行,一份智能合约可以发行一种或多种NFT资产,包括实体收藏品、活动门票等实物资产和图 像、音乐、游戏道具等虚拟资产。
目前市面上使用最广泛、知名度最高的NFT主流协议标准中,ERC721协议下一份合约只能发行一种NFT 资产(如BAYC代币),ERC1155协议支持一份合约发行任意种类的NFT资产(如交易平台Opensea的代币OpenStore)。一种NFT资产 可映射多个NFT(如BAYC代币共发行了一万个),NFT智能合约记录了每个NFT资产的token ID、资源存储地址及它们的各项信息。 NFT储存于区块链上,但受到成本影响,其映射的实物资产或数字资产一般不上链,而是储存于其他中心化或非中心化的存储系统中,如 IPFS,并通过哈希值或URL映射上链。


/**
*Submitted for verification at Etherscan.io on 2017-07-19
*/
pragma solidity ^0.4.8;
contract CryptoPunksMarket {
// You can use this hash to verify the image file containing all the punks
string public imageHash = "ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b";
address owner;
string public standard = 'CryptoPunks';
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
uint public nextPunkIndexToAssign = 0;
bool public allPunksAssigned = false;
uint public punksRemainingToAssign = 0;
//mapping (address => uint) public addressToPunkIndex;
mapping (uint => address) public punkIndexToAddress;
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
struct Offer {
bool isForSale;
uint punkIndex;
address seller;
uint minValue; // in ether
address onlySellTo; // specify to sell only to a specific person
}
struct Bid {
bool hasBid;
uint punkIndex;
address bidder;
uint value;
}
// A record of punks that are offered for sale at a specific minimum value, and perhaps to a specific person
mapping (uint => Offer) public punksOfferedForSale;
// A record of the highest punk bid
mapping (uint => Bid) public punkBids;
mapping (address => uint) public pendingWithdrawals;
event Assign(address indexed to, uint256 punkIndex);
event Transfer(address indexed from, address indexed to, uint256 value);
event PunkTransfer(address indexed from, address indexed to, uint256 punkIndex);
event PunkOffered(uint indexed punkIndex, uint minValue, address indexed toAddress);
event PunkBidEntered(uint indexed punkIndex, uint value, address indexed fromAddress);
event PunkBidWithdrawn(uint indexed punkIndex, uint value, address indexed fromAddress);
event PunkBought(uint indexed punkIndex, uint value, address indexed fromAddress, address indexed toAddress);
event PunkNoLongerForSale(uint indexed punkIndex);
/* Initializes contract with initial supply tokens to the creator of the contract */
function CryptoPunksMarket() payable {
// balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
owner = msg.sender;
totalSupply = 10000; // Update total supply
punksRemainingToAssign = totalSupply;
name = "CRYPTOPUNKS"; // Set the name for display purposes
symbol = "Ͼ"; // Set the symbol for display purposes
decimals = 0; // Amount of decimals for display purposes
}
function setInitialOwner(address to, uint punkIndex) {
if (msg.sender != owner) throw;
if (allPunksAssigned) throw;
if (punkIndex >= 10000) throw;
if (punkIndexToAddress[punkIndex] != to) {
if (punkIndexToAddress[punkIndex] != 0x0) {
balanceOf[punkIndexToAddress[punkIndex]]--;
} else {
punksRemainingToAssign--;
}
punkIndexToAddress[punkIndex] = to;
balanceOf[to]++;
Assign(to, punkIndex);
}
}
function setInitialOwners(address[] addresses, uint[] indices) {
if (msg.sender != owner) throw;
uint n = addresses.length;
for (uint i = 0; i < n; i++) {
setInitialOwner(addresses[i], indices[i]);
}
}
function allInitialOwnersAssigned() {
if (msg.sender != owner) throw;
allPunksAssigned = true;
}
function getPunk(uint punkIndex) {
if (!allPunksAssigned) throw;
if (punksRemainingToAssign == 0) throw;
if (punkIndexToAddress[punkIndex] != 0x0) throw;
if (punkIndex >= 10000) throw;
punkIndexToAddress[punkIndex] = msg.sender;
balanceOf[msg.sender]++;
punksRemainingToAssign--;
Assign(msg.sender, punkIndex);
}
// Transfer ownership of a punk to another user without requiring payment
function transferPunk(address to, uint punkIndex) {
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] != msg.sender) throw;
if (punkIndex >= 10000) throw;
if (punksOfferedForSale[punkIndex].isForSale) {
punkNoLongerForSale(punkIndex);
}
punkIndexToAddress[punkIndex] = to;
balanceOf[msg.sender]--;
balanceOf[to]++;
Transfer(msg.sender, to, 1);
PunkTransfer(msg.sender, to, punkIndex);
// Check for the case where there is a bid from the new owner and refund it.
// Any other bid can stay in place.
Bid bid = punkBids[punkIndex];
if (bid.bidder == to) {
// Kill bid and refund value
pendingWithdrawals[to] += bid.value;
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
}
}
function punkNoLongerForSale(uint punkIndex) {
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] != msg.sender) throw;
if (punkIndex >= 10000) throw;
punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, 0x0);
PunkNoLongerForSale(punkIndex);
}
function offerPunkForSale(uint punkIndex, uint minSalePriceInWei) {
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] != msg.sender) throw;
if (punkIndex >= 10000) throw;
punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, 0x0);
PunkOffered(punkIndex, minSalePriceInWei, 0x0);
}
function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) {
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] != msg.sender) throw;
if (punkIndex >= 10000) throw;
punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress);
PunkOffered(punkIndex, minSalePriceInWei, toAddress);
}
function buyPunk(uint punkIndex) payable {
if (!allPunksAssigned) throw;
Offer offer = punksOfferedForSale[punkIndex];
if (punkIndex >= 10000) throw;
if (!offer.isForSale) throw; // punk not actually for sale
if (offer.onlySellTo != 0x0 && offer.onlySellTo != msg.sender) throw; // punk not supposed to be sold to this user
if (msg.value < offer.minValue) throw; // Didn't send enough ETH
if (offer.seller != punkIndexToAddress[punkIndex]) throw; // Seller no longer owner of punk
address seller = offer.seller;
punkIndexToAddress[punkIndex] = msg.sender;
balanceOf[seller]--;
balanceOf[msg.sender]++;
Transfer(seller, msg.sender, 1);
punkNoLongerForSale(punkIndex);
pendingWithdrawals[seller] += msg.value;
PunkBought(punkIndex, msg.value, seller, msg.sender);
// Check for the case where there is a bid from the new owner and refund it.
// Any other bid can stay in place.
Bid bid = punkBids[punkIndex];
if (bid.bidder == msg.sender) {
// Kill bid and refund value
pendingWithdrawals[msg.sender] += bid.value;
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
}
}
function withdraw() {
if (!allPunksAssigned) throw;
uint amount = pendingWithdrawals[msg.sender];
// Remember to zero the pending refund before
// sending to prevent re-entrancy attacks
pendingWithdrawals[msg.sender] = 0;
msg.sender.transfer(amount);
}
function enterBidForPunk(uint punkIndex) payable {
if (punkIndex >= 10000) throw;
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] == 0x0) throw;
if (punkIndexToAddress[punkIndex] == msg.sender) throw;
if (msg.value == 0) throw;
Bid existing = punkBids[punkIndex];
if (msg.value <= existing.value) throw;
if (existing.value > 0) {
// Refund the failing bid
pendingWithdrawals[existing.bidder] += existing.value;
}
punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value);
PunkBidEntered(punkIndex, msg.value, msg.sender);
}
function acceptBidForPunk(uint punkIndex, uint minPrice) {
if (punkIndex >= 10000) throw;
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] != msg.sender) throw;
address seller = msg.sender;
Bid bid = punkBids[punkIndex];
if (bid.value == 0) throw;
if (bid.value < minPrice) throw;
punkIndexToAddress[punkIndex] = bid.bidder;
balanceOf[seller]--;
balanceOf[bid.bidder]++;
Transfer(seller, bid.bidder, 1);
punksOfferedForSale[punkIndex] = Offer(false, punkIndex, bid.bidder, 0, 0x0);
uint amount = bid.value;
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
pendingWithdrawals[seller] += amount;
PunkBought(punkIndex, bid.value, seller, bid.bidder);
}
function withdrawBidForPunk(uint punkIndex) {
if (punkIndex >= 10000) throw;
if (!allPunksAssigned) throw;
if (punkIndexToAddress[punkIndex] == 0x0) throw;
if (punkIndexToAddress[punkIndex] == msg.sender) throw;
Bid bid = punkBids[punkIndex];
if (bid.bidder != msg.sender) throw;
PunkBidWithdrawn(punkIndex, bid.value, msg.sender);
uint amount = bid.value;
punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
// Refund the bid money
msg.sender.transfer(amount);
}
}
而上面这段代码就是大名鼎鼎的 NFT CryptoPunks 的合约代码。
CryptoPunks 是啥?就是下面这一组图片,总市值超10亿美金。
项目方也是赚得盆满锅满的,目前合约上的ETH价值1亿美金
mint的意思是nft上链,或者更简单的理解就是生产出这个nft。目前mint这个过程项目方是直接对接到用户的,他们会为项目建一个官方网站,然后在网站上提供mint的按钮,用户在当前页面链接钱包之后,点击mint 按钮就会生成nft到自己的账户里面。
比如最近话题度比较高的NBA nft项目的官网https://theassociationnft.com/

当然不是所有的项目都是free mint的,mint是否收费要看项目方了。还有一种是空投,项目方直接mint到你的账号里面,这种一般都是项目成员或者大V了。
mint目前比较主流的都有2轮,第一轮是白名单用户mint,第二轮是全员均可mint。白名单用户mint既有确定性,又可以避免gas war,所以但凡有点流量的项目白名单的申请都非常火爆。如何成为白名单用户就有各种玩法了,什么唱歌、跳舞、写感想、聊天啥都有,所有也派生出了一个玩法:炒白名单,甚至还有打白工作室。
mint nft之后还有一个玩法就是开彩蛋,还是用NBA项目举例,你mint下来的nft在项目方约定的时间之前,只是个纯盲盒,每个盲盒里面对应的图片是什么,原则上只有项目方知道,而这个时候其实NFT也可以在二级市场上买卖,所以炒盲盒也是一种玩法。(技术层面怎么实现的后文会讲)

盲盒开出来之后,每个nft都不一样,那价格高低如何界定?目前主流的是按属性稀有度来估价。我们以azuki项目来举例说明下,实际每个NFT本身是有很多属性(properties),而属性的稀有度就直接影响NFT的价格。

比如Type下属性为 spirit的肯定价格就高,因为有这个属性的只有97个nft

当所有nft都mint结束后,就只有二级市场可以买卖了,目前最大的 NFT 二级交易市场就是https://opensea.io/。原则上就是buy now就完了。

NFT玩法中的不同角色 在实际的操作中,NFT玩法又可以细分为以下4种角色,各种在玩法里面操作和获利各不相同。当然食物链的顶端是项目方,食物链的最低端就是普通散户玩家。
散户玩NFT其实劣势是比较多的,但是是不是就一定不能玩,我觉得也不是,核心还是要自己能分析项目,自己能估值(但最大的问题就在这里,NFT目前估值非常难,最简单的锚定法基本没法操作)那其实也可以玩。
散户的玩法2种,一种是玩一级市场,一种是玩二级市场。
玩一级市场成本最低,主要就是几点:
关注大ip的发售信息,什么是大ip这个问题其实是有一定门槛,比如村上隆(就是那个花的项目),如果不是潮流圈的人可能不知道,但是其实这个ip是非常牛逼的。
积极参与项目方的活动,争取到白名单机会
如果争取不到,那就蹲点刷公开mint
盲盒时第一波收割其实就可以
如果说对项目信心非常足,而且想赌一波大的,可以等盲盒开出来再卖
玩二级市场:
如果二级市场再去收NFT,价格原则上肯定是涨了一波的,到底项目值不值得买,这个有个非常重要的指标,项目fomo的程度。这个就跟短线炒股差不多了,有个建议的参考指数,含鲸量(就一些大户是否持有),以及社区的讨论度,但这个也不能完全说明后续涨跌,这个对于市场的把控还是非常考验能力的。
项目方这块我主要就讲讲技术角度的东西,实操下如何发一组NFT。
先讲几个基础知识点:
虽然一个NFT原则上对应一个图片,但是图片本身并不是存在链上,而且存在IPFS上,然后通过合约代码将每个token与对应的图片链接进行关联来保证一一对应的关系。
为什么用IPFS,而不用HTTP直接存?核心在于HTTP服务器的所有者,可以在链接不变的情况下,随意更换内容,而且作为中心化服务,其他人也无从查证和监督。而IPFS本质是分布式存储,没有人可以在不破坏链接的情况下替换或更改内容。但NFT项目方就不能改IPFS地址了吗?其实也可以改,只是只要改了就有记录,这就是区块链的价值所在,当然通过改合约的IPFS地址本质就是前文的开盲盒玩法的技术方案。
前文提到的NFT属性以及它的所有参数,在技术层面上对应的是一个MetaData的协议,结构是JSON的。这个JSON文件也存储在IPFS上。
这是专为智能合约而生的语言,具体写法就不讲了,有兴趣的直接看文档
Hardhat是一个便于在以太坊上进行构建的开发环境。它帮助开发人员管理和自动化构建智能合约和dApp的过程中固有的重复任务,以及轻松地围绕此工作流程引入更多功能,并且内置了开发专用以太坊网络,这意味着从根本上进行编译和测试。
首先是有个小狐狸(metamask)账号,这个自己先官网下个小狐狸的chrome插件,然后按流程注册个账号既可。这是你进入币圈的通行证,没有就赶紧注册一个。
因为是测试网,所以ETH是可以随便领的,因为后面合约部署是需要钱的,我们需要先准备一点ETH,打开https://faucets.chain.link/rinkeby,链接自己的钱包就可以领钱了。后续我们所有的操作都是在Rinkeby测试网进行。
创建一个文件夹
mkdir nft-tutorial
cd nft-tutorial
确认安装了npm和node,前端应该很熟悉这个
npm init
生成默认的package.json配置
修改package.json配置,并安装依赖
{
"name": "nft-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.5",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"ethers": "^5.6.4",
"hardhat": "^2.9.3",
"node-fetch": "^2.6.7"
},
"dependencies": {
"@openzeppelin/contracts": "^4.5.0",
"dotenv": "^16.0.0"
}
}
安装Hardhat
> npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.7.0 👷
? What do you want to do? …
Create a basic sample project
Create an advanced sample project
Create an advanced sample project that uses TypeScript
❯ Create an empty hardhat.config.js
Quit
...
✨ Config file created ✨
写核心合约代码,openzeppelin就是一堆的协议库,NFT对应的协议就是ERC721,里面代码对于NFT的实现已经做好了封装,我们只需要在上面再添加我们自己的逻辑就可以了。因为智能合约的代码执行就要烧钱,对代码的要求是非常高的,一般都建议用成熟的方案和依赖,非必要不要自己乱造。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFT is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private currentTokenId;
/// @dev Base token URI used as a prefix by tokenURI().
string public baseTokenURI;
constructor() ERC721("NFTTutorial", "NFT") {
baseTokenURI = "";
}
function mintTo(address recipient) public returns (uint256) {
currentTokenId.increment();
uint256 newItemId = currentTokenId.current();
_safeMint(recipient, newItemId);
return newItemId;
}
/// @dev Returns an URI for a given token ID
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}
/// @dev Sets the base token URI prefix.
function setBaseTokenURI(string memory _baseTokenURI) public {
baseTokenURI = _baseTokenURI;
}
}
修改 hardhat.config.js ,配置一些本地命令行操作
/**
* @type import('hardhat/config').HardhatUserConfig
*/
require('dotenv').config();
require("@nomiclabs/hardhat-ethers");
require("./scripts/deploy.js");
require("./scripts/mint.js");
const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY } = process.env;
module.exports = {
solidity: "0.8.1",
defaultNetwork: "rinkeby",
networks: {
hardhat: {},
rinkeby: {
url: `https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_KEY}`,
accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
},
ethereum: {
chainId: 1,
url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
},
},
}
同时新增相关的3个script文件
const { task } = require("hardhat/config");
const { getAccount } = require("./helpers");
task("check-balance", "Prints out the balance of your account").setAction(async function (taskArguments, hre) {
const account = getAccount();
console.log(`Account balance for ${account.address}: ${await account.getBalance()}`);
});
task("deploy", "Deploys the NFT.sol contract").setAction(async function (taskArguments, hre) {
const nftContractFactory = await hre.ethers.getContractFactory("NFT", getAccount());
const nft = await nftContractFactory.deploy();
console.log(`Contract deployed to address: ${nft.address}`);
});
const { ethers } = require("ethers");
const { getContractAt } = require("@nomiclabs/hardhat-ethers/internal/helpers");
// Helper method for fetching environment variables from .env
function getEnvVariable(key, defaultValue) {
if (process.env[key]) {
return process.env[key];
}
if (!defaultValue) {
throw `${key} is not defined and no default value was provided`;
}
return defaultValue;
}
// Helper method for fetching a connection provider to the Ethereum network
function getProvider() {
return ethers.getDefaultProvider(getEnvVariable("NETWORK", "rinkeby"), {
alchemy: getEnvVariable("ALCHEMY_KEY"),
});
}
// Helper method for fetching a wallet account using an environment variable for the PK
function getAccount() {
return new ethers.Wallet(getEnvVariable("ACCOUNT_PRIVATE_KEY"), getProvider());
}
// Helper method for fetching a contract instance at a given address
function getContract(contractName, hre) {
const account = getAccount();
return getContractAt(hre, contractName, getEnvVariable("NFT_CONTRACT_ADDRESS"), account);
}
module.exports = {
getEnvVariable,
getProvider,
getAccount,
getContract,
}
const { task } = require("hardhat/config");
const { getContract } = require("./helpers");
const fetch = require("node-fetch");
task("mint", "Mints from the NFT contract")
.addParam("address", "The address to receive a token")
.setAction(async function (taskArguments, hre) {
const contract = await getContract("NFT", hre);
const transactionResponse = await contract.mintTo(taskArguments.address, {
gasLimit: 500_000,
});
console.log(`Transaction Hash: ${transactionResponse.hash}`);
});
task("set-base-token-uri", "Sets the base token URI for the deployed smart contract")
.addParam("baseUrl", "The base of the tokenURI endpoint to set")
.setAction(async function (taskArguments, hre) {
const contract = await getContract("NFT", hre);
const transactionResponse = await contract.setBaseTokenURI(taskArguments.baseUrl, {
gasLimit: 500_000,
});
console.log(`Transaction Hash: ${transactionResponse.hash}`);
});
task("token-uri", "Fetches the token metadata for the given token ID")
.addParam("tokenId", "The tokenID to fetch metadata for")
.setAction(async function (taskArguments, hre) {
const contract = await getContract("NFT", hre);
const response = await contract.tokenURI(taskArguments.tokenId, {
gasLimit: 500_000,
});
const metadata_url = response;
console.log(`Metadata URL: ${metadata_url}`);
const metadata = await fetch(metadata_url).then(res => res.json());
console.log(`Metadata fetch response: ${JSON.stringify(metadata, null, 2)}`);
});
添加 .env 文件,存一些关键参数
ALCHEMY_KEY = "" --alchemy api key
ACCOUNT_PRIVATE_KEY = "" --你小狐狸账号的私钥
NETWORK="rinkeby"
NFT_CONTRACT_ADDRESS="" --合约部署之后再添加
alchemy 相当于区块链世界的亚马逊云服务,用它可以降低我们的部署和开发的成本。
到这为止,其实我们的合约代码已经写完了。

准备几张图片,都可以
npx ipfs-car --pack images --output images.car
打开nft.storage,注册完成,把.car文件传到上面




写好与图片一一对应的metadata,链接换成生成的IPFS链接

{
"attributes" : [
{
"trait_type" : "level",
"value" : 3
},
{
"trait_type" : "stamina",
"value" : 11.7
},
{
"trait_type" : "personality",
"value" : "sleepy"
},
{
"display_type" : "boost_number",
"trait_type" : "aqua_power",
"value" : 30
},
{
"display_type" : "boost_percentage",
"trait_type" : "stamina_increase",
"value" : 15
},
{
"display_type" : "number",
"trait_type" : "generation",
"value" : 1
}
],
"description" : "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"external_url" : "https://example.com/?token_id=2",
"image" : "https://bafybeiddnebsbc3hfhdvoxp4m7qi4gdwunivy7xxg2euszecgraapnbnwm.ipfs.nftstorage.link/images/2.png",
"name" : "OolongTea Boris McCoy"
}
npx ipfs-car --pack metadata --output metadata.car
也上传到nft.storage,这个 metadata.car 链接地址后面要用
> npx hardhat compile
Compiling 11 files with 0.8.0
Compilation finished successfully
> npx hardhat deploy
Contract deployed to address: 0x3d87Da4E9be05D1559694C758001438aebE0B32C
Set your NFT_CONTRACT_ADDRESS env variable and source your .env file
> . .env
> npx hardhat set-base-token-uri --base-url "https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/"
Transaction Hash: 0xdd9a44d4131ee48db493d0484b0b294bff8135b678fa0e029677d04e3683eca8
> npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E
Transaction Hash: 0x99b09300643f6e95563a6f54b94366f79daef835969a53933b1dc43208341a18
> npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E
Transaction Hash: 0x661c48b2e306210fde28765786116a6dc4f955f2d44f09dee2ed66f9c9c4f79a
> npx hardhat token-uri --token-id 1
Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/1
Metadata fetch response: {
"description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"external_url": "https://example.com/?token_id=1",
"image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/1.png",
"name": "Sprinkles Fisherton"
}
> npx hardhat token-uri --token-id 2
Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/2
Metadata fetch response: {
"description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"external_url": "https://example.com/?token_id=1",
"image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/2.png",
"name": "Boris McCoy"
}
打开o
也上传到nft.storage,这个 metadata.car 链接地址后面要用 合约上线
npx hardhat compile Compiling 11 files with 0.8.0 Compilation finished successfully
npx hardhat deploy Contract deployed to address: 0x3d87Da4E9be05D1559694C758001438aebE0B32C
Set your NFT_CONTRACT_ADDRESS env variable and source your .env file
. .env npx hardhat set-base-token-uri --base-url "https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/" Transaction Hash: 0xdd9a44d4131ee48db493d0484b0b294bff8135b678fa0e029677d04e3683eca8
npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E Transaction Hash: 0x99b09300643f6e95563a6f54b94366f79daef835969a53933b1dc43208341a18
npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E Transaction Hash: 0x661c48b2e306210fde28765786116a6dc4f955f2d44f09dee2ed66f9c9c4f79a
npx hardhat token-uri --token-id 1 Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/1 Metadata fetch response: { "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", "external_url": "https://example.com/?token_id=1", "image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/1.png", "name": "Sprinkles Fisherton" }
npx hardhat token-uri --token-id 2 Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/2 Metadata fetch response: { "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", "external_url": "https://example.com/?token_id=1", "image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/2.png", "name": "Boris McCoy" } 打开opensea就可以看到我们mint了的nft了 https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1 也可以在以太坊浏览器上面看 https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1 我们可以在etherscan查看任何链上信息,同时也可以进行读写操作,所以这个地方有个财富密码就是直接绕过NFT项目方官网,在etherscan直接进行mint,可以快于在官网mint的的人。
打开opensea就可以看到我们mint了的nft了
https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1
也可以在以太坊浏览器上面看
https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1
我们可以在etherscan查看任何链上信息,同时也可以进行读写操作,所以这个地方有个财富密码就是直接绕过NFT项目方官网,在etherscan直接进行mint,可以快于在官网mint的的人。
