本文是区块链葡萄酒交易市场系列文章的第二部分,重点介绍了前端集成,包括如何使用 javascript 和 ethers.js 连接 WineCollection 智能合约到 Web 市场。文章详细讲解了钱包连接、网络处理、Provider 和 Signer 设置,以及智能合约的部署、NFT 的铸造、token metadata 的更新、NFT 销毁以及存储评论等功能实现,并提供了示例代码。
在第一部分中,我们探讨了基于区块链的葡萄酒市场的后端架构和智能合约开发。如果你是这个旅程的新手,你可能还想查看我们之前的文章用区块链改革葡萄酒产业,其中概述了我们的愿景。
现在,在第二部分中,我们将重点转移到 前端集成 ,在这里我们将 WineCollection 智能合约连接到 Web 市场。
为了从 Web 市场与 WineCollection 智能合约交互,我们使用 javascript 和 ethers.js 进行区块链交互。Web3Service.js 模块管理钱包连接、签名交易以及与 Arbitrum 区块链的交互。
import {web3Networks} from "../common/Web3Networks.js";
import { ethers } from "ethers";
class Web3Service {
constructor() {
this.initialize();
}
async initialize() {
try {
this.provider = null;
this.walletAddress = null;
} catch (error) {
console.error('Error during initialization:', error);
}
}
钱包连接和网络处理
connectWallet(network)
函数检查是否安装了 MetaMask 并连接到指定的区块链网络。如果网络(Arbitrum Sepolia 或 Arbitrum One)尚未添加,它会请求 MetaMask 添加它。async connectWallet(network) {
if (!window.ethereum) {
alert('Please install Wallet (Metamask) extension!');
return;
}
if (!network) {
console.error('Active network not set.');
return;
}
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{\
chainId: `0x${network.chainId.toString(16)}`, // Convert chainId to hex\
rpcUrls: [network.rpcUrl],\
chainName: network.chainName,\
nativeCurrency: network.nativeCurrency,\
blockExplorerUrls: network.blockExplorerUrl,\
}],
});
const provider = await this.getProvider();
const signer = await this.getSigner();
this.walletAddress = await signer.getAddress();
return { provider: this.provider, walletAddress: this.walletAddress };
} catch (error) {
console.error('Error connecting to network:', error);
throw new Error('Failed to connect to the network.');
}
}
Provider 和 Signer 设置
getProvider()
使用 cryprtowallet(例如 Metamask (window.ethereum))初始化一个 ethers.js provider。async getProvider() {
if (!this.provider) {
try {
this.provider = new ethers.BrowserProvider(window.ethereum);
await this.provider.send("eth_requestAccounts", []);
} catch (error) {
console.error('Error initializing provider:', error);
throw new Error('Failed to initialize provider.');
}
}
return this.provider;
}
getSigner()
检索用户的钱包签名者,允许他们发送交易。 async getSigner() {
const provider = await this.getProvider();
return await provider.getSigner();
}
网络配置 (Web3Networks.js)
export const web3Networks = {
"sepoliaArbitrum": {
"id": "sepoliaArbitrum",
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
"chainId": 421614,
"chainName": "Arbitrum Sepolia",
"nativeCurrency": {
"name": "Arbitrum Sepolia ETH",
"symbol": "ETH",
"decimals": 18
},
"blockExplorerUrl": ["https://sepolia.arbiscan.io"]
},
"arbitrum": {
"id": "arbitrum",
"rpcUrl": "https://arb1.arbitrum.io/rpc",
"chainId": 42161,
"chainName": "Arbitrum One",
"nativeCurrency": {
"name": "Arbitrum ETH",
"symbol": "ETH",
"decimals": 18
},
"blockExplorerUrl": ["https://arbiscan.io"]
}
};
getWalletAddress()
函数检索当前连接用户的钱包地址
async getWalletAddress() {
try {
const signer = await this.getSigner();
const walletAddress = await signer.getAddress();
return walletAddress;
} catch (error) {
console.error('Error retrieving wallet address:', error);
return null;
}
}
deployContractWithMetadata()
函数处理部署葡萄酒收藏智能合约的 端到端过程,同时将其元数据存储在 IPFS 上。
· contractCodeURI 是指向 WineCollectionAbiByteCode.json 的链接
· 创建合约元数据文件 — 生成一个 JSON 文件,其中包含收藏详细信息 (contractURI
)。
· 上传到 IPFS — 将元数据文件存储在 IPFS 上并检索其 CID (Content Identifier)。
· 部署智能合约 — 调用 deployContract()
,传递 合约元数据 URI 和其他参数。
async deployContractWithMetadata(contractCodeURI, metadata) {
try {
const { name, description, image, bannerImage, externalLink, contractSymbol, maxSupply } = metadata;
const fileContent = await this.createContractURIJson(name, description, image, bannerImage, externalLink);
if (!fileContent) {
throw new Error("Failed to create contractURI JSON.");
}
const contractUriCid = await this.uploadFileToIPFS(fileContent);
if (!contractUriCid) {
throw new Error("Failed to upload contractURI to IPFS.");
}
const contractUri = `ipfs://${contractUriCid}`;
const contractAddress = await this.deployContract(contractCodeURI, name, contractSymbol, maxSupply, contractUri);
return { contractAddress, contractUriCid, networkId: await this.getCurrentNetworkId()};
} catch (error) {
console.error('Error deploying contract with metadata:', error);
throw error;
}
}
deployContract()
函数执行到区块链的实际部署。
async deployContract(contractCodeURI, contractName, contractSymbol, maxSupply, contractURI) {
const signer = await this.getSigner();
if (!signer) {
console.error('Signer is required to deploy the contract.');
throw new Error('Please connect your wallet first.');
}
const { abi, bytecode } = await this.getContractData(contractCodeURI);
if (!abi || !bytecode) {
throw new Error('ABI or Bytecode is missing from the contract data.');
}
const contractFactory = new ethers.ContractFactory(abi, bytecode, signer);
try {
const ownerAddress = await this.getWalletAddress();
const contract = await contractFactory.deploy(contractName, contractSymbol, ownerAddress, maxSupply, contractURI);
await contract.waitForDeployment();
const contractAddress = contract.target;
console.log('Contract deployed at:', contractAddress);
return contractAddress;
} catch (error) {
console.error('Error deploying contract:', error);
if (error.code === 4001) {
console.log('User rejected the contract deployment.');
} else {
throw new Error('Failed to deploy contract.');
}
}
}
uploadFileToIPFS()
函数使用 Pinata 服务将元数据文件上传到 IPFS。由于 Pinata 需要身份验证和托管 API 调用,我们通过 Java 后端处理交互,这将在后面介绍。
async uploadFileToIPFS(fileContent) {
try {
const cid = await PinataIPFSService.uploadFileToIPFS(fileContent);
if (cid) {
return cid;
} else {
throw new Error("Failed to fetch CID after uploading the file.");
}
} catch (error) {
console.error("Failed to upload file:", error);
return null;
}
}
createContractURIJson()
函数生成 JSON 格式的元数据文件,其中包含葡萄酒收藏的 名称、描述、图像、横幅图像和外部链接,该文件稍后将上传到 IPFS。
async createContractURIJson(name, description, image, bannerImage, externalLink) {
try {
const metadata = {
name: name,
description: description,
image: image,
banner_image: bannerImage,
external_link: externalLink
};
const fileContent = JSON.stringify(metadata);
return fileContent;
} catch (error) {
console.error("Error creating ContractURI JSON:", error);
return null;
}
}
铸造过程包括生成 NFT 元数据、将其上传到 IPFS 以及与智能合约交互以在区块链上铸造 NFT。
processNFTsAndMint()
函数:
nftsData
列表。createTokenURIJson()
为每个 NFT 创建 JSON 元数据。ipfs://<CID>
) 并存储它以供铸造。mintNFTs()
来铸造所有已处理的 NFT。async processNFTsAndMint(nftsData, contractAddress, contractCodeURI, addressTo) {
const uris = []; // Array to hold the URIs for the minted NFTs
const tokenIds = []; // Array to hold the token IDs
for (const nft of nftsData) {
const fileContent = await this.createTokenURIJson(nft.name, nft.description, nft.image, nft.attributes);
if (!fileContent) {
console.error(`Failed to create Token URI JSON for ${nft.id}`);
throw new Error(`JSON creation failed for ${nft.id}`);
}
const cid = await this.uploadFileToIPFS(fileContent);
if (!cid) {
console.error(`Failed to upload file to IPFS for ${nft.id}`);
throw new Error(`IPFS upload failed for ${nft.id}. Aborting...`);
}
const uri = `ipfs://${cid}`;
uris.push(uri);
tokenIds.push(nft.tokenId);
}
console.log("Processing NFTs for minting...");
// Mint NFTs with the generated URIs
if (uris.length > 0) {
console.log("Minting NFTs...");
const mintedTokens = await this.mintNFTs(contractAddress, nftsData.map((nft, index) => ({
id: tokenIds[index],
uri: uris[index]
})), contractCodeURI, addressTo);
return mintedTokens;
} else {
console.error("No NFTs to mint after processing.");
return null;
}
}
mintNFTs()
函数:
safeMintBatch()
函数,一次铸造多个 NFT,将每个 token ID 链接到其对应的 IPFS URI。async mintNFTs(contractAddress, nftsData, contractCodeURI, addressTo) {
const signer = await this.getSigner();
if (!signer) {
throw new Error('Signer is required to mint NFTs.');
}
const { abi, bytecode } = await this.getContractData(contractCodeURI);
if (!abi || !bytecode) {
throw new Error('ABI or Bytecode is missing from the contract data.');
}
const contract = new ethers.Contract(contractAddress, abi, signer);
const tokenIds = nftsData.map((nft) => nft.id);
const uris = nftsData.map((nft) => nft.uri);
try {
const tx = await contract.safeMintBatch(addressTo, tokenIds, uris);
await tx.wait();
const mintedNFTs = nftsData.map((nft, index) => ({
tokenId: tokenIds[index],
tokenUri: uris[index]
}));
return mintedNFTs;
} catch (error) {
console.error('Error minting NFTs:', error);
if (error.code === 4001) {
console.log('User rejected the NFT minting.');
} else {
throw new Error('Failed to mint NFTs.');
}
}
}
createTokenURIJson()
函数生成 JSON 格式的元数据文件,其中包含单个葡萄酒瓶 NFT 的 名称、描述、图像和属性,该文件稍后将上传到 IPFS 以进行去中心化存储。
async createTokenURIJson(name, description, image, attributes) {
try {
const metadata = {
name: name,
description: description,
image: image,
attributes: attributes // Expecting attributes to be an array
};
const fileContent = JSON.stringify(metadata);
return fileContent;
} catch (error) {
console.error("Error creating Token URI JSON file:", error);
return null;
}
}
updateTokenURI()
函数通过生成新的 IPFS URI 并将其记录在区块链上来更新现有 NFT 的元数据。
async updateTokenURI(tokenId, fileContent, contractAddress, contractCodeURI, chain) {
const cid = await this.uploadFileToIPFS(fileContent);
if (!cid) {
throw new Error(`IPFS upload failed for token ${tokenId}`);
}
const newURI = `ipfs://${cid}`;
const { abi, bytecode } = await this.getContractData(contractCodeURI);
if (!abi || !bytecode) {
throw new Error('ABI or Bytecode is missing from the contract data.');
}
try {
const contract = new ethers.Contract(contractAddress, abi, await this.getSigner());
const updateTx = await contract.updateTokenURI(tokenId, newURI);
await updateTx.wait();
await OpenSeaService.refreshMetadata(contractAddress, tokenId, chain);
return newURI;
} catch (error) {
throw new Error('Failed to update token URI.');
}
}
burnToken()
函数通过调用智能合约的 burn()
方法从区块链中永久删除 NFT。
async burnToken(tokenId, contractAddress, contractCodeURI) {
const { abi } = await this.getContractData(contractCodeURI);
if (!abi) {
throw new Error('ABI is missing from the contract data.');
}
try {
const contract = new ethers.Contract(contractAddress, abi, await this.getSigner());
const burnTx = await contract.burn(tokenId);
await burnTx.wait();
console.log(`Token with ID ${tokenId} burned successfully.`);
} catch (error) {
throw new Error(`Failed to burn token ID ${tokenId}: ${error.message}`);
}
}
setReviewURI()
函数将专家的评论内容上传到 IPFS 并在区块链上记录其 不可变的 URI,从而确保透明性和可验证性。
async setReviewURI(reviewContent, contractAddress, contractCodeURI) {
const cid = await this.uploadFileToIPFS(reviewContent);
if (!cid) {
throw new Error('IPFS upload failed for review content.');
}
const reviewURI = `ipfs://${cid}`;
const { abi, bytecode } = await this.getContractData(contractCodeURI);
if (!abi || !bytecode) {
throw new Error('ABI or Bytecode is missing from the contract data.');
}
try {
const contract = new ethers.Contract(contractAddress, abi, await this.getSigner());
const setReviewTx = await contract.setReviewURI(reviewURI);
await setReviewTx.wait();
return cid;
} catch (error) {
console.error('Failed to set review URI:', error);
throw new Error('Failed to set review URI on contract.');
}
}
在这一部分中,我们探讨了 Web3 市场的 前端 ,重点介绍了我们的 javascript 代码如何 与智能合约交互。
- 原文链接: coinsbench.com/building-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!