MetaMask Snaps ist der Fahrplan, um MetaMask zur erweiterbarsten Wallet der Welt zu machen. Als Entwickler kannst du MetaMask auf völlig neue Weise mit deinen Features und APIs bereichern. Web3-Entwickler sind das Herzstück dieses Wachstums, und diese Serie soll die innovativen MetaMask Snaps vorstellen, die heute entwickelt werden.
FoxTagger Snap
Snap-Repo: https://github.com/shree675/FoxTagger
FoxTagger ist ein Tool, das die Zuordnung von Adressen zu benutzerdefinierten Tags ermöglicht, um Nutzern dabei zu helfen, ihre Ausgaben im Blick zu behalten – durch Warnmeldungen und die Anzeige von Nutzungsanalysen.
Erkläre uns die technische Umsetzung.
Das technische Setup
Unsere MetaMask-Erweiterung FoxTagger besteht aus zwei Hauptkomponenten: einem MetaMask-Snaps-Backend (auch bekannt als der Snap) und einem Gatsby.js-Frontend. Das Frontend ist eine einfache Webanwendung, die die Benutzeroberfläche bereitstellt, und fungiert gleichzeitig als Companion-DApp für unsere Snaps-Anwendung.
Der Großteil der Funktionalität ist im Snap implementiert, der Rest befindet sich im Frontend, um den Nutzern zu ermöglichen, Beträge von anderen anzufordern und/oder ihre Tags über unsere Benutzeroberfläche zu konfigurieren. Während die Funktion Betrag anfordern das XMTP-Protokoll verwendet, nutzt die Tagging-Funktion die MetaMask Snaps API.
Als Ausgangspunkt haben wir das Snap-Monorepo als Vorlage verwendet. Die vollständige Implementierung unseres Projekts ist hier zu finden.
Die Snaps-Anwendung
In unserer gesamten Implementierung nutzen wir die folgenden Hauptfunktionen der Snaps API:
Persistenter Speicher
Benachrichtigungen
Cron-Jobs
Transaction Insights
Unsere Implementierung beginnt damit, einige Funktionen für das Frontend in packages\snap\src\index.ts bereitzustellen.
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.');
}
};
Die Funktionen getPersistenceStorage und setPersistenceStorage sind grundlegend, da sie die Speicherung und den Abruf von Tags und anderen Informationen ermöglichen, und sind wie im folgenden Code-Snippet dargestellt definiert.
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],
});
};
Für alle Feature-Implementierungen haben wir eine geeignete Datenstruktur für unseren persistenten Speicher erstellt.
{
"from_account0": {
mainMapping: {
"to_account0": ["tag0","tag1"],
...
},
usage: {
"tag0": {
limit: "100000000000",
used: "800000",
notified: false
},
...
},
latestHash: "transaction_hash0"
},
...
}
Ein Nutzer kann mehrere Konten haben, und jedes Konto verfügt über eigene Tags, Nutzungsdaten und benutzerdefinierte Limits.
Transaction Insights
Die Transaction-Insights-Funktion zeigt die zugehörigen Tags, die entsprechenden Ausgaben und eventuelle Warnmeldungen für eine laufende Transaktion an.
In derselben Datei index.ts fügen wir einen Transaction-Insights-Handler hinzu, der es uns ermöglicht, eine laufende Transaktion abzufangen und mit der MetaMask-Wallet zu interagieren.
export const onTransaction: OnTransactionHandler = async ({ transaction }) => {
const insights = await getDetails(transaction);
return {
insights,
};
};
Die Funktion getDetails implementiert die gesamte Logik zur Berechnung der Ausgaben und zur Anzeige von Warnmeldungen. Diese ist in packages\snap\src\transaction.ts implementiert.
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,
};
};
Dabei prüfen wir zunächst, ob der to-Hash (oder die to-Adresse) der aktuellen Transaktion im Speicher vorhanden ist. Anschließend rufen wir den Nutzungsprozentsatz ab und berechnen ihn. Dann prüfen wir, ob die Nutzung das festgelegte Limit für diesen Tag bald erreicht oder bereits überschritten hat, und senden eine entsprechende Warnmeldung.
Beachte, dass wir die Klassen BigNumber und FixedNumber aus dem npm-Paket ethers verwenden, um arithmetische Operationen mit großen Zahlen durchzuführen.
Transaction Insights ist nun abgeschlossen, und dies ist das Endergebnis:
Cron-Jobs
Hier sind drei Cron-Jobs definiert: weeklySummary zur wöchentlichen Zusammenfassung, checkLimits zur Überprüfung, ob der Nutzer bei einem seiner Tags das Limit überschritten hat, und updateAmount, das die Nutzungsinformationen neuer Transaktionen des Nutzers aktualisiert.
Diese Cron-Jobs sind in packages\snap\snap.manifest.json mit ihrer jeweiligen Ausführungshäufigkeit angegeben. Zusätzlich ist ein Cron-Job-Handler in der Datei index.ts integriert.
Diese Funktionalitäten sind in packages\snap\src\cron.ts definiert.
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;
};
Hier iterieren wir durch den Speicher und prüfen, ob der Nutzer bei einem der Tags ein Limit überschritten hat. Anschließend setzen wir notified auf false zurück, damit es vom Cron-Job checkLimits wiederverwendet werden kann. Wir geben einen booleschen Wert zurück, damit der Cron-Job-Handler ihn für jedes Nutzerkonto aufgreifen und am Ende der Woche eine entsprechende Benachrichtigung senden kann.
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;
};
In der obigen Funktion iterieren wir erneut durch alle nicht benachrichtigten Nutzer-Tags und prüfen deren Nutzung. Wenn das festgelegte Limit überschritten wurde, setzen wir notified auf true, damit dieser Tag bis zur nächsten Woche nicht erneut erfasst wird. Wir geben eine Nachricht an den Cron-Job-Handler zurück, die die Anzahl der Tags, die das Limit überschritten haben, sowie den Account-Hash des Nutzers enthält.
Als Nächstes vervollständigen wir die Implementierung für 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;
};
Hier verwenden wir die Etherscan-API, um alle Nutzertransaktionen abzurufen und die Nutzungsdetails für jeden Tag zu aktualisieren. Wir sortieren die erhaltenen Transaktionen in absteigender zeitlicher Reihenfolge und scannen sie ab der latestHash-Transaktion, um ein erneutes Scannen zu vermeiden. Abschließend aktualisieren wir latestHash. Dieser Cron-Job läuft häufiger als checkLimits.
Hier sind einige Ausgaben der obigen Implementierungen:
Die Frontend-DApp
Tagging-Funktion
Die Website besteht aus zwei Seiten. Die erste Seite ist die Startseite, auf der der Nutzer sich einloggen, Tags hinzufügen/entfernen, die Verteilung seiner Tag-Nutzung einsehen, Limits festlegen sowie Filter auf Tags und Transaktionen anwenden kann. All dies wird durch die vertraute React.js-Funktionalität realisiert. Die Transaktionen werden über die zuvor erwähnte Etherscan-API abgerufen, und die Adressen werden ihren entsprechenden Tags zugeordnet. Diese Informationen stammen aus dem persistenten Speicher der verbundenen Snaps.
Die Logik für die meisten der oben genannten Funktionen ist in packages\site\src\pages\GetTableData.jsx geschrieben. Die Integration des Snaps mit dem Frontend ist anspruchsvoll, aber nach Abschluss intuitiv verständlich.
Die Snap-Methoden werden dem Frontend durch die folgenden Funktionen in packages\site\src\utils\snap.ts bereitgestellt:
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',
},
],
});
};
Wir verwenden außerdem das npm-Paket react-chartjs-2, um Analysen der Nutzerausgaben darzustellen.
So sieht die Website am Ende aus:
Funktion „Betrag anfordern"
Dies ist eine einzigartige Funktion, mit der ein Nutzer über XMTP eine Benachrichtigung an einen anderen Nutzer senden kann, um ETH anzufordern. Dafür müssen beide Nutzer diesen Snap aktiviert haben. Diese Funktion ist unter http://localhost:8000/request verfügbar.
Dafür verwenden wir das npm-Paket @xmtp/xmtp-js, um WalletContext und XmtpContext in packages\site\src\contexts\WalletContext.tsx bzw. packages\site\src\contexts\XmtpContext.tsx zu erstellen. Wir erstellen außerdem Hooks und Komponenten. Ein Großteil dieses Codes stammt direkt aus einem Beispiel in der Dokumentation.
Wir erstellen eine neue Seite, packages\site\src\pages\request.tsx, auf der die Benutzeroberfläche für diese Funktion untergebracht ist. Nachrichten werden über die Funktion sendMessage aus dem Hook useSendMessage gesendet.
import useSendMessage from '../hooks/useSendMessage';
const sendNewMessage = () => {
const payload = {
id: Date.now(),
message: msgTxt,
};
sendMessage(JSON.stringify(payload));
setMsgTxt('');
};
Die empfangenen Nachrichten werden über den Provider-State von XmtpContext angezeigt.
import { XmtpContext } from '../contexts/XmtpContext';
const Home = () => {
const [providerState] = useContext(XmtpContext);
const { convoMessages, client } = providerState;
return (
<>
<ConversationList
convoMessages={convoMessages}
setSelectedConvo={setSelectedConvo}
/>
</>
)
};
Nach dem Befolgen des Beispiels aus der oben genannten Dokumentation erreichen wir schließlich die gewünschte Funktionalität:
Fazit
Damit ist FoxTagger abgeschlossen – das gesamte Projekt, in dem unsere Idee in eine MetaMask-Snaps-Anwendung umgesetzt wurde, kombiniert mit einer DApp.
Was wären die nächsten Schritte bei einer Weiterentwicklung?
Der nächste Schritt wäre die Erweiterung der Tagging-Funktionalität durch zusätzliche Analysen und den Einsatz eines Machine-Learning-Modells, um Vorhersagen und Empfehlungen zur Minimierung der Nutzerausgaben zu machen. Anschließend könnte eine Funktion zur Transaktionsaufteilung integriert werden, mit der der Nutzer eine Gebühr auf mehrere Konten aufteilen kann – als Erweiterung der Funktion „Betrag anfordern".
Darüber hinaus könnte die neue Custom-UI-Funktionalität von MetaMask Snaps genutzt werden, um ein ansprechenderes Nutzererlebnis innerhalb des Snaps zu bieten. Schließlich kann der Snap auf andere Netzwerke als das Goerli-Testnet ausgeweitet werden.
Erzählt uns ein wenig über euch und euer Team.
Von der Ideenfindung bis zur Umsetzung hat unser Team zusammengearbeitet, um Ideen zu entwickeln, zu verfeinern und Herausforderungen zu meistern, um FoxTagger Snaps zu realisieren.
Sachin Sahu spielte eine entscheidende Rolle bei der Entwicklung des Frontends und der Funktionalitäten der DApp. Er half nicht nur bei der Integration von MetaMask Snaps in die Plattform, sondern arbeitete auch an der Demo und der Dokumentation, um sicherzustellen, dass Nutzer alle Informationen haben, die sie für den Einstieg in die Snap-DApp benötigen.
Siddhartha Gs Expertise in Frontend-Design und -Implementierung war entscheidend für die Entwicklung der Plattform. Er erstellte intuitive Frontend-Komponenten und -Methoden zur Darstellung und Bearbeitung von Daten und half bei der Integration von MetaMask Snaps in die DApp.
Shreetesh M übernahm die Verantwortung für die Backend-Implementierung und stellte sicher, dass alle Funktionen dem Frontend zugänglich gemacht wurden. Er implementierte Transaction Insights und Cron-Jobs, was dem Team half, das Backend nahtlos mit dem Frontend zu integrieren.
Noble Saji Mathews' Beiträge waren maßgeblich bei der Ausarbeitung des Frameworks für Transaktionen über On-Chain-Messaging. Er war auch am Ideenfindungsprozess für das Tagging- und Transaktionsanforderungssystem beteiligt, das die Benutzerfreundlichkeit der Plattform verbessert.
Kranthi brachte seine einzigartige Perspektive ein, indem er die Idee des Tagging-Systems vorschlug und neue Methoden zur Einbindung dezentraler Kommunikation erkundete. Seine Arbeit führte zur Implementierung der „Request"-Funktion, die die Art und Weise, wie Nutzer mit der Snap-DApp interagieren, grundlegend verändert hat.
Ansh Anands Expertise in Frontend-Design und im Ideenfindungsprozess machte ihn zu einem unverzichtbaren Teammitglied. Er arbeitete am Demo-Video, das die Möglichkeiten der Plattform präsentierte und dazu beitrug, mehr Nutzer anzuziehen.
Welche Möglichkeiten seht ihr mit MetaMask Snaps und den Chancen, die es für den Web3-Bereich eröffnet?
Mit mehr als 21 Millionen aktiven Nutzern auf MetaMask ist es schwierig, eine Funktionalität zu entwickeln, die den Bedürfnissen aller gerecht wird. Genau hier kommen MetaMask Snaps ins Spiel – die Entwickler-Community hat nun mehr Freiheit, MetaMask basierend auf den Anforderungen der Nutzer anzupassen und zu personalisieren, was die Interaktion mit verschiedenen DApps, die auf unterschiedlichen Blockchains aufgebaut sein oder unterschiedliche Protokolle verwenden können, einfacher denn je macht.
Die Vorteile von MetaMask Snaps sind zahlreich: die Möglichkeit, Ausgaben im Laufe der Zeit zu verfolgen, Ausgaben zu analysieren, einen kleinen Prozentsatz jeder Transaktion für wohltätige Zwecke zu spenden, Zahlungen von Kunden anzufordern, Autopay für Netflix-Abonnements einzurichten und vieles mehr! Die Integration von MetaMask mit verschiedenen DeFi-Protokollen, NFT-Marktplätzen und Social-Media-Plattformen ist mit MetaMask Snaps jetzt ein Kinderspiel.
Aber Snaps sind nicht nur für komplexe Finanztransaktionen gedacht. Stell dir vor, du gehst mit Freunden zum Mittagessen und bezahlst mit Kryptowährung – mit MetaMask Snaps ist das Aufteilen der Rechnung und das Anfordern der Zahlung von deinen Freunden so einfach wie ein einziger Tipp! Das ist die Stärke von MetaMask Snaps!
Da sich der Web3-Bereich weiterentwickelt, wird MetaMask Snaps sicherlich zu einem immer wichtigeren Werkzeug für Entwickler und Nutzer gleichermaßen. Angesichts des Potenzials des Metaversums am Horizont sind die Anwendungsfälle von Snaps schier grenzenlos.
Hast du Ratschläge für Entwickler, die MetaMask Snaps ausprobieren möchten?
Wir würden ihnen empfehlen, zunächst die vorhandenen Snaps auf GitHub durchzusehen. Das hilft ihnen sehr dabei, das Potenzial der von MetaMask Snaps bereitgestellten Funktionen zu verstehen – auch wenn diese auf dem Papier einfach erscheinen mögen.
Wir würden außerdem empfehlen, die vorhandene Monorepo-Vorlage als Ausgangspunkt für die Implementierung zu verwenden. Sie bietet viele Vorteile: Das Repo ist sehr gut strukturiert, inklusive Linting-Prüfungen und Actions; Entwickler können sowohl eine Companion-Web-App als auch einen Snap in einem einzigen Paket implementieren. Darüber hinaus ist das Repo stets auf dem neuesten Stand der offiziellen Dokumentation.
Abschließend empfehlen wir, Fragen und Zweifel in Community-Kanälen zu posten und so die eigene Vernetzung rund um MetaMask Snaps zu stärken.
Mit MetaMask Snaps entwickeln
So startest du mit MetaMask Snaps:
Schau dir die Entwicklerdokumentation an
Installiere MetaMask Flask
Lies einen MetaMask-Snaps-Leitfaden
Bleib mit uns in Kontakt auf Twitter, in den GitHub-Diskussionen und auf Discord
Halte Ausschau nach unserem Team beim nächsten Hackathon in deiner Nähe! Happy BUIDLing ⚒️
Haftungsausschluss: MetaMask Snaps werden im Allgemeinen von Drittanbietern entwickelt, die nicht mit Consensys Software verbunden sind. Die Nutzung von durch Dritte entwickelten MetaMask Snaps erfolgt nach eigenem Ermessen und auf eigenes Risiko, und du stimmst zu, dass du allein für etwaige Verluste oder Schäden verantwortlich bist, die aus solchen Aktivitäten resultieren. Consensys gibt keine ausdrückliche oder stillschweigende Garantie, weder mündlich noch schriftlich, in Bezug auf von Dritten entwickelte MetaMask Snaps und lehnt jede Haftung für von Dritten entwickelte Snaps ab. Die Nutzung von Blockchain-bezogener Software ist mit Risiken verbunden, die du bei der Verwendung von MetaMask Snaps vollständig übernimmst.