Skip to content

Commit

Permalink
feat: chainagnostic provider, coreprovider test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
vvava committed Jul 29, 2024
1 parent 37a6f8b commit fd6a6df
Show file tree
Hide file tree
Showing 6 changed files with 786 additions and 190 deletions.
132 changes: 132 additions & 0 deletions src/background/providers/ChainAgnosticProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ethErrors } from 'eth-rpc-errors';
import AutoPairingPostMessageConnection from '../utils/messaging/AutoPairingPostMessageConnection';
import { ChainAgnostinProvider } from './ChainAgnosticProvider';

jest.mock('./utils/onDomReady');
jest.mock('../utils/messaging/AutoPairingPostMessageConnection', () => {
const mocks = {
connect: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
request: jest.fn().mockResolvedValue({}),
};
return jest.fn().mockReturnValue(mocks);
});
describe('src/background/providers/ChainAgnosticProvider', () => {
const channelMock = new AutoPairingPostMessageConnection(false);

describe('initialization', () => {
it('should connect to the backgroundscript', async () => {
new ChainAgnostinProvider(channelMock);

expect(channelMock.connect).toHaveBeenCalled();
expect(channelMock.request).not.toHaveBeenCalled();
});
it('waits for message channel to be connected', async () => {
const mockedChannel = new AutoPairingPostMessageConnection(false);

const provider = new ChainAgnostinProvider(channelMock);
expect(mockedChannel.connect).toHaveBeenCalled();
expect(mockedChannel.request).not.toHaveBeenCalled();

await provider.request({
data: { method: 'some-method', params: [{ param1: 1 }] },
sessionId: '00000000-0000-0000-0000-000000000000',
chainId: '1',
});
expect(mockedChannel.request).toHaveBeenCalled();
});
});

describe('request', () => {
it('should use the rate limits on `eth_requestAccounts` requests', async () => {
const provider = new ChainAgnostinProvider(channelMock);
(channelMock.request as jest.Mock).mockResolvedValue('success');

const firstCallCallback = jest.fn();
const secondCallCallback = jest.fn();
provider
.request({
data: { method: 'eth_requestAccounts' },
} as any)
.then(firstCallCallback)
.catch(firstCallCallback);
provider
.request({
data: { method: 'eth_requestAccounts' },
} as any)
.then(secondCallCallback)
.catch(secondCallCallback);

await new Promise(process.nextTick);
expect(firstCallCallback).toHaveBeenCalledWith('success');
expect(secondCallCallback).toHaveBeenCalledWith(
ethErrors.rpc.resourceUnavailable(
`Request of type eth_requestAccounts already pending for origin. Please wait.`
)
);
});
it('shoud not use the rate limits on `random_method` requests', async () => {
const provider = new ChainAgnostinProvider(channelMock);
(channelMock.request as jest.Mock).mockResolvedValue('success');

const firstCallCallback = jest.fn();
const secondCallCallback = jest.fn();
provider
.request({
data: { method: 'random_method' },
} as any)
.then(firstCallCallback)
.catch(firstCallCallback);
provider
.request({
data: { method: 'random_method' },
} as any)
.then(secondCallCallback)
.catch(secondCallCallback);

await new Promise(process.nextTick);
expect(firstCallCallback).toHaveBeenCalledWith('success');
expect(secondCallCallback).toHaveBeenCalledWith('success');
});

it('should call the request of the connection', async () => {
const provider = new ChainAgnostinProvider(channelMock);
(channelMock.request as jest.Mock).mockResolvedValueOnce('success');

await provider.request({
data: { method: 'some-method', params: [{ param1: 1 }] },
sessionId: '00000000-0000-0000-0000-000000000000',
chainId: '1',
});
expect(channelMock.request).toHaveBeenCalled();
});
describe('CAIP-27', () => {
it('should wrap the incoming request into CAIP-27 envelope and reuses the provided ID', async () => {
const provider = new ChainAgnostinProvider(channelMock);
// response for the actual call
(channelMock.request as jest.Mock).mockResolvedValueOnce('success');

provider.request({
data: { method: 'some-method', params: [{ param1: 1 }] },
sessionId: '00000000-0000-0000-0000-000000000000',
chainId: '1',
});

await new Promise(process.nextTick);

expect(channelMock.request).toHaveBeenCalledWith({
jsonrpc: '2.0',
method: 'provider_request',
params: {
scope: 'eip155:1',
sessionId: '00000000-0000-0000-0000-000000000000',
request: {
method: 'some-method',
params: [{ param1: 1 }],
},
},
});
});
});
});
});
87 changes: 87 additions & 0 deletions src/background/providers/ChainAgnosticProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import EventEmitter from 'events';
import {
JsonRpcRequest,
JsonRpcRequestPayload,
} from '../connections/dAppConnection/models';
import { PartialBy } from '../models';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import AbstractConnection from '../utils/messaging/AbstractConnection';
import { ChainId } from '@avalabs/chains-sdk';
import RequestRatelimiter from './utils/RequestRatelimiter';

export class ChainAgnostinProvider extends EventEmitter {
#contentScriptConnection: AbstractConnection;

#requestRateLimiter = new RequestRatelimiter([
'eth_requestAccounts',
'avalanche_selectWallet',
]);

constructor(connection) {
super();
this.#contentScriptConnection = connection;
this.#init();
}

async #init() {
await this.#contentScriptConnection.connect();
}

#request = async ({
data,
sessionId,
chainId,
}: {
data: PartialBy<JsonRpcRequestPayload, 'id' | 'params'>;
sessionId: string;
chainId: string | null;
}) => {
if (!data) {
throw ethErrors.rpc.invalidRequest();
}

const result = this.#contentScriptConnection
.request({
method: 'provider_request',
jsonrpc: '2.0',
params: {
scope: `eip155:${
chainId ? parseInt(chainId) : ChainId.AVALANCHE_MAINNET_ID
}`,
sessionId,
request: {
params: [],
...data,
},
},
} as JsonRpcRequest)
.catch((err) => {
// If the error is already a JsonRPCErorr do not serialize them.
// eth-rpc-errors always wraps errors if they have an unkown error code
// even if the code is valid like 4902 for unrecognized chain ID.
if (!!err.code && Number.isInteger(err.code) && !!err.message) {
throw err;
}
throw serializeError(err);
});
return result;
};

request = async ({
data,
sessionId,
chainId,
}: {
data: PartialBy<JsonRpcRequestPayload, 'id' | 'params'>;
sessionId: string;
chainId: string | null;
}) => {
return this.#requestRateLimiter.call(data.method, () =>
this.#request({ data, chainId, sessionId })
);
};

subscribeToMessage = (callback) => {
this.#contentScriptConnection.on('message', callback);
};
}
Loading

0 comments on commit fd6a6df

Please sign in to comment.