chainlink small practice web3 "donate me" project contract and front-end interaction——About the Chinese detailed explanation of the course code of "Patrick web3 course Lesson 7-8"

The example of FundMe lesson is essentially a demonstration of receiving and sending eth on a contract, but this demonstration adds the interaction of front-end ethers and the use of chainlink oracle price feed.

Generally speaking, it is the epitome of a certain function of some Defi projects, but overall it is quite simple.

If you don’t know how to use chainlink price feed, you can read my article "The use of oracle machine chainlink-feed price, VRF" .

The content of this section fully refers to Patrick's code in the course, and the course link is: https://github.com/smartcontractkit/full-blockchain-solidity-course-js

1. Interpretation of library

Let's start with the contract block first. After all, the contract can exist independently from the front end, that is, an API, which can be easily tested after being written.

1.1 Simple understanding of library library

The library code is as follows:
//The price converter mainly calls the price feed of chainlink

//价格转化器 主要是调用 chainlink 的喂价
library PriceConverter {
    
    
    //传入一个 AggregatorV3Interface 的对象,AggregatorV3Interface 是chainlink 中的 interface 对象
    function getPrice(AggregatorV3Interface priceFeed)internal view returns (uint256){
    
    
        //得到交易对价
        (, int256 answer, , , ) = priceFeed.latestRoundData();
        // 整数位单位得到每个 eth 价钱
        return uint256(answer/100000000);//answer * 10000000000
    }

    // 1000000000
    // call it get fiatConversionRate, since it assumes something about decimals
    // It wouldn't work for every aggregator
    function getConversionRate(uint256 ethAmount, AggregatorV3Interface priceFeed)internal view returns (uint256){
    
    
        //传入 AggregatorV3Interface 对消得到当前的 eth 价
        uint256 ethPrice = getPrice(priceFeed);
        //价钱单价乘上数量,由于是 wei 单位,直接除个 10 的18次方使其为总的 eth 个数价钱
        uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
        // the actual ETH/USD conversation rate, after adjusting the extra 0s.
        return ethAmountInUsd;
    }
}

Although the above code has been commented, it still needs to be explained here.

The library PriceConverter is mainly used to obtain the price of the chainlink oracle machine. The obtained trading pair prices are eth and usd. There are two methods in this library:

  • getPrice
  • getConversionRate

Among them, getConversionRate calls getPrice to get the price.

1.2 getPrice method

The getPrice method receives a parameter priceFeed of type AggregatorV3Interface. AggregatorV3Interface is the oracle type we need to obtain the price of the transaction pair, and priceFeed can be understood as an object of this type (although we give the address when passing the value).

Then, you only need to use priceFeed to call latestRoundData to get the price of the transaction pair (you will not read the article I mentioned above).

Next, I made some changes to the price of the eth and usd trading pair, which was originally:

After acquisition, due to the problem of digits (mainly because this value needs to be converted into wei and compared with the value of wei), so multiply a value here and return it:return uint256(answer * 10000000000);

Then I changed it return uint256(answer/100000000);so that I just got the price in the corresponding US dollar (I forgot the unit of the price returned by chainlink, this is the result of my test).

In this way, a method of obtaining a price is completed.

1.3 getConversionRate

In the getConversionRate method, it is mainly to multiply the incoming eth quantity by the unit price, and finally divide 10 to the 18th power, so as to obtain the total dollar price of the total eth donated by the donor: , where is the obtained unit price, and priceFeed uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;isuint256 ethPrice = getPrice(priceFeed); It is the address of the transaction pair you obtained, which can be viewed in https://docs.chain.link/docs/data-feeds/price-feeds/addresses/ :
insert image description here

2. Interpretation of fundme contract

2.1 State variables

In the fundme contract, the state variable is given to uint256 as an "attached method": using PriceConverter for uint256;, the function is to make the value of uint256 directly operate the library method through the dot "." operator (if not, you can check the usage of library for).

Then I rewrote the minimum amount of donated dollars: uint256 public constant MINIMUM_USD =1;, where I directly set it to be 1 dollar to be able to donate.

Then create an immutable modified "constant" that can be declared and then initialized: address private immutable i_owner;, as well as state variables for recording non-withdrawal donors, donation records, and oracle interfaces, as follows:

//把这个库给到一个 uint256类型,方便操作
using PriceConverter for uint256;

// State variables
//最少 1 个 usd
uint256 public constant MINIMUM_USD =1;
//immutable 可以在声明或构造函数中再初始化
address private immutable i_owner;
//未提现的捐赠记录捐赠人
address[] public s_funders;
//捐赠记录
mapping(address => uint256) private s_addressToAmountFunded;
// chainlink 预言机 的 interface 接口对象
AggregatorV3Interface private s_priceFeed;

2.2 Method

2.2.1 onlyOwner

Next, a Modifiers is defined in the code, which is convenient for permission setting:

// Modifiers 前置操作
modifier onlyOwner() {
    
    
    // require(msg.sender == i_owner);
    if (msg.sender != i_owner) revert FundMe__NotOwner();
    _;
}

The function of this method is that if you are not the owner, you will not be able to withdraw cash. After all, there will be problems if you come to withdraw cash by yourself.

2.2.2 constructor constructor

The constructor receives a priceFreed. This parameter is to pass in an address to the contract that you use the chainlink oracle to feed the price when deploying the contract, and set the owner to msg.sender:

constructor(address priceFeed) {
    
    
    s_priceFeed = AggregatorV3Interface(priceFeed);//初始化预言机
    i_owner = msg.sender;//设置 owner
}

2.2.3 Fund main donation methods

The fund method is a method that receives eth, and the donation is to use this method.

In this method, payable is used to enable it to receive the eth attached to the method call, and a minimum donation amount is set.

require is used when checking the donation amount:

require(
   //由于在上面写了 PriceConverter for uint256 所以可以进行操作 getConversionRate
   //接着传入 s_priceFeed ,判断此时的价钱是否大于 最小金额的设置 MINIMUM_USD
   msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD,
   "You need to spend more ETH!"
);

The reason why you can directly use msg.value to call getConversionRate in require is that in this contract, the method of library for is directly used to give the uint256 type with the library where the getConversionRate method is located; what you get at this time is what the donor msg.value.getConversionRate(s_priceFeed)donated The total amount, then the total only needs to be greater than the minimum amount I set.

Then directly record the current donation address and amount in the donation record and non-withdrawal array:

//记录一下哪个地址捐赠了多少钱 捐赠记录
s_addressToAmountFunded[msg.sender] += msg.value;
//把当前这个人添加到 s_funders 捐赠人数组之中
s_funders.push(msg.sender);

The complete code of this method:

function fund() public payable {
    
    
   require(
       //由于在上面写了 PriceConverter for uint256 所以可以进行操作 getConversionRate
       //接着传入 s_priceFeed ,判断此时的价钱是否大于 最小金额的设置 MINIMUM_USD
       msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD,
       "You need to spend more ETH!"
   );
   // require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
   //记录一下哪个地址捐赠了多少钱
   s_addressToAmountFunded[msg.sender] += msg.value;
   //把当前这个人添加到 s_funders 捐赠人数组之中
   s_funders.push(msg.sender);
}

2.2.4 withdrawal method

The cash withdrawal method is also relatively simple. Directly set the for loop to clear the balance record of the person in the donation record, and clear the array s_funders that does not reflect the record. The code of this method is as follows:

//提现方法 onlyOwner Modff
function withdraw() public onlyOwner {
    
    
    //从捐赠人数组里面进行循环
    for (uint256 funderIndex = 0;funderIndex < s_funders.length;funderIndex++) {
    
    
        //找到当前的捐赠地址
        address funder = s_funders[funderIndex];
        //设置捐赠人的 map 余额为0
        s_addressToAmountFunded[funder] = 0;
    }
    //设置捐赠人数组的值为0
    s_funders = new address[](0);
    // Transfer vs call vs Send
    // payable(msg.sender).transfer(address(this).balance);
    //调用 call 方法转账提现当前合约的全部的捐赠
    (bool success, ) = i_owner.call{
    
    value: address(this).balance}("");
    require(success);
}

The logic of the loop is to loop through the array s_funders of unwithdrawn records, get the address from it, give the record donation to the address corresponding to s_addressToAmountFunded to 0, and finally clear s_funders, and use i_owner to call call to transfer all the balance of the current contract, where address(this ).balance represents all the balances of the current contract, and transfers to i_owner, thus completing the withdrawal operation.

2.2.5 Changes to withdrawal method

Since I personally don't particularly understand why Patrick wants to do this, I wrote a method myself:

//我自己写的提现方法 主要是捐赠记录不归零
//我自己写的提现方法 主要是捐赠记录不归零
function customWithdraw()public onlyOwner{
    
    
    s_funders = new address[](0);
    (bool success, ) = i_owner.call{
    
    value: address(this).balance}("");
    require(success);
}

Correspondingly delete the contents of the record array that are not reflected, and then directly withdraw all balances, which also reduces the change of state variables, and does not need for loops, and saves gas.

The last remaining methods are relatively simple, so I won’t repeat them here. The complete contract code is as follows (I added some myself, and put them together directly in the library for convenience):

// SPDX-License-Identifier: MIT
// 1. Pragma
pragma solidity ^0.8.7;
// 2. Imports
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

//价格转化器 主要是调用 chainlink 的喂价
library PriceConverter {
    
    
    //传入一个 AggregatorV3Interface 的对象,AggregatorV3Interface 是chainlink 中的 interface 对象
    function getPrice(AggregatorV3Interface priceFeed)internal view returns (uint256){
    
    
        //得到交易对价
        (, int256 answer, , , ) = priceFeed.latestRoundData();
        // 整数位单位得到每个 eth 价钱
        return uint256(answer/100000000);//answer * 10000000000
    }

    // 1000000000
    // call it get fiatConversionRate, since it assumes something about decimals
    // It wouldn't work for every aggregator
    function getConversionRate(uint256 ethAmount, AggregatorV3Interface priceFeed)internal view returns (uint256){
    
    
        //传入 AggregatorV3Interface 对消得到当前的 eth 价
        uint256 ethPrice = getPrice(priceFeed);
        //价钱单价乘上数量,由于是 wei 单位,直接除个 10 的18次方使其为总的 eth 个数价钱
        uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
        // the actual ETH/USD conversation rate, after adjusting the extra 0s.
        return ethAmountInUsd;
    }
}


    // 3. Interfaces, Libraries, Contracts
    //自定义错误
    error FundMe__NotOwner();

/**@title A sample Funding Contract
 * @author Patrick Collins
 * @notice This contract is for creating a sample funding contract
 * @dev This implements price feeds as our library
 */
contract FundMe {
    
    
    // Type Declarations 
    //把这个库给到一个 uint256类型,方便操作
    using PriceConverter for uint256;

    // State variables
    //最少 1 个 usd
    uint256 public constant MINIMUM_USD =1;
    //immutable 可以在声明或构造函数中再初始化
    address private immutable i_owner;
    //未提现的捐赠记录捐赠人
    address[] public s_funders;
    //捐赠记录
    mapping(address => uint256) private s_addressToAmountFunded;
    // chainlink 预言机 的 interface 接口对象
    AggregatorV3Interface private s_priceFeed;

    // Events (we have none!)

    // Modifiers 前置操作
    modifier onlyOwner() {
    
    
        // require(msg.sender == i_owner);
        if (msg.sender != i_owner) revert FundMe__NotOwner();
        _;
    }

    // Functions Order:
     constructor
     receive
     fallback
     external
     public
     internal
     private
     view / pure

    constructor(address priceFeed) {
    
    
        s_priceFeed = AggregatorV3Interface(priceFeed);//初始化预言机
        i_owner = msg.sender;//设置 owner
    }

    /// @notice Funds our contract based on the ETH/USD price
    //根据以太币/美元价格为我们的合同提供资金
    function fund() public payable {
    
    
        require(
            //由于在上面写了 PriceConverter for uint256 所以可以进行操作 getConversionRate
            //接着传入 s_priceFeed ,判断此时的价钱是否大于 最小金额的设置 MINIMUM_USD
            msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD,
            "You need to spend more ETH!"
        );
        // require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
        //记录一下哪个地址捐赠了多少钱
        s_addressToAmountFunded[msg.sender] += msg.value;
        //把当前这个人添加到 s_funders 捐赠人数组之中
        s_funders.push(msg.sender);
    }

    //提现方法 onlyOwner Modff
    function withdraw() public onlyOwner {
    
    
        //从捐赠人数组里面进行循环
        for (uint256 funderIndex = 0;funderIndex < s_funders.length;funderIndex++) {
    
    
            //找到当前的捐赠地址
            address funder = s_funders[funderIndex];
            //设置捐赠人的 map 余额为0
            s_addressToAmountFunded[funder] = 0;
        }
        //设置捐赠人数组的值为0
        s_funders = new address[](0);
        // Transfer vs call vs Send
        // payable(msg.sender).transfer(address(this).balance);
        //调用 call 方法转账提现当前合约的全部的捐赠
        (bool success, ) = i_owner.call{
    
    value: address(this).balance}("");
        require(success);
    }

    //这个便宜的提现方法,没有用到过多的状态遍历
    function cheaperWithdraw() public onlyOwner {
    
    
        address[] memory funders = s_funders;//这里是 memory
        // mappings can't be in memory, sorry!
        for (uint256 funderIndex = 0;funderIndex < funders.length;funderIndex++) {
    
    
            address funder = funders[funderIndex];
            s_addressToAmountFunded[funder] = 0;
        }
        s_funders = new address[](0);
        // payable(msg.sender).transfer(address(this).balance);
        (bool success, ) = i_owner.call{
    
    value: address(this).balance}("");
        require(success);
    }

    //我自己写的提现方法 主要是捐赠记录不归零
    function customWithdraw()public onlyOwner{
    
    
        s_funders = new address[](0);
        (bool success, ) = i_owner.call{
    
    value: address(this).balance}("");
        require(success);
    }

    /** @notice Gets the amount that an address has funded
     *  @param fundingAddress the address of the funder
     *  @return the amount funded
     */
     //获取捐助人的钱
    function getAddressToAmountFunded(address fundingAddress)public view returns (uint256){
    
    
        return s_addressToAmountFunded[fundingAddress];
    }
    //chainlink 预言机的版本
    function getVersion() public view returns (uint256) {
    
    
        return s_priceFeed.version();
    }

    //查看
    function getFunder(uint256 index) public view returns (address) {
    
    
        return s_funders[index];
    }
    //owner
    function getOwner() public view returns (address) {
    
    
        return i_owner;
    }
    //返回 AggregatorV3Interface 预言机对象
    function getPriceFeed() public view returns (AggregatorV3Interface) {
    
    
        return s_priceFeed;
    }
    //返回当前 eth/usd价钱
    function getETHUSEDPrice() public view returns(int){
    
    
        (
            /*uint80 roundID*/,
            int price,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        )=s_priceFeed.latestRoundData();
        return price/100000000;
    }
}

3. Front-end interpretation

Here you need to operate on metamask. I wrote another article and put it outside. The content is too much to look good. The address is: https://i1bit.blog.csdn.net/article/details/127349452

3.1 General operation

The front-end code mainly explains the js code, and the html content is very basic, so I won’t repeat it here.

In the js code, first introduce the corresponding ethers and a contract address:

import {
    
     ethers } from "ethers";
//合约地址
export const contractAddress = "0xe65d94905f5BFaa0Ec382e8652d4E39E41E83205";

Then there is abi, abi is too long to post here, but here is another way to write abi using function signatures (just put the function declaration and it’s ok, if the constructor has no parameters, you don’t need to add it, use Add any abi for any interface):

//函数签名的方式 编写abi
export const abi = [
    "constructor(address priceFeed)",
    "function fund() public payable",
    "function withdraw() public onlyOwner",
    "function customWithdraw()public onlyOwner",
];

Then get all the elements in the html code:

//获取元素对象
const connectButton = document.getElementById("connectButton")
const withdrawButton = document.getElementById("withdrawButton")
const fundButton = document.getElementById("fundButton")
const balanceButton = document.getElementById("balanceButton")

Then bind the event for this:

//绑定事件
connectButton.onclick = connect
withdrawButton.onclick = withdraw
fundButton.onclick = fund
balanceButton.onclick = getBalance

3.2 connect link wallet

Next is the connect method. The connect method if (typeof window.ethereum !== "undefined")is to determine whether the metamask plug-in is installed in the current browser. For the operation of metamask, please refer to the metamask document: https://docs.metamask.io/guide/#why-metamask

Then there is a try catch exception detection:

try {
    
    
   //连接账户
   await ethereum.request({
    
     method: "eth_requestAccounts" });
} catch (error) {
    
    
   console.log(error);
}

The code await ethereum.request({ method: "eth_requestAccounts" });is to link metamask through ethereum.request. Under normal circumstances, if the webpage does not execute the link metamask, it will display the unconnected state:
insert image description here
then change the prompt, and then request eth_accounts to obtain the address of the currently selected account:

//更改提示
connectButton.innerHTML = "Connected";
//得到当前用户的 address
const accounts = await ethereum.request({
    
     method: "eth_accounts" });
console.log(accounts);

3.3 Withdraw withdrawal

For the withdraw method, we mainly look at the code after judging the metamask, because it is roughly the same as connect.

Check out first:

const provider = new ethers.providers.Web3Provider(window.ethereum);

At this time, ethers.providers.Web3Provider indicates a direct link to the existing web3 network. At this time, window.ethereum is specified, which you can understand as the network provided by metamask.

Then send a request for a link by await provider.send('eth_requestAccounts', []);sending a request, that is, the codeawait ethereum.request({ method: "eth_requestAccounts" });

At this point, you may think, why use the provider directly here? We can directly look at the source code and right click to realize:
insert image description here

At this time, I found the send method:
insert image description here
at this time, I found that this method belongs to the Web3Provider class, and looked at the constructor:
insert image description here
before we used Web3Provider to specify a web3 network, which was the network provided by metamask, and the previous window.ethereum also indicated the metamask Network, we need to understand this.

Then check that the jsonRpcFetchFunc method is called after using send:
insert image description here
At this point, you can see that this jsonRpcFetchFunc is window.ethereum:
insert image description here
then of course you can write like this:await provider.send('eth_requestAccounts', []);

Then call provider.getSigner();to get the currently selected account and use it as the account to operate the contract:

//当前登录的账户(只有一个被选择)
const signer = provider.getSigner();
//当做钱包用
const contract = new ethers.Contract(contractAddress, abi, signer);

Finally, we can directly use the "object" of this contract to operate our own method to withdraw money (at this time, I use customWithdraw written by myself):

try {
    
    
    //调用合约方法提现
    const transactionResponse = await contract.customWithdraw();
    await listenForTransactionMine(transactionResponse, provider);
    // await transactionResponse.wait(1)
} catch (error) {
    
    
    console.log(error);
}

Then the rest of the other codes are very similar, and they all get the account and then call the contract. There is no difference in essence, so I won’t repeat them here.

3.4 Determine whether the confirmation is complete listenForTransactionMine

Next, look at the code inside the Promise of listenForTransactionMine.

provider.once mainly waits once, and monitors whether the transaction is completed. The prototype is as follows (I searched for the once interface for a long time, but I didn’t find it, and I didn’t see it in the document, so I was dazzled):

once(eventName: EventType, listener: Listener)

code show as below:

provider.once(transactionResponse.hash, (transactionReceipt) => {
    
    
    console.log(
        `Completed with ${
      
      transactionReceipt.confirmations} confirmations. `
    )
    resolve();
});

At this time, wait for the transaction to be completed, and if the transactionResponse.hash is obtained, it will end once.

The once documentation states: https://docs.ethers.io/v5/api/providers/provider/

The annotated description code is as follows (I changed the abi, it is recommended to use the original version):

import {
    
     ethers } from "ethers";

//合约地址
export const contractAddress = "0xe65d94905f5BFaa0Ec382e8652d4E39E41E83205";
//函数签名的方式 编写abi
export const abi = [
    "constructor(address priceFeed)",
    "function fund() public payable",
    "function withdraw() public onlyOwner",
    "function customWithdraw()public onlyOwner",
];

//获取元素对象
const connectButton = document.getElementById("connectButton")
const withdrawButton = document.getElementById("withdrawButton")
const fundButton = document.getElementById("fundButton")
const balanceButton = document.getElementById("balanceButton")
//绑定事件
connectButton.onclick = connect
withdrawButton.onclick = withdraw
fundButton.onclick = fund
balanceButton.onclick = getBalance
//链接 metamask
async function connect() {
    
    
    //判断是否安装 metamask
    if (typeof window.ethereum !== "undefined") {
    
    
        try {
    
    
            //连接账户
            await ethereum.request({
    
     method: "eth_requestAccounts" });
        } catch (error) {
    
    
            console.log(error);
        }
        //更改提示
        connectButton.innerHTML = "Connected";
        //得到当前用户的 address
        const accounts = await ethereum.request({
    
     method: "eth_accounts" });
        console.log(accounts);
    } else {
    
    
        connectButton.innerHTML = "Please install MetaMask";
    }
}

//调用合约的提现方法
async function withdraw() {
    
    
    console.log(`Withdrawing...`);
    if (typeof window.ethereum !== "undefined") {
    
    
        //ethers.providers.Web3Provider 连接到现有的 web3 网络提供者
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        //向web3 网络发送 eth_requestAccounts api 请求
        await provider.send('eth_requestAccounts', []);
        //当前登录的账户(只有一个被选择)
        const signer = provider.getSigner();
        //当做钱包用
        const contract = new ethers.Contract(contractAddress, abi, signer);

        try {
    
    
            //调用合约方法提现
            const transactionResponse = await contract.customWithdraw();
            await listenForTransactionMine(transactionResponse, provider);
            // await transactionResponse.wait(1)
        } catch (error) {
    
    
            console.log(error);
        }
    } else {
    
    
        withdrawButton.innerHTML = "Please install MetaMask";
    }
}

//捐赠
async function fund() {
    
    
    const ethAmount = document.getElementById("ethAmount").value;

    console.log(`Funding with ${
      
      ethAmount}...`);
    if (typeof window.ethereum !== "undefined") {
    
    

        const provider = new ethers.providers.Web3Provider(window.ethereum);
        const signer = provider.getSigner();

        const contract = new ethers.Contract(contractAddress, abi, signer);

        try {
    
    
            //传入捐赠的 eth
            const transactionResponse = await contract.fund({
    
    
                value: ethers.utils.parseEther(ethAmount),
            })
            await listenForTransactionMine(transactionResponse, provider);
        } catch (error) {
    
    
            console.log(error);
        }
    } else {
    
    
        fundButton.innerHTML = "Please install MetaMask";
    }
}

async function getBalance() {
    
    
    if (typeof window.ethereum !== "undefined") {
    
    
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        try {
    
    
            const balance = await provider.getBalance(contractAddress);
            console.log(ethers.utils.formatEther(balance));
        } catch (error) {
    
    
            console.log(error);
        }
    } else {
    
    
        balanceButton.innerHTML = "Please install MetaMask";
    }
}

function listenForTransactionMine(transactionResponse, provider) {
    
    
    console.log(`Mining ${
      
      transactionResponse.hash}`);
    return new Promise((resolve, reject) => {
    
    
        try {
    
    
            provider.once(transactionResponse.hash, (transactionReceipt) => {
    
    
                console.log(
                    `Completed with ${
      
      transactionReceipt.confirmations} confirmations. `
                )
                resolve();
            });
        } catch (error) {
    
    
            reject(error);
        }
    })
}

Guess you like

Origin blog.csdn.net/A757291228/article/details/127356262