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

Implement Ethereum adapter #432

Merged
merged 1 commit into from
Oct 10, 2019
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
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ matrix:
include:
- env: BENCHMARK=composer
- env: BENCHMARK=fabric
- env: BENCHMARK=ethereum
dist: trusty
before_install: |
set -ev
Expand Down
2 changes: 2 additions & 0 deletions .travis/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ if [[ "${BENCHMARK}" == "composer" ]]; then
npx caliper bind --caliper-bind-sut composer
elif [[ "${BENCHMARK}" == "fabric" ]]; then
npx caliper bind --caliper-bind-sut fabric
elif [[ "${BENCHMARK}" == "ethereum" ]]; then
npx caliper bind --caliper-bind-sut ethereum
else
echo "Unknown target benchmark ${BENCHMARK}"
npm run cleanup
Expand Down
4 changes: 4 additions & 0 deletions packages/caliper-cli/lib/bind/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ sut:
settings:
- *new-node-old-grpc
latest: *composer-latest
ethereum:
1.2.1: &ethereum-latest
packages: ['[email protected]']
aklenik marked this conversation as resolved.
Show resolved Hide resolved
latest: *ethereum-latest
1 change: 1 addition & 0 deletions packages/caliper-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@hyperledger/caliper-fabric": "^0.1.0",
"@hyperledger/caliper-iroha": "^0.1.0",
"@hyperledger/caliper-sawtooth": "^0.1.0",
"@hyperledger/caliper-ethereum": "^0.1.0",
"chalk": "1.1.3",
"yargs": "10.0.3"
},
Expand Down
26 changes: 26 additions & 0 deletions packages/caliper-ethereum/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding the SPDX header for Apache 2

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
16 changes: 16 additions & 0 deletions packages/caliper-ethereum/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# Licensed under the Apache License, Version 2.0 (the "License");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding the SPDX header for Apache 2

# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

coverage
node_modules
48 changes: 48 additions & 0 deletions packages/caliper-ethereum/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
env:
es6: true
node: true
mocha: true
extends: 'eslint:recommended'
parserOptions:
ecmaVersion: 8
sourceType:
- script
rules:
indent:
- error
- 4
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always
no-unused-vars:
- error
- args: none
no-console: warn
curly: error
eqeqeq: error
no-throw-literal: error
strict: error
no-var: error
dot-notation: error
no-tabs: error
no-trailing-spaces: error
no-use-before-define: error
no-useless-call: error
no-with: error
operator-linebreak: error
require-jsdoc:
- error
- require:
ClassDeclaration: true
MethodDefinition: true
FunctionDeclaration: true
valid-jsdoc:
- error
- requireReturn: false
yoda: error
18 changes: 18 additions & 0 deletions packages/caliper-ethereum/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding the SPDX header for Apache 2

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

module.exports.AdminClient = require('./lib/ethereum');
module.exports.ClientFactory = require('./lib/ethereumClientFactory');
244 changes: 244 additions & 0 deletions packages/caliper-ethereum/lib/ethereum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding the SPDX header for Apache 2

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @file, definition of the Ethereum class, which implements the Caliper's NBI for Ethereum Web3 interface.
*/

'use strict';

const Web3 = require('web3');
const {BlockchainInterface, CaliperUtils, TxStatus} = require('@hyperledger/caliper-core');
const logger = CaliperUtils.getLogger('ethereum.js');

/**
* @typedef {Object} EthereumInvoke
*
* @property {string} verb Required. The name of the smart contract function
* @property {string} args Required. Arguments of the smart contract function in the order in which they are defined
* @property {boolean} isView Optional. If method to call is a view.
*/

/**
* Implements {BlockchainInterface} for a web3 Ethereum backend.
*/
class Ethereum extends BlockchainInterface {

/**
* Create a new instance of the {Ethereum} class.
* @param {string} config_path The path of the network configuration file.
* @param {string} workspace_root The absolute path to the root location for the application configuration files.
*/
constructor(config_path, workspace_root) {
super(config_path);
this.bcType = 'ethereum';
this.workspaceRoot = workspace_root;
this.ethereumConfig = require(config_path).ethereum;
this.web3 = new Web3(this.ethereumConfig.url);
this.web3.transactionConfirmationBlocks = this.ethereumConfig.transactionConfirmationBlocks;
}

/**
* Initialize the {Ethereum} object.
* @return {object} Promise<boolean> True if the account got unlocked successful otherwise false.
*/
init() {
return this.web3.eth.personal.unlockAccount(this.ethereumConfig.contractDeployerAddress, this.ethereumConfig.contractDeployerAddressPassword, 1000);
}

/**
* Deploy smart contracts specified in the network configuration file.
* @return {object} Promise execution for all the contract creations.
*/
async installSmartContract() {
let promises = [];
let self = this;
logger.info('Creating contracts...');
for (const key of Object.keys(this.ethereumConfig.contracts)) {
let contractData = require(CaliperUtils.resolvePath(this.ethereumConfig.contracts[key].path, this.workspaceRoot)); // TODO remove path property
this.ethereumConfig.contracts[key].abi = contractData.abi;
promises.push(new Promise(async function(resolve, reject) {
let contractInstance = await self.deployContract(contractData);
logger.info('Deployed contract ' + contractData.name + ' at ' + contractInstance.options.address);
self.ethereumConfig.contracts[key].address = contractInstance.options.address;
resolve(contractInstance);
}));
}
return Promise.all(promises);
}

/**
* Return the Ethereum context associated with the given callback module name.
* @param {string} name The name of the callback module as defined in the configuration files.
* @param {object} args Unused.
* @return {object} The assembled Ethereum context.
* @async
*/
async getContext(name, args) {
let context = {fromAddress: this.ethereumConfig.fromAddress};
context.web3 = this.web3;
context.contracts = {};
for (const key of Object.keys(args.contracts)) {
context.contracts[key] = new this.web3.eth.Contract(args.contracts[key].abi, args.contracts[key].address);
}
await context.web3.eth.personal.unlockAccount(this.ethereumConfig.fromAddress, this.ethereumConfig.fromAddressPassword, 1000);
return context;
}

/**
* Release the given Ethereum context.
* @param {object} context The Ethereum context to release.
* @async
*/
async releaseContext(context) {
// nothing to do
}

/**
* Invoke a smart contract.
* @param {Object} context Context object.
* @param {String} contractID Identity of the contract.
* @param {String} contractVer Version of the contract.
* @param {EthereumInvoke|EthereumInvoke[]} invokeData Smart contract methods calls.
* @param {Number} timeout Request timeout, in seconds.
* @return {Promise<object>} The promise for the result of the execution.
*/
async invokeSmartContract(context, contractID, contractVer, invokeData, timeout) {
let invocations;
if (!Array.isArray(invokeData)) {
invocations = [invokeData];
} else {
invocations = invokeData;
}
let promises = [];
invocations.forEach((item, index) => {
promises.push(this.sendTransaction(context, contractID, contractVer, item, timeout));
});
return Promise.all(promises);
}

/**
* Query a smart contract.
* @param {Object} context Context object.
* @param {String} contractID Identity of the contract.
* @param {String} contractVer Version of the contract.
* @param {EthereumInvoke|EthereumInvoke[]} invokeData Smart contract methods calls.
* @param {Number} timeout Request timeout, in seconds.
* @return {Promise<object>} The promise for the result of the execution.
*/
async querySmartContract(context, contractID, contractVer, invokeData, timeout) {
let invocations;
if (!Array.isArray(invokeData)) {
invocations = [invokeData];
} else {
invocations = invokeData;
}
let promises = [];
invocations.forEach((item, index) => {
item.isView = true;
promises.push(this.sendTransaction(context, contractID, contractVer, item, timeout));
});
return Promise.all(promises);
}

/**
* Submit a transaction to the ethereum context.
* @param {Object} context Context object.
* @param {String} contractID Identity of the contract.
* @param {String} contractVer Version of the contract.
* @param {EthereumInvoke} methodCall Methods call data.
* @param {Number} timeout Request timeout, in seconds.
* @return {Promise<TxStatus>} Result and stats of the transaction invocation.
*/
async sendTransaction(context, contractID, contractVer, methodCall, timeout) {
let status = new TxStatus();
try {
context.engine.submitCallback(1);
let receipt = null;
let methodType = 'send';
if (methodCall.isView) {
methodType = 'call';
}
if (methodCall.args) {
receipt = await context.contracts[contractID].methods[methodCall.verb](...methodCall.args)[methodType]({from: context.fromAddress});
} else {
receipt = await context.contracts[contractID].methods[methodCall.verb]()[methodType]({from: context.fromAddress});
}
status.SetID(receipt.transactionHash);
status.SetResult(receipt);
status.SetVerification(true);
status.SetStatusSuccess();
} catch (err) {
status.SetStatusFail();
logger.error('Failed tx on ' + contractID + ' calling method ' + methodCall.verb);
logger.error(err);
}
return Promise.resolve(status);
}

/**
* Query the given smart contract according to the specified options.
* @param {object} context The Ethereum context returned by {getContext}.
* @param {string} contractID The name of the contract.
* @param {string} contractVer The version of the contract.
* @param {string} key The argument to pass to the smart contract query.
* @param {string} [fcn=query] The contract query function name.
* @return {Promise<object>} The promise for the result of the execution.
*/
async queryState(context, contractID, contractVer, key, fcn = 'query') {
aklenik marked this conversation as resolved.
Show resolved Hide resolved
let methodCall = {
verb: fcn,
args: [key],
isView: true
};
return this.sendTransaction(context, contractID, contractVer, methodCall, 60);
}

/**
* Deploys a new contract using the given web3 instance
* @param {JSON} contractData Contract data with abi, bytecode and gas properties
* @returns {Promise<web3.eth.Contract>} The deployed contract instance
*/
deployContract(contractData) {
let web3 = this.web3;
let contractDeployerAddress = this.ethereumConfig.contractDeployerAddress;
return new Promise(function(resolve, reject) {
let contract = new web3.eth.Contract(contractData.abi);
let contractDeploy = contract.deploy({
data: contractData.bytecode
});
contractDeploy.send({
from: contractDeployerAddress,
gas: contractData.gas
}).on('error', (error) => {
reject(error);
}).then((newContractInstance) => {
resolve(newContractInstance);
});
});
}

/**
* It passes deployed contracts addresses to all clients
* @param {Number} number of clients to prepare
* @returns {Array} client args
*/
async prepareClients(number) {
let result = [];
for (let i = 0 ; i< number ; i++) {
result[i] = {contracts: this.ethereumConfig.contracts};
}
return result;
}
}

module.exports = Ethereum;
Loading