MetaMask Snapsは、MetaMaskを世界で最も拡張性の高いウォレットにするためのロードマップです。開発者として、まったく新しい方法でMetaMaskに機能やAPIを追加することができます。Web3開発者はこの成長の中核を担っており、本シリーズでは現在構築されている革新的なMetaMask Snapsを紹介することを目的としています。
FoxTagger Snap
Snapリポジトリ: https://github.com/shree675/FoxTagger
FoxTaggerは、ユーザーが定義したタグとアドレスのマッピングを容易にするツールで、使用状況の分析を警告・表示することでユーザーの支出管理をサポートします。
技術的な実装について説明してください。
技術的なセットアップ
MetaMask拡張機能であるFoxTaggerは、MetaMask Snapsバックエンド(スナップとも呼ばれる)とGatsby.jsフロントエンドという2つの主要コンポーネントで構成されています。フロントエンドはUIをホストするシンプルなWebアプリケーションですが、SnapsアプリケーションのコンパニオンdAppとしても機能します。
機能の大部分はスナップ内で実現されていますが、残りの機能はフロントエンドに実装されており、ユーザーが他のユーザーへの送金リクエストや、UIを通じたタグの設定を行えるようになっています。送金リクエスト機能はXMTPプロトコルを使用し、タグ付け機能はMetaMask Snaps APIを使用しています。
実装の出発点としてスナップモノレポをテンプレートとして使用しました。プロジェクトの完全な実装はこちらでご確認いただけます。
Snapsアプリケーション
実装全体を通じて、Snaps APIが提供する以下の主要機能を活用しています:
永続ストレージ
通知
Cronジョブ
トランザクションインサイト
実装は、まず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ジョブ
ここでは3つのCronジョブを定義しています。毎週サマリーを生成するweeklySummary、ユーザーがいずれかのタグの上限を超えていないかを確認するcheckLimits、そしてユーザーが実行した新しいトランザクションの使用情報を更新するupdateAmountです。
これらのCronジョブは、適切な実行頻度とともにpackages\snap\snap.manifest.jsonに記載されています。また、Cronジョブハンドラーが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 Cronジョブで再利用できるようにします。Cronジョブハンドラーが各ユーザーアカウントに対して処理を行い、週末に適切な通知メッセージを送信できるよう、ブール値を返します。
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に設定して、翌週まで再度処理されないようにします。上限を超えたタグの数とユーザーのアカウントハッシュを示すメッセージをCronジョブハンドラーに返します。
次に、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を更新します。このCronジョブはcheckLimitsよりも高い頻度で実行されます。
上記の実装の出力例をいくつか示します:
フロントエンドdApp
タグ付け機能
ウェブサイトには2つのページがあります。最初のページはランディングページで、ユーザーはログイン、タグの追加/削除、タグの使用状況分布の確認、上限の設定、タグとトランザクションへのフィルター適用を行うことができます。これらはすべて、おなじみのReact.jsの機能によって実現されています。トランザクションは前述のetherscan APIから取得され、アドレスは対応するタグに関連付けられます。この情報は接続されたSnapsの永続ストレージから取得されます。
上記の機能のほとんどのロジックはpackages\site\src\pages\GetTableData.jsxに記述されています。スナップとフロントエンドの統合は複雑ですが、完成すると直感的に使用できます。
スナップのメソッドは、packages\site\src\utils\snap.tsに以下の関数を記述することでフロントエンドに公開されます:
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の送金を要求する通知を送ることができます。この機能を使用するには、両方のユーザーがこのスナップを有効にしている必要があります。この機能はhttp://localhost:8000/requestで利用できます。
この機能のために、@xmtp/xmtp-js npmパッケージを使用して、packages\site\src\contexts\WalletContext.tsxとpackages\site\src\contexts\XmtpContext.tsxにそれぞれWalletContextとXmtpContextを作成します。また、フックとコンポーネントも作成します。このコードの大部分はドキュメントのサンプルから直接引用しています。
この機能のUIを収容するために、ウェブサイトに新しいページpackages\site\src\pages\request.tsxを作成します。useSendMessageフックのsendMessage関数を使用してメッセージを送信します。
import useSendMessage from '../hooks/useSendMessage';
const sendNewMessage = () => {
const payload = {
id: Date.now(),
message: msgTxt,
};
sendMessage(JSON.stringify(payload));
setMsgTxt('');
};
そして、XmtpContextのプロバイダー状態を使用して受信メッセージを表示します。
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機能を活用して、スナップ内でより快適なユーザー体験を提供することができます。最後に、スナップをGoerli Testnetネットワーク以外のチェーンにも拡張することができます。
あなた自身とチームについて少し教えてください。
アイデア出しから実装まで、チームが協力してアイデアを生み出し、洗練させ、課題を乗り越えてFoxTagger Snapsを完成させました。
Sachin Sahuは、dAppのフロントエンドと機能の開発において中心的な役割を果たしました。MetaMask Snapsをプラットフォームに統合するだけでなく、ユーザーがSnapのdAppを使い始めるために必要な情報をすべて得られるよう、デモとドキュメントの作成にも取り組みました。
Siddhartha Gのフロントエンドデザインと実装における専門知識は、プラットフォームの開発に不可欠でした。直感的なフロントエンドコンポーネントとメソッドを作成してデータの表示と変更を実現し、MetaMask SnapsとdAppの統合を支援しました。
Shreetesh Mはバックエンドの実装を担当し、すべての関数がフロントエンドに公開されるよう確保しました。トランザクションインサイトとCronジョブを実装し、バックエンドとフロントエンドのシームレスな統合に貢献しました。
Noble Saji Mathewsの貢献は、オンチェーンメッセージングを通じたトランザクションのフレームワーク策定において重要な役割を果たしました。また、プラットフォームの使いやすさとユーザーフレンドリーさを高めるタグ付けとトランザクションリクエストシステムのアイデア出しにも参加しました。
Kranthiは、タグ付けシステムのアイデアを提案し、分散型コミュニケーションを組み込む新しい方法を探ることで独自の視点をもたらしました。彼の努力により「リクエスト」機能の実装が実現し、ユーザーがSnapのdAppと連携する方法に革新をもたらしました。
Ansh Anandのフロントエンドデザインとアイデア出しにおける専門知識は、チームにとって非常に貴重なものでした。プラットフォームの機能を紹介し、より多くのユーザーを引き付けるのに役立ったデモ動画の制作に取り組みました。
MetaMask SnapsとWeb3空間に対して解き放つ可能性についてどのような機会を見出していますか?
MetaMaskには2,100万人以上のアクティブユーザーがいるため、すべてのユーザーのニーズに合った機能を実現することは非常に難しいことです。そこでMetaMask Snapsの出番です。開発者コミュニティは、ユーザーの要件に基づいてMetaMaskをカスタマイズおよびパーソナライズする自由度が高まり、異なるブロックチェーン上に構築されたり異なるプロトコルを使用したりするさまざまなdAppとの連携がこれまで以上に簡単になりました。
MetaMask Snapsのメリットは数多くあります。時間をかけて支出を追跡する機能、支出の分析、各トランザクションの一定割合を慈善団体に寄付する機能、クライアントへの支払いリクエスト、Netflixサブスクリプションの自動支払い設定など、さまざまな用途があります!MetaMask Snapsを使えば、MetaMaskとさまざまなDeFiプロトコル、NFTマーケットプレイス、ソーシャルメディアプラットフォームとの統合も簡単です。
しかし、Snapsは複雑な金融取引だけのものではありません。友人と昼食に出かけて暗号通貨で支払う場面を想像してみてください。MetaMask Snapsを使えば、割り勘や友人への支払いリクエストがワンタップで完了します!これがMetaMask Snapsの力です!
Web3空間が進化し続ける中、MetaMask Snapsは開発者とユーザーの双方にとってますます重要なツールになることは間違いありません。メタバースの可能性が視野に入ってきた今、Snapsのユースケースは本当に無限大です。
MetaMask Snapsを試したい開発者へのアドバイスはありますか?
まずGitHub上の既存のSnapsを確認することをお勧めします。機能は一見シンプルに見えても、MetaMask Snapsが提供する機能の可能性を理解するのに大いに役立ちます。
また、実装の出発点として既存のモノレポテンプレートを使用することをお勧めします。多くのメリットがあります。リポジトリはリンティングチェックとアクションを含めて非常によく構造化されており、開発者はコンパニオンWebアプリとスナップの両方を1つのパッケージで実装できます。さらに、リポジトリは常に公式ドキュメントと同期しています。
最後に、コミュニティチャンネルに問題や疑問を投稿して、MetaMaskのSnapsへの理解を深めることをお勧めします。
MetaMask Snapsで構築する
MetaMask Snapsを始めるには:
開発者ドキュメントをご確認ください
MetaMask Flaskをインストールしてください
MetaMask Snapsガイドをご覧ください
Twitter、GitHubディスカッション、Discordでつながりましょう
次のハックソンであなたの近くでお会いしましょう!Happy BUIDLing ⚒️
免責事項:MetaMask Snapsは一般的にConsensys Software以外のサードパーティによって開発されています。サードパーティが開発したMetaMask Snapsの使用は、ご自身の判断とリスクのもとで行われ、そのような活動から生じる損失や損害についてはお客様が単独で責任を負うことに同意したものとみなされます。Consensysはサードパーティが開発したMetaMask Snapsについて、口頭または書面による明示的または黙示的な保証を行わず、サードパーティが開発したSnapsに関するすべての責任を否認します。ブロックチェーン関連ソフトウェアの使用にはリスクが伴い、MetaMask Snapsを使用する際にはそのリスクをすべて自己負担するものとします。