Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/stellar-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "E8ptwYRcKs5dUP6eKBAJu4cRzIYNfGVI1kDp1znZkYI=",
"shasum": "MGLWLhlYBn+se/MUt9Lq70AkSl4F4oZaVAVXRgqtoLQ=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
1 change: 0 additions & 1 deletion packages/stellar-wallet-snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,6 @@ const clientRequestMethodHandlers: Record<
};

const clientRequestHandler = new ClientRequestHandler({
logger,
handlers: clientRequestMethodHandlers,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MethodNotFoundError } from '@metamask/snaps-sdk';

import { ClientRequestMethod } from './api';
import { ClientRequestHandler } from './clientRequest';

describe('ClientRequestHandler', () => {
const mockHandle = jest.fn();

function setup(): ClientRequestHandler {
mockHandle.mockReset();
return new ClientRequestHandler({
handlers: {
[ClientRequestMethod.ComputeFee]: { handle: mockHandle },
} as never,
});
}

const request = {
jsonrpc: '2.0',
id: 1,
method: ClientRequestMethod.ComputeFee,
} as const;

it('routes the request to the handler for its method', async () => {
const handler = setup();
mockHandle.mockResolvedValue({ fee: '100' });

expect(await handler.handle(request)).toStrictEqual({ fee: '100' });
expect(mockHandle).toHaveBeenCalledWith(request);
});

it('returns null when the method handler returns nothing', async () => {
const handler = setup();
mockHandle.mockResolvedValue(undefined);

expect(await handler.handle(request)).toBeNull();
});

it('throws MethodNotFoundError for an unknown method', async () => {
const handler = setup();

await expect(
handler.handle({ ...request, method: 'unknownMethod' }),
).rejects.toThrow(MethodNotFoundError);
expect(mockHandle).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import type { Logger } from '@metamask/snap-networks-utils';
import { MethodNotFoundError } from '@metamask/snaps-sdk';
import type { Json, JsonRpcRequest } from '@metamask/utils';
import { ensureError } from '@metamask/utils';

import { withCatchAndThrowSnapError } from '../../utils';
import type { ClientRequestMethod } from './api';
import { ClientRequestMethodStruct } from './api';
import type { IClientRequestHandler } from './base';

export class ClientRequestHandler {
readonly #logger: Logger;

readonly #handlers: Record<ClientRequestMethod, IClientRequestHandler>;

constructor({
logger,
handlers,
}: {
logger: Logger;
handlers: Record<ClientRequestMethod, IClientRequestHandler>;
}) {
this.#logger = logger.withPrefix('[👋 ClientRequestHandler]');
this.#handlers = handlers;
}

Expand All @@ -35,12 +28,7 @@ export class ClientRequestHandler {
* @throws {InvalidParamsError} If the params are invalid.
*/
async handle(request: JsonRpcRequest): Promise<Json> {
const result =
(await withCatchAndThrowSnapError(async () => {
return this.#handleClientRequest(request);
}, this.#logger.error.bind(this.#logger))) ?? null;

return result;
return (await this.#handleClientRequest(request)) ?? null;
}

/**
Expand Down
15 changes: 6 additions & 9 deletions packages/stellar-wallet-snap/src/handlers/cronjob/cronjob.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { JsonRpcRequest } from '@metamask/snaps-sdk';

import { withCatchAndThrowSnapError } from '../../utils';
import { getClientStatus } from '../../utils/snap';
import type { BackgroundEventMethod, ICronjobRequestHandler } from './api';
import { BackgroundEventMethodStruct } from './api';
Expand All @@ -18,16 +17,14 @@ export class CronjobHandler {
}

async handle(request: JsonRpcRequest): Promise<void> {
await withCatchAndThrowSnapError(async () => {
const { active, locked } = await getClientStatus();
const { active, locked } = await getClientStatus();

// if the client is not active or locked, we dont execute the cronjob
if (!active || locked) {
return;
}
// if the client is not active or locked, we dont execute the cronjob
if (!active || locked) {
return;
}

await this.#handleRequest(request);
});
await this.#handleRequest(request);
}

async #handleRequest(request: JsonRpcRequest): Promise<void> {
Expand Down
32 changes: 14 additions & 18 deletions packages/stellar-wallet-snap/src/handlers/keyring/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ import {
isSlip44Id,
rethrowIfInstanceElseThrow,
validateRequest,
withCatchAndThrowSnapError,
} from '../../utils';
import { getSupportedScopes } from '../../utils/scopes';
import { SyncAccountsHandler } from '../cronjob/syncAccounts';
Expand All @@ -81,6 +80,8 @@ import {
import type { IKeyringRequestHandler } from './base';
import { ExportAccountException } from './exceptions';

export const KEYRING_HANDLER_LOGGER_PREFIX = '[🔑 KeyringHandler]';

export class KeyringHandler implements KeyringSnapRpc {
readonly #logger: Logger;

Expand Down Expand Up @@ -109,7 +110,7 @@ export class KeyringHandler implements KeyringSnapRpc {
walletService: WalletService;
handlers: Record<MultichainMethod, IKeyringRequestHandler>;
}) {
this.#logger = logger.withPrefix('[🔑 KeyringHandler]');
this.#logger = logger.withPrefix(KEYRING_HANDLER_LOGGER_PREFIX);
this.#accountService = accountService;
this.#onChainAccountService = onChainAccountService;
this.#transactionService = transactionService;
Expand All @@ -118,22 +119,17 @@ export class KeyringHandler implements KeyringSnapRpc {
}

async handle(origin: string, request: JsonRpcRequest): Promise<Json> {
const result =
(await withCatchAndThrowSnapError(async () => {
this.#logger.debug('Handle keyring request', {
origin,
method: request.method,
});
validateOrigin(origin, request.method, originPermissions);
const keyringRequestResult = await handleKeyringRequest(this, request);
this.#logger.debug('Keyring request handled', {
origin,
method: request.method,
});
return keyringRequestResult;
}, this.#logger.error.bind(this.#logger))) ?? null;

return result;
this.#logger.debug('Handle keyring request', {
origin,
method: request.method,
});
validateOrigin(origin, request.method, originPermissions);
const result = await handleKeyringRequest(this, request);
this.#logger.debug('Keyring request handled', {
origin,
method: request.method,
});
return result ?? null;
}

async getAccount(accountId: GetAccountRequest): Promise<KeyringAccount> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { logger } from '../../utils/logger';
import type { UserInputUiEventHandler } from './api';
import { UserInputHandler } from './userInput';

const mockEventHandler = jest.fn();

type MockEventsModule = {
createEventHandlers: () => Record<string, UserInputUiEventHandler>;
};

/**
* Builds a mocked `events` module exposing the given UI event handlers.
*
* @param handlers - The UI event handlers keyed by event name.
* @returns The mocked module.
*/
function mockEventsModule(
handlers: Record<string, UserInputUiEventHandler> = {},
): MockEventsModule {
return { createEventHandlers: (): typeof handlers => handlers };
}

jest.mock('../../utils/logger');

jest.mock(
'../../ui/confirmation/views/ConfirmSignMessage/events',
(): MockEventsModule =>
mockEventsModule({
testEvent: async (...args): Promise<void> => mockEventHandler(...args),
}),
);
jest.mock(
'../../ui/confirmation/views/ConfirmSendTransaction/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/ConfirmSignAuthEntry/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/ConfirmSignChangeTrustOptIn/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/ConfirmSignChangeTrustOptOut/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/ConfirmSignTransaction/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/MaliciousAcknowledgement/events',
(): MockEventsModule => mockEventsModule(),
);
jest.mock(
'../../ui/confirmation/views/MemoEdit/events',
(): MockEventsModule => mockEventsModule(),
);

describe('UserInputHandler', () => {
const handler = new UserInputHandler({ logger });

beforeEach(() => {
mockEventHandler.mockReset().mockResolvedValue(undefined);
});

it('routes the event to the handler matching its name', async () => {
const params = {
id: 'interface-id',
event: { type: 'ButtonClickEvent', name: 'testEvent' },
context: null,
} as never;

await handler.handle(params);

expect(mockEventHandler).toHaveBeenCalledWith(params);
});

it('propagates errors from the event handler', async () => {
mockEventHandler.mockRejectedValue(new Error('Event failed'));

await expect(
handler.handle({
id: 'interface-id',
event: { type: 'ButtonClickEvent', name: 'testEvent' },
context: null,
} as never),
).rejects.toThrow('Event failed');
});

it.each([
['has no name', { type: 'ButtonClickEvent' }],
['has no matching handler', { type: 'ButtonClickEvent', name: 'unknown' }],
])('ignores an event that %s', async (_case, event) => {
await handler.handle({
id: 'interface-id',
event,
context: null,
} as never);

expect(mockEventHandler).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { createEventHandlers as createSignMessageEvents } from '../../ui/confirm
import { createEventHandlers as createSignTransactionEvents } from '../../ui/confirmation/views/ConfirmSignTransaction/events';
import { createEventHandlers as createMaliciousAcknowledgementEvents } from '../../ui/confirmation/views/MaliciousAcknowledgement/events';
import { createEventHandlers as createMemoEditEvents } from '../../ui/confirmation/views/MemoEdit/events';
import { withCatchAndThrowSnapError } from '../../utils';
import type { UserInputUiEventHandler } from './api';

export class UserInputHandler {
Expand Down Expand Up @@ -63,8 +62,6 @@ export class UserInputHandler {
return;
}

await withCatchAndThrowSnapError(async () =>
handler({ id, event, context }),
);
await handler({ id, event, context });
}
}
Loading
Loading