Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 93 additions & 7 deletions packages/keyring-api/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import {
object,
UuidStruct,
JsonRpcRequestStruct,
} from '@metamask/keyring-utils';
import { UuidStruct, JsonRpcRequestStruct } from '@metamask/keyring-utils';
import type { Infer } from '@metamask/superstruct';
import {
array,
object,
literal,
nullable,
number,
Expand All @@ -15,6 +12,7 @@ import {
} from '@metamask/superstruct';
import { JsonStruct } from '@metamask/utils';

import type { Keyring } from './api';
import {
CaipAssetTypeStruct,
CaipAssetTypeOrIdStruct,
Expand Down Expand Up @@ -51,6 +49,7 @@ export enum KeyringRpcMethod {
SubmitRequest = 'keyring_submitRequest',
ApproveRequest = 'keyring_approveRequest',
RejectRequest = 'keyring_rejectRequest',
Batch = 'keyring_batch',
}

/**
Expand All @@ -63,6 +62,19 @@ export function isKeyringRpcMethod(method: string): boolean {
return Object.values(KeyringRpcMethod).includes(method as KeyringRpcMethod);
}

/**
* Keyring RPC interface.
*/
export type KeyringRpc = Keyring & {
/**
* Batch keyring RPC requests.
*
* @returns A promise that resolves to an array of results or errors for each
* keyring RPC requests.
*/
batch(requests: BatchRequestRequest[]): Promise<BatchResponse>;
};

// ----------------------------------------------------------------------------

const CommonHeader = {
Expand Down Expand Up @@ -238,7 +250,7 @@ export type ResolveAccountAddressResponse = Infer<
// ----------------------------------------------------------------------------
// Filter account chains

export const FilterAccountChainsStruct = object({
export const FilterAccountChainsRequestStruct = object({
...CommonHeader,
method: literal('keyring_filterAccountChains'),
params: object({
Expand All @@ -248,7 +260,7 @@ export const FilterAccountChainsStruct = object({
});

export type FilterAccountChainsRequest = Infer<
typeof FilterAccountChainsStruct
typeof FilterAccountChainsRequestStruct
>;

export const FilterAccountChainsResponseStruct = array(string());
Expand Down Expand Up @@ -388,3 +400,77 @@ export type RejectRequestRequest = Infer<typeof RejectRequestRequestStruct>;
export const RejectRequestResponseStruct = literal(null);

export type RejectRequestResponse = Infer<typeof RejectRequestResponseStruct>;

// ----------------------------------------------------------------------------
// Batch RPC requests

export const BatchRequestRequestStruct = union([
ListAccountsRequestStruct,
GetAccountRequestStruct,
CreateAccountRequestStruct,
DiscoverAccountsRequestStruct,
ListAccountAssetsRequestStruct,
ListAccountTransactionsRequestStruct,
GetAccountBalancesRequestStruct,
ResolveAccountAddressRequestStruct,
FilterAccountChainsRequestStruct,
UpdateAccountRequestStruct,
DeleteAccountRequestStruct,
ExportAccountRequestStruct,
ListRequestsRequestStruct,
GetRequestRequestStruct,
SubmitRequestRequestStruct,
ApproveRequestRequestStruct,
RejectRequestRequestStruct,
]);

export type BatchRequestRequest = Infer<typeof BatchRequestRequestStruct>;

export const BatchRequestResponseStruct = union([
ListAccountsResponseStruct,
ListAccountsResponseStruct,
GetAccountResponseStruct,
CreateAccountResponseStruct,
DiscoverAccountsResponseStruct,
ListAccountAssetsResponseStruct,
ListAccountTransactionsResponseStruct,
GetAccountBalancesResponseStruct,
ResolveAccountAddressResponseStruct,
FilterAccountChainsResponseStruct,
UpdateAccountResponseStruct,
DeleteAccountResponseStruct,
ExportAccountResponseStruct,
ListRequestsResponseStruct,
GetRequestResponseStruct,
SubmitRequestResponseStruct,
ApproveRequestResponseStruct,
RejectRequestResponseStruct,
]);

export type BatchRequestResponse = Infer<typeof BatchRequestResponseStruct>;

export const BatchRequestStruct = object({
...CommonHeader,
method: literal('keyring_batch'),
params: object({
id: UuidStruct,
requests: array(BatchRequestRequestStruct),
}),
});

export type BatchRequest = Infer<typeof BatchRequestStruct>;

export const BatchResponseStruct = array(
union([
object({
response: BatchRequestResponseStruct,
}),
object({
error: string(),
}),
]),
);

export type BatchResponse = Infer<typeof BatchResponseStruct>;

export type BatchResponseOne = BatchResponse[number];
17 changes: 15 additions & 2 deletions packages/keyring-snap-client/src/KeyringClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {
KeyringRpcMethod,
ResolveAccountAddressResponseStruct,
DiscoverAccountsResponseStruct,
BatchResponseStruct,
} from '@metamask/keyring-api';
import type {
Keyring,
KeyringAccount,
KeyringRequest,
KeyringAccountData,
Expand All @@ -33,6 +33,9 @@ import type {
CaipAssetTypeOrId,
EntropySourceId,
DiscoveredAccount,
KeyringRpc,
BatchRequestRequest,
BatchResponse,
} from '@metamask/keyring-api';
import type { JsonRpcRequest } from '@metamask/keyring-utils';
import { strictMask } from '@metamask/keyring-utils';
Expand All @@ -44,7 +47,7 @@ export type Sender = {
send(request: JsonRpcRequest): Promise<Json>;
};

export class KeyringClient implements Keyring {
export class KeyringClient implements KeyringRpc {
readonly #sender: Sender;

/**
Expand Down Expand Up @@ -259,4 +262,14 @@ export class KeyringClient implements Keyring {
RejectRequestResponseStruct,
);
}

async batch(requests: BatchRequestRequest[]): Promise<BatchResponse> {
return strictMask(
await this.send({
method: KeyringRpcMethod.Batch,
params: { requests },
}),
BatchResponseStruct,
);
}
}
7 changes: 7 additions & 0 deletions packages/keyring-snap-client/src/KeyringPublicClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type {
BatchRequestRequest,
BatchResponse,
KeyringAccount,
KeyringAccountData,
KeyringRequest,
Expand Down Expand Up @@ -35,6 +37,7 @@ export const KeyringPublicRpcMethod = [
KeyringRpcMethod.ApproveRequest,
KeyringRpcMethod.RejectRequest,
KeyringRpcMethod.ListRequests,
KeyringRpcMethod.Batch,
] as const;

/**
Expand Down Expand Up @@ -105,4 +108,8 @@ export class KeyringPublicClient
async exportAccount(id: string): Promise<KeyringAccountData> {
return this.#client.exportAccount(id);
}

async batch(requests: BatchRequestRequest[]): Promise<BatchResponse> {
return this.#client.batch(requests);
}
}
47 changes: 43 additions & 4 deletions packages/keyring-snap-sdk/src/rpc-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Keyring } from '@metamask/keyring-api';
import type {
Keyring,
BatchRequest,
BatchResponse,
} from '@metamask/keyring-api';
import {
KeyringRpcMethod,
GetAccountRequestStruct,
Expand All @@ -11,13 +15,14 @@ import {
RejectRequestRequestStruct,
SubmitRequestRequestStruct,
UpdateAccountRequestStruct,
FilterAccountChainsStruct,
FilterAccountChainsRequestStruct,
ListAccountsRequestStruct,
ListRequestsRequestStruct,
GetAccountBalancesRequestStruct,
ListAccountAssetsRequestStruct,
ResolveAccountAddressRequestStruct,
DiscoverAccountsRequestStruct,
BatchRequestStruct,
} from '@metamask/keyring-api';
import type { JsonRpcRequest } from '@metamask/keyring-utils';
import { JsonRpcRequestStruct } from '@metamask/keyring-utils';
Expand All @@ -39,11 +44,13 @@ export class MethodNotSupportedError extends Error {
*
* @param keyring - Keyring instance.
* @param request - Keyring JSON-RPC request.
* @param handleBatch - Batched requests handler.
* @returns A promise that resolves to the keyring response.
*/
async function dispatchRequest(
keyring: Keyring,
request: JsonRpcRequest,
handleBatch?: (batched: BatchRequest) => BatchResponse,
): Promise<Json | void> {
// We first have to make sure that the request is a valid JSON-RPC request so
// we can check its method name.
Expand Down Expand Up @@ -119,7 +126,7 @@ async function dispatchRequest(
}

case `${KeyringRpcMethod.FilterAccountChains}`: {
assert(request, FilterAccountChainsStruct);
assert(request, FilterAccountChainsRequestStruct);
return keyring.filterAccountChains(
request.params.id,
request.params.chains,
Expand Down Expand Up @@ -181,6 +188,36 @@ async function dispatchRequest(
return keyring.rejectRequest(request.params.id);
}

case `${KeyringRpcMethod.Batch}`: {
assert(request, BatchRequestStruct);

if (handleBatch) {
return handleBatch(request);
}

// If there's no custom handler for batched requests, we just fallback by executing them 1
// by 1.
const responses: Json = await Promise.all(
request.params.requests.map(async (subRequest) => {
try {
let response = await dispatchRequest(keyring, subRequest);

// For batch responses, we "convert" any `void` return type to null so it can
// be casted to a normal `Json` type.
if (response === undefined) {
response = null;
}

return { response };
} catch (error) {
return { error: (error as Error).message };
}
}),
);

return responses;
}

default: {
throw new MethodNotSupportedError(request.method);
}
Expand All @@ -195,6 +232,7 @@ async function dispatchRequest(
*
* @param keyring - Keyring instance.
* @param request - Keyring JSON-RPC request.
* @param handleBatch - Batched requests handler.
* @returns A promise that resolves to the keyring response.
* @example
* ```ts
Expand All @@ -209,9 +247,10 @@ async function dispatchRequest(
export async function handleKeyringRequest(
keyring: Keyring,
request: JsonRpcRequest,
handleBatch?: (batched: BatchRequest) => BatchResponse,
): Promise<Json | void> {
try {
return await dispatchRequest(keyring, request);
return await dispatchRequest(keyring, request, handleBatch);
} catch (error) {
const message =
error instanceof Error && typeof error.message === 'string'
Expand Down
Loading