Skip to main content

Snap anatomy

If you look at the directory structure of the Snaps template repository used in the Snaps quickstart, it looks something like this:

template-snap-monorepo/
├─ packages/
│ ├─ site/
| | |- src/
| | | |- App.tsx
| | ├─ package.json
| | |- ...(react app content)
| |
│ ├─ snap/
| | ├─ src/
| | | |- index.ts
| | ├─ snap.manifest.json
| | ├─ package.json
| | |- ... (snap content)
├─ package.json
├─ ... (other stuff)

Source files other than index.ts are located through its imports. The defaults can be overwritten in the configuration file.

Create a snap project

When you create a new snap project using mm-snap init, it has all these files. Still, we recommend cloning the template snap repository to get started.

This page examines the major components of a snap:

Source code

If you're familiar with JavaScript or TypeScript development, developing a snap might feel familiar to you. Consider this simple snap, hello-snap:

index.ts
module.exports.onRpcRequest = async ({ origin, request }) => {
switch (request.method) {
// Expose a "hello" RPC method to dapps
case 'hello':
return 'world!';

default:
throw new Error('Method not found.');
}
};

To communicate with the outside world, the snap must implement its own JSON-RPC API by exposing the exported function onRpcRequest. Whenever the snap receives a JSON-RPC request from a dapp or another snap, this handler function is called with the specified parameters.

In addition to being able to expose a JSON-RPC API, snaps can access the global object snap. You can use this object to make Snaps-specific JSON-RPC requests.

If a dapp wants to use hello-snap, it can implement something like this:

// Connect to the snap, enabling its usage inside the dapp
await window.ethereum.request({
method: 'wallet_enable',
params: [
{
wallet_snap: {
'npm:hello-snap': {
version: '^1.0.0',
},
},
},
],
});

// Invoke the "hello" RPC method exposed by the snap
const hello = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: { snapId: 'npm:hello-snap', request: { method: 'hello' } },
});

console.log(hello); // 'world!'

The snap's RPC API is completely up to you, as long as it's a valid JSON-RPC API.

Does my snap need to have an RPC API?

No, that's also up to you! If your snap can do something useful without receiving and responding to JSON-RPC requests, such as providing transaction insights, then you can skip exporting onRpcRequest. However, if you want to do something such as manage the user's keys for a particular protocol and create a dapp that, for example, sends transactions for that protocol using your snap, you must specify an RPC API.

Manifest file

To get MetaMask to execute your snap, you must have a valid manifest file named snap.manifest.json, located in your package root directory. The manifest file of hello-snap would look something like this:

{
"version": "1.0.0",
"proposedName": "hello-snap",
"description": "A snap that says hello!",
"repository": {
"type": "git",
"url": "https://github.com/Hello/hello-snap.git"
},
"source": {
"shasum": "w3FltkDjKQZiPwM+AThnmypt0OFF7hj4ycg/kxxv+nU=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
"iconPath": "images/icon.svg",
"packageName": "hello-snap",
"registry": "https://registry.npmjs.org/"
}
}
},
"initialPermissions": {},
"manifestVersion": "0.1"
}

The manifest tells MetaMask important information about your snap, such as where it's published (using source.location) and how to verify the integrity of the snap source code (by attempting to reproduce the source.shasum value).

note

Currently, snaps can only be published to the official npm registry, and the manifest must also match the corresponding fields of the package.json file. In the future, developers will be able to distribute snaps in different ways, and the manifest will expand to support different publishing solutions.

The snaps publishing specification details the requirements of both snap.manifest.json and its relationship to package.json.

You might need to modify some manifest fields manually. For example, if you change the location of the (optional) icon SVG file, you must update source.location.npm.iconPath to match. You can also use the command line to update some fields for you. For example, mm-snap build or mm-snap manifest --fix updates source.shasum.

Configuration file

The snap configuration file, snap.config.js, should be placed in the project root directory. You can override the default values of the Snaps CLI options by specifying them in the cliOptions property of the configuration file. For example:

module.exports = {
cliOptions: {
src: 'lib/index.js',
dist: 'out',
port: 9000,
},
};

If you want to customize the Browserify build process, you can provide the bundlerCustomizer property. It's a function that takes one argument, the browserify object which MetaMask uses internally to bundle the snap. You can transform it in any way you want, for example, adding plugins. The bundleCustomizer function looks something like this:

const brfs = require('brfs');

module.exports = {
cliOptions: {
/* ... */
},
bundlerCustomizer: (bundler) => {
bundler.transform(brfs);
},
};
note

You should not publish the configuration file to NPM, since it's only used for development and building. However, you can commit the file to GitHub to share the configuration with your team, since it shouldn't contain any secrets.

Bundle file

Because of the way snaps are executed, they must be published as a single .js file containing the entire source code and all dependencies. Moreover, the Snaps execution environment has no DOM, no Node.js APIs, and no filesystem access, so anything that relies on the DOM doesn't work, and any Node built-ins must be bundled along with the snap.

Use the command mm-snap build to bundle your snap using Browserify. This command finds all dependencies using your specified main entry point and outputs a bundle file to your specified output path.