錢包喚起時的混亂
連結錢包是進入Web3 世界的關鍵一步,Web3 用戶經常需要在一些DApp 網站上連接錢包。但是,只是這個簡單的動作,也可能對使用者造成嚴重的不便。
連接錢包
想像一下這樣一個場景:一位新入門的Web3 用戶(出於好奇,他安裝了許多個插件錢包),訪問了某個DApp 網站,並且想要使用自己的瀏覽器插件錢包來連接它,但是當他們點擊網站提供的「Connect Wallet」按鈕,並選擇某個錢包以便想使用它來連接DApp 時,可能會發現彈出的錢包並不是自己選擇的。這很可能會讓他感到慌張和窒息,以為是自己的電腦中了病毒,所以才執行了他意料之外的操作。
區塊鏈錢包是連接區塊鏈的重要入口,而為了佔據這個入口,各個錢包使用了它們能想到的各種方式。其中最讓DApp 開發者以及DApp 用戶頭痛的要數各錢包對全域變數的竄改。
在目前的瀏覽器錢包實作邏輯中,都有透過向瀏覽器注入全域變數來暴露錢包提供的功能(例如以太坊平台的錢包會將其提供的功能注入到「 window.ethereum 」上),以便DApp可以呼叫錢包提供的方法來與之互動。
只是,由於許多錢包都會將自己注入到同一個window.ethereum 變數上,就導致在後面註冊的錢包會覆蓋之前註冊的錢包,以至於只能通過這種方式只能喚起最後註冊的那個。
有時候DApp 用戶為了可以正常使用自己想要使用的錢包,只好臨時將其他錢包外掛程式停用,或是直接只安裝某一個錢包。這樣一來,反而與錢包開發者最初的想法大相逕庭了。而新錢包哪怕做的更出色,也很難吸引到已經使用其他錢包的用戶。
有朋友可能會奇怪,為什麼一定要注入同一個變數呢?假設有兩個錢包:A 和B,其實只要A 將自己注入到“ window.a ”,B 將自己注入到“ window.b ”,想要喚起哪個錢包,就調用其對應的對像中提供的方法,就不會發生上述想要呼叫A 卻反而將B 喚起的問題。這樣確實可以解決競爭問題,但是,問題在於,如此以來,假如DApp 將要支持多個錢包連接,就必須將開發者想要適配的所有錢包名稱全部預先定義在代碼中,並且在用戶選中某個錢包時,呼叫該錢包的相關方法。導致相關程式碼維護起來相當麻煩。而統一將錢包注入到同一個物件上,則可以免於這個麻煩。
解決方案
為了脫離上述的兩難困境,社區中有兩個相似的標準。
以太坊的解決方案:EIP-6963
以太坊社群在2023 年5 月提出了EIP-6963 提案。
其中的基本邏輯很簡單,就是捨棄全域變量,轉而使用約定的事件,來解決錢包註冊與發現的問題。
具體來說,插件錢包載入成功後,觸發統一的「 eip6963:announceProvider 」事件,通知DApp 有新的錢包可用。而DApp 則透過監聽此事件,來得知自己目前可用的錢包有哪些。
這樣,透過一套抽象的事件監聽邏輯,避免了直接使用全域變數所造成的問題,並且能夠自動發現目前使用者環境中可用的錢包。如此一來,兩難自解。
社區標準:Wallet Standard
EIP-6963 是以太坊生態標準,但不只以太坊,其他鏈平台也會有類似的問題。例如Solana 鏈的錢包,普遍會將自己注入「 window.solana 」變數上,同樣會造成競爭狀況。
那麼可否讓Solana 生態也能實現這個標準呢?雖然EIP-6963 只是為了解決以太坊生態的錢包發現問題,但其中所蘊含的想法其實可以套用在所有鏈平台。那麼,我們能否再進一步,提供一套通用的標準,讓所有區塊鏈平台的錢包和DApp 實現,讓所有鏈平台的開發者和使用者都能享受到EIP-6963 所提供的便利?理論上是完全沒有問題的,而且已經有開發者在這麼做了,也就是Wallet Standard。
Wallet Standard 所做的核心工作,在於提供了兩個函數:「 registerWallet 」和「 getWallets 」,前者用於錢包,後者用於DApp。
錢包呼叫“ registerWallet ”,傳入一個錢包對象,這個對象封裝了錢包提供的功能(例如Connect 方法,用於連接錢包)。函數內部會先觸發一個RegisterWalletEvent 事件,事件的參數其實是一個回呼函數,用來讓DApp 監聽到RegisterWalletEvent 事件時調用,而這個回呼函數其實會將wallet 物件傳入,於是DApp 就可以拿到錢包對象引用,也就可以與錢包互動了。
DApp 開發者沒有必要自己來寫監聽、接收錢包物件的程式碼,這部分也已經被Wallet Standard 內建到「 getWallets 」當中。但是,getWallets 只是監聽了事件,具體要怎麼處理事件,還是需要開發者考慮。例如取得到的Wallets 放到哪裡?有些錢包在DApp 加載前就已經加載,而另一些錢包可能在之後才加載,這些錢包的狀態如何維護? Wallet Standard 針對以上細節問題,同時提供了「 @wallet-standard/react 」包,開發者直接使用它提供的React Hooks 就可以獲得到想要的數據,包括錢包列表、當前連接的錢包、錢包提供的方法等。
Wallet Standard Features
除了最基本的取得Wallet 物件外,Wallet Standard 也定義了一些Features 格式。
實際上,錢包都具有一些最基本的功能,例如連接、監聽錢包事件等。 Wallet Standard 提供了「 standard:connect 」、「 standard:events 」等features,錢包供應商實現這些特性後,DApp 可以直接根據這些值來判斷錢包是否支援某些操作。
上面提到的”standard:*” 是它內建定義的特性,實際上它們的值並沒有特別強硬的要求,所以可以隨意擴展。不同的連結平台也會有其獨特的特性,例如Solana,直接約定”solana:*” 即可。 Solana 平台常見的features 包括「 solana:signTransaction 」, “ solana:signMessage ”等。
Wallet Standard 現狀
目前實現了Wallet Standard 標準的項目其實不多,值得一提的有Solana 和Sui。
在Ant Design Web3 的Solana 適配器中,也支援適合了Wallet Standard 的錢包的自動檢測,開發者基本上只需要透過一個「 autoAddRegisteredWallets 」開啟即可,不需要配置一大堆的錢包元數據,開發體驗和使用者使用體驗直線上升。
ZAN.TOP 連接錢包的邏輯在早期同樣遇到相同的問題,不過現在,得益於Ant Design Web3 提供的配置,很輕鬆就適應了EIP-6963 標準。大家在 https://zan.top/personal/account?chInfo=ch_wxdyh 綁定位址時應該已經體驗到這一點了。
各區塊鏈生態的實現
目前各個區塊鏈平台對Wallet Standard(或EIP-6963)標準的態度並不相同,這裡舉幾個例子:
Bitcoin
比特幣目前為止似乎沒有類似的標準,有一個實現了Wallet Standard 標準的項目,但並沒有引起太多關注,現在也很久沒有提交新的程式碼。
目前開發者只能手動維護狀態,或使用一些開發包來輔助工作。例如在Ant Design Web3 中的Bitcoin 適配器實作中,針對不同的錢包,會從不同的全域變數上獲取,並存到統一的狀態。這其實是等於函式庫開發者將繁瑣的狀態維護工作接手了。而且,這僅僅解決了錢包衝突問題,無法自動感知可用錢包的問題仍然存在。
Ethereum
以太坊平台已經有了EIP-6963 標準,相關庫和錢包也大多提供了支援。
Solana
如上文,官方提供了實現:https://github.com/solana-labs/wallet-standard
Sui
Sui 目前已經對Wallet Standard 提供了實現,在官方文件上可以找到使用方法:https://docs.sui.io/standards/wallet-standard
DApps 開發庫的支持
wagmi
wagmi 透過mipd (https://github.com/wevm/mipd) 庫對EIP-6963 提供了支持,具體方式可以查看wagmi 的文檔。
RainbowKit
RainbowKit(https://www.rainbowkit.com/)內部邏輯是基於wagmi,所以也已經對EIP-6963 提供了內建支援。
Ant Design Web3
Ant Design Web3(https://web3.ant.design/) 的Ethereum 和Solana 轉接器對這兩個標準都進行了非常好的支持,並且開發者開啟起來非常便攜。
對以太坊DApp 開發者而言,只需要增加eip6963 配置即可,注意其中與EIP-6963 相關的在23-25 行:
而如果你是Solana 生態的DApp 開發者,方式也是類似的。它提供了autoAddRegisteredWallets 屬性:
總結
EIP-6963 和Wallet Standard 可大幅改善用戶連接錢包的體驗,並降低新錢包供應商的進入門檻。希望以後能有更多鏈平台以及錢包、DApp 開發者可以提供或實現相關標準,這有利於Web3 朝向更好的方向發展。