# Getting Started with MetaMask Snaps _Devcon VI Workshop_ Kudos to [@Mrtenz](https://github.com/Mrtenz) for the Snap implementation. ## Resources - [Snaps landing page](https://metamask.io/snaps) - [Snaps documentation](https://docs.metamask.io/guide/snaps.html) - [Snaps discussion board](https://github.com/MetaMask/snaps-monorepo/discussions) - [Template snap monorepo](https://github.com/MetaMask/template-snap-monorepo) - [Completed workshop code](https://github.com/rekmarks/devcon-2022-snap/tree/completed) ## Before We Begin 1. Ensure that you have the following dependencies: - A Chromium browser or Firefox - `git` - [Node.js](https://nodejs.org/en/) `^16.0.0` - [`yarn`](https://yarnpkg.com/getting-started/install) - `yarn` can also be installed after cloning the repository, see below. 2. Install [MetaMask Flask](https://metamask.io/flask/). - _During Devcon VI, a prerelease build of MetaMask Flask was required, but this is no longer the case._ 3. Add an existing secret recovery phrase that you aren't afraid to lose, or create a new one. ## The Workshop Our goal is to create a snap that decodes contract call function names and parameters using the [4byte API](https://www.4byte.directory/docs/) and [`@metamask/abi-utils`](https://npmjs.com/package/@metamask/abi-utils). 1. Create a new GitHub repository using the [template snap monorepo](https://github.com/MetaMask/template-snap-monorepo). - This template contains two workspaces, one for the snap and one for the website / dapp that will serve as its user interface. - `git clone` your new repository. - Run `yarn install` at the monorepo root. - If you do not already have `yarn` installed, you can start the installation by directly running `.yarn/releases/yarn-3.2.1.cjs install` from the monorepo root. 2. Boot the snap and website by running `yarn start` at the monorepo root. - This will set up live reloading for both the snap and the website. 3. Install the example snap by clicking `Connect` on the website, then try out its functionality by clicking `Send message`. - The snap will be executed by MetaMask in a sandboxed environment and display a confirmation. 4. Rewrite the snap to use the transaction insights API. - In `snap.manifest.json`, replace the `snap_confirm` permission with `endowment:transaction-insight`. - In `index.ts`, replace the `onRpcRequest` handler with an `onTransaction` handler. Make sure it is exported. - We will add some basic validation. Add [`@metamask/utils`](https://npmjs.com/package/@metamask/utils) as a dependency of your snap and import it in your snap's `index.ts` file. 5. Customize the website to suit your snap, and rewrite the `Send message` logic to create an Ethereum transaction instead. - Don't forget to add logic for requesting Ethereum accounts so that you are able to submit transactions to MetaMask. - You can use the address from `TransactionConstants` as the recipient address. See [Transaction Snippets](#Transaction-Snippets) below. 6. Click `Reconnect` in the UI to reinstall the snap, then click `Send message` to observe your "decoded transaction". 7. Call the 4byte API from within your snap. - Snaps do not get network access by default. Add `endowment:network-access` to your snap's permissions. - Copy the [4byte Snippet](#4byte-Snippet) below for the API URL and some helpful types. - Return some of the data from 4byte as an "insight" so we know that it works. 8. Finally, decode the parameters using [`@metamask/abi-utils`](https://npmjs.com/package/@metamask/abi-utils). - Add `@metamask/abi-utils` as a dependency of your snap and import it in your snap's `index.ts` file. - Copy the `normalizeAbiValue` function from the [Value Normalizer Snippet](#Value-Normalizer-Snippet) and paste it into your snap's `index.ts` file. - Extract the parameter types, decode them using `@metamaks/abi-utils`, and return your your completed insights. ### Transaction Snippets We will demonstrate our snap's functionality by decoding some mock contract transactions that we create. You can of course try some contract interactions on your own as well. ```typescript enum TransactionConstants { // The address of an arbitrary contract that will reject any transactions it receives Address = '0x08A8fDBddc160A7d5b957256b903dCAb1aE512C5', // Some example encoded contract transaction data UpdateWithdrawalAccount = '0x83ade3dc00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000047170ceae335a9db7e96b72de630389669b334710000000000000000000000006b175474e89094c44da98b954eedeac495271d0f', UpdateMigrationMode = '0x2e26065e0000000000000000000000000000000000000000000000000000000000000000', UpdateCap = '0x85b2c14a00000000000000000000000047170ceae335a9db7e96b72de630389669b334710000000000000000000000000000000000000000000000000de0b6b3a7640000', } ``` ```typescript // A function that sends contract transactions export const sendContractTransaction = async (data: string) => { // Get the user's account from MetaMask. const [from] = (await window.ethereum.request({ method: 'eth_requestAccounts', })) as string[]; // Send a transaction to MetaMask. await window.ethereum.request({ method: 'eth_sendTransaction', params: [ { from, to: TransactionConstants.Address, value: '0x0', data, }, ], }); }; ``` ### 4byte Snippet We will use the [4byte API](https://www.4byte.directory/docs/) in our Snap. Here is a snippet with some constants: ```typescript // The API endpoint to get a list of functions by 4 byte signature. const API_ENDPOINT = 'https://www.4byte.directory/api/v1/signatures/?hex_signature='; /* eslint-disable camelcase */ type FourByteSignature = { id: number; created_at: string; text_signature: string; hex_signature: string; bytes_signature: string; }; /* eslint-enable camelcase */ ``` ### Value Normalizer Snippet ```typescript /** * The ABI decoder returns certain which are not JSON serializable. This * function converts them to strings. * * @param value - The value to convert. * @returns The converted value. */ function normalizeAbiValue(value: unknown): Json { if (Array.isArray(value)) { return value.map(normalizeAbiValue); } if (value instanceof Uint8Array) { return bytesToHex(value); } if (typeof value === 'bigint') { return value.toString(); } return value as Json; } ```