diff --git a/.eslintignore b/.eslintignore
index 9b4fa7e2c7f..59dbc1627b1 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
/build
-/.yarn
\ No newline at end of file
+/.yarn
+/external/keyring-api
\ No newline at end of file
diff --git a/.github/workflows/build-lint.yml b/.github/workflows/build-lint.yml
index f01758d2528..799f42ba7f7 100644
--- a/.github/workflows/build-lint.yml
+++ b/.github/workflows/build-lint.yml
@@ -9,6 +9,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: true
- name: Use Node.js
uses: actions/setup-node@v3
with:
@@ -24,6 +26,8 @@ jobs:
- prepare
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: true
- name: Use Node.js
uses: actions/setup-node@v3
with:
@@ -46,6 +50,8 @@ jobs:
- prepare
steps:
- uses: actions/checkout@v3
+ with:
+ submodules: true
- name: Use Node.js
uses: actions/setup-node@v3
with:
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index 86fed01b580..9be26bdfce3 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -20,6 +20,7 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ inputs.ref }}
+ submodules: true
- name: Use Node.js
uses: actions/setup-node@v3
with:
diff --git a/.gitignore b/.gitignore
index 8dea2eddc4a..aae1c6400f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
.docusaurus
.cache-loader
.idea
+/snaps/reference/keyring-api
# yarn v3 (w/o zero-install)
# See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000000..a7e1104515b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "external/keyring-api"]
+ path = external/keyring-api
+ url = git@github.com:MetaMask/keyring-api.git
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2ab108013fa..970b48f3b37 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,5 @@
{
"eslint.format.enable": true,
- "eslint.packageManager": "yarn",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
diff --git a/docusaurus.config.js b/docusaurus.config.js
index f6b347a722c..62024f81c22 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -3,6 +3,7 @@
const codeTheme = require("prism-react-renderer/themes/dracula");
const remarkCodesandbox = require("remark-codesandbox");
+const path = require("path");
/** @type {import('@docusaurus/types').Config} */
const config = {
@@ -79,6 +80,19 @@ const config = {
],
}),
],
+ [
+ "docusaurus-plugin-typedoc",
+ {
+ entryPoints: ["./external/keyring-api/src/index.ts"],
+ tsconfig: "./external/keyring-api/tsconfig.json",
+ out: path.join(__dirname, "snaps/reference/keyring-api"),
+ sidebar: {
+ categoryLabel: "Keyring API",
+ position: 99,
+ },
+ identifiersAsCodeBlocks: true,
+ },
+ ],
[
"@docusaurus/plugin-client-redirects",
{
diff --git a/external/keyring-api b/external/keyring-api
new file mode 160000
index 00000000000..1c8eeb9beef
--- /dev/null
+++ b/external/keyring-api
@@ -0,0 +1 @@
+Subproject commit 1c8eeb9beef7f21ca0bd394b6fe06fea856e3520
diff --git a/package.json b/package.json
index 476541971be..432e235a909 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"lint": "eslint .",
- "lint:fix": "eslint . --fix"
+ "lint:fix": "eslint . --fix",
+ "postinstall": "cd external/keyring-api && yarn install"
},
"dependencies": {
"@docusaurus/core": "2.4.1",
@@ -24,12 +25,17 @@
"@metamask/design-tokens": "^1.11.1",
"@metamask/docusaurus-openrpc": "^0.2.2",
"clsx": "^1.2.1",
+ "docusaurus-plugin-typedoc": "next",
"node-polyfill-webpack-plugin": "^2.0.1",
+ "prettier": "^3.0.0",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remark-codesandbox": "^0.10.1",
- "remark-docusaurus-tabs": "^0.2.0"
+ "remark-docusaurus-tabs": "^0.2.0",
+ "typedoc": "^0.25.1",
+ "typedoc-plugin-frontmatter": "^0.0.2",
+ "typedoc-plugin-markdown": "next"
},
"devDependencies": {
"@docusaurus/eslint-plugin": "2.4.1",
@@ -75,7 +81,8 @@
"@metamask/docusaurus-openrpc>@metamask/open-rpc-docs-react>@stoplight/mosaic>@fortawesome/fontawesome-svg-core": false,
"@metamask/docusaurus-openrpc>@metamask/open-rpc-docs-react>@stoplight/mosaic>@fortawesome/fontawesome-svg-core>@fortawesome/fontawesome-common-types": false,
"@docusaurus/core>webpack-dev-server>ws>bufferutil": false,
- "@docusaurus/core>webpack-dev-server>ws>utf-8-validate": false
+ "@docusaurus/core>webpack-dev-server>ws>utf-8-validate": false,
+ "$root$": false
}
}
}
diff --git a/snaps/assets/keyring/accounts-ui.png b/snaps/assets/keyring/accounts-ui.png
new file mode 100644
index 00000000000..08f13f4f150
Binary files /dev/null and b/snaps/assets/keyring/accounts-ui.png differ
diff --git a/snaps/assets/keyring/add-snap-account.png b/snaps/assets/keyring/add-snap-account.png
new file mode 100644
index 00000000000..54253be9ff2
Binary files /dev/null and b/snaps/assets/keyring/add-snap-account.png differ
diff --git a/snaps/assets/keyring/asynchronous-flow.png b/snaps/assets/keyring/asynchronous-flow.png
new file mode 100644
index 00000000000..a2450b6184c
Binary files /dev/null and b/snaps/assets/keyring/asynchronous-flow.png differ
diff --git a/snaps/assets/keyring/components-diagram.png b/snaps/assets/keyring/components-diagram.png
new file mode 100644
index 00000000000..50d9470040c
Binary files /dev/null and b/snaps/assets/keyring/components-diagram.png differ
diff --git a/snaps/assets/keyring/create-account-flow.png b/snaps/assets/keyring/create-account-flow.png
new file mode 100644
index 00000000000..952f17cb652
Binary files /dev/null and b/snaps/assets/keyring/create-account-flow.png differ
diff --git a/snaps/assets/keyring/synchronous-flow.png b/snaps/assets/keyring/synchronous-flow.png
new file mode 100644
index 00000000000..7ee914fe85d
Binary files /dev/null and b/snaps/assets/keyring/synchronous-flow.png differ
diff --git a/snaps/concepts/keyring-api.md b/snaps/concepts/keyring-api.md
new file mode 100644
index 00000000000..7d4e752eeeb
--- /dev/null
+++ b/snaps/concepts/keyring-api.md
@@ -0,0 +1,166 @@
+---
+description: Learn about the Snaps Keyring API.
+sidebar_position: 6
+---
+
+# About the Keyring API
+
+:::caution important
+This API is only available in [MetaMask Flask](../get-started/install-flask.md), the canary
+distribution of MetaMask.
+:::
+
+:::tip API documentation
+See the [Keyring API reference](../reference/keyring-api/index.md) for all the Keyring API methods.
+:::
+
+The Snaps Keyring API integrates custom EVM accounts inside MetaMask.
+Previously, you needed a companion dapp to display custom EVM accounts, such multi-party computation
+(MPC) accounts.
+Now you can display these custom accounts alongside regular MetaMask accounts in the UI:
+
+
+
+
+
+[Create a Keyring snap to integrate custom EVM accounts in MetaMask.](../tutorials/custom-evm-accounts.md)
+Your dapp can then use the [`eth_requestAccounts`](/wallet/reference/eth_requestaccounts) MetaMask
+JSON-RPC API method to connect to the custom accounts, and seamlessly interact with them using other
+[JSON-RPC methods](/wallet/reference/eth_subscribe).
+
+## Terminology
+
+The following terminology is used across the Keyring API:
+
+- **Blockchain account**: An object in a single blockchain, representing an account, with its
+ balance, nonce, and other account details.
+- **Request**: A request from a dapp to MetaMask.
+- **Keyring account**: An account model that represents one or more blockchain accounts.
+- **Keyring snap**: A snap that implements the Keyring API.
+- **Keyring request**: A request from MetaMask to a Keyring snap.
+ MetaMask wraps the original request sent by the dapp and adds some metadata to it.
+
+## Components diagram
+
+The following diagram shows the components you encounter when interacting with accounts managed by a
+Keyring snap:
+
+
+
+
+
+
+
+- **User**: The user interacting with the snap, the dapp, and MetaMask.
+- **Dapp**: The dapp requesting an action to be performed on an account.
+- **MetaMask**: The wallet the dapp connects to.
+ MetaMask routes requests to the Keyring snaps and lets the user perform some level of account management.
+- **Snap**: A snap that implements the Keyring API to manage the user's accounts, and to handle
+ requests that use these accounts.
+- **Snap UI**: The snap's UI component that allows the user to interact with the snap to perform
+ custom operations on accounts and requests.
+
+## Keyring interface
+
+The first step to create a Keyring snap is to implement the
+[`Keyring`](../reference/keyring-api/03-Type%20Aliases/02-type-alias.Keyring.md) interface.
+This interface describes all the methods necessary to make your custom EVM accounts work inside
+MetaMask with your own logic.
+
+The following sections describe the different flows that the `Keyring` interface handles.
+
+### Snap account creation flow
+
+The first interaction between users and the Keyring snap is the snap account creation process.
+The flow looks like the following:
+
+
+
+The MetaMask account selection modal has an option called **Add snap account**:
+
+
+
+
+
+This option shows a list of Keyring snaps.
+Each snap redirects the user to the companion dapp that contains all the UI to configure and manage the snap.
+
+The dapp presents a custom UI allowing the user to configure their custom EVM account.
+The dapp uses the [`createAccount`](../reference/keyring-api/02-Classes/04-class.KeyringSnapRpcClient.md#createaccount)
+method of the `KeyringSnapRpcClient`, which calls the `Keyring` interface's method of the same name.
+You can find an example of this in the [example Keyring snap companion dapp](https://github.com/MetaMask/snap-simple-keyring/blob/d3f7f0156c59059c995fea87f90a3d0ad3a4c135/packages/site/src/pages/index.tsx#L136).
+
+The `createAccount` method of the `Keyring` interface creates the account based on the parameters passed
+to the method.
+The snap keeps track of the accounts that it creates using [`snap_manageState`](../reference/rpc-api.md#snap_managestate).
+Once the snap has created an account, it notifies MetaMask using the
+[`createAccount`](../reference/rpc-api.md#createaccount) sub-method of `snap_manageAccounts`.
+You can find an example of this process in the
+[example companion dapp](https://github.com/MetaMask/snap-simple-keyring/blob/d3f7f0156c59059c995fea87f90a3d0ad3a4c135/packages/snap/src/keyring.ts#L61).
+
+Once the snap has created an account, that account can be used to sign messages and transactions.
+
+### Synchronous signing flow
+
+If the Keyring snap can sign transactions directly, it implements a simple synchronous signing flow.
+If the snap needs a third party such as a hardware key or a second account's signature (for example,
+in a threshold signature scheme), it implements an [asynchronous signing flow](#asynchronous-signing-flow).
+The synchronous flow looks like the following:
+
+
+
+See the [example Keyring snap companion dapp](https://github.com/MetaMask/snap-simple-keyring) for a
+full example.
+
+The flow starts when a dapp calls a [MetaMask JSON-RPC method](/wallet/reference/eth_subscribe), or
+when the user initiates a new funds transfer from the MetaMask UI.
+At that point, MetaMask detects that this interaction is requested for an account controlled by the
+Keyring snap.
+
+After the user approves the transaction in the UI, MetaMask calls the `submitRequest` method of the
+`Keyring` interface.
+`submitRequest` receives the original RPC request, and returns a
+[`SubmitRequestResponse`](../reference/keyring-api/04-Variables/05-variable.SubmitRequestResponseStruct.md)
+with `pending` set to `false`, and `result` set to the requested signature.
+
+:::caution important
+If the Keyring snap receives an
+[`eth_sendTransaction`](/wallet/reference/eth_sendTransaction) request, it should treat it like an
+[`eth_signTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction) request.
+That is, the snap is responsible for providing the signature in the response, and MetaMask is
+responsible for broadcasting the transaction.
+:::
+
+### Asynchronous signing flow
+
+If the Keyring snap implements a complex scheme such as threshold signing, it implements an
+asynchronous signing flow with more `Keyring` methods.
+The asynchronous flow looks like the following:
+
+
+
+The flow starts the same way as the [synchronous flow](#synchronous-signing-flow): a dapp or user
+initiates a request to sign a transaction or arbitrary data.
+After approval, the `submitRequest` method of the snap's `Keyring` interface is called.
+
+Since the snap doesn't answer the request directly, it stores the pending request in its internal
+state using [`snap_manageState`](../reference/rpc-api.md#snap_managestate).
+This list of pending requests is returned when the `listRequests` or `getRequest` methods of the
+`Keyring` interface are called.
+
+After storing the pending request, the snap creates a pop-up using
+[`snap_dialog`](../reference/rpc-api.md#snap_dialog) instructing the user to go to the companion
+dapp's URL.
+
+The dapp lists the snap's pending requests using an RPC call facilitated by the
+[`listRequests`](../reference/keyring-api/02-Classes/04-class.KeyringSnapRpcClient.md#listrequests)
+method of the `KeyringSnapRpcClient`.
+The user can then act on those requests using whatever process applies to the snap.
+
+Once the signing process completes, the companion dapp resolves the request using the
+[`approveRequest`](../reference/keyring-api/02-Classes/04-class.KeyringSnapRpcClient.md#approverequest)
+method of the `KeyringSnapRpcClient`, which calls the `Keyring` interface's method of the same name.
+This method receives the request's ID and final result.
+
+When `approveRequest` is called, it can resolve the pending request by using the
+[`submitResponse`](../reference/rpc-api.md#submitresponse) sub-method of `snap_manageAccounts`.
diff --git a/snaps/how-to/troubleshoot.md b/snaps/how-to/troubleshoot.md
index 270f83c62bd..eaf2e6ebf9b 100644
--- a/snaps/how-to/troubleshoot.md
+++ b/snaps/how-to/troubleshoot.md
@@ -1,6 +1,6 @@
---
description: Solve common issues.
-sidebar_position: 6
+sidebar_position: 7
---
# Troubleshoot
diff --git a/snaps/how-to/use-keyring-api.md b/snaps/how-to/use-keyring-api.md
new file mode 100644
index 00000000000..6b1b1f74d49
--- /dev/null
+++ b/snaps/how-to/use-keyring-api.md
@@ -0,0 +1,163 @@
+---
+description: Use the KeyringSnapRpcClient from a dapp.
+sidebar_label: Use the Keyring API
+sidebar_position: 5
+---
+
+# Use the Keyring API from a dapp
+
+Your dapp can use the [Keyring API](../concepts/keyring-api.md) to interact with custom EVM accounts.
+Use the [`KeyringSnapRpcClient`](../reference/keyring-api/02-Classes/04-class.KeyringSnapRpcClient.md)
+of the Keyring API to invoke Keyring RPC methods on your [Keyring snap](../concepts/keyring-api.md#terminology).
+
+:::tip tutorial
+You can follow the end-to-end tutorial to [create a snap to connect to custom EVM accounts](../tutorials/custom-evm-accounts.md).
+:::
+
+:::info API documentation
+See the [Keyring API reference](../reference/keyring-api/index.md) for all the Keyring API methods.
+:::
+
+## Create the KeyringSnapRpcClient
+
+To use the `KeyringSnapRpcClient`, install `@metamask/keyring-api` in your project directory using
+Yarn or npm:
+
+```bash
+yarn add @metamask/keyring-api
+```
+
+or
+
+```bash
+npm install @metamask/keyring-api
+```
+
+Create the client by adding the following to your project script:
+
+```ts
+import { KeyringSnapRpcClient } from "@metamask/keyring-api";
+
+let client = new KeyringSnapRpcClient(snapId, window.ethereum);
+```
+
+## Call Keyring API methods
+
+You can now use the `KeyringSnapRpcClient` to invoke the following
+[`Keyring API`](../reference/keyring-api/index.md) methods on your snap.
+
+### createAccount
+
+Creates a Keyring account.
+
+```ts
+let keyringAccount = await client.createAccount("KeyringAccount1");
+```
+
+### getAccount
+
+Gets a Keyring account.
+
+```ts
+// accountId is returned when the account is created using createAccount.
+let keyringAccount = await client.getAccount(accountId);
+```
+
+### listAccounts
+
+Lists all Keyring accounts created by the snap.
+
+```ts
+let keyringAccounts = await client.listAccounts();
+```
+
+### updateAccount
+
+Updates a Keyring account.
+
+```ts
+let updatedAccount = await client.updateAccount(modifiedKeyringAccount);
+```
+
+### deleteAccount
+
+Deletes a Keyring account.
+
+```ts
+let snapResponse = await client.deleteAccount(accountId);
+```
+
+### submitRequest
+
+Submits a Keyring request.
+
+```ts
+import { v4 as uuid } from "uuid";
+
+// Example submitting an eth_sendTransaction request
+let submitRequestResponse = await client.submitRequest({
+ // ID of the account to which you want to submit this request
+ account: accountId,
+ scope: "eip155:1", // Ethereum Mainnet
+ request: {
+ jsonrpc: "2.0",
+ // Unique ID to identify every request
+ id: uuid(),
+ // The method and parameter structure is subjective to the Keyring API implementation in the snap code.
+ method: "eth_sendTransaction",
+ params:
+ {
+ from: "",
+ to: "0xcEF0f7f7ee1650b4A8151f605d9258bA65D733F5",
+ data,
+ chainId: "1",
+ },
+ ,
+ },
+});
+```
+
+### getRequest
+
+Gets a Keyring request.
+
+```ts
+// requestId is returned during request submission.
+let keyringRequest = await client.getRequest(requestId);
+```
+
+### listRequests
+
+Lists all requests submitted to the snap.
+
+```ts
+let requests = await client.listRequests();
+```
+
+### approveRequest
+
+Approves a request.
+
+```ts
+// requestId is returned during request submission.
+await client.approveRequest(requestId);
+```
+
+### rejectRequest
+
+Rejects a request.
+
+```ts
+// requestId is returned during request submission.
+await client.rejectRequest(requestId);
+```
+
+### filterAccountChains
+
+Returns a filtered list of CAIP-2 IDs representing the supported chains.
+
+```ts
+// accountId - ID of the account to be checked
+// chains - List of chains (CAIP-2) to be checked
+let supportedChains = await client.filterAccountChains(accountId, chains);
+```
diff --git a/snaps/how-to/work-with-existing-snaps.md b/snaps/how-to/work-with-existing-snaps.md
index 0e542e9705e..0fe88f6c239 100644
--- a/snaps/how-to/work-with-existing-snaps.md
+++ b/snaps/how-to/work-with-existing-snaps.md
@@ -1,6 +1,6 @@
---
description: Connect your dapp to existing, third-party snaps.
-sidebar_position: 5
+sidebar_position: 6
---
# Work with third-party snaps
diff --git a/snaps/reference/rpc-api.md b/snaps/reference/rpc-api.md
index f9d11217511..4cbd2406d26 100644
--- a/snaps/reference/rpc-api.md
+++ b/snaps/reference/rpc-api.md
@@ -160,7 +160,7 @@ An object containing the contents of the alert dialog:
- `type` - The type of dialog (`'Alert'`).
- `content` - The content of the alert, as a [custom UI](../how-to/use-custom-ui.md) component.
-#### Example
+##### Example
```javascript
import { panel, text, heading } from '@metamask/snaps-ui';
@@ -194,7 +194,7 @@ An object containing the contents of the confirmation dialog:
`true` if the confirmation was accepted, `false` otherwise.
-#### Example
+##### Example
```javascript
import { panel, text, heading } from '@metamask/snaps-ui';
@@ -231,7 +231,7 @@ An object containing the contents of the prompt dialog:
The text entered by the user if the prompt was submitted or `null` if the prompt was rejected or closed. If the user does not enter any text and submits the prompt, the value is an empty string.
-#### Example
+##### Example
```javascript
import { panel, text, heading } from '@metamask/snaps-ui';
@@ -562,6 +562,234 @@ console.log(entropy);
+### snap_manageAccounts
+
+Manages [Keyring snap](../concepts/keyring-api.md) accounts.
+This method is organized into multiple sub-methods which each take their own parameters:
+
+- [`createAccount`](#createaccount)
+- [`updateAccount`](#updateaccount)
+- [`deleteAccount`](#deleteaccount)
+- [`listAccounts`](#listaccounts)
+- [`submitResponse`](#submitresponse)
+
+This method is only callable by snaps.
+
+#### `createAccount`
+
+Creates a new snap account.
+
+:::note
+The snap is responsible for maintaining its own record of accounts.
+This can be done using [`snap_manageState`](#snap_managestate).
+:::
+
+##### Parameters
+
+`account` - A [`KeyringAccount`](./keyring-api/04-Variables/02-variable.KeyringAccountStruct.md) object.
+
+##### Returns
+
+`null`
+
+##### Example
+
+```typescript
+import { Keyring, KeyringAccount } from '@metamask/keyring-api';
+
+class MyKeyring implements Keyring {
+ // ... other methods
+
+ async createAccount(
+ name: string,
+ options: Record | null = null,
+ ): Promise {
+
+ const account: KeyringAccount = {
+ id: uuid(),
+ name,
+ options,
+ address,
+ supportedMethods: [
+ 'eth_sendTransaction',
+ 'eth_sign',
+ 'eth_signTransaction',
+ 'eth_signTypedData_v1',
+ 'eth_signTypedData_v2',
+ 'eth_signTypedData_v3',
+ 'eth_signTypedData_v4',
+ 'eth_signTypedData',
+ 'personal_sign',
+ ],
+ type: 'eip155:eoa',
+ };
+
+ // Store the account in state
+
+ await snap.request({
+ method: 'snap_manageAccounts',
+ params: {
+ method: 'createAccount',
+ params: { account },
+ },
+ });
+
+ return account;
+ }
+}
+```
+
+#### `updateAccount`
+
+Updates an existing snap account.
+
+:::note
+The snap is responsible for maintaining its own record of accounts.
+This can be done using [`snap_manageState`](#snap_managestate).
+:::
+
+##### Parameters
+
+`account` - A [`KeyringAccount`](./keyring-api/04-Variables/02-variable.KeyringAccountStruct.md) object.
+
+##### Returns
+
+`null`
+
+##### Example
+
+```typescript
+import { Keyring, KeyringAccount } from '@metamask/keyring-api';
+
+class MyKeyring implements Keyring {
+ // ... other methods
+
+ async updateAccount(account: KeyringAccount): Promise {
+ // Store the new account details in state
+
+ await snap.request({
+ method: 'snap_manageAccounts',
+ params: {
+ method: 'updateAccount',
+ params: { account },
+ },
+ });
+ }
+}
+```
+
+#### `deleteAccount`
+
+Deletes a snap account.
+
+:::note
+The snap is responsible for maintaining its own record of accounts.
+This can be done using [`snap_manageState`](#snap_managestate).
+:::
+
+##### Parameters
+
+`id` - The ID of the account to be deleted.
+
+##### Returns
+
+`null`
+
+##### Example
+
+```typescript
+import { Keyring } from '@metamask/keyring-api';
+
+class MyKeyring implements Keyring {
+ // ... other methods
+
+ async deleteAccount(id: string): Promise {
+ // Delete the account from state
+
+ await snap.request({
+ method: 'snap_manageAccounts',
+ params: {
+ method: 'deleteAccount',
+ params: { id },
+ },
+ });
+ }
+}
+```
+
+#### `listAccounts`
+
+Lists the calling snap's accounts that are known to MetaMask.
+This method does not call back to the snap.
+Instead, the snap can use it to check whether there's a discrepancy between the snap's internal
+state of accounts and the state known to MetaMask.
+
+##### Returns
+
+An array of [keyring accounts](./keyring-api/04-Variables/02-variable.KeyringAccountStruct.md).
+
+##### Example
+
+```typescript
+import { Keyring, KeyringAccount } from '@metamask/keyring-api';
+
+class MyKeyring implements Keyring {
+ // ... other methods
+
+ async checkIfAccountsInSync(): Promise {
+
+ const knownAccounts: KeyringAccount[] = /* grab accounts from snap state */;
+
+ const listedAccounts: KeyringAccount[] = await snap.request({
+ method: 'snap_manageAccounts',
+ params: {
+ method: 'listAccounts'
+ },
+ });
+
+ // compare the arrays and return the response
+ }
+}
+```
+
+#### `submitResponse`
+
+Finalizes a signing request.
+This is usually called as part of the `approveRequest` method of the
+[`Keyring`](keyring-api/03-Type%20Aliases/02-type-alias.Keyring.md) interface.
+
+##### Parameters
+
+- `id` - The ID of the request to finalize.
+- `result` - The result that should be returned to the original JSON-RPC caller.
+
+##### Returns
+
+`null`
+
+##### Example
+
+```typescript
+import { Keyring } from '@metamask/keyring-api';
+import { Json } from '@metamask/utils';
+
+class MyKeyring implements Keyring {
+ // ... other methods
+
+ async approveRequest(id: string, result?: Json): Promise {
+ // Do any snap-side logic to finish approving the request
+
+ await snap.request({
+ method: 'snap_manageAccounts',
+ params: {
+ method: 'submitResponse',
+ params: { id, result}
+ },
+ });
+ }
+}
+```
+
### snap_manageState
Allows the snap to persist up to 100 MB of data to disk and retrieve it at will.
diff --git a/snaps/tutorials/custom-evm-accounts.md b/snaps/tutorials/custom-evm-accounts.md
new file mode 100644
index 00000000000..a122a9a76eb
--- /dev/null
+++ b/snaps/tutorials/custom-evm-accounts.md
@@ -0,0 +1,153 @@
+---
+description: Create a Keyring snap to connect to custom EVM accounts in MetaMask.
+---
+
+# Create a snap to connect to custom EVM accounts
+
+This tutorial walks you through creating a snap that uses the [Keyring API](../concepts/keyring-api.md)
+to integrate custom EVM accounts in MetaMask.
+
+:::caution important
+The [Keyring API methods](../reference/keyring-api/index.md) mentioned in this tutorial are only
+available in [MetaMask Flask](../get-started/install-flask.md), the canary distribution of MetaMask.
+:::
+
+## Prerequisites
+
+- A snap set up using the [Snaps quickstart](../get-started/quickstart.md)
+- Business logic written for your custom EVM account type
+
+## Steps
+
+### 1. Add the snap_manageAccounts permission
+
+Request permission to call [`snap_manageAccounts`](../reference/rpc-api.md#snap_manageaccounts) by
+editing the `snap.manifest.json` file in your snap:
+
+```json title="snap.manifest.json"
+{
+ // ...other settings
+ "initialPermissions": {
+ // ...other permissions
+ "snap_manageAccounts": {}
+ }
+}
+```
+
+### 2. Expose the Keyring interface as a JSON-RPC API
+
+Export the [`onRpcRequest`](../reference/exports.md#onrpcrequest) function from the snap to expose
+the [`Keyring`](../reference/keyring-api/03-Type%20Aliases/02-type-alias.Keyring.md)
+interface as a JSON-RPC API.
+
+The Keyring API provides a helper called
+[`handleKeyringRequest`](../reference/keyring-api/05-Functions/03-function.handleKeyringRequest.md).
+This helper takes an instance of your `Keyring` interface and a request object.
+It responds to requests where the `method` is of type `keyring_*`, and throws a
+[`MethodNotSupportedError`](../reference/keyring-api/02-Classes/05-class.MethodNotSupportedError.md)
+if it doesn't recognize the request method.
+
+Since your snap most likely wants to answer other JSON-RPC requests in addition to the `keyring_*` ones,
+another helper called [`buildHandlersChain`](../reference/keyring-api/05-Functions/02-function.buildHandlersChain.md)
+lets you chain multiple RPC handlers together.
+As each handler in the chain throws a
+[`MethodNotSupportedError`](../reference/keyring-api/02-Classes/05-class.MethodNotSupportedError.md),
+the next handler in the chain is called.
+The return value of `buildHandlersChain` is a function that can be used as the `onRpcRequest` export.
+
+The following is an example of composing two handlers: the keyring handler and a custom handler.
+This code goes in the `packages/snap/src/index.ts` file:
+
+```typescript title="index.ts"
+import {
+ MethodNotSupportedError,
+ buildHandlersChain,
+ handleKeyringRequest,
+} from '@metamask/keyring-api';
+import type { OnRpcRequestHandler } from '@metamask/snaps-types';
+
+// This is your custom EVM account implementation
+import { MyKeyring } from './keyring';
+
+let keyring: MyKeyring;
+
+/**
+ * Handle keyring requests.
+ *
+ * @param args - Request arguments.
+ * @param args.request - Request to execute.
+ * @returns The execution result.
+ */
+const keyringHandler: OnRpcRequestHandler = async ({ request }) => {
+ if (!keyring) {
+ const state = await snap.request({
+ method: 'snap_manageState',
+ params: { operation: 'get' },
+ });
+ if (!keyring) {
+ keyring = new MyKeyring(state);
+ }
+ }
+ return await handleKeyringRequest(keyring, request);
+};
+
+/**
+ * Execute a custom snap request.
+ *
+ * @param args - Request arguments.
+ * @param args.request - Request to execute.
+ * @returns The execution result.
+ */
+const customHandler: OnRpcRequestHandler = async ({
+ request,
+}): Promise => {
+ switch (request.method) {
+ // internal methods
+ case 'mysnap_hello': {
+ return 'Hello World!';
+ }
+
+ default: {
+ throw new MethodNotSupportedError(request.method);
+ }
+ }
+};
+
+/**
+ * Compose both handlers
+ */
+export const onRpcRequest: OnRpcRequestHandler = buildHandlersChain(
+ keyringHandler,
+ customHandler,
+);
+```
+
+### 3. Use the Keyring API from a dapp
+
+As you build a companion dapp to provide a user interface for your Keyring snap, you'll need to
+interact with your snap's JSON-RPC API.
+While you could do this by making regular RPC calls using
+[`wallet_invokeSnap`](../reference/rpc-api.md#wallet_invokesnap), we recommend
+[using the Keyring API from your dapp](../how-to/use-keyring-api.md):
+
+```typescript
+import { KeyringSnapRpcClient } from '@metamask/keyring-api';
+import { defaultSnapOrigin as snapId } from '../config';
+
+const keyringClient = new KeyringSnapRpcClient(snapId, window.ethereum);
+
+// Example usage, after the user fills the steps to create an account...
+keyringClient.createAccount(name, options);
+
+// The above call is equivalent to
+window.ethereum.request({
+ method: 'wallet_invokeSnap',
+ params: {
+ snapId,
+ request: {
+ method: 'keyring_createAccount',
+ params: { name, options }
+ }
+ },
+});
+```
diff --git a/snaps/tutorials/gas-estimation.md b/snaps/tutorials/gas-estimation.md
index 8c10f3e199e..f87e3b316b0 100644
--- a/snaps/tutorials/gas-estimation.md
+++ b/snaps/tutorials/gas-estimation.md
@@ -3,7 +3,7 @@ description: Create a snap that estimates gas fees.
sidebar_position: 1
---
-# Create a gas estimation snap
+# Create a snap to estimate gas fees
This tutorial walks you through creating a snap that estimates gas fees.
The snap uses the `fetch` API to request information from the internet, and displays custom
diff --git a/snaps/tutorials/transaction-insights.md b/snaps/tutorials/transaction-insights.md
index 4170db9fe0f..4bf6d119bf2 100644
--- a/snaps/tutorials/transaction-insights.md
+++ b/snaps/tutorials/transaction-insights.md
@@ -3,7 +3,7 @@ description: Create a snap that provides transaction insights.
sidebar_position: 2
---
-# Create a transaction insights snap
+# Create a snap to calculate gas fee percentages
This tutorial walks you through creating a snap that calculates the percentage of gas fees that
a user would pay when creating a transaction.
diff --git a/tsconfig.json b/tsconfig.json
index 72c23e90b63..262563a3474 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
"./**/*"
],
"exclude": [
- "node_modules"
+ "node_modules",
+ "external"
],
}
diff --git a/wallet/concepts/apis.md b/wallet/concepts/apis.md
index a9152f75748..36186f46809 100644
--- a/wallet/concepts/apis.md
+++ b/wallet/concepts/apis.md
@@ -3,7 +3,7 @@ sidebar_position: 3
description: Learn about the MetaMask Ethereum provider API.
---
-# What are the MetaMask APIs?
+# About the MetaMask APIs
MetaMask supports an [Ethereum provider API](#ethereum-provider-api), which wraps a [JSON-RPC API](#json-rpc-api).
diff --git a/wallet/concepts/sdk.md b/wallet/concepts/sdk.md
index 3912b8dc54b..be5d79127ce 100644
--- a/wallet/concepts/sdk.md
+++ b/wallet/concepts/sdk.md
@@ -3,7 +3,7 @@ description: Learn about MetaMask SDK.
sidebar_position: 2
---
-# What is MetaMask SDK?
+# About MetaMask SDK
MetaMask SDK is a library that provides a reliable, secure, and seamless connection from your dapp
to the MetaMask browser extension and MetaMask Mobile.
diff --git a/yarn.lock b/yarn.lock
index d96ba5220f6..445c71e1259 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4600,6 +4600,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-sequence-parser@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "ansi-sequence-parser@npm:1.1.0"
+ checksum: 75f4d3a4c555655a698aec05b5763cbddcd16ccccdbfd178fb0aa471ab74fdf98e031b875ef26e64be6a95cf970c89238744b26de6e34af97f316d5186b1df53
+ languageName: node
+ linkType: hard
+
"ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
@@ -6848,6 +6855,17 @@ __metadata:
languageName: node
linkType: hard
+"docusaurus-plugin-typedoc@npm:next":
+ version: 1.0.0-next.15
+ resolution: "docusaurus-plugin-typedoc@npm:1.0.0-next.15"
+ dependencies:
+ "@docusaurus/types": ^2.4.1
+ peerDependencies:
+ typedoc-plugin-markdown: ">=4.0.0-next.19"
+ checksum: 69fcadcb90288151d40140d75cb06612dedf60c34fd0806c3ea309267dfe3b25406f280d4b883676d3924e06588483b8d5ac895a5cd67957e715a3d0425de628
+ languageName: node
+ linkType: hard
+
"dom-converter@npm:^0.2.0":
version: 0.2.0
resolution: "dom-converter@npm:0.2.0"
@@ -10280,6 +10298,13 @@ __metadata:
languageName: node
linkType: hard
+"jsonc-parser@npm:^3.2.0":
+ version: 3.2.0
+ resolution: "jsonc-parser@npm:3.2.0"
+ checksum: 946dd9a5f326b745aa326d48a7257e3f4a4b62c5e98ec8e49fa2bdd8d96cef7e6febf1399f5c7016114fd1f68a1c62c6138826d5d90bc650448e3cf0951c53c7
+ languageName: node
+ linkType: hard
+
"jsonc-parser@npm:~2.2.1":
version: 2.2.1
resolution: "jsonc-parser@npm:2.2.1"
@@ -10645,6 +10670,13 @@ __metadata:
languageName: node
linkType: hard
+"lunr@npm:^2.3.9":
+ version: 2.3.9
+ resolution: "lunr@npm:2.3.9"
+ checksum: 176719e24fcce7d3cf1baccce9dd5633cd8bdc1f41ebe6a180112e5ee99d80373fe2454f5d4624d437e5a8319698ca6837b9950566e15d2cae5f2a543a3db4b8
+ languageName: node
+ linkType: hard
+
"lz-string@npm:^1.4.4":
version: 1.5.0
resolution: "lz-string@npm:1.5.0"
@@ -10746,6 +10778,15 @@ __metadata:
languageName: node
linkType: hard
+"marked@npm:^4.3.0":
+ version: 4.3.0
+ resolution: "marked@npm:4.3.0"
+ bin:
+ marked: bin/marked.js
+ checksum: 0db6817893952c3ec710eb9ceafb8468bf5ae38cb0f92b7b083baa13d70b19774674be04db5b817681fa7c5c6a088f61300815e4dd75a59696f4716ad69f6260
+ languageName: node
+ linkType: hard
+
"md5.js@npm:^1.3.4":
version: 1.3.5
resolution: "md5.js@npm:1.3.5"
@@ -11045,15 +11086,20 @@ __metadata:
"@typescript-eslint/eslint-plugin": ^5.41.0
"@typescript-eslint/parser": ^5.41.0
clsx: ^1.2.1
+ docusaurus-plugin-typedoc: next
eslint: ^8.26.0
eslint-plugin-react: ^7.31.10
eslint-plugin-unused-imports: ^2.0.0
node-polyfill-webpack-plugin: ^2.0.1
+ prettier: ^3.0.0
prism-react-renderer: ^1.3.5
react: ^17.0.2
react-dom: ^17.0.2
remark-codesandbox: ^0.10.1
remark-docusaurus-tabs: ^0.2.0
+ typedoc: ^0.25.1
+ typedoc-plugin-frontmatter: ^0.0.2
+ typedoc-plugin-markdown: next
typescript: ^4.7.4
languageName: unknown
linkType: soft
@@ -11504,7 +11550,7 @@ __metadata:
languageName: node
linkType: hard
-"minimatch@npm:^9.0.1":
+"minimatch@npm:^9.0.1, minimatch@npm:^9.0.3":
version: 9.0.3
resolution: "minimatch@npm:9.0.3"
dependencies:
@@ -13185,6 +13231,15 @@ __metadata:
languageName: node
linkType: hard
+"prettier@npm:^3.0.0":
+ version: 3.0.2
+ resolution: "prettier@npm:3.0.2"
+ bin:
+ prettier: bin/prettier.cjs
+ checksum: 118b59ddb6c80abe2315ab6d0f4dd1b253be5cfdb20622fa5b65bb1573dcd362e6dd3dcf2711dd3ebfe64aecf7bdc75de8a69dc2422dcd35bdde7610586b677a
+ languageName: node
+ linkType: hard
+
"pretty-error@npm:^4.0.0":
version: 4.0.0
resolution: "pretty-error@npm:4.0.0"
@@ -14872,6 +14927,18 @@ __metadata:
languageName: node
linkType: hard
+"shiki@npm:^0.14.1":
+ version: 0.14.3
+ resolution: "shiki@npm:0.14.3"
+ dependencies:
+ ansi-sequence-parser: ^1.1.0
+ jsonc-parser: ^3.2.0
+ vscode-oniguruma: ^1.7.0
+ vscode-textmate: ^8.0.0
+ checksum: a4dd98e3b2a5dd8be207448f111ffb9ad2ed6c530f215714d8b61cbf91ec3edbabb09109b8ec58a26678aacd24e8161d5a9bc0c1fa1b4f64b27ceb180cbd0c89
+ languageName: node
+ linkType: hard
+
"shortid@npm:^2.2.8":
version: 2.2.16
resolution: "shortid@npm:2.2.16"
@@ -15968,6 +16035,40 @@ __metadata:
languageName: node
linkType: hard
+"typedoc-plugin-frontmatter@npm:^0.0.2":
+ version: 0.0.2
+ resolution: "typedoc-plugin-frontmatter@npm:0.0.2"
+ dependencies:
+ yaml: ^2.2.2
+ checksum: 44cbdb82e3fd8f4eb89cdf54783b5b07b03a57edc7bda85a48280edba73f401a2f5439cbba97426dd79e9584c410244af5dd20d5d7281c27d67d61675fa7aaef
+ languageName: node
+ linkType: hard
+
+"typedoc-plugin-markdown@npm:next":
+ version: 4.0.0-next.20
+ resolution: "typedoc-plugin-markdown@npm:4.0.0-next.20"
+ peerDependencies:
+ typedoc: ">=0.24.0"
+ checksum: f1217bc822940cdc978cde54e7fead86441254ca443e9760ccb63e9cc9c0ee05df1d3e846f081fc35ee1385d75b85f92d18e5474a5a5a698a0e5679fbcb9121f
+ languageName: node
+ linkType: hard
+
+"typedoc@npm:^0.25.1":
+ version: 0.25.1
+ resolution: "typedoc@npm:0.25.1"
+ dependencies:
+ lunr: ^2.3.9
+ marked: ^4.3.0
+ minimatch: ^9.0.3
+ shiki: ^0.14.1
+ peerDependencies:
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x
+ bin:
+ typedoc: bin/typedoc
+ checksum: 6c1c28cbf51b6ab1741429f58f540c5c12d6119ce30054866b879ef2a3a2120a6adbaf59919f7411d3bb51b9113fc926522c40934a3d8ef601785abdf0134eed
+ languageName: node
+ linkType: hard
+
"typescript@npm:^4.7.4":
version: 4.9.5
resolution: "typescript@npm:4.9.5"
@@ -16715,6 +16816,20 @@ __metadata:
languageName: node
linkType: hard
+"vscode-oniguruma@npm:^1.7.0":
+ version: 1.7.0
+ resolution: "vscode-oniguruma@npm:1.7.0"
+ checksum: 53519d91d90593e6fb080260892e87d447e9b200c4964d766772b5053f5699066539d92100f77f1302c91e8fc5d9c772fbe40fe4c90f3d411a96d5a9b1e63f42
+ languageName: node
+ linkType: hard
+
+"vscode-textmate@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "vscode-textmate@npm:8.0.0"
+ checksum: 127780dfea89559d70b8326df6ec344cfd701312dd7f3f591a718693812b7852c30b6715e3cfc8b3200a4e2515b4c96f0843c0eacc0a3020969b5de262c2a4bb
+ languageName: node
+ linkType: hard
+
"wait-on@npm:^6.0.1":
version: 6.0.1
resolution: "wait-on@npm:6.0.1"
@@ -17238,6 +17353,13 @@ __metadata:
languageName: node
linkType: hard
+"yaml@npm:^2.2.2":
+ version: 2.3.2
+ resolution: "yaml@npm:2.3.2"
+ checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146
+ languageName: node
+ linkType: hard
+
"yargs-parser@npm:^20.2.2":
version: 20.2.9
resolution: "yargs-parser@npm:20.2.9"