Skip to content

Commit

Permalink
Merge pull request #2426 from ethereum/issue/2266
Browse files Browse the repository at this point in the history
CustomProvider
  • Loading branch information
nivida authored Feb 28, 2019
2 parents b88ab82 + 91d925a commit 2eeedca
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 14 deletions.
14 changes: 14 additions & 0 deletions packages/web3-providers/src/factories/ProvidersModuleFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import BatchRequest from '../batch-request/BatchRequest';
import EthereumProvider from '../providers/EthereumProvider';
import MetamaskProvider from '../providers/MetamaskProvider';
import MistEthereumProvider from '../providers/MistEthereumProvider';
import CustomProvider from '../providers/CustomProvider';

export default class ProvidersModuleFactory {
/**
Expand Down Expand Up @@ -203,4 +204,17 @@ export default class ProvidersModuleFactory {
createMistEthereumProvider(mistEthereumProvider) {
return new MistEthereumProvider(mistEthereumProvider);
}

/**
* Returns an CustomProvider object
*
* @method createCustomProvider
*
* @param {Object} connection
*
* @returns {CustomProvider}
*/
createCustomProvider(connection) {
return new CustomProvider(connection);
}
}
138 changes: 138 additions & 0 deletions packages/web3-providers/src/providers/CustomProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
This file is part of web3.js.
web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file CustomProvider.js
* @author Samuel Furter <samuel@ethereum.org>
* @date 2019
*/
import JsonRpcMapper from '../mappers/JsonRpcMapper';
import JsonRpcResponseValidator from '../validators/JsonRpcResponseValidator';

export default class CustomProvider {
/**
* @param {Object} connection
*
* @constructor
*/
constructor(connection) {
this.host = 'CustomProvider';
this.connection = connection;
this.checkConnectionMethods();
}

/**
* Checks if the given custom provider does have the required methods
*
* @method checkConnectionMethods
*
* @returns {Boolean}
*/
checkConnectionMethods() {
if (this.connection.send || this.connection.sendAsync) {
return true;
}

throw new Error('Invalid provider injected!');
}

/**
* Added this method to have a better error message if someone is trying to create a subscription with this provider.
*/
subscribe() {
throw new Error('Subscriptions are not supported with the CustomProvider.');
}

/**
* Added this method to have a better error message if someone is trying to unsubscribe with this provider.
*/
unsubscribe() {
throw new Error('Subscriptions are not supported with the CustomProvider.');
}

/**
* Creates the JSON-RPC payload and sends it to the node.
*
* @method send
*
* @param {String} method
* @param {Array} parameters
*
* @returns {Promise<any>}
*/
send(method, parameters) {
return this.sendPayload(JsonRpcMapper.toPayload(method, parameters)).then((response) => {
const validationResult = JsonRpcResponseValidator.validate(response);

if (validationResult instanceof Error) {
throw validationResult;
}

return response.result;
});
}

/**
* Creates the JSON-RPC batch payload and sends it to the node.
*
* @method sendBatch
*
* @param {AbstractMethod[]} methods
* @param {AbstractWeb3Module} moduleInstance
*
* @returns Promise<Object[]>
*/
sendBatch(methods, moduleInstance) {
let payload = [];

methods.forEach((method) => {
method.beforeExecution(moduleInstance);
payload.push(JsonRpcMapper.toPayload(method.rpcMethod, method.parameters));
});

return this.sendPayload(payload);
}

/**
* Sends the JSON-RPC payload to the node.
*
* @method sendPayload
*
* @param {Object} payload
*
* @returns {Promise<any>}
*/
sendPayload(payload) {
return new Promise((resolve, reject) => {
if (this.connection.sendAsync) {
this.connection.sendAsync(payload, (error, response) => {
if (!error) {
resolve(response);
}

reject(error);
});

return;
}

this.connection.send(payload, (error, response) => {
if (!error) {
resolve(response);
}

reject(error);
});
});
}
}
15 changes: 10 additions & 5 deletions packages/web3-providers/src/resolvers/ProviderResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,19 @@ export default class ProviderResolver {
return this.providersModuleFactory.createMistEthereumProvider(provider);
}

if (provider.constructor.name === 'MetamaskInpageProvider') {
return this.providersModuleFactory.createMetamaskProvider(provider);
}

if (provider.isEIP1193) {
return this.providersModuleFactory.createEthereumProvider(provider);
}

return provider;
switch (provider.constructor.name) {
case 'MetamaskInpageProvider':
return this.providersModuleFactory.createMetamaskProvider(provider);
case 'HttpProvider':
case 'IpcProvider':
case 'WebsocketProvider':
return provider;
default:
return this.providersModuleFactory.createCustomProvider(provider);
}
}
}
137 changes: 137 additions & 0 deletions packages/web3-providers/tests/src/providers/CustomProviderTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import JsonRpcMapper from '../../../src/mappers/JsonRpcMapper';
import JsonRpcResponseValidator from '../../../src/validators/JsonRpcResponseValidator';
import AbstractWeb3Module from '../../__mocks__/AbstractWeb3Module';
import AbstractMethod from '../../__mocks__/AbstractMethod';
import CustomProvider from '../../../src/providers/CustomProvider';

// Mocks
jest.mock('../../../src/factories/ProvidersModuleFactory');

/**
* CustomProvider test
*/
describe('CustomProviderTest', () => {
let customProvider, connectionMock;

beforeEach(() => {
connectionMock = {send: jest.fn(), sendAsync: jest.fn()};

customProvider = new CustomProvider(connectionMock);
});

it('constructor check', () => {
expect(customProvider.connection).toEqual(connectionMock);
});

it('calls subscribe and throws error', () => {
expect(() => {
customProvider.subscribe();
}).toThrow('Subscriptions are not supported with the CustomProvider.');
});

it('calls unsubscribe and throws error', () => {
expect(() => {
customProvider.unsubscribe();
}).toThrow('Subscriptions are not supported with the CustomProvider.');
});

it('calls send and returns with a resolved promise', async () => {
JsonRpcMapper.toPayload = jest.fn();
JsonRpcMapper.toPayload.mockReturnValueOnce({id: '0x0'});

JsonRpcResponseValidator.validate = jest.fn();
JsonRpcResponseValidator.validate.mockReturnValueOnce(true);

connectionMock.sendAsync = jest.fn((payload, callback) => {
expect(payload).toEqual({id: '0x0'});

callback(false, {result: true});
});

const response = await customProvider.send('rpc_method', []);

expect(response).toEqual(true);

expect(JsonRpcMapper.toPayload).toHaveBeenCalledWith('rpc_method', []);

expect(JsonRpcResponseValidator.validate).toHaveBeenCalledWith({result: true});

expect(connectionMock.sendAsync).toHaveBeenCalled();
});

it('calls send without the sendAsync method and returns with a resolved promise', async () => {
JsonRpcMapper.toPayload = jest.fn();
JsonRpcMapper.toPayload.mockReturnValueOnce({id: '0x0'});

JsonRpcResponseValidator.validate = jest.fn();
JsonRpcResponseValidator.validate.mockReturnValueOnce(true);

connectionMock.sendAsync = false;
connectionMock.send = jest.fn((payload, callback) => {
expect(payload).toEqual({id: '0x0'});

callback(false, {result: true});
});

const response = await customProvider.send('rpc_method', []);

expect(response).toEqual(true);

expect(JsonRpcMapper.toPayload).toHaveBeenCalledWith('rpc_method', []);

expect(JsonRpcResponseValidator.validate).toHaveBeenCalledWith({result: true});

expect(connectionMock.send).toHaveBeenCalled();
});

it('calls send and returns with a rejected promise because of an invalid JSON-RPC response', async () => {
JsonRpcMapper.toPayload = jest.fn();
JsonRpcMapper.toPayload.mockReturnValueOnce({id: '0x0'});

JsonRpcResponseValidator.validate = jest.fn();
JsonRpcResponseValidator.validate.mockReturnValueOnce(new Error('invalid'));

connectionMock.sendAsync = jest.fn((payload, callback) => {
expect(payload).toEqual({id: '0x0'});

callback(false, true);
});

await expect(customProvider.send('rpc_method', [])).rejects.toThrow('invalid');

expect(JsonRpcResponseValidator.validate).toHaveBeenCalledWith(true);

expect(JsonRpcMapper.toPayload).toHaveBeenCalledWith('rpc_method', []);

expect(connectionMock.sendAsync).toHaveBeenCalled();
});

it('calls sendBatch and returns with a resolved promise', async () => {
const abstractMethodMock = new AbstractMethod();

const moduleInstanceMock = new AbstractWeb3Module();

abstractMethodMock.beforeExecution = jest.fn();
abstractMethodMock.rpcMethod = 'rpc_method';
abstractMethodMock.parameters = [];

JsonRpcMapper.toPayload = jest.fn();
JsonRpcMapper.toPayload.mockReturnValueOnce({id: '0x0'});

connectionMock.sendAsync = jest.fn((payload, callback) => {
expect(payload).toEqual([{id: '0x0'}]);

callback(false, true);
});

const response = await customProvider.sendBatch([abstractMethodMock], moduleInstanceMock);

expect(response).toEqual(true);

expect(JsonRpcMapper.toPayload).toHaveBeenCalledWith('rpc_method', []);

expect(connectionMock.sendAsync).toHaveBeenCalled();

expect(abstractMethodMock.beforeExecution).toHaveBeenCalled();
});
});
Loading

0 comments on commit 2eeedca

Please sign in to comment.