From 5bd52c867cfd87f277f21ca87d908e0fafa7d6d8 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 4 Oct 2018 15:43:15 +0200 Subject: [PATCH] Support importing contract artifacts by url --- src/support/errors.js | 2 +- src/support/tx.js | 59 +++++++++++++++-------- test/assets/contracts/mockContract.json | 3 ++ test/tx.js | 64 ++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 test/assets/contracts/mockContract.json diff --git a/src/support/errors.js b/src/support/errors.js index b02f0d6..b03b204 100644 --- a/src/support/errors.js +++ b/src/support/errors.js @@ -57,7 +57,7 @@ function mapError(error) { module.exports = { mapError, - CvcError: CvcError, + CvcError, InvalidNonceError, FailedTxChainError, NotDeployedError, diff --git a/src/support/tx.js b/src/support/tx.js index aaea15f..2cb1595 100644 --- a/src/support/tx.js +++ b/src/support/tx.js @@ -96,6 +96,27 @@ const fallbackToAutodetectDeployedContract = (contract, contractName) => }) .then(assertCodeAtAddress); +/** + * Returns the contract artifact + * @param {string} contractName - The contact name. + * @returns {Promise} The contract artifact. + */ +// eslint-disable-next-line consistent-return +const getContractArtifact = contractName => { + if (config.contracts.url) { + // For frontend apps you can pass the url of the contracts + // eslint-disable-next-line no-undef + return fetch(`${config.contracts.url}/${contractName}.json`) + .then(res => res.json()) + .then(data => Promise.resolve(data)) + .catch(err => Promise.reject(err)); + } else if (config.contracts.dir) { + // For backend servers you can pass the path of the contracts + // eslint-disable-next-line import/no-dynamic-require, global-require + return Promise.resolve(require(`./${path.join(config.contracts.dir, `${contractName}.json`)}`)); + } +}; + /** * Returns the contract instance by name. * @type {Function} @@ -106,26 +127,27 @@ tx.contractInstance = _.memoize(contractName => { throw new Error(`Invalid contract name "${contractName}"`); } // Load contract artifact file. - // eslint-disable-next-line import/no-dynamic-require, global-require - const contractArtifact = require(`./${path.join(config.contracts.dir, `${contractName}.json`)}`); - // Create contract object. - const contract = truffleContract(contractArtifact); - contract.setProvider(tx.web3.currentProvider); - - if (_.has(config, ['contracts', 'addresses', contractName])) { - const contractAddress = config.contracts.addresses[contractName]; - try { - return contract.at(contractAddress).then(assertCodeAtAddress); - } catch (e) { - logger.debug( - `Contract '${contractName}' could not be found at configured '${contractAddress}'. Falling back to autodetect` - ); + return getContractArtifact(contractName).then(contractArtifact => { + // Create contract object. + const contract = truffleContract(contractArtifact); + contract.setProvider(tx.web3.currentProvider); + + if (_.has(config, ['contracts', 'addresses', contractName])) { + const contractAddress = config.contracts.addresses[contractName]; + try { + return contract.at(contractAddress).then(assertCodeAtAddress); + } catch (e) { + logger.debug( + `Contract '${contractName}' could not be found at configured '${contractAddress}'. + Falling back to autodetect` + ); + return fallbackToAutodetectDeployedContract(contract, contractName); + } + } else { + logger.debug(`Address not configured for '${contractName}' contract. Using autodetect...`); return fallbackToAutodetectDeployedContract(contract, contractName); } - } else { - logger.debug(`Address not configured for '${contractName}' contract. Using autodetect...`); - return fallbackToAutodetectDeployedContract(contract, contractName); - } + }); } catch (error) { logger.error(`Error loading contract ${contractName}`, error); return Promise.reject(new CvcError(`Error loading contract: ${contractName}`, error)); @@ -146,7 +168,6 @@ tx.contractInstance = _.memoize(contractName => { */ tx.contractInstances = function(...contractNames) { const contractInstancePromises = contractNames.map(tx.contractInstance); - return Promise.all(contractInstancePromises).then(contractInstances => _.zipObject(contractNames, contractInstances)); }; diff --git a/test/assets/contracts/mockContract.json b/test/assets/contracts/mockContract.json new file mode 100644 index 0000000..e9fec4e --- /dev/null +++ b/test/assets/contracts/mockContract.json @@ -0,0 +1,3 @@ +{ + "contractName": "CvcValidatorRegistry" +} \ No newline at end of file diff --git a/test/tx.js b/test/tx.js index 6e0420c..e0bcc17 100644 --- a/test/tx.js +++ b/test/tx.js @@ -3,13 +3,16 @@ require('longjohn'); const chai = require('chai'); const sandbox = require('sinon').createSandbox(); const nonce = require('../src/support/nonce'); -const tx = require('../src/support/tx'); +const fetchMock = require('fetch-mock'); +const proxyquire = require('proxyquire'); const { expect } = chai; chai.use(require('chai-as-promised')); describe('tx.js', () => { describe('when createTx throws', () => { + // eslint-disable-next-line global-require + const tx = require('../src/support/tx'); let releaseNonceSpy; beforeEach('stub', () => { const getDataApplyStub = sandbox.stub().throws('Error', 'Invalid number of arguments to Solidity function'); @@ -46,6 +49,8 @@ describe('tx.js', () => { }); describe('waitForMine', () => { + // eslint-disable-next-line global-require + const tx = require('../src/support/tx'); before('Mining never resolves to a tx receipt', () => { sandbox.stub(tx, 'getTransactionReceipt').resolves(null); }); @@ -59,4 +64,61 @@ describe('tx.js', () => { /getTransactionReceiptMined timeout/ )).timeout(4000); }); + + describe('When we pass contract.url to contractInstance', () => { + const tx = proxyquire('../src/support/tx', { + '../../config/index': () => ({ + contracts: { url: './contracts' } + }), + 'truffle-contract': () => ({ + setProvider: () => {}, + deployed: () => ({ + catch: () => ({ + then: () => Promise.resolve({ foo: 'bar' }) + }) + }) + }) + }); + before('stub', () => { + tx.web3 = sandbox.stub().returns({}); + fetchMock.mock('./contracts/CvcEscrow.json', { contractName: 'CvcEscrow' }); + }); + + after(() => { + sandbox.restore(); + }); + + it('should fetch the contract', async () => { + const result = await tx.contractInstance('CvcEscrow'); + expect(result).to.deep.equal({ foo: 'bar' }); + }); + }); + + describe('When we pass contract.dir to contractInstance', () => { + const tx = proxyquire('../src/support/tx', { + '../../config/index': () => ({ + contracts: { dir: '../../test/assets/contracts' } + }), + 'truffle-contract': () => ({ + setProvider: () => {}, + deployed: () => ({ + catch: () => ({ + then: () => Promise.resolve({ foo: 'bar' }) + }) + }) + }) + }); + before('stub', () => { + tx.web3 = sandbox.stub().returns({}); + }); + + after(() => { + sandbox.restore(); + }); + + it('should fetch the contract', async () => { + const result = await tx.contractInstance('mockContract'); + expect(result).to.deep.equal({ foo: 'bar' }); + }); + }); });