之前自己做 Ethereum 的專案時,常常是自己拿 Web3.js 或 Ethers.js 來做,包含前一次的幣安黑客松作品也是這樣來的。最近看到 Solidity 讀書會 上面有人分享 EthWorks 的 Bounty Program 而發現新玩具,也稍微把玩了一下。這篇就來記錄一下,順便看能不能做出什麼去投 Bounty。

TL;DR

在快速把玩下,可以簡單說這個工具有點過新,所以文件上有許多是直接沒寫內容的(說不定有些隱藏功能甚至沒有寫進文件)。但這個工具提供了許多 Hook,也幫忙處理許多如切換帳戶的瑣事。過去利用 Web3/Ethers 時需要自己去註冊監聽,透過 useDApp 就可以更好的運用 Hook 來完成這些事情。

此外,useDApp 會將多個 Function Call 透過 Multicall 的方式包裝,進而降低 RPC 的呼叫。一面對於使用者端降低網路開銷,一面也減輕 RPC 節點的消耗,特別是如 Infura 這種有限額的服務。

你可以在 https://github.com/flyinglimao/usedapp-demo 下載下面的程式碼,或到 https://flyinglimao.github.io/usedapp-demo 看看效果。

開始使用

你可能會需要一個 React 專案來開始:

1
npx create-react-app usedapp-demo --template typescript

後面的 --template typescript 是我習慣,同時也因為文件可能不夠完整,透過 TypeScript 來幫忙,不喜歡或不會 TypeScript 的可以自行移除。

備註:下面的圖片有用 Styled-Component 包過,看起來和實際程式碼會不同

接著安裝 useDApp:

1
yarn add @usedapp/core

同樣,喜歡 npm 的朋友可以改用 npm install @usedapp/core

安裝完後,我們需要一個 DAppProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// App.tsx
import { Config, ChainId, DAppProvider } from '@usedapp/core';

const config: Config = {
readOnlyChainId: ChainId.Kovan,
readOnlyUrls: {
[ChainId.Kovan]: '', // 如果沒有的話可以到 Infura.io 上辦一個
},
// multicallAddresses: Mulitcall 用,如果不是原生支援的鏈可能需要設定
// supportedChains: 如果使用者切錯鏈會提示,透過下面的 useEther 的 error 回報
// pollingInterval: 輪詢間隔,如果 RPC Node 要付錢可以考慮降低,但資料更新就會比較慢的樣子
}

function App() {
return (
<DAppProvider>
// ...
</DappProvider>
);
}

連接錢包

連接錢包的函數通過 useEthers() 提供,可以通過檢查 account 來判斷是否連接:

1
2
3
4
5
6
7
8
9
10
11
12
function Connect() {
const { activateBrowserWallet, account, error } = useEthers();
useEffect(() => {
if (error) alert(error);
}, [error]);

return account ? (
<h1>{account}</h1>
) : (
<button onClick={() => activateBrowserWallet()}>連接錢包</button>
);
}

當使用者端沒有錢包卻點連接時,error 會設定,要注意的是 error 只會出現一瞬間,所以這邊用 useEffect 來抓,但感覺應該會有變動,參考本文時請留意這點。

可以試著在錢包中切換帳戶,會注意到他的地址跟著變了。 連接到錢包的 Demo

取得餘額

取得 ETH 餘額應該也是經常用到的功能,可以利用 useEtherBalance。如果要取得 ERC-20 的代幣可以用 useTokenBalance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEtherBalance, useEthers, useTokenBalance } from "@usedapp/core";
import { formatEther, formatUnits } from "@ethersproject/units";

export default function Balance() {
const { account } = useEthers();
const ethBalance = useEtherBalance(account);
const usdcBalance = useTokenBalance(
"0xe22da380ee6b445bb8273c81944adeb6e8450422",
account
); // Faucet: https://testnet-v1.aave.com/faucet/USDC-0xe22da380ee6b445bb8273c81944adeb6e84504220x506b0b2cf20faa8f38a4e2b524ee43e1f4458cc5

return account ? (
<div>
<h1>Ethers: {ethBalance ? formatEther(ethBalance) : 0}</h1>
<h1>USDC: {usdcBalance ? formatUnits(usdcBalance, 6) : 0}</h1>
</div>
) : (
<h1>Not Connect Yet</h1>
);
}

這邊用了 Ethers.js 的函數來顯示,需要的話可以用 yarn add ethers 安裝。另外可以試著在錢包裡面轉帳,過一段時間後就會看到數字變了。 顯示餘額(ETH 與 USDC)

呼叫合約

作為一個 DApp,沒有合約可說是太奇怪了。如果要取得合約的值(不必發起交易的那些),可以用 useContractCall,這裡會需要用 Ethers.js 的 Interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { Interface } from "@ethersproject/abi";
import { useContractCall, useContractCalls } from "@usedapp/core";
import { ChangeEvent, useState } from "react";

const abiJSON = [
/* ERC-20 ABI */
];
const abi: Interface = new Interface(abiJSON);

export default function Contract() {
const { account } = useEthers();
const [address, setAddress] = useState(
"0xe22da380ee6b445bb8273c81944adeb6e8450422"
);
const tokenName = useContractCall({
abi,
address,
method: "name",
args: [],
});
// 可以用 useContractCalls 來打包
const [tokenDecimals, tokenBalance] = useContractCalls([
{
abi,
address,
method: "decimals",
args: [],
},
{
abi,
address,
method: "balanceOf",
args: [account],
},
]);
return (
<div>
<h1>
代幣地址:
<Input
type="text"
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setAddress(event.currentTarget.value)
}
value={address}
/>
</h1>
<h1>代幣名稱:{tokenName}</h1>
{tokenBalance && (
<h1>代幣餘額:{formatUnits(tokenBalance[0], tokenDecimals)}</h1>
)}
</div>
);
}

useContractCall 回傳的實際上是一個陣列,上面的有點小髒這點請注意。 合約靜態函數呼叫範例

發起交易的呼叫用的則是 useContractFunction,第一個參數要放的是從 Ethers.js 產生的 Contract 物件,Singer 部分不需要設置,useDApp 會自動完成,也可以在第三個參數中設定,第二個參數則是要呼叫的函數名稱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { Contract } from "@ethersproject/contracts";
import {
ChainId,
getExplorerTransactionLink,
useContractFunction,
useEthers,
} from "@usedapp/core";

const abiJSON = [
{
constant: false,
inputs: [
{
name: "_to",
type: "address",
},
{
name: "_value",
type: "uint256",
},
],
name: "transfer",
outputs: [
{
name: "",
type: "bool",
},
],
payable: false,
stateMutability: "nonpayable",
type: "function",
},
];
const usdcContract = new Contract(
"0xe22da380ee6b445bb8273c81944adeb6e8450422",
abiJSON
);

export default function Transaction() {
const { send, state } = useContractFunction(usdcContract, "transfer");
function shortenTransactionHash(hash: string) {
return hash.substr(0, 6) + "..." + hash.substr(-4);
}
return (
<div>
{state.status === "None" && <></>}
{state.status === "Exception" && <h1>交易失敗,參數不正確</h1>}
{state.status === "Mining" && (
<h1>
交易進行中
<br />
<a
href={getExplorerTransactionLink(
state.transaction.hash,
ChainId.Kovan
)}
>
{shortenTransactionHash(state.transaction.hash)}
</a>
</h1>
)}
{state.status === "Success" && (
<h1>
交易成功
<br />
<a
href={getExplorerTransactionLink(
state.transaction.hash,
ChainId.Kovan
)}
>
{shortenTransactionHash(state.transaction.hash)}
</a>
位於 {state.receipt.blockNumber}
</h1>
)}
{state.status === "Fail" && (
<h1>
交易失敗
<br />
<a
href={getExplorerTransactionLink(
state.transaction.hash,
ChainId.Kovan
)}
>
{shortenTransactionHash(state.transaction.hash)}
</a>
位於 {state.receipt.blockNumber}
</h1>
)}
<button
onClick={() =>
send("0x000000000000000000000000000000000000dead", "1000000")
}
>
燒毀 1 USDC
</button>
</div>
);
}

state 下必定會有 status,用來表示交易狀態,一開始會是 None。交易送出後會開始有 transaction,具體物件結構要參考 Ethers.js 的文件(TransactionTransactionRespones)。需要留意的是 transaction 不會在交易完成後更新,因此如區塊高度等出塊後才有的資料要到 receipt 取得,參考文件是 TransactionReceipt發起交易 Demo 上面偷渡了 getExplorerTransactionLink 的用法,相似的還有 getExplorerAddressLink

小結

目前用下來覺得還滿實用的,他可以處理掉很多滿麻煩的事情,例如前面提到的切換帳戶的問題,同時也避免掉需要自行包裝成 Hook 的麻煩。美中不足的地方應該是對 ABI 的支援,目前看下來還是得回到 Ethers.js 上,如果能在 useDApp 中提供或許會更好。其次是合約相關的 Hook 沒辦法根據 ABI 去制定型別,因此無法發揮 TypeScript 的能力去避免錯誤的 Code。最後不確定是不是我的問題,BigNumber 直接用會提示 Overflow。 由於文件沒寫完,所以有些 Provider 和 Hook 我就沒去看了,有興趣的可以翻翻文件