web3.js 使用

目前主流的 ETH 測試網路為 Sepolia

v1.0 有 API更改,以下以 v1.0 為主

初始化 Web3.js

v4 版本需要用以下方式引入

const { Web3 } = require("web3");
const web3 = new Web3();

初始化合約

if (window.ethereum) {
  window.web3 = new Web3(window.ethereum); // 參數內填入 provider
  window.ethereum.enable();
}
try {
  var web3 = window.web3;
  var web3 = new Web3(web3.currentProvider);
} catch (err) {
  alert("Please install Metamask first");
  // window.location =
  //   "https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn";
}

const ContractERC721 = new web3.eth.Contract(ERC721.ABI, ERC721.address);
window.ContractERC721 = ContractERC721;

執行不需要交易的 function

const totalSupply = await ContractERC721.methods.totalSupply().call();
console.log(totalSupply.toNumber())

Big Number

web3Utils.BN

建議改用 npm: bignumber.js,多了很多錯誤處理。

監聽帳戶轉帳交易

這邊使用 Alchemy API

免費幣水龍頭:https://sepoliafaucet.com/

新增 Alchemy APP: https://dashboard.alchemy.com/apps

Pending Transaction

https://docs.alchemy.com/reference/alchemy-pendingtransactions

const { Network, Alchemy } = require("alchemy-sdk");

// Optional Config object, but defaults to demo api-key and eth-mainnet.
const settings = {
  apiKey: "", // Replace with your Alchemy API Key.
  network: Network.ETH_SEPOLIA, // Replace with your network.
};

const alchemy = new Alchemy(settings);

/// Subscription for Alchemy's pendingTransactions API
alchemy.ws.on(
  {
    method: "alchemy_pendingTransactions",
    toAddress: "0x220356c76414A3a12843560352ADb69443956196", // Replace with the address you're monitoring.
    fromAddress: "sender_address", // Optionally, replace with the sender's address.
  },
  (tx) => console.log(tx)
);

Mined Transaction (confirmed transaction)

const { Network, Alchemy } = require("alchemy-sdk");

// Optional Config object, but defaults to demo api-key and eth-mainnet.
const settings = {
  apiKey: "", // Replace with your Alchemy API Key.
  network: Network.ETH_SEPOLIA, // Replace with your network.
};

const alchemy = new Alchemy(settings);

/// Subscription for Alchemy's minedTransactions API
alchemy.ws.on(
  {
    method: "alchemy_minedTransactions",
    addresses: [{ to: "0x220356c76414A3a12843560352ADb69443956196" }],
  },
  (tx) => console.log(tx)
);

獲取帳戶歷史交易

Alchemy SDK 的 rate limit 用 CUP 計算,免費帳戶的 CUP 為 330 / sec,而 getAssetTransfers 耗費 150 CUP,所以每秒只能發送兩個請求

https://docs.alchemy.com/reference/pricing-plans

const fetchTx = async (toAddress) => {
  //The response fetches the transactions the specified addresses.
  try {
    let response = await alchemy.core.getAssetTransfers({
      fromBlock: "0x47c66d",
      toAddress,
      excludeZeroValue: true,
      category: ["external"],
    });

    return response;
  } catch (error) {
    console.log(error);
  }
};

監聽合約 Event

記得 infura 的測試網路 url 要填對,例如 Rinkeby 不要寫成 ropsten

const Web3 = require("web3");

const provider = new Web3.providers.WebsocketProvider(
  "wss://rinkeby.infura.io/ws/v3/<API KEY>"
  );
const CONTRACT_ADDRESS = "0x00129c695b5c3d95b1e9511c4d454dc6ab276936";
const ABI = require("./abi/t.js");
provider.on("connect", () => {
  console.log("Websocket connected.");
});

provider.on("close", (event) => {
  console.log(event);
  console.log("Websocket closed.");
});

provider.on("error", (error) => {
  console.error(error);
});

const web3 = new Web3(provider);
const testContract = new web3.eth.Contract(ABI, CONTRACT_ADDRESS);
testContract.events
  .TDeposit()
  .on("connected", (id) => {
    console.log(`TDeposit subscription connected (${id})`);
  })
  .on("data", (event) => {
    console.log('ondata', event);
  })
  .on("error", (error) => {
    console.log(error);
  });

後端使用ECDSA 簽名與驗證

使用 createIdentity 產生私鑰與公鑰,並且公鑰產生地址,之後用私鑰簽名(sign)然後用 recovery 還原出簽名的地址。

const EthCrypto = require("eth-crypto");

const identity = EthCrypto.createIdentity();

console.log(identity);

const message = "foobar";
const messageHash = EthCrypto.hash.keccak256(message);
const signature = EthCrypto.sign(
  identity.privateKey, // privateKey
  messageHash // hash of message
);

const signer = EthCrypto.recover(
  signature,
  EthCrypto.hash.keccak256("foobar") // signed message hash
);

const address = EthCrypto.publicKey.toAddress(identity.publicKey);

console.log("address", address);

console.log("signer", signer);

前端使用簽名與驗證

只有前端能用 eth.personal

  var signature = await web3.eth.personal.sign("Hello", accounts[0])
  console.log(signature)

  const signer = await web3.eth.personal.ecRecover("Hello", signature)
  console.log(signer) // account[0]

使用後端驗證前端的簽名,使用 eth-sig-util 模組

const ethSigUtil = require("eth-sig-util");
function checkSignature(nonce, signature) {
    const msgParams = {
        data: nonce,
        sig: signature
    };
    return ethSigUtil.recoverPersonalSignature(msgParams);
}
console.log(checkSignature(
  "Hello", //貼上 metamask 簽名的內容,不用 hash
  <貼上 metamask sign 出的簽名>,
))

或是使用 eth-crypto 模組

const EthCrypto = require("eth-crypto");
const Web3 = require("web3");
const provider = new Web3.providers.WebsocketProvider(
  "wss://rinkeby.infura.io/ws/v3/..."
);
const web3 = new Web3(provider);

const msg = "Hello";
 const messageHash1 =  web3.utils.soliditySha3(
    { type: "string", value: `\x19Ethereum Signed Message:\n${msg.length}`}, // 記得 :\n 後面數字要改成你的訊息長度
    { type: "string", value: msg }
  );
const signer1 = EthCrypto.recover(
  "<貼上 metamask sign 出的簽名>",
  messageHash1
);
console.log('signer1', signer1)

有時看到前端簽名的內容是比較格式化的,可以參考:sign-typed-data-v4

https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4

重新整理與初始化頁面時的 web3 地址顯示與簽名

  useEffect(() => {
    if (localStorage.getItem(accountLocalStorageKey)) {
      initWeb3();
    }
  }, []);

  const initWeb3 = async () => {
    if (window.ethereum) {
      window.web3 = new Web3(window.ethereum);
      window.ethereum.enable();
    }
    try {
      var web3 = window.web3;
      var web3 = new Web3(web3.currentProvider);
    } catch (err) {
      alert("Please install Metamask first");
    }

    const checkActive = async () => {
      const accounts = await web3.eth.getAccounts();
      if (accounts[0] && !localStorage.getItem(accountLocalStorageKey)) {
        // Init
        localStorage.setItem(accountLocalStorageKey, accounts[0]);
        setCurrentAccount(accounts[0]);
        var signature = await web3.eth.personal.sign(
          signMsg,
          accounts[0]
        );
        localStorage.setItem(accountSigLocalStorageKey, signature);
        console.log(signature);
      }
      setCurrentAccount(accounts[0]);
      if (!accounts[0]) {
        localStorage.removeItem(accountLocalStorageKey);
        localStorage.removeItem(accountSigLocalStorageKey);
      }
    };
    checkActive();
    setInterval(checkActive, 1500);
  };

建立 raw transaction

建立交易但不廣播:

web3.js 版本

myContract.methods.myMethod(123).encodeABI();

ether.js 版本

myContract.populateTransaction.myMethod(123);

後端使用私鑰發送轉帳交易

const EthereumTx = require("ethereumjs-tx").Transaction;
const Web3 = require("web3");
const web3 = new Web3(
  new Web3.providers.HttpProvider(
    "https://ropsten.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161"
  )
);

function signTx(tra) {
  return new Promise(async (resolve, reject) => {
    const prvKey = Buffer.from(process.env.prvkey, "hex");
    const accountNonce = await web3.eth.getTransactionCount(process.env.ethereumAccount)
    var tx = new EthereumTx(
      {
        nonce: accountNonce,
        gasPrice: 100,
        gasLimit: 100000,
        value: 100000,
      },
      { chain: "ropsten" }
    );
    tx.sign(prvKey);
    var stx = tx.serialize();
    web3.eth.sendSignedTransaction("0x" + stx.toString("hex"), (err, hash) => {
      if (err) {
        console.log(err);
        reject(err);
        return;
      }
      resolve(hash);
      console.log("tx creation: " + hash);
    });
  });
}

轉帳交易的話 tx 內容改如下即可

{
      gasPrice: 12, // (gwei)
      gasLimit: 21000, 
      to: "to address",
      value: Number(web3.utils.toWei("0.0001", "ether")),
}

如果出現 Invalid sender 的話記得檢查下 web3 rpc url 跟 chainid 有沒有對應:

const web3 = new Web3(
  new Web3.providers.HttpProvider("https://bsc-dataseed1.binance.org/")
);
const common = Common.custom({ chainId: 56 });

單位換算

建議可以用 bignumber.js

不同 ERC-20 合約 token 有不同的 decimals,例如 USDC 為 6,大部分為 18 等等。

讀取合約餘額然後到網頁顯示

今天讀取到合約的 balanceOf 後會需要除以 decimals,例如 balance / 1e18

換算回原本的精度然後寫回合約

如果今天合約是要 deposit ,並且用原本 token 的精度的話,已經除以 1e18 的數字可以如下返回原本的精度,然後寫回合約,記得要用 bigNumber,不然 js 會有問題。

  const toBN18 = (value) => {
    const web3Utils = window.web3.utils;
    return new web3Utils.BN(
      web3Utils.toWei(String(value)) // toWei 剛好為 * 1e18
    );
  }

Approve max allowance

address spend other address's ERC20 token

const ethers = require("ethers");
const MAX = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')       
const result = await tokenContract.methods
    .approve(
        VaultRelayerAddress,
        MAX
    )
    .send({
      from: accounts[0],
    });

從後端私鑰發送合約交易

const Web3 = require("web3");
const BigNumber = require("bignumber.js");
const { ABI } = require("../abi/megaswapper");
const web3 = new Web3(
  new Web3.providers.HttpProvider("https://bsc-dataseed1.binance.org/")
);
const account = web3.eth.accounts.privateKeyToAccount(
  "0x" + process.env.prvkey
);
web3.eth.accounts.wallet.add(account);
web3.eth.defaultAccount = account.address;

const decimalStr = (value, decimal) => {
  return new BigNumber(value)
    .multipliedBy(10 ** decimal)
    .toFixed(0, BigNumber.ROUND_DOWN);
};
const megaSwapperAddress = ".....";
function swap(tra) {
  return new Promise(async (resolve, reject) => {
    try {
      const tokenContract = new web3.eth.Contract(ABI, megaSwapperAddress);
      const result = await tokenContract.methods
        .swap(
          tra.fromToken,
          tra.toToken,
          decimalStr(String(tra.amountIn), tra.fromTokenDecimal),
          tra.from,
          tra.to,
          tra.data
        )
        .send({
          gas: 500000,
          from: web3.eth.defaultAccount,
        });
      console.log(result);
      resolve(result);
    } catch (err) {
      console.log(err);
      reject(err);
    }
  });
}

轉換 blockNumber 為 timestamp

轉為 UTC +8

  const blockInfo = await web3.eth.getBlock(blockNumber);
  const timestamp = (blockInfo.timestamp) * 1000 + (60 * 60 * 8 * 1000)
  console.log(timestamp);

獲取合約 event 歷史

const getUSDTTransfers = async () => {
  const Web3 = require("web3");
  const erc20ABI = require("./abis/USDT")
  const provider = new Web3.providers.HttpProvider(
    "https://eth-mainnet.alchemyapi.io/v2/<api key>"
  );

  const web3 = new Web3(provider);

  const currentBlock = await web3.eth.getBlockNumber();
  const contract = new web3.eth.Contract(erc20ABI, USDT_ADDRESS);
  contract
    .getPastEvents("Transfer",{
      fromBlock: currentBlock - 1000,
      toBlock: "latest",
      filter: {to: "0x28C6c06298d514Db089934071355E5743bf21d60"},
    })
    .then(async (events) => {
      console.log(events)
    });
};

注意事項

1.使用 CRA 5 版本以上引入 web3 會出現 error

https://stackoverflow.com/questions/70472965/web3-issue-react-application-not-compiling/70512623#70512623

解決方法:

npm uninstall react-scripts
npm i react-scripts@4.0.3

2.Metamask - "Params specify an EIP-1559 transaction but the current network does not support EIP-1559"

有些網路還沒升級到 EIP1559後版本,所以 metamask 有時呼叫 function 後會出現此錯誤。

解決方法:

add type: "0x1"

const response = await contract.methods.stake().send({
  from: userAddress,
  value: web3.utils.toWei(stakeAmount, "ether").toString(),
  type: "0x1",
});

https://ethereum.stackexchange.com/a/119863

Last updated