构建基于区块链的葡萄酒交易市场:技术之旅第二部分:前端

本文是区块链葡萄酒交易市场系列文章的第二部分,重点介绍了前端集成,包括如何使用 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 添加它。
  • 连接后,钱包地址和 provider 将被存储以供进一步交互。
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)

  • 使用相应的 RPC URL 和浏览器链接定义 Arbitrum Sepolia (testnet)Arbitrum One (mainnet)
  • 此配置允许前端在测试和生产环境之间动态切换。
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

铸造过程包括生成 NFT 元数据、将其上传到 IPFS 以及与智能合约交互以在区块链上铸造 NFT。

processNFTsAndMint() 函数:

  • 迭代提供的 nftsData 列表。
  • 使用 createTokenURIJson() 为每个 NFT 创建 JSON 元数据
  • 将元数据文件上传到 IPFS,检索其 CID
  • 构建 IPFS URI (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() 函数:

  • 检索 签名者 以执行交易。
  • 加载 合约 ABI 和 bytecode
  • 调用 智能合约的 safeMintBatch() 函数,一次铸造多个 NFT,将每个 token ID 链接到其对应的 IPFS URI。
  • 等待交易完成并返回铸造的 NFT 详细信息。
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;
        }
    }

更新 Token 元数据

updateTokenURI() 函数通过生成新的 IPFS URI 并将其记录在区块链上来更新现有 NFT 的元数据。

  1. 将新元数据上传到 IPFS — 存储更新的 fileContent 并检索其 CID
  2. 在区块链上更新 Token 元数据 — 获取合约的 ABI 和 bytecode 并在合约上调用 updateTokenURI(),将 token ID 链接到新的元数据 URI。
  3. 触发 OpenSea 元数据刷新 — 调用 refreshMetadata() 以确保 OpenSea 反映更改。OpenSea Service 是在后端实现的。
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.');
        }
    }

销毁 NFT

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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
CoinsBench
CoinsBench
https://coinsbench.com/