Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adapt to eip-1193 provider changes #190

Merged
merged 4 commits into from
Jul 17, 2024
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"dependencies": {
"@metamask/base-controller": "^3.0.0",
"@metamask/controller-utils": "^8.0.1",
"@metamask/network-controller": "^17.0.0",
"@metamask/network-controller": "^20.0.0",
"@metamask/rpc-errors": "^6.3.1",
"@metamask/utils": "^8.3.0",
"await-semaphore": "^0.1.3",
"crypto-js": "^4.2.0",
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export type {
PPOMState,
UsePPOM,
PPOMControllerActions,
PPOMControllerEvents,
PPOMControllerMessenger,
} from './ppom-controller';
export { NETWORK_CACHE_DURATION, PPOMController } from './ppom-controller';
Expand Down
106 changes: 72 additions & 34 deletions src/ppom-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ describe('PPOMController', () => {
buildFetchSpy();
const { ppomController } = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2(undefined, 'DUMMY_VALUE');
request: async () => {
return Promise.resolve('DUMMY_VALUE');
},
},
});
Expand Down Expand Up @@ -244,6 +244,7 @@ describe('PPOMController', () => {
}),
state: {
storageMetadata: StorageMetadata,
versionInfo: [],
},
});

Expand Down Expand Up @@ -306,14 +307,29 @@ describe('PPOMController', () => {
status: 304,
json: () => VERSION_INFO,
});
const { changeNetwork, ppomController } = buildPPOMController();
const { changeNetwork, mockGetNetworkClientById, ppomController } =
buildPPOMController();
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
expect(spy).toHaveBeenCalledTimes(3);

changeNetwork('0x2');
changeNetwork(Utils.SUPPORTED_NETWORK_CHAINIDS.MAINNET);
mockGetNetworkClientById.mockReturnValue({
configuration: {
chainId: '0x2',
},
});
changeNetwork({
selectedNetworkClientId: 'selectedNetworkClientId1',
});
mockGetNetworkClientById.mockReturnValue({
configuration: {
chainId: Utils.SUPPORTED_NETWORK_CHAINIDS.MAINNET,
},
});
changeNetwork({
selectedNetworkClientId: 'selectedNetworkClientId2',
});
await ppomController.usePPOM(async () => {
return Promise.resolve();
});
Expand All @@ -326,7 +342,7 @@ describe('PPOMController', () => {
const { ppomController } = buildPPOMController({
ppomProvider: {
ppomInit: async () => {
return Promise.resolve('123');
return Promise.resolve();
},
PPOM: new PPOMClass(undefined, freeMock),
},
Expand Down Expand Up @@ -365,36 +381,51 @@ describe('PPOMController', () => {
const freeMock = jest.fn().mockImplementation(() => {
throw new Error('some error');
});
const { changeNetwork, ppomController } = buildPPOMController({
ppomProvider: {
ppomInit: async () => {
return Promise.resolve('123');
const { changeNetwork, mockGetNetworkClientById, ppomController } =
buildPPOMController({
ppomProvider: {
ppomInit: async () => {
return Promise.resolve('123');
},
PPOM: new PPOMClass(undefined, freeMock),
},
PPOM: new PPOMClass(undefined, freeMock),
},
});
});
// calling usePPOM initialises PPOM
await ppomController.usePPOM(async (ppom: any) => {
expect(ppom).toBeDefined();
return Promise.resolve();
});

mockGetNetworkClientById.mockReturnValue({
configuration: {
chainId: '0x2',
},
});
expect(async () => {
changeNetwork('0x2');
changeNetwork({
selectedNetworkClientId: 'selectedNetworkClientId1',
});
}).not.toThrow();
expect(mockMutexUse).toHaveBeenCalledTimes(2);
});

it('should not do anything when networkChange called for same network', async () => {
buildFetchSpy();
const { changeNetwork, ppomController } = buildPPOMController();
const { changeNetwork, mockGetNetworkClientById, ppomController } =
buildPPOMController();
// calling usePPOM initialises PPOM
await ppomController.usePPOM(async (ppom: any) => {
expect(ppom).toBeDefined();
return Promise.resolve();
});

changeNetwork(Utils.SUPPORTED_NETWORK_CHAINIDS.MAINNET);
mockGetNetworkClientById.mockReturnValue({
configuration: {
chainId: Utils.SUPPORTED_NETWORK_CHAINIDS.MAINNET,
},
});
changeNetwork({
selectedNetworkClientId: 'selectedNetworkClientId1',
});
expect(mockMutexUse).toHaveBeenCalledTimes(1);
});
});
Expand Down Expand Up @@ -434,7 +465,7 @@ describe('PPOMController', () => {
},
ppomProvider: {
ppomInit: async () => {
return Promise.resolve('123');
return Promise.resolve();
},
PPOM: new PPOMClass(undefined, freeMock),
},
Expand Down Expand Up @@ -473,41 +504,46 @@ describe('PPOMController', () => {
describe('jsonRPCRequest', () => {
it('should propagate to ppom in correct format if JSON RPC request on provider fails', async () => {
buildFetchSpy();

const { ppomController } = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2('DUMMY_ERROR');
request: () => {
throw new Error('DUMMY_ERROR');
},
},
});

const result = await ppomController.usePPOM(async (ppom: any) => {
return await ppom.testJsonRPCRequest();
await ppomController.usePPOM(async (ppom: any) => {
await expect(ppom.testJsonRPCRequest()).rejects.toThrow('DUMMY_ERROR');
});
expect(result.error).toBe('DUMMY_ERROR');
});

it('should not call provider if method call on provider is not allowed to PPOM', async () => {
buildFetchSpy();
const sendAsyncMock = jest.fn();
const requestMock = jest.fn();
const { ppomController } = buildPPOMController({
provider: {
sendAsync: sendAsyncMock,
request: requestMock,
},
});

await ppomController.usePPOM(async (ppom: any) => {
await ppom.testJsonRPCRequest('DUMMY_METHOD');
await expect(ppom.testJsonRPCRequest('DUMMY_METHOD')).rejects.toThrow(
expect.objectContaining({
code: Utils.PROVIDER_ERRORS.methodNotSupported().code,
message: Utils.PROVIDER_ERRORS.methodNotSupported().message,
}),
);
});
expect(sendAsyncMock).toHaveBeenCalledTimes(0);
expect(requestMock).toHaveBeenCalledTimes(0);
});

it('should rate limit number of requests by PPOM on provider', async () => {
buildFetchSpy();
const { ppomController } = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2(undefined, 'DUMMY_VALUE');
request: async () => {
return Promise.resolve('DUMMY_VALUE');
},
},
providerRequestLimit: 5,
Expand All @@ -520,9 +556,11 @@ describe('PPOMController', () => {
await ppom.testJsonRPCRequest();
await ppom.testJsonRPCRequest();
await ppom.testJsonRPCRequest();
const result = await ppom.testJsonRPCRequest();
expect(result.error.code).toBe(
Utils.PROVIDER_ERRORS.limitExceeded().error.code,
await expect(ppom.testJsonRPCRequest()).rejects.toThrow(
expect.objectContaining({
code: Utils.PROVIDER_ERRORS.limitExceeded().code,
message: Utils.PROVIDER_ERRORS.limitExceeded().message,
}),
);
});
});
Expand All @@ -531,8 +569,8 @@ describe('PPOMController', () => {
buildFetchSpy();
const { ppomController } = buildPPOMController({
provider: {
sendAsync: (_arg1: any, arg2: any) => {
arg2(undefined, 'DUMMY_VALUE');
request: async () => {
return Promise.resolve('DUMMY_VALUE');
},
},
providerRequestLimit: 25,
Expand Down
92 changes: 39 additions & 53 deletions src/ppom-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller';
import { BaseControllerV2 } from '@metamask/base-controller';
import { safelyExecute, timeoutFetch } from '@metamask/controller-utils';
import type {
NetworkControllerGetNetworkClientByIdAction,
NetworkControllerStateChangeEvent,
NetworkState,
Provider,
} from '@metamask/network-controller';
import type {
JsonRpcFailure,
Json,
JsonRpcParams,
JsonRpcSuccess,
} from '@metamask/utils';
import { JsonRpcError } from '@metamask/rpc-errors';
import type { Json, JsonRpcParams } from '@metamask/utils';
import { Mutex } from 'await-semaphore';

import type {
Expand All @@ -26,7 +23,6 @@ import {
checkFilePath,
constructURLHref,
createPayload,
IdGenerator,
PROVIDER_ERRORS,
validateSignature,
} from './util';
Expand Down Expand Up @@ -133,14 +129,16 @@ export type UsePPOM = {

export type PPOMControllerActions = UsePPOM;

export type PPOMControllerEvents = NetworkControllerStateChangeEvent;
export type AllowedEvents = NetworkControllerStateChangeEvent;

export type AllowedActions = NetworkControllerGetNetworkClientByIdAction;

export type PPOMControllerMessenger = RestrictedControllerMessenger<
typeof controllerName,
PPOMControllerActions,
NetworkControllerStateChangeEvent,
never,
NetworkControllerStateChangeEvent['type']
PPOMControllerActions | AllowedActions,
AllowedEvents,
AllowedActions['type'],
AllowedEvents['type']
>;

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down Expand Up @@ -382,7 +380,12 @@ export class PPOMController extends BaseControllerV2<
* 2. reset PPOM
*/
#onNetworkChange(networkControllerState: NetworkState): void {
const id = addHexPrefix(networkControllerState.providerConfig.chainId);
const selectedNetworkClient = this.messagingSystem.call(
'NetworkController:getNetworkClientById',
networkControllerState.selectedNetworkClientId,
);
const { chainId } = selectedNetworkClient.configuration;
const id = addHexPrefix(chainId);
if (id === this.#chainId) {
return;
}
Expand Down Expand Up @@ -620,47 +623,30 @@ export class PPOMController extends BaseControllerV2<
* Send a JSON RPC request to the provider.
* This method is used by the PPOM to make requests to the provider.
*/
async #jsonRpcRequest(
method: string,
params: JsonRpcParams,
): Promise<
| JsonRpcSuccess<Json>
| (Omit<JsonRpcFailure, 'error'> & { error: unknown })
| ReturnType<(typeof PROVIDER_ERRORS)[keyof typeof PROVIDER_ERRORS]>
> {
return new Promise((resolve) => {
// Resolve with error if number of requests from PPOM to provider exceeds the limit for the current transaction
if (this.#providerRequests > this.#providerRequestLimit) {
resolve(PROVIDER_ERRORS.limitExceeded());
return;
}
this.#providerRequests += 1;
// Resolve with error if the provider method called by PPOM is not allowed for PPOM
if (!ALLOWED_PROVIDER_CALLS.includes(method)) {
resolve(PROVIDER_ERRORS.methodNotSupported());
return;
}

this.#providerRequestsCount[method] = this.#providerRequestsCount[method]
? Number(this.#providerRequestsCount[method]) + 1
: 1;

// Invoke provider and return result
this.#provider.sendAsync(
createPayload(method, params),
(error, res: JsonRpcSuccess<Json>) => {
if (error) {
resolve({
jsonrpc: '2.0',
id: IdGenerator(),
error,
});
} else {
resolve(res);
}
},
async #jsonRpcRequest(method: string, params: JsonRpcParams): Promise<Json> {
// Resolve with error if number of requests from PPOM to provider exceeds the limit for the current transaction
if (this.#providerRequests > this.#providerRequestLimit) {
const limitExceededError = PROVIDER_ERRORS.limitExceeded();
throw new JsonRpcError(
limitExceededError.code,
limitExceededError.message,
);
});
}
this.#providerRequests += 1;
// Resolve with error if the provider method called by PPOM is not allowed for PPOM
if (!ALLOWED_PROVIDER_CALLS.includes(method)) {
const methodNotSupportedError = PROVIDER_ERRORS.methodNotSupported();
throw new JsonRpcError(
methodNotSupportedError.code,
methodNotSupportedError.message,
);
}

this.#providerRequestsCount[method] = this.#providerRequestsCount[method]
? Number(this.#providerRequestsCount[method]) + 1
: 1;

return await this.#provider.request(createPayload(method, params));
}

/*
Expand Down
23 changes: 5 additions & 18 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,11 @@ export const createPayload = (method: string, params: JsonRpcParams) =>
} as const);

export const PROVIDER_ERRORS = {
limitExceeded: () =>
({
jsonrpc: '2.0',
id: IdGenerator(),
error: {
code: -32005,
message: 'Limit exceeded',
},
} as const),
methodNotSupported: () =>
({
jsonrpc: '2.0',
id: IdGenerator(),
error: {
code: -32601,
message: 'Method not supported',
},
} as const),
limitExceeded: () => ({ code: -32005, message: 'Limit exceeded' }),
methodNotSupported: () => ({
code: -32601,
message: 'Method not supported',
}),
};

const getHash = async (
Expand Down
Loading
Loading