This is a next.js
app made to showcase some of the basic functionality of Metamask Snaps
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
- Metamask Flask installed (Metamask uninstalled or disabled)
yarn add -D @metamask/snaps-cli @metamask/snap-types @metamask/eslint-config @metamask/eslint-config-nodejs @metamask/eslint-config-typescript
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 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.
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
Snaps communicate with your dapp
through a protocol called JSON-RPC
. A JSON-RPC
call consists of 3 main properties.
method
: This is a string refering to the called remote procidure.params
: This can be an Object or an Array sent to the called remote function.id
: This is a number or string
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
.
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 acces 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.");
}
};
Thats it! Restart your application and you should see "Hello, world !"
alerted when clicking the button.
You can use built in RPC to ask 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";
You can also manage state with the built in snap_manageState
RPC. You can 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
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