MetaMask Snaps 是讓 MetaMask 成為全球最具擴展性錢包的發展藍圖。身為開發者,您可以透過全新方式將您的功能與 API 帶入 MetaMask。Web3 開發者是這項成長的核心,本系列旨在展示當今正在建構的創新 MetaMask Snaps。
FoxTagger Snap
Snap 儲存庫:https://github.com/shree675/FoxTagger
FoxTagger 是一款工具,能夠將地址與使用者自訂標籤進行對應,透過警示與顯示使用分析,協助使用者掌控自身的支出狀況。
請帶我們了解技術實作的細節?
技術架構
我們的 MetaMask 擴充功能 FoxTagger 由兩個主要元件組成:MetaMask Snaps 後端(即 snap 本身)以及 Gatsby.js 前端。前端是一個簡單的網頁應用程式,負責承載使用者介面,同時也作為我們 Snaps 應用程式的配套 DApp。
大部分功能在 snap 中實現,其餘部分則在前端處理,讓使用者能夠向他人請款,以及/或透過我們的介面設定標籤。其中,請款功能使用 XMTP 協議,而標籤功能則使用 MetaMask Snaps API。
我們以 snap monorepo 作為起始範本,完整的專案實作可在此處找到。
Snaps 應用程式
在整個實作過程中,我們使用了 Snaps API 提供的以下主要功能:
持久化儲存
通知
排程任務(Cron jobs)
交易洞察
我們的實作首先在 packages\snap\src\index.ts 中向前端公開部分函式。
export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
switch (request.method) {
case 'getPersistentStorage':
return await getPersistentStorage();
case 'setPersistentStorage':
await setPersistentStorage(
request.params as void | Record<string, unknown>,
);
return null;
case 'clearPersistentStorage':
await clearPersistentStorage();
return null;
default:
throw new Error('Method not found.');
}
};
getPersistenceStorage 和 setPersistenceStorage 這兩個函式至關重要,負責標籤及其他資訊的儲存與讀取,其定義如以下程式碼片段所示。
export const getPersistentStorage = async () => {
return await wallet.request({
method: 'snap_manageState',
params: ['get'],
});
};
export const clearPersistentStorage = async () => {
await wallet.request({
method: 'snap_manageState',
params: ['clear'],
});
};
export const setPersistentStorage = async (
data: Record<string, unknown> | void,
) => {
await wallet.request({
method: 'snap_manageState',
params: ['update', data],
});
};
針對所有功能的實作,我們為持久化儲存建立了合適的資料結構。
{
"from_account0": {
mainMapping: {
"to_account0": ["tag0","tag1"],
...
},
usage: {
"tag0": {
limit: "100000000000",
used: "800000",
notified: false
},
...
},
latestHash: "transaction_hash0"
},
...
}
使用者可以擁有多個帳戶,每個帳戶都有各自的標籤、使用記錄及使用者自訂限額。
交易洞察
交易洞察功能會在進行中的交易中顯示相關標籤、對應的支出金額,以及任何警示訊息。
在同一個 index.ts 檔案中,我們新增了一個交易洞察處理器,讓我們能夠攔截進行中的交易並與 MetaMask 錢包互動。
export const onTransaction: OnTransactionHandler = async ({ transaction }) => {
const insights = await getDetails(transaction);
return {
insights,
};
};
getDetails 函式實作了計算支出與顯示警示的完整邏輯,定義於 packages\snap\src\transaction.ts。
export const getDetails = async (transaction: Record<string, unknown>) => {
const toAddress = (transaction.to as string).toLowerCase();
const account = (transaction.from as string).toLowerCase();
const completeStorage = (await getPersistentStorage()) as any;
if (!completeStorage?.[account]) {
throw new Error('Storage initialization failed.');
}
const storage = completeStorage[account];
if (!storage.mainMapping || !storage.usage) {
throw new Error('Data corrput. Please re-install the snap.');
}
const tagList = storage.mainMapping[toAddress];
if (!tagList?.length) {
return {
Tag: NO_TAG_MESSAGE,
};
}
let tags = '';
let alerts = '';
let usageMsg = '';
for (const tag of tagList) {
if (tags === '') {
tags += `${tag}`;
} else {
tags += `, ${tag}`;
}
let { used } = storage.usage[tag];
let { limit } = storage.usage[tag];
const fixedUsed = FixedNumber.from(used);
const fixedLimit = FixedNumber.from(limit);
const usedPercent = (
Number(fixedUsed.divUnsafe(fixedLimit).toString()) * 100
).toFixed(2);
used = BigNumber.from(used);
limit = BigNumber.from(limit);
const amount = BigNumber.from(transaction.value as string);
const gas = BigNumber.from(transaction.gas as string);
const total = amount.add(gas);
if (usageMsg === '') {
usageMsg += `${tag}: ${usedPercent}%`;
} else {
usageMsg += ` | ${tag}: ${usedPercent}%`;
}
if (!limit.eq('0')) {
if (used.gt(limit)) {
alerts += `${EXCEEDED_MESSAGE + toEth(limit)} for the tag ${tag}. `;
} else if (used.add(total).gte(limit)) {
alerts += `${WILL_EXCEED_MESSAGE + toEth(limit)} for the tag ${tag}. `;
}
}
}
if (alerts === '') {
return {
Tag: tags,
Usage: usageMsg + FOOTER_NOTE,
};
}
return {
Tag: tags,
Usage: usageMsg + FOOTER_NOTE,
Alerts: alerts,
};
};
在此,我們首先檢查當前交易的 to 雜湊值(即 to 地址)是否存在於儲存中,接著讀取並計算使用百分比。然後我們檢查使用量是否即將達到或已超過該標籤所設定的限額,並發送相應的警示。
請注意,我們使用 ethers npm 套件中的 BigNumber 和 FixedNumber 類別來處理大數字的算術運算。
交易洞察功能至此完成,以下是最終結果:
排程任務(Cron Jobs)
此處定義了三個排程任務:weeklySummary 用於每週產生摘要報告,checkLimits 用於檢查使用者是否超過任何標籤的限額,updateAmount 則用於更新使用者新交易的使用資訊。
這些排程任務連同其執行頻率,均記錄於 packages\snap\snap.manifest.json 中。此外,index.ts 檔案中也加入了排程任務處理器。
這些功能定義於 packages\snap\src\cron.ts。
export const getSummary = async (account: string, completeStorage: any) => {
const storage = completeStorage[account];
if (!storage.usage) {
return null;
}
const { usage } = storage;
const newUsage: any = {};
let exceeded = false;
let hasTag = false;
for (const tag in usage) {
if (Object.prototype.hasOwnProperty.call(usage, tag)) {
hasTag = true;
const { used } = usage[tag];
const { limit } = usage[tag];
newUsage[tag] = usage[tag];
if (
BigNumber.from(used).gt(BigNumber.from(limit)) &&
BigNumber.from(limit).gt('0')
) {
exceeded = true;
}
newUsage[tag].notified = false;
newUsage[tag].used = '0';
}
}
if (hasTag) {
storage.usage = newUsage;
completeStorage[account] = storage;
await setPersistentStorage(completeStorage);
}
return exceeded;
};
在此,我們遍歷儲存資料,檢查使用者是否超過任何標籤的限額。接著將 notified 重設為 false,以便 checkLimits 排程任務能夠重複使用。我們回傳一個布林值,供排程任務處理器針對每個使用者帳戶進行判斷,並在每週結束時發送適當的通知訊息。
export const checkLimits = async (account: string, completeStorage: any) => {
const storage = completeStorage[account];
if (!storage.usage) {
return null;
}
const { usage } = storage;
const tags: string[] = [];
const newUsage: any = {};
for (const tag in usage) {
if (Object.prototype.hasOwnProperty.call(usage, tag)) {
const { used } = usage[tag];
const { limit } = usage[tag];
const { notified } = usage[tag];
newUsage[tag] = usage[tag];
if (
!notified &&
BigNumber.from(used).gt(BigNumber.from(limit)) &&
BigNumber.from(limit).gt(BigNumber.from('0'))
) {
tags.push(tag);
newUsage[tag].notified = true;
}
}
}
if (tags.length > 0) {
storage.usage = newUsage;
completeStorage[account] = storage;
await setPersistentStorage(completeStorage);
const message = `${LIMIT_ALERT_HEADER + tags.length} tags on ${compact(
account,
)}`;
return message;
}
return null;
};
在上述函式中,我們再次遍歷所有尚未通知的使用者標籤並檢查其使用狀況。若已超過設定的限額,則將 notified 設為 true,使該標籤在下週前不再被重複處理。我們向排程任務處理器回傳一則訊息,說明超過限額的標籤數量及使用者的帳戶雜湊值。
接下來,我們完成 updateAmount 的實作。
export const updateAmount = async (account: string, completeStorage: any) => {
const response = await fetch(
`https://api-goerli.etherscan.io/api?module=account&action=txlist&address=${account}&startblock=0&endblock=9999999999&sort=asc&apikey=${process.env.REACT_API_KEY}`,
);
const result = await response.json();
if (!result.result) {
return null;
}
let transactions = result.result;
if (transactions.length === 0) {
return null;
}
transactions = transactions.sort(
(a: any, b: any) => b.timeStamp - a.timeStamp,
);
const { latestHash } = completeStorage[account];
const { prevHash } = completeStorage[account];
if (transactions[0].hash.toLowerCase() === latestHash) {
return null;
}
for (const transaction of transactions) {
if (transaction.hash.toLowerCase() === latestHash) {
break;
}
if (transaction.to !== account) {
const toAddress = (transaction.to as string).toLowerCase();
const tagList = completeStorage[account].mainMapping[toAddress];
if (tagList !== null && tagList !== undefined) {
for (const tag of tagList) {
const gas = BigNumber.from(transaction.gasPrice).mul(
BigNumber.from(transaction.gasUsed),
);
const value = BigNumber.from(transaction.value);
const total = gas.add(value);
if (prevHash !== latestHash) {
completeStorage[account].usage[tag].used = BigNumber.from(
completeStorage[account].usage[tag].used,
)
.add(total)
.toString();
} else {
completeStorage[account].usage[tag].used = BigNumber.from('0')
.add(total)
.toString();
}
}
}
}
}
completeStorage[account].prevHash = latestHash;
completeStorage[account].latestHash = transactions[0].hash.toLowerCase();
return completeStorage;
};
在此,我們使用 etherscan API 獲取所有使用者交易,並更新每個標籤的使用詳情。我們將獲取的交易依時間降序排列,並從 latestHash 交易開始掃描,以避免重複掃描已處理的交易,最後更新 latestHash。此排程任務的執行頻率高於 checkLimits。
以下是上述實作的部分輸出結果:
前端 DApp
標籤功能
網站共有兩個頁面。第一個頁面是登陸頁面,使用者可以在此登入、新增/移除標籤、查看標籤使用分佈、設定限額,以及對標籤和交易套用篩選條件。這些功能均透過熟悉的 React.js 功能實現。交易資料從前述的 etherscan API 獲取,並將地址與對應的標籤進行關聯。這些資訊來自已連接 Snaps 的持久化儲存。
上述大部分功能的邏輯寫於 packages\site\src\pages\GetTableData.jsx。Snap 與前端的整合雖然複雜,但完成後便顯得直觀易懂。
透過在 packages\site\src\utils\snap.ts 中撰寫以下函式,snap 方法得以向前端公開:
export const getStorage = async () => {
return await window.ethereum.request({
method: 'wallet_invokeSnap',
params: [
defaultSnapOrigin,
{
method: 'getPersistentStorage',
},
],
});
};
export const setStorage = async (data: Record<string, unknown> | void) => {
await window.ethereum.request({
method: 'wallet_invokeSnap',
params: [
defaultSnapOrigin,
{
method: 'setPersistentStorage',
params: data,
},
],
});
};
export const clearStorage = async () => {
return await window.ethereum.request({
method: 'wallet_invokeSnap',
params: [
defaultSnapOrigin,
{
method: 'clearPersistentStorage',
},
],
});
};
我們也使用 react-chartjs-2 npm 套件來顯示使用者支出的分析圖表。
以下是網站的最終外觀:
請款功能
這是一項獨特功能,使用者可透過 XMTP 向另一位使用者發送通知,請求對方支付 ETH。使用此功能時,雙方都需要啟用此 snap。此功能可於 http://localhost:8000/request 使用。
為此,我們使用 @xmtp/xmtp-js npm 套件,分別在 packages\site\src\contexts\WalletContext.tsx 和 packages\site\src\contexts\XmtpContext.tsx 中建立 WalletContext 和 XmtpContext,並建立相應的 hooks 與元件。這部分程式碼大多直接來自說明文件中的範例。
我們在網站中建立了新頁面 packages\site\src\pages\request.tsx,以容納此功能的使用者介面。我們使用 useSendMessage hook 中的 sendMessage 函式來發送訊息。
import useSendMessage from '../hooks/useSendMessage';
const sendNewMessage = () => {
const payload = {
id: Date.now(),
message: msgTxt,
};
sendMessage(JSON.stringify(payload));
setMsgTxt('');
};
並透過 XmtpContext 的 provider 狀態來顯示收到的訊息。
import { XmtpContext } from '../contexts/XmtpContext';
const Home = () => {
const [providerState] = useContext(XmtpContext);
const { convoMessages, client } = providerState;
return (
<>
<ConversationList
convoMessages={convoMessages}
setSelectedConvo={setSelectedConvo}
/>
</>
)
};
依照上述說明文件中的範例操作後,我們最終實現了所需的功能:
結語
至此,FoxTagger 全部完成——整個專案將我們的構想實現為一個 MetaMask Snaps 應用程式,並與 DApp 相互結合。
若要繼續實作,下一步計劃是什麼?
下一步是擴展標籤功能,加入更多分析項目,並運用機器學習模型來預測並建議如何降低使用者的支出。接著,我們可以考慮加入交易分攤功能,讓使用者能夠在多個帳戶之間分攤費用,作為請款功能的延伸。
此外,MetaMask Snap 的全新自訂 UI 功能可用於在 snap 內部提供更出色的使用者體驗。最後,snap 可擴展至 Goerli 測試網以外的其他鏈。
能否介紹一下您自己和您的團隊?
從構思到實作,我們的團隊攜手合作,共同發想、精煉想法,並克服各種挑戰,最終打造出 FoxTagger Snaps。
Sachin Sahu 在 DApp 前端開發與功能實作上扮演了關鍵角色。他不僅協助將 MetaMask Snaps 與平台整合,還參與了示範影片與說明文件的製作,確保使用者擁有開始使用 Snap DApp 所需的所有資訊。
Siddhartha G 在前端設計與實作方面的專業知識,對平台的開發至關重要。他建立了直觀的前端元件與方法,用於呈現和修改資料,並協助將 MetaMask Snaps 與 DApp 整合。
Shreetesh M 負責後端實作,確保所有函式均向前端公開。他實作了交易洞察與排程任務,協助團隊將後端與前端無縫整合。
Noble Saji Mathews 的貢獻在於為鏈上訊息傳遞的交易框架奠定基礎。他也參與了標籤系統與交易請款系統的構思過程,提升了平台的可用性與使用者友善度。
Kranthi 以獨特的視角提出了標籤系統的構想,並探索將去中心化通訊融入其中的創新方法。他的努力促成了「請款」功能的實現,徹底改變了使用者與 Snap DApp 互動的方式。
Ansh Anand 在前端設計與構思過程中的專業知識,使他成為團隊中不可或缺的一員。他參與了示範影片的製作,展示了平台的各項功能,並協助吸引更多使用者。
您認為 MetaMask Snaps 為 Web3 領域帶來了哪些機遇與可能性?
MetaMask 擁有超過 2,100 萬名活躍使用者,要實現滿足所有人需求的功能確實相當困難。這正是 MetaMask Snaps 的用武之地——開發者社群現在擁有更大的自由度,可以根據使用者需求自訂和個人化 MetaMask,讓與各種 DApp 的互動變得前所未有地便捷,無論這些 DApp 是建構在不同的區塊鏈上,還是使用不同的協議。
MetaMask Snaps 的優勢不勝枚舉,包括追蹤一段時間內的支出、分析消費習慣、將每筆交易的一小部分捐給慈善機構、向客戶請款、為 Netflix 訂閱設定自動付款等等!透過 MetaMask Snaps,將 MetaMask 與各種 DeFi 協議、NFT 市場和社群媒體平台整合,如今已輕而易舉。
但 Snaps 不僅限於複雜的金融交易。試想與朋友外出用餐並以加密貨幣付款——有了 MetaMask Snaps,分攤帳單和向朋友請款只需輕點一下即可完成!這就是 MetaMask Snaps 的強大之處!
隨著 Web3 領域持續演進,MetaMask Snaps 必將成為開發者和使用者越來越重要的工具。在元宇宙潛力即將到來的當下,Snaps 的應用場景真的是無窮無盡。
對於有意嘗試 MetaMask Snaps 的開發者,您有什麼建議?
我們建議他們先瀏覽 GitHub 上現有的 snaps,這將大大幫助他們了解 MetaMask Snaps 所提供功能的潛力,儘管這些功能在紙面上看起來很簡單。
我們也建議他們使用現有的 monorepo 範本作為實作的起點,這有許多好處:儲存庫結構完善,並附有 linting 檢查與 actions;開發者可以在同一個套件中同時實作配套網頁應用程式和 snap;此外,該儲存庫始終與官方說明文件保持同步。
最後,我們建議他們在社群頻道上發布問題或疑惑,擴大自己對 MetaMask Snaps 的認識與接觸。
使用 MetaMask Snaps 進行建構
開始使用 MetaMask Snaps:
查閱開發者文件
安裝 MetaMask Flask
參考 MetaMask Snaps 指南
透過 Twitter、GitHub 討論區和 Discord 與我們保持聯繫
請留意我們的團隊將出現在您附近的下一場黑客松!祝 BUIDL 愉快 ⚒️
免責聲明:MetaMask Snaps 通常由 Consensys Software 以外的第三方開發。使用第三方開發的 MetaMask Snaps 須由您自行判斷與承擔風險,並同意您將對因此類活動所造成的任何損失或損害負完全責任。Consensys 對第三方開發的 MetaMask Snaps 不作任何明示或暗示的保證,並對第三方開發的 Snaps 免除一切責任。使用區塊鏈相關軟體存在風險,使用 MetaMask Snaps 即表示您完全承擔這些風險。