Welcome to this workshop on MetaMask π¦ Snaps! In this workshop, we're going to extend the functionality of the MetaMask wallet by providing users with useful transaction insights. More specifically, for simple ETH transfers, we'll be showing users what percentage of the value of their ETH transfer they'd be paying in gas fees.
Here is how the final interaction will look like:
π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯
snaps.mp4
π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯π₯
In this first step, we'll be initializing a new Snaps project using yarn create. We'll then cleanup the project by removing some unneeded files. Finally, we'll make the project our own by giving it a name other than "Example Snap".
Creating a new snap project is as easy as:
yarn create @metamask/snap transaction-insights-snaps-workshopThe initial project includes some MetaMask organization-specific files. These can be cleaned up by running the cleanup script from the root of the project:
./scripts/cleanup.shRunning this script will delete unneeded files, delete the script itself, and commit the changes automatically.
The initial project has generic names in multiple places. Here we will edit some files to customize the project:
-
Edit
/package.jsonπ¦π¨π»βπ»:- Modify the
namefield to be unique to your project - Optionally add a
description - Customize or remove
homepage,repository,author, andlicense
- Modify the
-
Edit
/packages/snap/package.jsonand/packages/snap/snap.manifest.jsonπ§Ώπ¨π»βπ»:The Snaps manifest file --
/packages/snap/snap.manifest.jsonis specified in the Snaps Publishing Specification. Refer to the specification, and edit theproposedName,description, andrepositoryfields, matching them in/packages/snap/package.jsonas described in the spec. In a further step, we'll be editinginitialPermissions. When publishing the snap to NPM, you'll also need to edit thelocation.packageNamefield to match that of/packages/snap/package.json -
Edit
/packages/site/package.json:This is the same pattern as before. The
siteworkspace is where the static React site lives. Normally it won't be published to NPM so thenamefield matters less, but feel free to make any changes necessary in there. -
Optionally edit or remove any configurations related to ESLint, Prettier, Editorconfig, etc. to match your preferences or those of your organization.
If coding your snap with Visual Studio Code, you can create or update the file /.vscode/settings.json with the following settings. This will make VSCode automatically fix linting errors when saving a file:
{
"eslint.format.enable": true,
"eslint.packageManager": "yarn",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.codeActionsOnSave.mode": "all",
"editor.tabSize": 2
}The template snap provided to you is setup to expose a JSON-RPC API with a simple hello command, which brings up a dialog box. In contrast, the snap we're creating for this workshop doesn't expose any API. Instead it provides transaction insights directly in the MetaMask transaction window. In this step, we'll be removing code and permissions related to the JSON-RPC API, adding basic transaction insights code, and testing the resulting snap. In the process, we'll also learn how to debug a snap.
- Remove all the code in
/packages/snap/src/index.ts - In
/packages/snap/snap.manifest.jsonremove the entriessnap_dialogandendowment:rpcunderinitialPermissions
-
In
/packages/snap/src/index.tsadd the following code:import { OnTransactionHandler } from '@metamask/snaps-types'; import { heading, panel, text } from '@metamask/snaps-ui'; // Handle outgoing transactions export const onTransaction: OnTransactionHandler = async ({ transaction }) => { console.log('Transaction insights transaction', transaction); return { content: panel([ heading('Percent Snap'), text( 'This snap will show you what percentage of your ETH transfers are paid in gas fees.', ), ]), }; };
-
In
/packages/snap/snap.manifest.json, makeinitialPermissionsthe following object:{ "endowment:transaction-insight": {} }
-
From the root of the project, run
yarn startornpm start. This will start two development servers: one for watching and compiling the snap, and another one for the React site. The snap bundle will be served fromlocalhost:8080, and the site will be served fromlocalhost:8000. -
Open
http://localhost:8000in your browser -
Press the "Connect" button, and accept the permission request.
-
On the next screen, notice that the "Install Snap" dialog is telling you that the snap wants the permission to "Fetch and display transaction insights". Press "Approve & install".
-
From MetaMask, create a new ETH transfer
-
On the confirmation window, you'll see a new tab named "PERCENT SNAP". Switch to that tab. Note that it's the switching to the tab that activates the
onTransactionexport of your snap to be called. -
Notice the Custom UI output from the snap.
-
If you look in your browser's dev tools for the
console.logthat we setup, you'll notice that it's not there. That's becauseconsole.logs from your snap are happening inside the extension. In the next section, we'll see how to debug a snap.
-
Go to
chrome://extensions/ -
On the top right-hand corner, make sure that "Developer mode" is on
-
Find MetaMask Flask, and click on "Details"
-
Under "Inspect views", click on
background.html -
Go back to the MetaMask transaction window, and switch back to the "PERCENT SNAP". You should now see the result of your
console.login the new developer tools window linked tobackground.html
To show the end user the percentage of their transfer that they're paying in gas fees, we have to know the current gas price. We can easily get this by calling the eth_gasPrice method using the global Ethereum provider made available to snaps.
To use the global Ethereum provider, we have to request permission for it. Open the file at /packages/snap/snap.manifest.json, and change the initialPermissions to:
{
"endowment:transaction-insight": {},
"endowment:ethereum-provider": {}
}Since you've made some changes to your snap, you'll have to reinstall it. Go back to the Dapp and press the "Reconnect" button. In the "Install snap" window, you'll see a new permission request to "Access the Ethereum provider". Press "Approve & install".
To fetch the gas price, we can simply use the ethereum global. Add this code between the console.log and the return in the onTransaction export of your snap:
const currentGasPrice = await ethereum.request({
method: 'eth_gasPrice',
});
console.log('Current gas price', currentGasPrice);Reinstall the snap, go back to the MetaMask transaction window, and switch to the "PERCENT SNAP" tab. This will activate the onTransaction callback. In the developer tools window you should see a console.log like Current gas price 0x66b04938. The gas price is returned as a hex string in wei.
In this step, we'll remove the console.log for the currentGasPrice. Instead, we'll display the current gas price in wei in the transaction insights UI.
-
Remove the
console.logfor the `currentGasPrice -
Replace the
returnstatement in theonTransactionwith the following:return { content: panel([ heading('Percent Snap'), text(`Current gas price: ${parseInt(currentGasPrice ?? '', 16)} wei`), ]), };
When implementing transaction insights, we get access to the following fields in the transaction object:
{
"from": "sender address",
"gas": "0x5208",
"maxFeePerGas": "0x1014e7ff3c",
"maxPriorityFeePerGas": "0x59682f00",
"to": "receiver address",
"type": "0x2",
"value": "0x16345785d8a0000"
}We can roughly calculate the gas fees that the user would pay like this:
const transactionGas = parseInt(transaction.gas as string, 16);
const currentGasPriceInWei = parseInt(currentGasPrice ?? '', 16);
const maxFeePerGasInWei = parseInt(transaction.maxFeePerGas as string, 16);
const maxPriorityFeePerGasInWei = parseInt(
transaction.maxPriorityFeePerGas as string,
16,
);
const gasFees = Math.min(
maxFeePerGasInWei * transactionGas,
(currentGasPriceInWei + maxPriorityFeePerGasInWei) * transactionGas,
);Let's update the Custom UI output to show that:
return {
content: panel([
heading('Percent Snap'),
text(
`As setup, this transaction would cost **${
gasFees / 1_000_000_000
}** gwei in gas.`,
),
]),
};Reinstall your snap, then reload the "PERCENT SNAP" transaction insights tab. You should now see a message like:
As setup, this transaction would cost 238377.74415 gwei in gas.
Calculating the percentage of gas fees paid should now be easy:
const transactionValueInWei = parseInt(transaction.value as string, 16);
const gasFeesPercentage = (gasFees / (gasFees + transactionValueInWei)) * 100;
return {
content: panel([
heading('Percent Snap'),
text(
`As setup, you are paying **${gasFeesPercentage.toFixed(
2,
)}%** in gas fees for this transaction.`,
),
]),
};Reinstall your snap, reactivate the "PERCENT SNAP" tab, and you should see a message like this:
As setup, you are paying 0.17% in gas fees for this transaction.
Well done! One more step to go π₯
Our transaction insights snap should only display a percentage if the user is doing a regular ETH transfer. For contract interactions, we should display a UI that conveys that message. Let's add this code to the beginning of our onTransaction export:
if (typeof transaction.data === 'string' && transaction.data !== '0x') {
return {
content: panel([
heading('Percent Snap'),
text(
'This snap only provides transaction insights for simple ETH transfers.',
),
]),
};
}This completes the creation of our snap. Good work π¦
In this workshop I chose to focus on the Transaction Insights feature of MetaMask Snaps. If you'd like to see a similar workshop on accounts and key management, look to the Dogecoin Snap Tutorial!
- KeystoneHQ's Bitcoin Snap - A snap to manage your bitcoin keys and transactions.
- StarkNet Snap by ConsenSys - Allows to deploy StarkNet accounts, make transactions on StarkNet, and interact with StarkNet smart contracts.
- Snappy Recovery - I built this Private Key Social Recovery snap for my interview as Developer Advocate at MetaMask. It's currently outdated vs. the latest MetaMask Flask.
The Snaps platform is extremely powerful. In addition to letting you provide transaction insights, Snaps also allow you to:
- Derive private keys for different coin types
- Derive snap-specific entropy
- Run cronjobs
- Display notifications
- Store encrypted data in the snaps' sandbox
We're excited to see what you'll be building with Snaps ππ¨π»βπππ§π₯ You can reach out to us on the following resources:
- Discord in the
#snaps-devchannel - GitHub Discussions for
@metamask/snaps-monorepo - Twitter @MetaMaskDev
Thank you for taking the time to go through this workshop, and learing more about MetaMask Snaps π§‘