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: + +

+Keyring snap accounts in Metamask 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: + +

+ +![Keyring snap component diagram](../assets/keyring/components-diagram.png) + +

+ +- **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: + +![Keyring snap account creation flow](../assets/keyring/create-account-flow.png) + +The MetaMask account selection modal has an option called **Add snap account**: + +

+Add snap account option +

+ +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: + +![Synchronous signing flow](../assets/keyring/synchronous-flow.png) + +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: + +![Asynchronous signing flow](../assets/keyring/asynchronous-flow.png) + +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"