-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: chainagnostic provider, coreprovider test cases
- Loading branch information
Showing
6 changed files
with
786 additions
and
190 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }], | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} |
Oops, something went wrong.