From 4edbbd7119c52c5fc1d4fbfb96a4154a50173dde Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Tue, 28 May 2024 10:27:12 +0200 Subject: [PATCH] refactor: split `api.ts` (#319) --- src/api.ts | 312 ----------------------- src/{api.test.ts => api/account.test.ts} | 2 +- src/api/account.ts | 73 ++++++ src/api/export.ts | 12 + src/api/index.ts | 5 + src/api/keyring.ts | 142 +++++++++++ src/api/request.ts | 40 +++ src/api/response.ts | 61 +++++ 8 files changed, 334 insertions(+), 313 deletions(-) delete mode 100644 src/api.ts rename src/{api.test.ts => api/account.test.ts} (94%) create mode 100644 src/api/account.ts create mode 100644 src/api/export.ts create mode 100644 src/api/index.ts create mode 100644 src/api/keyring.ts create mode 100644 src/api/request.ts create mode 100644 src/api/response.ts diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index bd8f638b8..000000000 --- a/src/api.ts +++ /dev/null @@ -1,312 +0,0 @@ -import type { Json } from '@metamask/utils'; -import { JsonStruct } from '@metamask/utils'; -import type { Infer } from 'superstruct'; -import { enums, array, literal, record, string, union } from 'superstruct'; - -import { exactOptional, object } from './superstruct'; -import { UuidStruct } from './utils'; - -// ! The `*AccountType` enums defined below should be kept in this file to -// ! avoid circular dependencies when the API is used by other files. - -/** - * Supported Ethereum account types. - */ -export enum EthAccountType { - Eoa = 'eip155:eoa', - Erc4337 = 'eip155:erc4337', -} - -/** - * Supported Bitcoin account types. - */ -export enum BtcAccountType { - P2wpkh = 'bip122:p2wpkh', -} - -/** - * Supported account types. - */ -export type KeyringAccountType = - | `${EthAccountType.Eoa}` - | `${EthAccountType.Erc4337}` - | `${BtcAccountType.P2wpkh}`; - -/** - * A struct which represents a Keyring account object. It is abstract enough to - * be used with any blockchain. Specific blockchain account types should extend - * this struct. - * - * See {@link KeyringAccount}. - */ -export const KeyringAccountStruct = object({ - /** - * Account ID (UUIDv4). - */ - id: UuidStruct, - - /** - * Account type. - */ - type: enums([ - `${EthAccountType.Eoa}`, - `${EthAccountType.Erc4337}`, - `${BtcAccountType.P2wpkh}`, - ]), - - /** - * Account main address. - */ - address: string(), - - /** - * Account options. - */ - options: record(string(), JsonStruct), - - /** - * Account supported methods. - */ - methods: array(string()), -}); - -/** - * Keyring Account type represents an account and its properties from the - * point of view of the keyring. - */ -export type KeyringAccount = Infer; - -export const KeyringRequestStruct = object({ - /** - * Keyring request ID (UUIDv4). - */ - id: UuidStruct, - - /** - * Request's scope (CAIP-2 chain ID). - */ - scope: string(), - - /** - * Account ID (UUIDv4). - */ - account: UuidStruct, - - /** - * Inner request sent by the client application. - */ - request: object({ - method: string(), - params: exactOptional( - union([array(JsonStruct), record(string(), JsonStruct)]), - ), - }), -}); - -/** - * Keyring request. - * - * Represents a request made to the keyring for account-related operations. - */ -export type KeyringRequest = Infer; - -export const KeyringAccountDataStruct = record(string(), JsonStruct); - -/** - * Response to a call to `exportAccount`. - * - * The exact response depends on the keyring implementation. - */ -export type KeyringAccountData = Infer; - -export const KeyringResponseStruct = union([ - object({ - /** - * Pending flag. - * - * Setting the pending flag to true indicates that the request will be - * handled asynchronously. The keyring must be called with `approveRequest` - * or `rejectRequest` to resolve the request. - */ - pending: literal(true), - - /** - * Redirect URL. - * - * If present in the response, MetaMask will display a confirmation dialog - * with a link to the redirect URL. The user can choose to follow the link - * or cancel the request. - */ - redirect: exactOptional( - object({ - message: exactOptional(string()), - url: exactOptional(string()), - }), - ), - }), - object({ - /** - * Pending flag. - * - * Setting the pending flag to false indicates that the request will be - * handled synchronously. The keyring must return the result of the - * request execution. - */ - pending: literal(false), - - /** - * Request result. - */ - result: JsonStruct, - }), -]); - -/** - * Response to a call to `submitRequest`. - * - * Keyring implementations must return a response with `pending: true` if the - * request will be handled asynchronously. Otherwise, the response must contain - * the result of the request and `pending: false`. - * - * In the asynchronous case, the keyring can return a redirect URL and message - * to be shown to the user. The user can choose to follow the link or cancel - * the request. The main use case for this is to redirect the user to the snap - * dapp to review the request. - */ -export type KeyringResponse = Infer; - -/** - * Keyring interface. - * - * Represents the functionality and operations related to managing accounts and - * handling requests. - */ -export type Keyring = { - /** - * List accounts. - * - * Retrieves an array of KeyringAccount objects representing the available - * accounts. - * - * @returns A promise that resolves to an array of KeyringAccount objects. - */ - listAccounts(): Promise; - - /** - * Get an account. - * - * Retrieves the KeyringAccount object for the given account ID. - * - * @param id - The ID of the account to retrieve. - * @returns A promise that resolves to the KeyringAccount object if found, or - * undefined otherwise. - */ - getAccount(id: string): Promise; - - /** - * Create an account. - * - * Creates a new account with optional, keyring-defined, account options. - * - * @param options - Keyring-defined options for the account (optional). - * @returns A promise that resolves to the newly created KeyringAccount - * object without any private information. - */ - createAccount(options?: Record): Promise; - - /** - * Filter supported chains for a given account. - * - * @param id - ID of the account to be checked. - * @param chains - List of chains (CAIP-2) to be checked. - * @returns A Promise that resolves to a filtered list of CAIP-2 IDs - * representing the supported chains. - */ - filterAccountChains(id: string, chains: string[]): Promise; - - /** - * Update an account. - * - * Updates the account with the given account object. Does nothing if the - * account does not exist. - * - * @param account - The updated account object. - * @returns A promise that resolves when the account is successfully updated. - */ - updateAccount(account: KeyringAccount): Promise; - - /** - * Delete an account from the keyring. - * - * Deletes the account with the given ID from the keyring. - * - * @param id - The ID of the account to delete. - * @returns A promise that resolves when the account is successfully deleted. - */ - deleteAccount(id: string): Promise; - - /** - * Exports an account's private key. - * - * If the keyring cannot export a private key, this function should throw an - * error. - * - * @param id - The ID of the account to export. - * @returns A promise that resolves to the exported account. - */ - exportAccount?(id: string): Promise; - - /** - * List all submitted requests. - * - * Retrieves an array of KeyringRequest objects representing the submitted - * requests. - * - * @returns A promise that resolves to an array of KeyringRequest objects. - */ - listRequests?(): Promise; - - /** - * Get a request. - * - * Retrieves the KeyringRequest object for the given request ID. - * - * @param id - The ID of the request to retrieve. - * @returns A promise that resolves to the KeyringRequest object if found, or - * undefined otherwise. - */ - getRequest?(id: string): Promise; - - /** - * Submit a request. - * - * Submits the given KeyringRequest object. - * - * @param request - The KeyringRequest object to submit. - * @returns A promise that resolves to the request response. - */ - submitRequest(request: KeyringRequest): Promise; - - /** - * Approve a request. - * - * Approves the request with the given ID and sets the response if provided. - * - * @param id - The ID of the request to approve. - * @param data - The response to the request (optional). - * @returns A promise that resolves when the request is successfully - * approved. - */ - approveRequest?(id: string, data?: Record): Promise; - - /** - * Reject a request. - * - * Rejects the request with the given ID. - * - * @param id - The ID of the request to reject. - * @returns A promise that resolves when the request is successfully - * rejected. - */ - rejectRequest?(id: string): Promise; -}; diff --git a/src/api.test.ts b/src/api/account.test.ts similarity index 94% rename from src/api.test.ts rename to src/api/account.test.ts index c3b3d000b..14d81a0dd 100644 --- a/src/api.test.ts +++ b/src/api/account.test.ts @@ -1,6 +1,6 @@ import { assert } from 'superstruct'; -import { KeyringAccountStruct } from './api'; +import { KeyringAccountStruct } from './account'; const supportedKeyringAccountTypes = Object.keys( KeyringAccountStruct.schema.type.schema, diff --git a/src/api/account.ts b/src/api/account.ts new file mode 100644 index 000000000..0eb33e927 --- /dev/null +++ b/src/api/account.ts @@ -0,0 +1,73 @@ +import { JsonStruct } from '@metamask/utils'; +import type { Infer } from 'superstruct'; +import { array, enums, record, string } from 'superstruct'; + +import { object } from '../superstruct'; +import { UuidStruct } from '../utils'; + +/** + * Supported Ethereum account types. + */ +export enum EthAccountType { + Eoa = 'eip155:eoa', + Erc4337 = 'eip155:erc4337', +} + +/** + * Supported Bitcoin account types. + */ +export enum BtcAccountType { + P2wpkh = 'bip122:p2wpkh', +} + +/** + * Supported account types. + */ +export type KeyringAccountType = + | `${EthAccountType.Eoa}` + | `${EthAccountType.Erc4337}` + | `${BtcAccountType.P2wpkh}`; + +/** + * A struct which represents a Keyring account object. It is abstract enough to + * be used with any blockchain. Specific blockchain account types should extend + * this struct. + * + * See {@link KeyringAccount}. + */ +export const KeyringAccountStruct = object({ + /** + * Account ID (UUIDv4). + */ + id: UuidStruct, + + /** + * Account type. + */ + type: enums([ + `${EthAccountType.Eoa}`, + `${EthAccountType.Erc4337}`, + `${BtcAccountType.P2wpkh}`, + ]), + + /** + * Account main address. + */ + address: string(), + + /** + * Account options. + */ + options: record(string(), JsonStruct), + + /** + * Account supported methods. + */ + methods: array(string()), +}); + +/** + * Keyring Account type represents an account and its properties from the + * point of view of the keyring. + */ +export type KeyringAccount = Infer; diff --git a/src/api/export.ts b/src/api/export.ts new file mode 100644 index 000000000..ef810fe11 --- /dev/null +++ b/src/api/export.ts @@ -0,0 +1,12 @@ +import { JsonStruct } from '@metamask/utils'; +import type { Infer } from 'superstruct'; +import { record, string } from 'superstruct'; + +export const KeyringAccountDataStruct = record(string(), JsonStruct); + +/** + * Response to a call to `exportAccount`. + * + * The exact response depends on the keyring implementation. + */ +export type KeyringAccountData = Infer; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 000000000..e1ca839a9 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,5 @@ +export * from './account'; +export * from './export'; +export * from './keyring'; +export * from './request'; +export * from './response'; diff --git a/src/api/keyring.ts b/src/api/keyring.ts new file mode 100644 index 000000000..de3b0bd13 --- /dev/null +++ b/src/api/keyring.ts @@ -0,0 +1,142 @@ +import type { Json } from '@metamask/utils'; + +import type { KeyringAccount } from './account'; +import type { KeyringAccountData } from './export'; +import type { KeyringRequest } from './request'; +import type { KeyringResponse } from './response'; + +/** + * Keyring interface. + * + * Represents the functionality and operations related to managing accounts and + * handling requests. + */ +export type Keyring = { + /** + * List accounts. + * + * Retrieves an array of KeyringAccount objects representing the available + * accounts. + * + * @returns A promise that resolves to an array of KeyringAccount objects. + */ + listAccounts(): Promise; + + /** + * Get an account. + * + * Retrieves the KeyringAccount object for the given account ID. + * + * @param id - The ID of the account to retrieve. + * @returns A promise that resolves to the KeyringAccount object if found, or + * undefined otherwise. + */ + getAccount(id: string): Promise; + + /** + * Create an account. + * + * Creates a new account with optional, keyring-defined, account options. + * + * @param options - Keyring-defined options for the account (optional). + * @returns A promise that resolves to the newly created KeyringAccount + * object without any private information. + */ + createAccount(options?: Record): Promise; + + /** + * Filter supported chains for a given account. + * + * @param id - ID of the account to be checked. + * @param chains - List of chains (CAIP-2) to be checked. + * @returns A Promise that resolves to a filtered list of CAIP-2 IDs + * representing the supported chains. + */ + filterAccountChains(id: string, chains: string[]): Promise; + + /** + * Update an account. + * + * Updates the account with the given account object. Does nothing if the + * account does not exist. + * + * @param account - The updated account object. + * @returns A promise that resolves when the account is successfully updated. + */ + updateAccount(account: KeyringAccount): Promise; + + /** + * Delete an account from the keyring. + * + * Deletes the account with the given ID from the keyring. + * + * @param id - The ID of the account to delete. + * @returns A promise that resolves when the account is successfully deleted. + */ + deleteAccount(id: string): Promise; + + /** + * Exports an account's private key. + * + * If the keyring cannot export a private key, this function should throw an + * error. + * + * @param id - The ID of the account to export. + * @returns A promise that resolves to the exported account. + */ + exportAccount?(id: string): Promise; + + /** + * List all submitted requests. + * + * Retrieves an array of KeyringRequest objects representing the submitted + * requests. + * + * @returns A promise that resolves to an array of KeyringRequest objects. + */ + listRequests?(): Promise; + + /** + * Get a request. + * + * Retrieves the KeyringRequest object for the given request ID. + * + * @param id - The ID of the request to retrieve. + * @returns A promise that resolves to the KeyringRequest object if found, or + * undefined otherwise. + */ + getRequest?(id: string): Promise; + + /** + * Submit a request. + * + * Submits the given KeyringRequest object. + * + * @param request - The KeyringRequest object to submit. + * @returns A promise that resolves to the request response. + */ + submitRequest(request: KeyringRequest): Promise; + + /** + * Approve a request. + * + * Approves the request with the given ID and sets the response if provided. + * + * @param id - The ID of the request to approve. + * @param data - The response to the request (optional). + * @returns A promise that resolves when the request is successfully + * approved. + */ + approveRequest?(id: string, data?: Record): Promise; + + /** + * Reject a request. + * + * Rejects the request with the given ID. + * + * @param id - The ID of the request to reject. + * @returns A promise that resolves when the request is successfully + * rejected. + */ + rejectRequest?(id: string): Promise; +}; diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 000000000..aebed6769 --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,40 @@ +import { JsonStruct } from '@metamask/utils'; +import type { Infer } from 'superstruct'; +import { array, record, string, union } from 'superstruct'; + +import { exactOptional, object } from '../superstruct'; +import { UuidStruct } from '../utils'; + +export const KeyringRequestStruct = object({ + /** + * Keyring request ID (UUIDv4). + */ + id: UuidStruct, + + /** + * Request's scope (CAIP-2 chain ID). + */ + scope: string(), + + /** + * Account ID (UUIDv4). + */ + account: UuidStruct, + + /** + * Inner request sent by the client application. + */ + request: object({ + method: string(), + params: exactOptional( + union([array(JsonStruct), record(string(), JsonStruct)]), + ), + }), +}); + +/** + * Keyring request. + * + * Represents a request made to the keyring for account-related operations. + */ +export type KeyringRequest = Infer; diff --git a/src/api/response.ts b/src/api/response.ts new file mode 100644 index 000000000..fbe0af33f --- /dev/null +++ b/src/api/response.ts @@ -0,0 +1,61 @@ +import { JsonStruct } from '@metamask/utils'; +import type { Infer } from 'superstruct'; +import { literal, string, union } from 'superstruct'; + +import { exactOptional, object } from '../superstruct'; + +export const KeyringResponseStruct = union([ + object({ + /** + * Pending flag. + * + * Setting the pending flag to true indicates that the request will be + * handled asynchronously. The keyring must be called with `approveRequest` + * or `rejectRequest` to resolve the request. + */ + pending: literal(true), + + /** + * Redirect URL. + * + * If present in the response, MetaMask will display a confirmation dialog + * with a link to the redirect URL. The user can choose to follow the link + * or cancel the request. + */ + redirect: exactOptional( + object({ + message: exactOptional(string()), + url: exactOptional(string()), + }), + ), + }), + object({ + /** + * Pending flag. + * + * Setting the pending flag to false indicates that the request will be + * handled synchronously. The keyring must return the result of the + * request execution. + */ + pending: literal(false), + + /** + * Request result. + */ + result: JsonStruct, + }), +]); + +/** + * Response to a call to `submitRequest`. + * + * Keyring implementations must return a response with `pending: true` if the + * request will be handled asynchronously. Otherwise, the response must contain + * the result of the request and `pending: false`. + * + * In the asynchronous case, the keyring can return a redirect URL and message + * to be shown to the user. The user can choose to follow the link or cancel + * the request. The main use case for this is to redirect the user to the snap + * dapp to review the request. + */ +export type KeyringResponse = Infer;