Snaps 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.
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:
- The source code contains the primary code of the snap.
- The manifest file tells MetaMask important information about the snap.
- The configuration file specifies configuration options for the snap.
- The bundle file is the output file of the published 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
:
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.
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).
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);
},
};
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.