Try   HackMD

Getting Started with MetaMask Snaps

Devcon VI Workshop

Kudos to @Mrtenz for the Snap implementation.

Resources

Before We Begin

  1. Ensure that you have the following dependencies:
    • A Chromium browser or Firefox
    • git
    • Node.js ^16.0.0
    • yarn
      • yarn can also be installed after cloning the repository, see below.
  2. Install MetaMask 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 and @metamask/abi-utils.

  1. Create a new GitHub repository using the 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 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 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 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.

    • 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 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.

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',
}
// 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 in our Snap. Here is a snippet with some constants:

// 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

/**
 * 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;
}