跳至內容

FoxTagger Snap:使用自訂標籤對應地址

InterIIT 黑客松冠軍

閱讀時間:12 分鐘
FoxTagger Snap:使用自訂標籤對應地址

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 提供的以下主要功能:

  1. 持久化儲存

  2. 通知

  3. 排程任務(Cron jobs)

  4. 交易洞察

我們的實作首先在 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;
      }

      // reset usage information for the next week
      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;
  }

  // sort in descending order
  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 方法得以向前端公開:

/**
 * Get the persisted data from the snap.
 *
 * @returns The persisted data, if any, 'null' otherwise.
 */
export const getStorage = async () => {
  return await window.ethereum.request({
    method: 'wallet_invokeSnap',
    params: [
      defaultSnapOrigin,
      {
        method: 'getPersistentStorage',
      },
    ],
  });
};

/**
 * Update the persistent storage in the snap.
 *
 * @param data - The complete data to be stored.
 */
export const setStorage = async (data: Record<string, unknown> | void) => {
  await window.ethereum.request({
    method: 'wallet_invokeSnap',
    params: [
      defaultSnapOrigin,
      {
        method: 'setPersistentStorage',
        params: data,
      },
    ],
  });
};

/**
 * Clear the persistent storage in the snap.
 */
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:

  1. 查閱開發者文件

  2. 安裝 MetaMask Flask

  3. 參考 MetaMask Snaps 指南

  4. 透過 Twitter、GitHub 討論區和 Discord 與我們保持聯繫

請留意我們的團隊將出現在您附近的下一場黑客松!祝 BUIDL 愉快 ⚒️

免責聲明:MetaMask Snaps 通常由 Consensys Software 以外的第三方開發。使用第三方開發的 MetaMask Snaps 須由您自行判斷與承擔風險,並同意您將對因此類活動所造成的任何損失或損害負完全責任。Consensys 對第三方開發的 MetaMask Snaps 不作任何明示或暗示的保證,並對第三方開發的 Snaps 免除一切責任。使用區塊鏈相關軟體存在風險,使用 MetaMask Snaps 即表示您完全承擔這些風險。

AI 翻譯。可能包含錯誤。請務必核實資訊。

評價此翻譯

本文作者:

  • MetaMask

    MetaMask,前身為 Consensys Software Inc,是全球最大的自託管金融平台,讓用戶能在單一平台上持有、消費、儲蓄並增值其資產,涵蓋加密貨幣與傳統資產。公司正致力於打造實現上述目標的消費者平台,將支付、儲蓄、投資與數位資產整合於一個無縫的使用體驗中。MetaMask 從全球使用最廣泛的自託管錢包發展而來,讓用戶直接掌控自己的資金與資產,服務範圍遍及約 190 個國家。自 2016 年起,MetaMask 在以太坊的發展歷程中扮演了奠基性的角色。時至今日,MetaMask 位居區塊鏈經濟的核心,致力於構建「開放金融」的作業系統,讓每個人都能全面掌握自己的財務生活。

    閱讀所有文章