1. Crypto

The Ultimate Guide to MetaMask Snaps

First of all, what is MetaMask?

MetaMask is one of the most popular wallets in the world. It’s a cryptocurrency wallet that allows storage of Ether and other ERC-20 tokens along with interacting with decentralized applications (dapps). Metamask became popular due to its elegant UX/UI and browser integration: it’s a browser plugin that allows users to transact with any Ethereum address.

What are MetaMask Snaps?

Snaps are dapps that can extend the features of the Metamask wallet. Metamask is the first wallet to provide this level of flexibility and extensibility to developers. Everyone is able to build and use Snaps without Metamask’s permission, opening up a multitude of possibilities to experiment with the wallet. Snaps can encompass a wide range of functionalities, ranging from opening up the wallet to other blockchains beyond Ethereum, to connecting with APIs, and integrating with any scaling solution which makes diving into the world of Layer 2s accessible to all.

In terms of security, a Snap executes in a sandboxed environment based on Hardened Javascript by Agoric. Snaps communicate with Metamask using the JSON-RPC.

It also allows developers to extend the security features of Metamask itself with creative methods of data protection and anti-phishing solutions.

Developing Snaps

Looking to build a Snap? If you’ve written a Javascript program before, they’re not that different than developing any other Javascript program, with exceptions in the details. We’ll dive more into a sample proof of concept app later in the article.

In terms of distribution, there isn’t a “snap store” directory right now to reach a wider audience, so the developer is either building snaps for themselves or needs to have a website that first asks user to install MetaMask Flask followed by installing the snap using the wallet_enable RPC method.
Features of Metamask Snaps

Snaps are built on any protocol. Ethereum, Metamask’s original protocol, now serves as just the jumping off point for developers. Enterprising developers are now able to build multichain projects to extend Metamask functionality in new and exciting ways. Snaps can integrate with any chosen scaling solution that allows Layer 2 accessibility, connect APIs and services to Metamask in new ways, create new ways to authenticate and keep user identity private, and allows users to protect their data.

MetaMask Snap Rundown

Goes beyond Ethereum: Built on any protocol
Scaling, Layer 2: Integrates scaling solutions that allows Layer 2 accessibility
API Functionality: Connects APIs and services to MetaMask in novel ways
Identity: Has authentication, privacy, and consent features for user control
Security: Allows users to protect their data and employ anti-phishing solutions

How to Run a Metamask Snap

In order to interact with your snaps, Metamask Flask needs to be installed – this is a canary distribution for developments that gives access to upcoming features. The core functionality allows snaps to communicate with websites and MetaMask via APIs.

Snaps can run untrusted Javascript inside of Metamask in a sandboxed environment. They have no DOM, Node.js built-ins, or platform-specific APIs, meaning they can run safely anywhere. Snaps run an isolated environment determined by user permissions and use Secure EcmaScript (SES), a subset of Javascript.

They need to be distributed as npm packages on the official npm registry at the moment.

How to Install MetaMask Flask

Here’s how to install MetaMask Flask so you can experiment and run snaps.

  • In a Firefox or Chrome browser, create a new browser profile
  • Make sure to disable any existing installed versions of MetaMask.
  • Shut down multiple instances of Metamask in the same browser profile
  • Download Flask and add it to your chosen browser.

About Snaps and RPC

Developer requests and responses used for snaps is JSON-RPC.

  • Snaps RPC API is entirely up to the developer as long as it’s valid
  • Snaps can create new RPC methods for websites to call
  • Call many of the same RPC methods websites can call
  • Access a limited set of snap-exclusive RPC methods

Technical Details of Snaps

Snaps have an “ephemeral” lifestyle – they shut down when idle or when they become unresponsive. A snap is considered idle if it not received a JSON-RPC request for 30 seconds and is considered unresponsive if it takes more than 60 seconds to process a JSON-RPC request.

Many snaps need to use websites (dapps) to show data to users since the only way a snap can modify the MetaMask UI is through the snap_confirm RPC method or through the settings page.

In order to publish a snap, you publish it as an npm package.

Build Tools for Snaps

Build tools are available for snaps. The ones currently available are:

Metamask Snaps Examples

Here are some existing examples of snaps.

How to Build a Snap

To view our example repository, click here: https://github.com/DakaiGroup/metamask-snaps

Metamask Snaps Test

This is a `next.js` app made to showcase some of the basic functionality of Metamask Snaps

Getting Started

Clone the repository by `git clone https://github.com/DakaiGroup/metamask-snaps.git`

Install all packages

yarn install

Run the development server:

yarn dev

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying `pages/index.tsx` and `packages/snap/src/index.ts`

Start your own project

Requirements

Metamask Flask installed (Metamask uninstalled or disabled)

Setup

Install Metamask packages

yarn add -D @metamask/snaps-cli @metamask/snap-types @metamask/eslint-config @metamask/eslint-config-nodejs @metamask/eslint-config-typescript

Create required files

Create a `snap.config.js` and a `snap.manifest.json` file in your project directory. These are required files and your snaps won`t even start without them.

Sample `snap.config.js` file:

module.exports = {
  cliOptions: {
    port: 8082, //Can be any free port
    dist: "dist",
    outfileName: "bundle.js",
    src: "./packages/snap/src/index.ts", //If you structure your snap files differently, be sure to update this to the relevant path
  },
};

Sample `snap.manifest.json` file:

{
  "version": "0.1.0",
  "description": "Your description",
  "proposedName": "Your snap`s name",
  "source": {
    "shasum": "hash of the package, managed and updated by mm-cli",
    "location": {
      "npm": {
        "filePath": "dist/bundle.js",
        "packageName": "your-package-name",
        "registry": "https://registry.npmjs.org/"
      }
    }
  },
  "initialPermissions": {
    "snap_confirm": {}
  },
  "manifestVersion": "0.1"
}

You can read more about the structure of snaps in the MetaMask Docs.

Add types and type definitions

Add the following line to your `tsconfig.json`.

"files": ["./node_modules/@metamask/snap-types/global.d.ts"],

Without this, typescript won’t be able to recognize the `wallet` object provided by `snap-types`.

Add the following code to your `package.json`.

 "files": [
    "dist/",
    "snap.manifest.json"
  ]

Create a `global.d.ts` file in your project directory and add the following code:

import { MetaMaskInpageProvider } from "@metamask/providers";

declare global {
  interface Window {
    ethereum?: MetaMaskInpageProvider;
  }
}

This will tell typescript that there is an `ethereum` object on the `window` global, provided by Metamask.

Scripts

To run both a `next.js developer server` and the `snaps node server` you have to add two simple scripts to the `package.json` file.

"dev": "next dev & yarn watch",
"watch": "mm-snap watch"

Now you can start your servers with

yarn dev

JSON-RPC

Snaps communicate with your `dapp` through a protocol called `JSON-RPC`. A `JSON-RPC` call consists of 3 main properties.

1. `method`: This is a string referring to the called remote procedure.
2. `params`: This can be an Object or an Array sent to the called remote function.
3. `id`: This is a number or string

Hello World

To get a basic `hello word` to work, you have to know the `snapId`. Paste the following code to the page of your application to set it dynamically on the first render of the page.

const [snapId, setSnapId] = useState<string>("");

useEffect(() => {
  if (typeof window === "undefined") {
    return;
  }
  const id =
    window.location.hostname === "localhost"
      ? `local:${window.location.protocol}//${window.location.hostname}:${snapCfg.cliOptions.port}`
      : `npm:${snapManifest.source.location.npm.packageName}`;
  setSnapId(id);
}, []);

With the `snapId` acquired you can connect the user’s metamask wallet to your application. The following code will connect your user and install your snap.

const handleConnectMetamask = async () => {
  try {
    await window?.ethereum?.request({
      method: "wallet_enable",
      params: [
        {
          wallet_snap: {
            [snapId]: {},
          },
        },
      ],
    });
  } catch (error) {
    console.error("Failed to connect wallet", error);
  }
};

> Snaps require a reinstall on changes. So when, in the future, you make changes to your snap reconnect and reinstall it for the changes to properly take effect.

To create your first method create/navigate to the `packages/snap/src/index.ts` file and paste the following code.

import { OnRpcRequestHandler } from "@metamask/snap-types";

export const onRpcRequest: OnRpcRequestHandler = async () => {
  //Your snap code...
};

Get the request object from the function arguments and add a `switch` statement evaluating the `request.method`.

export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
  switch (request.method) {
    case "hello":
      return "World";
    default:
      throw new Error("Method not found.");
  }
};

With that done, your snap is ready to be called. Navigate back to `pages/index.tsx` file and add the following function call.

const handleClick = async () => {
  try {
    const response = await window?.ethereum?.request({
      method: "wallet_invokeSnap",
      params: [
        snapId,
        {
          method: "hello",
        },
      ],
    });
    window.alert(response);
  } catch (error) {
    console.error(error);
  }
};

Assign the function to a `click` event and restart your development server. Reconnect metamask, install your snap and hit the button. You should see an alert popup containing your response message `World`.

Send Data to your snap

You can also send data to your snap through the params property of the `JSON-RPC` call.

First, add the params property and pass some data into it. The second element of the params array is also a `JSON-RPC` request object, so you can send data to your snap with it.

const handleClick = async () => {
  try {
    const response = await window?.ethereum?.request({
      method: "wallet_invokeSnap",
      params: [
        snapId,
        {
          method: "hello",
          params: { hello: "world" },
        },
      ],
    });
    window.alert(response);
  } catch (error) {
    console.error(error);
  }
};

Now head back to your snap source file and modify it to use the passed data. You can access the sent data through `request.params`.

export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
  switch (request.method) {
    case "hello":
      const { hello } = request.params as { hello: string };
      return `Hello, ${hello} !`;
    default:
      throw new Error("Method not found.");
  }
};

That’s it! Now restart your application and you should see `”Hello, world !”` alerted when clicking the button.

Use Built-in RPCs

You can use built-in RPC to ask for confirmation, manage state, and so on. All you need to do is to call them in your snap.

export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
  switch (request.method) {
    case "hello":
      return wallet.request({
        method: "snap_confirm",
        params: [
          {
            prompt: `Hello, World!`,
          },
        ],
      });
    default:
      throw new Error("Method not found.");
  }
};

The `snap_confirm` method returns a `true` or `false` depending on if the user has approved or denied your confirmation.

case "hello":
    const isApproved = await wallet.request({
    method: "snap_confirm",
    params: [
        {
        prompt: `Hello, World?`,
        },
    ],
    });
    return isApproved ? "Hello" : "World";

Manage State

You can also manage state with the built-in `snap_manageState` RPC. Use `update`,`get` and `clear` parameter to perform state operations.

case "save_state":
    const {state} = request.params;
    await wallet.request({
        method:"snap_manageState",
        params:["update", {state}]
    });
    return "OK";

case "get_state":
    const state = await waller.request({
        method:"snap_manageState",
        params:["get"]
    });
    return state;

case "clear_state":
    await wallet.request({
        method:"snap_manageState",
        params:["clear"]
    });
    return "OK";

You can read more about built-in methods in the MetaMask Docs.

Use 3rd party API data

You can call webAPIs in your snap as well. First, you need to ask for `network-access` permission. This needs to happen when installing the snap, so you need to add `endowment:network-access` to your `snap.manifest.json`’s `initialPermissions` property.

"initialPermissions": {
    "snap_confirm": {},
    "endowment:network-access": {}
  }

With the permission added, you can use `fetch` in your snap to interact with any API.

case "pikachu":
    const { name } = await (
    await fetch("https://pokeapi.co/api/v2/pokemon/pikachu")
    ).json();
    return name;

You can read more about the execution environment and globals in the MetaMask Docs.

Useful links and resources

MetaMask Snaps Monorepo
MetaMask Docs