diff --git a/src/KeyringClient.test.ts b/src/KeyringClient.test.ts index f5a394ab3..2d6bc684f 100644 --- a/src/KeyringClient.test.ts +++ b/src/KeyringClient.test.ts @@ -1,7 +1,7 @@ import { type KeyringAccount, type KeyringRequest, - type SubmitRequestResponse, + type KeyringResponse, KeyringClient, } from '.'; // Import from `index.ts` to test the public API @@ -225,7 +225,7 @@ describe('KeyringClient', () => { params: ['0xe9a74aacd7df8112911ca93260fc5a046f8a64ae', '0x0'], }, }; - const expectedResponse: SubmitRequestResponse = { + const expectedResponse: KeyringResponse = { pending: true, }; diff --git a/src/KeyringClient.ts b/src/KeyringClient.ts index 69850ceac..597f42900 100644 --- a/src/KeyringClient.ts +++ b/src/KeyringClient.ts @@ -2,7 +2,13 @@ import type { Json } from '@metamask/utils'; import { assert } from 'superstruct'; import { v4 as uuid } from 'uuid'; -import type { Keyring, KeyringAccount, KeyringRequest } from './api'; +import type { + Keyring, + KeyringAccount, + KeyringRequest, + KeyringAccountData, + KeyringResponse, +} from './api'; import { ApproveRequestResponseStruct, CreateAccountResponseStruct, @@ -15,17 +21,13 @@ import { ListRequestsResponseStruct, RejectRequestResponseStruct, SubmitRequestResponseStruct, - type ExportAccountResponse, - type InternalRequest, - type InternalResponse, - type SubmitRequestResponse, UpdateAccountResponseStruct, - InternalResponseStruct, -} from './internal-api'; +} from './internal/api'; +import type { JsonRpcRequest } from './JsonRpcRequest'; import { type OmitUnion, strictMask } from './utils'; export type Sender = { - send(request: InternalRequest): Promise; + send(request: JsonRpcRequest): Promise; }; export class KeyringClient implements Keyring { @@ -43,20 +45,17 @@ export class KeyringClient implements Keyring { /** * Send a request to the snap and return the response. * - * @param partial - Partial internal request (method and params). + * @param partial - A partial JSON-RPC request (method and params). * @returns A promise that resolves to the response to the request. */ async #send( - partial: OmitUnion, - ): Promise { - return strictMask( - await this.#sender.send({ - jsonrpc: '2.0', - id: uuid(), - ...partial, - }), - InternalResponseStruct, - ); + partial: OmitUnion, + ): Promise { + return this.#sender.send({ + jsonrpc: '2.0', + id: uuid(), + ...partial, + }); } async listAccounts(): Promise { @@ -120,7 +119,7 @@ export class KeyringClient implements Keyring { ); } - async exportAccount(id: string): Promise { + async exportAccount(id: string): Promise { return strictMask( await this.#send({ method: 'keyring_exportAccount', @@ -149,7 +148,7 @@ export class KeyringClient implements Keyring { ); } - async submitRequest(request: KeyringRequest): Promise { + async submitRequest(request: KeyringRequest): Promise { return strictMask( await this.#send({ method: 'keyring_submitRequest', diff --git a/src/KeyringSnapControllerClient.ts b/src/KeyringSnapControllerClient.ts index b19b63870..536e474c2 100644 --- a/src/KeyringSnapControllerClient.ts +++ b/src/KeyringSnapControllerClient.ts @@ -1,7 +1,8 @@ import type { SnapController } from '@metamask/snaps-controllers'; import type { HandlerType, ValidatedSnapId } from '@metamask/snaps-utils'; +import type { Json } from '@metamask/utils'; -import type { InternalRequest, InternalResponse } from './internal-api'; +import type { JsonRpcRequest } from './JsonRpcRequest'; import { KeyringClient, type Sender } from './KeyringClient'; /** @@ -43,15 +44,13 @@ class SnapControllerSender implements Sender { * @param request - JSON-RPC request to send to the snap. * @returns A promise that resolves to the response of the request. */ - async send(request: InternalRequest): Promise { - const response = await this.#controller.handleRequest({ + async send(request: JsonRpcRequest): Promise { + return this.#controller.handleRequest({ snapId: this.#snapId as ValidatedSnapId, origin: this.#origin, handler: this.#handler, request, - }); - - return response as InternalResponse; + }) as Promise; } } diff --git a/src/KeyringSnapRpcClient.ts b/src/KeyringSnapRpcClient.ts index 18e4f6370..e03528371 100644 --- a/src/KeyringSnapRpcClient.ts +++ b/src/KeyringSnapRpcClient.ts @@ -1,6 +1,7 @@ import type { MetaMaskInpageProvider } from '@metamask/providers'; +import type { Json } from '@metamask/utils'; -import type { InternalRequest, InternalResponse } from './internal-api'; +import type { JsonRpcRequest } from './JsonRpcRequest'; import { KeyringClient, type Sender } from './KeyringClient'; /** @@ -29,16 +30,14 @@ export class SnapRpcSender implements Sender { * @param request - The JSON-RPC request to send to the snap. * @returns A promise that resolves to the response of the request. */ - async send(request: InternalRequest): Promise { - const response = await this.#provider.request({ + async send(request: JsonRpcRequest): Promise { + return this.#provider.request({ method: 'wallet_invokeSnap', params: { snapId: this.#origin, request, }, - }); - - return response as InternalResponse; + }) as Promise; } } diff --git a/src/api.ts b/src/api.ts index bd0908634..a6f0e5424 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,18 +1,18 @@ import { type Json, JsonStruct } from '@metamask/utils'; -import { object, string, enums, record, array, type Infer } from 'superstruct'; +import { + type Infer, + array, + enums, + literal, + object, + record, + string, + union, +} from 'superstruct'; -import type { - ExportAccountResponse, - SubmitRequestResponse, -} from './internal-api'; import { JsonRpcRequestStruct } from './JsonRpcRequest'; import { UuidStruct } from './utils'; -export type { - ExportAccountResponse, - SubmitRequestResponse, -} from './internal-api'; - /** * Supported Ethereum methods. */ @@ -104,6 +104,34 @@ export const KeyringRequestStruct = object({ */ 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: literal(true), + }), + object({ + pending: literal(false), + 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`. + */ +export type KeyringResponse = Infer; + /** * Keyring interface. * @@ -183,7 +211,7 @@ export type Keyring = { * @param id - The ID of the account to export. * @returns A promise that resolves to the exported account. */ - exportAccount(id: string): Promise; + exportAccount(id: string): Promise; /** * List all submitted requests. @@ -214,7 +242,7 @@ export type Keyring = { * @param request - The KeyringRequest object to submit. * @returns A promise that resolves to the request response. */ - submitRequest(request: KeyringRequest): Promise; + submitRequest(request: KeyringRequest): Promise; /** * Approve a request. diff --git a/src/index.ts b/src/index.ts index 8b4719024..75fe0edc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './KeyringClient'; export * from './rpc-handler'; export * from './KeyringSnapControllerClient'; export * from './KeyringSnapRpcClient'; +export * from './internal'; diff --git a/src/internal-api.ts b/src/internal/api.ts similarity index 79% rename from src/internal-api.ts rename to src/internal/api.ts index f73c3f83a..3d61ac25c 100644 --- a/src/internal-api.ts +++ b/src/internal/api.ts @@ -5,12 +5,16 @@ import { object, record, string, - union, type Infer, } from 'superstruct'; -import { KeyringAccountStruct, KeyringRequestStruct } from './api'; -import { UuidStruct } from './utils'; +import { + KeyringAccountDataStruct, + KeyringAccountStruct, + KeyringRequestStruct, + KeyringResponseStruct, +} from '../api'; +import { UuidStruct } from '../utils'; const CommonHeader = { jsonrpc: literal('2.0'), @@ -134,7 +138,7 @@ export const ExportAccountRequestStruct = object({ export type ExportAccountRequest = Infer; -export const ExportAccountResponseStruct = record(string(), JsonStruct); +export const ExportAccountResponseStruct = KeyringAccountDataStruct; export type ExportAccountResponse = Infer; @@ -180,15 +184,7 @@ export const SubmitRequestRequestStruct = object({ export type SubmitRequestRequest = Infer; -export const SubmitRequestResponseStruct = union([ - object({ - pending: literal(true), - }), - object({ - pending: literal(false), - result: JsonStruct, - }), -]); +export const SubmitRequestResponseStruct = KeyringResponseStruct; export type SubmitRequestResponse = Infer; @@ -226,43 +222,3 @@ export type RejectRequestRequest = Infer; export const RejectRequestResponseStruct = literal(null); export type RejectRequestResponse = Infer; - -// ---------------------------------------------------------------------------- -// Internal request - -export const InternalRequestStruct = union([ - ListAccountsRequestStruct, - GetAccountRequestStruct, - CreateAccountRequestStruct, - FilterAccountChainsStruct, - UpdateAccountRequestStruct, - DeleteAccountRequestStruct, - ExportAccountRequestStruct, - ListRequestsRequestStruct, - GetRequestRequestStruct, - SubmitRequestRequestStruct, - ApproveRequestRequestStruct, - RejectRequestRequestStruct, -]); - -export type InternalRequest = Infer; - -// ---------------------------------------------------------------------------- -// Internal response - -export const InternalResponseStruct = union([ - ListAccountsResponseStruct, - GetAccountResponseStruct, - CreateAccountResponseStruct, - FilterAccountChainsResponseStruct, - UpdateAccountResponseStruct, - DeleteAccountResponseStruct, - ExportAccountResponseStruct, - ListRequestsResponseStruct, - GetRequestResponseStruct, - SubmitRequestResponseStruct, - ApproveRequestResponseStruct, - RejectRequestResponseStruct, -]); - -export type InternalResponse = Infer; diff --git a/src/internal/index.ts b/src/internal/index.ts new file mode 100644 index 000000000..4d4b4e299 --- /dev/null +++ b/src/internal/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './types'; diff --git a/src/internal/types.test.ts b/src/internal/types.test.ts new file mode 100644 index 000000000..c2eba773b --- /dev/null +++ b/src/internal/types.test.ts @@ -0,0 +1,88 @@ +import { assert } from 'superstruct'; + +import { InternalAccountStruct } from '.'; + +describe('InternalAccount', () => { + it('should have the correct structure', () => { + const account = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + keyring: { + type: 'Test Keyring', + }, + }, + }; + + expect(() => assert(account, InternalAccountStruct)).not.toThrow(); + }); + + it('should throw if metadata.keyring.type is not set', () => { + const account = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + keyring: {}, + }, + }; + + expect(() => assert(account, InternalAccountStruct)).toThrow( + 'At path: metadata.keyring.type -- Expected a string, but received: undefined', + ); + }); + + it('should throw if metadata.keyring is not set', () => { + const account = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: {}, + }; + + expect(() => assert(account, InternalAccountStruct)).toThrow( + 'At path: metadata.keyring -- Expected an object, but received: undefined', + ); + }); + + it('should throw if metadata is not set', () => { + const account = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + type: 'eip155:eoa', + }; + + expect(() => assert(account, InternalAccountStruct)).toThrow( + 'At path: metadata -- Expected an object, but received: undefined', + ); + }); + + it('should throw if there are extra fields', () => { + const account = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + keyring: { + type: 'Test Keyring', + }, + extra: 'field', + }, + }; + + expect(() => assert(account, InternalAccountStruct)).toThrow( + 'At path: metadata.extra -- Expected a value of type `never`', + ); + }); +}); diff --git a/src/internal/types.ts b/src/internal/types.ts new file mode 100644 index 000000000..387ce0a96 --- /dev/null +++ b/src/internal/types.ts @@ -0,0 +1,27 @@ +import { boolean, object, optional, string, type Infer } from 'superstruct'; + +import { KeyringAccountStruct } from '../api'; + +export const InternalAccountStruct = object({ + ...KeyringAccountStruct.schema, + metadata: object({ + snap: optional( + object({ + id: optional(string()), + name: optional(string()), + enabled: optional(boolean()), + }), + ), + keyring: object({ + type: string(), + }), + }), +}); + +/** + * Internal account representation. + * + * This type is used internally by MetaMask to add additional metadata to the + * account object. It's should not be used by external applications. + */ +export type InternalAccount = Infer; diff --git a/src/rpc-handler.ts b/src/rpc-handler.ts index c31a6b90c..a1e5431b5 100644 --- a/src/rpc-handler.ts +++ b/src/rpc-handler.ts @@ -15,7 +15,7 @@ import { FilterAccountChainsStruct, ListAccountsRequestStruct, ListRequestsRequestStruct, -} from './internal-api'; +} from './internal/api'; import { type JsonRpcRequest, JsonRpcRequestStruct } from './JsonRpcRequest'; /**