diff --git a/packages/core/code-runner/src/index.ts b/packages/core/code-runner/src/index.ts index 024475d50..fa6cec066 100644 --- a/packages/core/code-runner/src/index.ts +++ b/packages/core/code-runner/src/index.ts @@ -11,7 +11,7 @@ class CodeRunner { private events: EmbarkEvents; private vm: VM; - constructor(embark: Embark, _options: any) { + constructor(private embark: Embark, _options: any) { this.logger = embark.logger; this.events = embark.events; @@ -49,8 +49,13 @@ class CodeRunner { cb(); } - private registerVar(varName: string, code: any, cb = () => { }) { - this.vm.registerVar(varName, code, cb); + private registerVar(varName: string, code: any, cb = (...args) => { }) { + this.embark.config.plugins.emitAndRunActionsForEvent(`runcode:register:${varName}`, code, (err, updated) => { + if (err) { + return cb(err); + } + this.vm.registerVar(varName, updated, cb); + }); } private evalCode(code: string, cb: Callback, tolerateError = false, logCode = true, logError = true) { diff --git a/packages/core/core/constants.json b/packages/core/core/constants.json index b0d3854ca..b0bd17603 100644 --- a/packages/core/core/constants.json +++ b/packages/core/core/constants.json @@ -41,7 +41,9 @@ "clients": { "geth": "geth", "parity": "parity", - "ganache": "ganache-cli" + "ganache": "ganache-cli", + "nethermind": "nethermind", + "quorum": "quorum" }, "defaultMnemonic": "example exile argue silk regular smile grass bomb merge arm assist farm", "blockchainReady": "blockchainReady", @@ -131,4 +133,4 @@ }, "generationDir": "embarkArtifacts" } -} +} \ No newline at end of file diff --git a/packages/core/core/src/index.ts b/packages/core/core/src/index.ts index cd5cfb8df..e4ac97981 100644 --- a/packages/core/core/src/index.ts +++ b/packages/core/core/src/index.ts @@ -15,13 +15,14 @@ export interface Contract { export interface ContractConfig { address?: string; - args?: any[]; + args?: any; instanceOf?: string; gas?: number; gasPrice?: number; silent?: boolean; track?: boolean; deploy?: boolean; + skipBytecodeCheck?: boolean; } export interface Plugin { @@ -35,6 +36,11 @@ export interface EmbarkPlugins { getPluginsProperty(pluginType: string, property: string, sub_property?: string): any[]; plugins: Plugin[]; runActionsForEvent(event: string, args: any, cb: Callback): void; + emitAndRunActionsForEvent( + name: string, + params: any, + cb: Callback + ): void; } export interface CompilerPluginObject { @@ -69,6 +75,14 @@ export interface EmbarkEvents { ): void; } +export interface ClientConfig { + miningMode?: "dev" | "auto" | "always" | "off"; +} + +export interface ContractsConfig { + [key: string]: ContractConfig; +} + export interface Configuration { contractsFiles: any[]; embarkConfig: _EmbarkConfig; @@ -86,9 +100,7 @@ export interface Configuration { isDev: boolean; client: string; enabled: boolean; - clientConfig: { - miningMode: string - } + clientConfig?: ClientConfig; }; webServerConfig: { certOptions: { @@ -98,6 +110,7 @@ export interface Configuration { }; contractsConfig: { tracking?: boolean | string; + contracts: ContractsConfig; }; plugins: EmbarkPlugins; reloadConfig(): void; diff --git a/packages/plugins/deploy-tracker/src/deploymentChecks.js b/packages/plugins/deploy-tracker/src/deploymentChecks.js index 10f26b335..436c24d56 100644 --- a/packages/plugins/deploy-tracker/src/deploymentChecks.js +++ b/packages/plugins/deploy-tracker/src/deploymentChecks.js @@ -92,7 +92,12 @@ export default class DeploymentChecks { catch (err) { return cb(err); } - if (codeInChain.length > 3 && codeInChain.substring(2) === contract.runtimeBytecode) { // it is "0x" or "0x0" for empty code, depending on web3 version + const skipBytecodeCheck = (this.contractsConfig?.contracts && this.contractsConfig.contracts[params.contract.className]?.skipBytecodeCheck) ?? false; + if (skipBytecodeCheck) { + this.logger.warn(__("WARNING: Skipping bytecode check for %s deployment. Performing an embark reset may cause the contract to be re-deployed to the current node regardless if it was already deployed on another node in the network.", params.contract.className)); + } + if (skipBytecodeCheck || + (codeInChain.length > 3 && codeInChain.substring(2) === contract.runtimeBytecode)) { // it is "0x" or "0x0" for empty code, depending on web3 version contract.deployedAddress = trackedContract.address; contract.log(contract.className.bold.cyan + __(" already deployed at ").green + contract.deployedAddress.bold.cyan); params.shouldDeploy = false; diff --git a/packages/plugins/deploy-tracker/src/test/deploymentChecksSpec.js b/packages/plugins/deploy-tracker/src/test/deploymentChecksSpec.js index 19e3bcd95..06f0843d5 100644 --- a/packages/plugins/deploy-tracker/src/test/deploymentChecksSpec.js +++ b/packages/plugins/deploy-tracker/src/test/deploymentChecksSpec.js @@ -22,6 +22,7 @@ describe('embark.deploymentChecks', function () { let readJSON; let writeJSON; let _web3; + let contractsConfig; beforeEach(() => { params = { @@ -77,7 +78,8 @@ describe('embark.deploymentChecks', function () { } }; trackingFunctions._web3 = _web3; - deploymentChecks = new DeploymentChecks({trackingFunctions, events, logger, contractsConfig: {}}); + contractsConfig = {}; + deploymentChecks = new DeploymentChecks({ trackingFunctions, events, logger, contractsConfig }); deploymentChecks._web3 = _web3; }); afterEach(() => { @@ -175,6 +177,20 @@ describe('embark.deploymentChecks', function () { expect(params.contract.deployedAddress).to.be("0xbe474fb88709f99Ee83901eE09927005388Ab2F1"); }); }); + it("should not deploy if contract is tracked and bytecode check is skipped", async function () { + deploymentChecks.contractsConfig = { + contracts: { + TestContract: { + skipBytecodeCheck: true + } + } + }; + return deploymentChecks.checkIfAlreadyDeployed(params, (err, params) => { + expect(err).to.be(null); + expect(params.shouldDeploy).to.be(false); + expect(params.contract.deployedAddress).to.be("0xbe474fb88709f99Ee83901eE09927005388Ab2F1"); + }); + }); it("should deploy if contract is tracked, but bytecode doesn't exist on chain", async function () { trackingFunctions._web3.eth.getCode = () => "0x0"; return deploymentChecks.checkIfAlreadyDeployed(params, (err, params) => { diff --git a/packages/plugins/ens/src/index.js b/packages/plugins/ens/src/index.js index 08dfdfbed..35df34417 100644 --- a/packages/plugins/ens/src/index.js +++ b/packages/plugins/ens/src/index.js @@ -320,11 +320,21 @@ class ENS { const registration = this.config.namesystemConfig.register; const doRegister = registration && registration.rootDomain; - await this.events.request2('deployment:contract:deploy', this.ensConfig.ENSRegistry); + try { + await this.events.request2('deployment:contract:deploy', this.ensConfig.ENSRegistry); + } catch (err) { + this.logger.error(__(`Error deploying the ENS Registry contract: ${err.message}`)); + this.logger.debug(err.stack); + } // Add Resolver to contract manager again but this time with correct arguments (Registry address) this.ensConfig.Resolver.args = [this.ensConfig.ENSRegistry.deployedAddress]; this.ensConfig.Resolver = await this.events.request2('contracts:add', this.ensConfig.Resolver); - await this.events.request2('deployment:contract:deploy', this.ensConfig.Resolver); + try { + await this.events.request2('deployment:contract:deploy', this.ensConfig.Resolver); + } catch (err) { + this.logger.error(__(`Error deploying the ENS Resolver contract: ${err.message}`)); + this.logger.debug(err.stack); + } const config = { registryAbi: self.ensConfig.ENSRegistry.abiDefinition, diff --git a/packages/plugins/ethereum-blockchain-client/src/index.js b/packages/plugins/ethereum-blockchain-client/src/index.js index 651604de2..318db4180 100644 --- a/packages/plugins/ethereum-blockchain-client/src/index.js +++ b/packages/plugins/ethereum-blockchain-client/src/index.js @@ -60,7 +60,7 @@ class EthereumBlockchainClient { return web3.currentProvider; } - async deployer(contract, done) { + async deployer(contract, additionalDeployParams, done) { try { const web3 = await this.web3; const [account] = await web3.eth.getAccounts(); @@ -88,7 +88,7 @@ class EthereumBlockchainClient { } embarkJsUtils.secureSend(web3, contractObject, { - from: account, gas: contract.gas + from: account, gas: contract.gas, ...additionalDeployParams }, true, (err, receipt) => { if (err) { return done(err); @@ -199,7 +199,7 @@ class EthereumBlockchainClient { if (Array.isArray(arg)) { return checkArgs(arg, nextEachCb); } - if (arg[0] === "$") { + if (arg && arg[0] === "$") { return parseArg(arg, nextEachCb); } nextEachCb(null, arg); diff --git a/packages/plugins/nethermind/src/index.js b/packages/plugins/nethermind/src/index.js index a4130564f..c12e86f96 100644 --- a/packages/plugins/nethermind/src/index.js +++ b/packages/plugins/nethermind/src/index.js @@ -2,8 +2,7 @@ import { __ } from 'embark-i18n'; import {BlockchainClient} from "./blockchain"; const {normalizeInput, testRpcWithEndpoint, testWsEndpoint} = require('embark-utils'); import {BlockchainProcessLauncher} from './blockchainProcessLauncher'; - -export const NETHERMIND_NAME = 'nethermind'; +import constants from "embark-core/constants"; class Nethermind { constructor(embark) { @@ -20,7 +19,7 @@ class Nethermind { return; } - this.events.request("blockchain:node:register", NETHERMIND_NAME, { + this.events.request("blockchain:node:register", constants.blockchain.clients.nethermind, { isStartedFn: (isStartedCb) => { this._doCheck((state) => { console.log('Started?', JSON.stringify(state)); @@ -53,7 +52,7 @@ class Nethermind { shouldInit() { return ( - this.blockchainConfig.client === NETHERMIND_NAME && + this.blockchainConfig.client === constants.blockchain.clients.nethermind && this.blockchainConfig.enabled ); } @@ -61,7 +60,7 @@ class Nethermind { _getNodeState(err, version, cb) { if (err) return cb({ name: "Ethereum node not found", status: 'off' }); - return cb({ name: `${NETHERMIND_NAME} (Ethereum)`, status: 'on' }); + return cb({ name: `${constants.blockchain.clients.nethermind} (Ethereum)`, status: 'on' }); } _doCheck(cb) { @@ -78,7 +77,7 @@ class Nethermind { startBlockchainNode(callback) { if (this.blockchainConfig.isStandalone) { return new BlockchainClient(this.blockchainConfig, { - clientName: NETHERMIND_NAME, + clientName: constants.blockchain.clients.nethermind, env: this.embark.config.env, certOptions: this.embark.config.webServerConfig.certOptions, logger: this.logger, diff --git a/packages/plugins/quorum/.npmrc b/packages/plugins/quorum/.npmrc new file mode 100644 index 000000000..e031d3432 --- /dev/null +++ b/packages/plugins/quorum/.npmrc @@ -0,0 +1,4 @@ +engine-strict = true +package-lock = false +save-exact = true +scripts-prepend-node-path = true diff --git a/packages/plugins/quorum/README.md b/packages/plugins/quorum/README.md new file mode 100644 index 000000000..6401d8053 --- /dev/null +++ b/packages/plugins/quorum/README.md @@ -0,0 +1,352 @@ +# `embark-quorum` + +> Quorum blockchain client plugin for Embark + +`embark-quorum` is an Embark plugin that allows ÐApps to **connect to** Quorum blockchains. This plugin will not start a Quorum node or nodes automatically as Embark does with other clients. + +Using this plugin, you can: + - deploy contracts publically and privately to a Quorum network using the Tessera private transaction manager. + - send public and private transactions on a Quorum network using the Tessera private transaction manager. + +## Overview +To use the Embark Quorum plugin, you'll need to do the following, which we'll cover in more detail in the sections below: +1. [Initialise and run a Quorum network](#1-initialise-and-run-a-quorum-network), such as the [7nodes example](https://github.com/jpmorganchase/quorum-examples/tree/master/examples/7nodes) +2. [Install the `embark-quorum` plugin in your ÐApp](#2-install-the-embark-quorum-plugin-in-your-ÐApp) +3. [Configure your ÐApp's blockchain config](#3-configure-your-ÐApps-blockchain-config) +4. [Configure your ÐApp's contract config (if necessary)](#4-configure-your-ÐApps-contract-config) +5. [Run Embark and deploy a private contract](#5-run-embark-and-deploy-a-private-contract) +6. [Sending a private transaction](#6-sending-a-private-transaction) + +## 1. Initialise and run a Quorum network +In order to get a Quorum network up quickly for development, the easiest path is to download the Quorum/Tessera binaries and use them to run the cloned 7nodes example. +1. Download the latest Quorum binary from https://github.com/jpmorganchase/quorum/releases. Let's say it was downloaded to `/repos/jpmorganchase/quorum/build/bin/geth`. +2. Update your `PATH` environment variable such that `geth` points to Quorum geth: +``` +export PATH=/repos/jpmorganchase/quorum/build/bin:$PATH +``` +3. Download the latest Tessera jar (`tessera-app-{version}-app.jar`, ie `tessera-app-0.10.3-app.jar`) from https://github.com/jpmorganchase/tessera/releases. +4. Update your `TESSERA_JAR` environment variable to point to the downloaded Tessera jar: +``` +export TESSERA_JAR=/path/to/tessera-app-0.10.3-app.jar +``` +5. Clone the 7nodes example repo: +``` +git clone https://github.com/jpmorganchase/quorum-examples +cd examples/7nodes +``` +6. Initialise Quorum using the desired consensus (`istanbul`, `raft`, `clique`): +``` +# ./{consensus}-init.sh +./istanbul-init.sh +``` +7. Start Quorum using the desired consensus from step 6: +``` +# ./{consensus}-start.sh tessera +./istanbul-start.sh +``` + > **NOTE:** the default 7nodes example does not start Quorum geth with WebSockets. We can get around this by updating `./istanbul-start.sh` before executing it, as per https://github.com/jpmorganchase/quorum-examples/pull/221. As an alternative, clone [the fork with these changes](https://github.com/emizzle/quorum-examples) (on the `7nodes-websocket-support` branch) in Step 5 instead. + +After a few moments, you should have 7 Quorum nodes up and running. + +To stop the network: +``` +./stop.sh +``` + +For more information on how to get the 7nodes example up and running, including information on using Vagrant and Docker, see https://github.com/jpmorganchase/quorum-examples. + +## 2. Install the `embark-quorum` plugin in your ÐApp + +### Creating a ÐApp if needed +First, ensure you have a ÐApp set up. We will be using the Embark's demo ÐApp in this README, which can be created using `embark demo`: +``` +# /bin/bash + +embark demo +cd embark_demo +``` + +For more information on how to get started with the Embark Demo, view our [Quick Start guide](https://framework.embarklabs.io/docs/quick_start.html). + +### Installing `embark-quorum` in your ÐApp +To install `embark-quorum` in to our ÐApp, we can add `embark-quorum` to our `packages.json`, then run `npm` or `yarn`: +``` +# package.json +"dependencies": { + "embark-quorum": "5.3.0" +} +``` +Then install the package with your package manager. +``` +# /bin/bash +npm i +// OR +yarn +``` +Next, update our ÐApp's `embark.json` to include the `embark-quorum` plugin: +``` +# embark.json +"plugins": { + "embark-quorum": {} +} +``` + + +## 3. Configure your ÐApp's blockchain config +We'll need to update our `blockchain.js` configuration to instruct Embark on how to connect to the seven Quorum nodes we now have running. + +The best option is to create an environment for each node in the network, so that we can connect to each node individually. The `rpcPort` and `wsPort` are determined by the `./{consensus}-init.sh` script run in step 1.6. + +Notice that by setting the `tesseraPrivateUrl` in the `default` environment, it is shared by all the environments defined. + +``` +# config/blockchain.js + +module.exports = { + // default applies to all environments + default: { + enabled: true, + client: 'quorum', + clientConfig: { + miningMode: 'dev', // Mode in which the node mines. Options: dev, auto, always, off + tesseraPrivateUrl: 'http://localhost:9081' + } + }, + + nodeone: { + clientConfig: { + rpcPort: 22000, + wsPort: 23000 + } + }, + + nodetwo: { + clientConfig: { + rpcPort: 22001, + wsPort: 23001 + } + }, + + nodethree: { + clientConfig: { + rpcPort: 22002, + wsPort: 23002 + } + }, + + nodefour: { + clientConfig: { + rpcPort: 22003, + wsPort: 23003 + } + }, + + nodefive: { + clientConfig: { + rpcPort: 22004, + wsPort: 23004 + } + }, + nodesix: { + clientConfig: { + rpcPort: 22005, + wsPort: 23005 + } + }, + nodeseven: { + clientConfig: { + rpcPort: 22006, + wsPort: 23006 + } + } +}; +``` + +## 4. Configure your ÐApp's contract config +We'll need to update our `contracts.js` configuration to instruct Embark on how to deploy our contracts. + +There are two important things to notice in this configuration. First, notice that `nodeone` has the field `privateFor`. This is telling Quorum to deploy the contract privately to Node 7, as indicated by Node 7's public key in the `privateFor` field. The means Node 1 and Node 7 will be privy to this contract's values once deployed. + +Also notice that we include `skipBytecodeCheck: true` for all nodes that we *assume should have the contract deployed in the **network**, but not deployed to that **node***. In our example, Node 1 doesn't need `skipBytecodeCheck` because it is deploying the contract and can let Embark handle tracking of the contract deploys. Node 7 also doesn't need this check because Node 1 will have privately deployed the contract to Node 1 and Node 7, so we know the bytecode will exist on Node 7. However, all other Nodes need `skipBytecodeCheck: true`, because the bytecode will **not exist** on those nodes and when Embark connects to any of those environments, we are going to rely on Embark's deployment tracking feature to determine if the contract has been deployed to the network, **instead of** checking to see if the bytecode for that contract exists on the node Embark is connected to. Please [check out the docs](https://framework.embarklabs.io//docs/contracts_configuration.html#Skipping-bytecode-check) for more information on this topic. + +``` +# config/contracts.js + +module.exports = { + // default applies to all environments + default: { + // order of connections the DApp should connect to + dappConnection: [ + "$EMBARK", + "$WEB3", // uses pre existing web3 object if available (e.g in Mist) + "ws://localhost:8546", + "http://localhost:8545" + ], + gas: "auto", + }, + + nodeone: { + deploy: { + SimpleStorage: { + fromIndex: 0, + args: [100], + privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="] + } + } + }, + nodetwo: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } + }, + nodethree: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } + }, + nodefour: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } + }, + nodefive: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } + }, + nodesix: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } + }, + nodeseven: {} +}; +``` + +## 5. Run Embark and deploy a private contract +Now that we have our ÐApp set up, let's run Embark and see the private contract deployment in action. We are going to deploy the `SimpleStorage` contract privately between Nodes 1 and 7 (based on the `privateFor` parameter specified in the contract configuration for the `nodeone` environment), and witness how we can only view the contract's values in Nodes 1 and 7, and not in Nodes 2 - 5. + +First, let's run Embark in console mode in our `nodeone` environment, ensuring we connect to Node 1 in our 7nodes example: +``` +# /bin/bash + +embark console nodeone +``` +You should see all the normal output of `embark run`, including information on how the contract `SimpleStorage` was deployed: +``` +compiling solidity contracts... +deploying contracts +Deploying SimpleStorage with 151414 gas at the price of 0 Wei. Estimated cost: 0 Wei (txHash: 0xecb96cebf2a4da17206d8b225b77f2a84e3ec2ae1a10cd3f95ddc094381ec5d1) +SimpleStorage deployed at 0x9d13C6D3aFE1721BEef56B55D303B09E021E27ab using 0 gas (txHash: 0xecb96cebf2a4da17206d8b225b77f2a84e3ec2ae1a10cd3f95ddc094381ec5d1) +finished deploying contracts +``` +Once the ÐApp is finished webpacking, you should see the console prompt: +``` +Embark (nodeone) > +``` +In this prompt, enter `SimpleStorage.get()` so we can find out what our current value stored in the contract is: +``` +Embark (nodeone) > SimpleStorage.get() +# 100 +``` +We can see that the value returned is the value we provided in our `SimpleStorage` constructor `args` in the contracts configuration. So far, so good. + +Now, let's kill our instace of Embark with `Ctrl+c`, and then spin up a new instance in the `nodeseven` environment. +``` + +embark console nodeseven +``` +The output, this time, will be a little bit different. Notice that Embark understands that the contract has been deployed to Node 7 (due to the bytecode check): +``` +deploying contracts +SimpleStorage already deployed at 0x9d13C6D3aFE1721BEef56B55D303B09E021E27ab +finished deploying contracts +``` +Now, do the same thing as before and retrieve the value stored in the contract: +``` +Embark (nodeone) > SimpleStorage.get() +# 100 +``` +Again, as expected, we get the value of 100, matching the constructor arguments. This is expected because Node 1 deployed the `SimpleStorage` contract privately between nodes 1 and 7. + +Let's take this one step further and check the value stored in `SimpleStorage` on a node that was not included in the private contract deploy. + +Exit the current Embark instance with `ctrl+c`, then start up a new instance in the `nodetwo` environment: +``` + +embark console nodetwo +``` +Notice the warning that the bytecode check is skipped, and also that Embark knows the contract has already been deployed, **even though the bytecode doesn't exist on Node 2**: +``` +compiling solidity contracts... +deploying contracts +WARNING: Skipping bytecode check for SimpleStorage deployment. Performing an Embark reset may cause the contract to be re-deployed to the current node regardless if it was already deployed on another node in the network. +SimpleStorage already deployed at 0x9d13C6D3aFE1721BEef56B55D303B09E021E27ab +finished deploying contracts +``` +Again, let's see now what Node 2 has stored for the `SimpleStorage` contract value: +``` +Embark (nodetwo) > SimpleStorage.get() +# 0 +``` +The result is `0`, which is exepcted because Node 2 does not contain the bytecode for `SimpleStorage` and does not have its values stored in its Ethereum state. + +## 6. Sending a private transaction +Let's see how the `embark-quorum` plugin allows us to send private transactions. + +First, run an Embark console in the `nodeone` environment to see what the current value of the `SimpleStorage` contract is. + +``` +# /bin/bash + +embark console nodeone +``` +Once started, get our contract value in the console prompt: +``` +Embark (nodeone) > SimpleStorage.get() +# 100 +``` +Now, let's update this value only for Node 7. In the console, we're going to send a private transaction between Node 1 and Node 7. This transaction is simply going to set the `SimpleStorage` value to `333` by calling the `SimpleStorage.set(333)` method in a transaction. Run each of these lines individually in the console: +``` +Embark (nodeone) > from = web3.eth.defaultAccount +Embark (nodeone) > privateFor = ['ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc='] +Embark (nodeone) > await SimpleStorage.methods.set(333).send({ from, privateFor }) +``` +We can check that the transaction set our `SimpleStorage` value to `333`: +``` +Embark (nodeone) > SimpleStorage.get() +# 333 +``` +Now, let's check Node 7: +``` + +embark console nodeseven +``` +Then, in the console prompt: +``` +Embark (nodeseven) > SimpleStorage.get() +# 333 +``` +Great! We have privately set our `SimpleStorage` contract value only in Node 7. + + +## Conclusion +Now you should have a good idea of how to run a Quorum network and use Embark to deploy public and private contracts, and send public and private transactions. + +In addition to running on a network of multiple Quorum nodes, `embark-quorum` works with any number of Quorum nodes, including one. + +Please feel free to explore all the possibilities of Quorum and Embark in your own ÐApp, and [report any issues found](https://github.com/embarklabs/embark/issues). +--- + +Visit [framework.embarklabs.io](https://framework.embarklabs.io/) to get started with +[Embark](https://github.com/embarklabs/embark). diff --git a/packages/plugins/quorum/package.json b/packages/plugins/quorum/package.json new file mode 100644 index 000000000..3c377ff81 --- /dev/null +++ b/packages/plugins/quorum/package.json @@ -0,0 +1,65 @@ +{ + "name": "embark-quorum", + "version": "5.3.0-nightly.4", + "author": "Iuri Matias ", + "contributors": [], + "description": "Quorum blockchain client plugin for Embark", + "homepage": "https://github.com/embarklabs/embark/tree/master/packages/plugins/quorum#readme", + "bugs": "https://github.com/embarklabs/embark/issues", + "keywords": [ + "blockchain", + "dapps", + "ethereum", + "serverless", + "quorum" + ], + "files": [ + "dist" + ], + "license": "MIT", + "repository": { + "directory": "packages/plugins/quorum", + "type": "git", + "url": "https://github.com/embarklabs/embark.git" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "embark-collective": { + "build:node": true, + "typecheck": true + }, + "scripts": { + "_build": "npm run solo -- build", + "_typecheck": "npm run solo -- typecheck", + "ci": "npm run qa", + "clean": "npm run reset", + "lint": "eslint src/", + "qa": "npm-run-all lint _typecheck _build", + "reset": "npx rimraf dist embark-*.tgz package", + "solo": "embark-solo" + }, + "eslintConfig": { + "extends": "../../../.eslintrc.json" + }, + "dependencies": { + "@babel/runtime-corejs3": "7.8.4", + "core-js": "3.4.3", + "embark-core": "^5.3.0-nightly.4", + "embark-i18n": "^5.3.0-nightly.4", + "embark-utils": "^5.3.0-nightly.4", + "ethers": "4.0.40", + "quorum-js": "0.3.4", + "web3": "1.2.6" + }, + "devDependencies": { + "embark-solo": "^5.1.1", + "eslint": "6.8.0", + "npm-run-all": "4.1.5", + "rimraf": "3.0.0" + }, + "engines": { + "node": ">=10.17.0", + "npm": ">=6.11.3", + "yarn": ">=1.19.1" + } +} \ No newline at end of file diff --git a/packages/plugins/quorum/src/deployer.ts b/packages/plugins/quorum/src/deployer.ts new file mode 100644 index 000000000..3b0b9ae53 --- /dev/null +++ b/packages/plugins/quorum/src/deployer.ts @@ -0,0 +1,55 @@ +import { __ } from 'embark-i18n'; +import Web3 from 'web3'; +import { Embark, EmbarkEvents, ContractsConfig } from 'embark-core'; + +declare module "embark-core" { + interface ContractConfig { + privateFor: string[]; + privateFrom: string; + } +} + +export default class QuorumDeployer { + private events: EmbarkEvents; + private _web3: Web3 | null = null; + private contractsConfig: ContractsConfig; + constructor(private embark: Embark) { + this.events = embark.events; + this.contractsConfig = embark.config.contractsConfig?.contracts; + } + + get web3() { + return (async () => { + if (!this._web3) { + const provider = await this.events.request2("blockchain:client:provider", "ethereum"); + this._web3 = new Web3(provider); + } + return this._web3; + })(); + } + + public registerDeployer() { + this.embark.registerActionForEvent("deployment:contract:beforeDeploy", this.addAdditionalDeployParams.bind(this)); + } + + private async addAdditionalDeployParams(params, callback) { + const web3 = await this.web3; + const contract = params.contract; + + const code = contract.code.substring(0, 2) === '0x' ? contract.code : "0x" + contract.code; + const privateFor = this.contractsConfig && this.contractsConfig[contract.className]?.privateFor; + if (privateFor) { + params.additionalDeployParams = { ...params.additionalDeployParams, privateFor }; + } + const privateFrom = this.contractsConfig && this.contractsConfig[contract.className]?.privateFrom; + if (privateFrom) { + params.additionalDeployParams = { ...params.additionalDeployParams, privateFrom }; + } + if (privateFor || privateFrom) { + const contractObj = new web3.eth.Contract(contract.abiDefinition, contract.address); + const bytecodeWithInitParam = contractObj.deploy({ arguments: (contract.args || []), data: code }).encodeABI(); + params.additionalDeployParams.bytecodeWithInitParam = bytecodeWithInitParam; + } + callback(null, params); + } +} diff --git a/packages/plugins/quorum/src/index.js b/packages/plugins/quorum/src/index.js new file mode 100644 index 000000000..3b40967bd --- /dev/null +++ b/packages/plugins/quorum/src/index.js @@ -0,0 +1,81 @@ +import { __ } from "embark-i18n"; +import {testRpcWithEndpoint, testWsEndpoint} from "embark-utils"; +import constants from "embark-core/constants"; +import { QuorumWeb3Extensions } from "./quorumWeb3Extensions"; +import QuorumDeployer from "./deployer"; +export { getBlock, getTransaction, getTransactionReceipt, decodeParameters } from "./quorumWeb3Extensions"; + +class Quorum { + constructor(embark) { + this.embark = embark; + this.embarkConfig = embark.config.embarkConfig; + this.blockchainConfig = embark.config.blockchainConfig; + this.locale = embark.config.locale; + this.logger = embark.logger; + this.client = embark.config.blockchainConfig.client; + this.isDev = embark.config.blockchainConfig.isDev; + this.events = embark.events; + + if (!this.shouldInit()) { + return; + } + + this.events.request("blockchain:node:register", constants.blockchain.clients.quorum, { + isStartedFn: (isStartedCb) => { + this._doCheck((state) => { + const isStarted = state.status === "on"; + if (isStarted) { + this.registerServiceCheck(); + } else { + this.logger.error(__('Cannot connect to the configured Quorum node. Please start Quorum manually.')); + } + return isStartedCb(null, isStarted); + }); + }, + launchFn: (readyCb) => { + this.logger.warn(__('Quorum must be started manually. Please start Quorum, then restart Embark.')); + readyCb(); + }, + stopFn: async (cb) => { + this.logger.warn(__('Quorum cannot be stopped using Embark. Please stop Quorum manually.')); + cb(); + } + }); + + this.init(); // fire and forget + } + + async init() { + const web3Overrides = new QuorumWeb3Extensions(this.embark); + await web3Overrides.registerOverrides(); + + const deployer = new QuorumDeployer(this.embark); + await deployer.registerDeployer(); + } + + shouldInit() { + return ( + this.blockchainConfig.client === constants.blockchain.clients.quorum && + this.blockchainConfig.enabled + ); + } + + _getNodeState(err, version, cb) { + if (err) return cb({ name: "Ethereum node not found", status: 'off' }); + + return cb({ name: `${constants.blockchain.clients.quorum} (Ethereum)`, status: 'on' }); + } + + _doCheck(cb) { + if (this.blockchainConfig.endpoint.startsWith('ws')) { + return testWsEndpoint(this.blockchainConfig.endpoint, (err, version) => this._getNodeState(err, version, cb)); + } + testRpcWithEndpoint(this.blockchainConfig.endpoint, (err, version) => this._getNodeState(err, version, cb)); + } + + registerServiceCheck() { + this.events.request("services:register", 'Ethereum', this._doCheck.bind(this), 5000, 'off'); + } + +} +module.exports = Quorum; diff --git a/packages/plugins/quorum/src/quorumWeb3Extensions.ts b/packages/plugins/quorum/src/quorumWeb3Extensions.ts new file mode 100644 index 000000000..cf82dd2bf --- /dev/null +++ b/packages/plugins/quorum/src/quorumWeb3Extensions.ts @@ -0,0 +1,184 @@ +import { Embark } from "embark-core"; +import Web3 from "web3"; +import { AbiCoder as EthersAbi } from 'ethers/utils/abi-coder'; +import quorumjs from "quorum-js"; + +// Create an augmentation for "web3-utils +declare module "web3-utils" { + // Augment the 'web3-utils' class definition with interface merging + interface Utils { + _: any; + BN: any; + } +} + +export class QuorumWeb3Extensions { + + constructor(private embark: Embark) { + } + + registerOverrides() { + return new Promise((resolve) => { + this.embark.registerActionForEvent("runcode:register:web3", (web3: Web3, cb) => { + web3 = extendWeb3(web3); + cb(null, web3); + resolve(); + }); + }); + } +} + +export function extendWeb3(web3: Web3) { + quorumjs.extend(web3); + web3 = getBlock(web3); + web3 = getTransaction(web3); + web3 = getTransactionReceipt(web3); + web3 = decodeParameters(web3); + return web3; +} + +// The ts-ignores are ignoring the checks that are +// saying that web3.eth.getBlock is a function and doesn't +// have a `method` property, which it does +export function getBlock(web3: Web3) { + // @ts-ignore + const _oldBlockFormatter = web3.eth.getBlock.method.outputFormatter; + // @ts-ignore + web3.eth.getBlock.method.outputFormatter = (block: any) => { + const _oldTimestamp = block.timestamp; + const _oldGasLimit = block.gasLimit; + const _oldGasUsed = block.gasUsed; + + // Quorum uses nanoseconds instead of seconds in timestamp + let timestamp = new Web3.utils.BN(block.timestamp.slice(2), 16); + timestamp = timestamp.div(Web3.utils.toBN(10).pow(Web3.utils.toBN(9))); + + block.timestamp = "0x" + timestamp.toString(16); + + // Since we're overwriting the gasLimit/Used later, + // it doesn't matter what it is before the call + // The same applies to the timestamp, but I reduced + // the precision since there was an accurate representation + // We do this because Quorum can have large block/transaction + // gas limits + block.gasLimit = "0x0"; + block.gasUsed = "0x0"; + + // @ts-ignore + const result = _oldBlockFormatter.call(web3.eth.getBlock.method, block); + + // Perhaps there is a better method of doing this, + // but the raw hexstrings work for the time being + result.timestamp = _oldTimestamp; + result.gasLimit = _oldGasLimit; + result.gasUsed = _oldGasUsed; + + return result; + }; + return web3; +} + +export function getTransaction(web3: Web3) { + const _oldTransactionFormatter = + // @ts-ignore + web3.eth.getTransaction.method.outputFormatter; + + // @ts-ignore + web3.eth.getTransaction.method.outputFormatter = (tx) => { + const _oldGas = tx.gas; + + tx.gas = "0x0"; + + const result = _oldTransactionFormatter.call( + // @ts-ignore + web3.eth.getTransaction.method, + tx + ); + + // Perhaps there is a better method of doing this, + // but the raw hexstrings work for the time being + result.gas = _oldGas; + + return result; + }; + return web3; +} + +export function getTransactionReceipt(web3: Web3) { + const _oldTransactionReceiptFormatter = + // @ts-ignore + web3.eth.getTransactionReceipt.method.outputFormatter; + + // @ts-ignore + web3.eth.getTransactionReceipt.method.outputFormatter = (receipt: any) => { + const _oldGasUsed = receipt.gasUsed; + + receipt.gasUsed = "0x0"; + + const result = _oldTransactionReceiptFormatter.call( + // @ts-ignore + web3.eth.getTransactionReceipt.method, + receipt + ); + + // Perhaps there is a better method of doing this, + // but the raw hexstrings work for the time being + result.gasUsed = _oldGasUsed; + + return result; + }; + return web3; +} + +// The primary difference between this decodeParameters function and web3's +// is that the 'Out of Gas?' zero/null bytes guard has been removed and any +// falsy bytes are interpreted as a zero value. +export function decodeParameters(web3: Web3) { + const _oldDecodeParameters = web3.eth.abi.decodeParameters; + + const ethersAbiCoder = new EthersAbi((type, value) => { + if ( + type.match(/^u?int/) && + !Web3.utils._.isArray(value) && + (!Web3.utils._.isObject(value) || value.constructor.name !== "BN") + ) { + return value.toString(); + } + return value; + }); + + // result method + function Result() { } + + web3.eth.abi.decodeParameters = (outputs: any[], bytes: string) => { + // if bytes is falsy, we'll pass 64 '0' bits to the ethers.js decoder. + // the decoder will decode the 64 '0' bits as a 0 value. + if (!bytes) { + bytes = "0".repeat(64); + } + const res = ethersAbiCoder.decode( + // @ts-ignore 'mapTypes' not existing on type 'ABI' + web3.eth.abi.mapTypes(outputs), + `0x${bytes.replace(/0x/i, "")}` + ); + // @ts-ignore complaint regarding Result method + const returnValue = new Result(); + returnValue.__length__ = 0; + + outputs.forEach((output, i) => { + let decodedValue = res[returnValue.__length__]; + decodedValue = decodedValue === "0x" ? null : decodedValue; + + returnValue[i] = decodedValue; + + if (Web3.utils._.isObject(output) && output.name) { + returnValue[output.name] = decodedValue; + } + + returnValue.__length__++; + }); + + return returnValue; + }; + return web3; +} diff --git a/packages/plugins/quorum/tsconfig.json b/packages/plugins/quorum/tsconfig.json new file mode 100644 index 000000000..a78c398ce --- /dev/null +++ b/packages/plugins/quorum/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "composite": true, + "declarationDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-quorum.tsbuildinfo" + }, + "extends": "../../../tsconfig.base.json", + "include": [ + "src/**/*" + ], + "references": [ + { + "path": "../../core/core" + }, + { + "path": "../../core/i18n" + }, + { + "path": "../../core/utils" + } + ] +} diff --git a/packages/plugins/rpc-manager/package.json b/packages/plugins/rpc-manager/package.json index 00ffb1b5c..45a3d7dd3 100644 --- a/packages/plugins/rpc-manager/package.json +++ b/packages/plugins/rpc-manager/package.json @@ -58,6 +58,8 @@ "embark-i18n": "^5.3.0-nightly.5", "embark-logger": "^5.3.0-nightly.12", "embark-utils": "^5.3.0-nightly.12", + "lodash.clonedeep": "4.5.0", + "quorum-js": "0.3.4", "web3": "1.2.6" }, "devDependencies": { diff --git a/packages/plugins/rpc-manager/src/lib/eth_sendRawTransaction.ts b/packages/plugins/rpc-manager/src/lib/eth_sendRawTransaction.ts new file mode 100644 index 000000000..3cbc31a54 --- /dev/null +++ b/packages/plugins/rpc-manager/src/lib/eth_sendRawTransaction.ts @@ -0,0 +1,137 @@ +import { Callback, Embark, EmbarkEvents } from "embark-core"; +import { __ } from "embark-i18n"; +import Web3 from "web3"; +const { blockchain: blockchainConstants } = require("embark-core/constants"); +import RpcModifier from "./rpcModifier"; +import quorumjs from "quorum-js"; + +const TESSERA_PRIVATE_URL_DEFAULT = "http://localhost:9081"; + +declare module "embark-core" { + interface ClientConfig { + tesseraPrivateUrl?: string; + } + interface ContractConfig { + privateFor?: string[]; + privateFrom?: string; + } +} + +// TODO: Because this is quorum-specific, move this entire file to the embark-quorum plugin where it +// should be registered in the RPC modifier (which doesn't yet exist). RPC modifier registration should be created as follows: +// TODO: 1. move embark-rpc-manager to proxy so it can be a stack component +// TODO: 2. Create command handler that allows registration of an RPC modifier (rpcMethodName: string | Regex, action) +// TODO: 3. add only 1 instance of registerActionForEvent('blockchain:proxy:request/response') +// TODO: 4. For each request/response, loop through registered RPC modifiers finding matches against RPC method name +// TODO: 5. run matched action for request/response +// TODO: This should be done after https://github.com/embarklabs/embark/pull/2150 is merged. +export default class EthSendRawTransaction extends RpcModifier { + private _rawTransactionManager: any; + constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) { + super(embark, rpcModifierEvents, nodeAccounts, accounts, web3); + + embark.registerActionForEvent("blockchain:proxy:request", this.ethSendRawTransactionRequest.bind(this)); + embark.registerActionForEvent("blockchain:proxy:response", this.ethSendRawTransactionResponse.bind(this)); + } + + protected get rawTransactionManager() { + return (async () => { + if (!this._rawTransactionManager) { + const web3 = await this.web3; + // RawTransactionManager doesn't support websockets, and uses the + // currentProvider.host URI to communicate with the node over HTTP. Therefore, we can + // populate currentProvider.host with our proxy HTTP endpoint, without affecting + // web3's websocket connection. + // @ts-ignore + web3.eth.currentProvider.host = await this.events.request2("proxy:endpoint:http:get"); + + this._rawTransactionManager = quorumjs.RawTransactionManager(web3, { + privateUrl: this.embark.config.blockchainConfig.clientConfig?.tesseraPrivateUrl ?? TESSERA_PRIVATE_URL_DEFAULT + }); + } + return this._rawTransactionManager; + })(); + } + + private async shouldHandle(params) { + if (params.request.method !== blockchainConstants.transactionMethods.eth_sendRawTransaction) { + return false; + } + + const accounts = await this.accounts; + if (!(accounts && accounts.length)) { + return false; + } + + // Only handle quorum requests that came via eth_sendTransaction + // If the user wants to send a private raw tx directly, + // web3.eth.sendRawPrivateTransaction should be used (which calls + // eth_sendRawPrivateTransaction) + const originalPayload = params.originalRequest?.params[0]; + const privateFor = originalPayload?.privateFor; + const privateFrom = originalPayload?.privateFrom; + if (!privateFor && !privateFrom) { + return false; + } + + const from = accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(originalPayload.from)); + if (!from?.privateKey) { + return false; + } + + return { originalPayload, privateFor, privateFrom, from }; + } + + private stripPrefix(value) { + return value?.indexOf('0x') === 0 ? value.substring(2) : value; + } + + private async ethSendRawTransactionRequest(params: any, callback: Callback) { + const shouldHandle = await this.shouldHandle(params); + if (!shouldHandle) { + return callback(null, params); + } + + // manually send to the node in the response + params.sendToNode = false; + callback(null, params); + } + + private async ethSendRawTransactionResponse(params: any, callback: Callback) { + const shouldHandle = await this.shouldHandle(params); + if (!shouldHandle) { + return callback(null, params); + } + + this.logger.trace(__(`Modifying blockchain '${params.request.method}' request:`)); + this.logger.trace(__(`Original request data: ${JSON.stringify({ request: params.request, response: params.response })}`)); + + const { originalPayload, privateFor, privateFrom, from } = shouldHandle; + const { gas, gasPrice, gasLimit, to, value, nonce, bytecodeWithInitParam, data } = originalPayload; + + try { + const rawTransactionManager = await this.rawTransactionManager; + + const rawTx = { + gasPrice: this.stripPrefix(gasPrice) ?? (0).toString(16), + gasLimit: this.stripPrefix(gasLimit) ?? (4300000).toString(16), + gas: this.stripPrefix(gas), + to, + value: this.stripPrefix(value) ?? (0).toString(16), + data: bytecodeWithInitParam ?? data, + from, + isPrivate: true, + privateFrom, + privateFor, + nonce + }; + + const { transactionHash } = await rawTransactionManager.sendRawTransaction(rawTx); + params.response.result = transactionHash; + this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request: params.request, response: params.response })}`)); + return callback(null, params); + } catch (err) { + return callback(err); + } + } +} diff --git a/packages/plugins/rpc-manager/src/lib/eth_sendTransaction.ts b/packages/plugins/rpc-manager/src/lib/eth_sendTransaction.ts index cdf0a88ea..d487f13ec 100644 --- a/packages/plugins/rpc-manager/src/lib/eth_sendTransaction.ts +++ b/packages/plugins/rpc-manager/src/lib/eth_sendTransaction.ts @@ -1,18 +1,23 @@ import async from "async"; -import { Callback, Embark, EmbarkEvents } from "embark-core"; +import { Callback, Embark, EmbarkEvents, EmbarkPlugins } from "embark-core"; import { __ } from "embark-i18n"; import Web3 from "web3"; const { blockchain: blockchainConstants } = require("embark-core/constants"); import RpcModifier from "./rpcModifier"; +import cloneDeep from "lodash.clonedeep"; export default class EthSendTransaction extends RpcModifier { private signTransactionQueue: any; private nonceCache: any = {}; + private plugins: EmbarkPlugins; constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) { super(embark, rpcModifierEvents, nodeAccounts, accounts, web3); + this.plugins = embark.config.plugins; + embark.registerActionForEvent("blockchain:proxy:request", this.ethSendTransactionRequest.bind(this)); + // TODO: pull this out in to rpc-manager/utils once https://github.com/embarklabs/embark/pull/2150 is merged. // Allow to run transaction in parallel by resolving the nonce manually. // For each transaction, resolve the nonce by taking the max of current transaction count and the cache we keep locally. // Update the nonce and sign it @@ -23,7 +28,7 @@ export default class EthSendTransaction extends RpcModifier { } payload.nonce = newNonce; try { - const result = await this.web3.eth.accounts.signTransaction(payload, account.privateKey); + const result = await web3.eth.accounts.signTransaction(payload, account.privateKey); callback(null, result.rawTransaction); } catch (err) { callback(err); @@ -32,8 +37,10 @@ export default class EthSendTransaction extends RpcModifier { }, 1); } + // TODO: pull this out in to rpc-manager/utils once https://github.com/embarklabs/embark/pull/2150 is merged. private async getNonce(address: string, callback: Callback) { - this.web3.eth.getTransactionCount(address, (error: any, transactionCount: number) => { + const web3 = await this.web3; + web3.eth.getTransactionCount(address, (error: any, transactionCount: number) => { if (error) { return callback(error, null); } @@ -69,9 +76,11 @@ export default class EthSendTransaction extends RpcModifier { if (err) { return callback(err, null); } + params.originalRequest = cloneDeep(params.request); params.request.method = blockchainConstants.transactionMethods.eth_sendRawTransaction; params.request.params = [newPayload]; - callback(err, params); + // allow for any mods to eth_sendRawTransaction + this.plugins.runActionsForEvent('blockchain:proxy:request', params, callback); }); } } catch (err) { diff --git a/packages/plugins/rpc-manager/src/lib/index.ts b/packages/plugins/rpc-manager/src/lib/index.ts index 848a6a0e1..806d7a498 100644 --- a/packages/plugins/rpc-manager/src/lib/index.ts +++ b/packages/plugins/rpc-manager/src/lib/index.ts @@ -5,6 +5,7 @@ import { AccountParser, dappPath } from "embark-utils"; import Web3 from "web3"; import EthAccounts from "./eth_accounts"; import EthSendTransaction from "./eth_sendTransaction"; +import EthSendRawTransaction from "./eth_sendRawTransaction"; import EthSignData from "./eth_signData"; import EthSignTypedData from "./eth_signTypedData"; import EthSubscribe from "./eth_subscribe"; @@ -82,6 +83,7 @@ export default class RpcManager { PersonalNewAccount, EthAccounts, EthSendTransaction, + EthSendRawTransaction, EthSignTypedData, EthSignData, EthSubscribe, diff --git a/packages/plugins/transaction-logger/src/index.js b/packages/plugins/transaction-logger/src/index.js index 2cc13f87d..e9fb0e1dc 100644 --- a/packages/plugins/transaction-logger/src/index.js +++ b/packages/plugins/transaction-logger/src/index.js @@ -149,7 +149,7 @@ export default class TransactionLogger { if (accounts.map(account => account.toLowerCase()).includes(address.toLowerCase())) { const web3 = await this.web3; - const value = web3.utils.fromWei(web3.utils.hexToNumberString(dataObject.value)); + const value = dataObject.value ? web3.utils.fromWei(web3.utils.hexToNumberString(dataObject.value)) : 0; return this.logger.info(`Blockchain>`.underline + ` transferring ${value} ETH from ${dataObject.from} to ${address}`.bold); } diff --git a/packages/stack/contracts-manager/src/index.js b/packages/stack/contracts-manager/src/index.js index c69f60004..87f483fab 100644 --- a/packages/stack/contracts-manager/src/index.js +++ b/packages/stack/contracts-manager/src/index.js @@ -333,9 +333,7 @@ export default class ContractsManager { contract.filename = compiledContract.filename; contract.originalFilename = compiledContract.originalFilename || ("contracts/" + contract.filename); contract.path = dappPath(contract.originalFilename); - contract.gas = (contractConfig && contractConfig.gas) || contractsConfig.gas || 'auto'; - contract.gasPrice = contract.gasPrice || gasPrice; contract.type = 'file'; contract.className = className; diff --git a/packages/stack/deployment/src/contract_deployer.js b/packages/stack/deployment/src/contract_deployer.js index 034abb64f..db6e41913 100644 --- a/packages/stack/deployment/src/contract_deployer.js +++ b/packages/stack/deployment/src/contract_deployer.js @@ -20,13 +20,12 @@ class ContractDeployer { }); }, (next) => { - this.plugins.emitAndRunActionsForEvent('deployment:contract:beforeDeploy', {contract: contract}, (err, _params) => { - // TODO: confirm this really works and shouldn't be next(err, params) - next(err); + this.plugins.emitAndRunActionsForEvent('deployment:contract:beforeDeploy', { contract }, (err, params) => { + next(err, params); }); }, - (next) => { - this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, (err, params) => { + (params, next) => { + this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {...params, contract, shouldDeploy: true}, (err, params) => { next(err, params); }); }, @@ -39,7 +38,7 @@ class ContractDeployer { // TODO: implement `blockchainType` a la `this.deployer[contract.blockchainType].apply(this.deployer, [contract, next])` this.deployer["ethereum"].apply(this.deployer, [ - contract, (err, receipt) => { + contract, params.additionalDeployParams, (err, receipt) => { if (!receipt) return next(err); this.plugins.emitAndRunActionsForEvent('deployment:contract:deployed', { contract, receipt }, (err, _params) => { next(err); diff --git a/packages/stack/deployment/test/deployment.spec.js b/packages/stack/deployment/test/deployment.spec.js index c72d40dfd..9cf611aea 100644 --- a/packages/stack/deployment/test/deployment.spec.js +++ b/packages/stack/deployment/test/deployment.spec.js @@ -27,7 +27,7 @@ describe('stack/deployment', () => { deployedAction = sinon.spy((params, cb) => { cb(null, params); }); afterAllAction = sinon.spy((params, cb) => { cb(null, params); }); - deployFn = sinon.spy((contract, done) => { + deployFn = sinon.spy((contract, addlDeployParams, done) => { deployedContracts.push(contract); done(null, {}); // deployer needs to finish with a receipt object }); diff --git a/packages/stack/proxy/src/index.ts b/packages/stack/proxy/src/index.ts index dcec19d56..cb7a18341 100644 --- a/packages/stack/proxy/src/index.ts +++ b/packages/stack/proxy/src/index.ts @@ -57,6 +57,12 @@ export default class ProxyManager { this.events.setCommandHandler("proxy:endpoint:get", async (cb) => { cb(null, (await this.endpoint)); }); + this.events.setCommandHandler("proxy:endpoint:ws:get", async (cb) => { + cb(null, buildUrl("ws", this.host, this.wsPort, "ws")); + }); + this.events.setCommandHandler("proxy:endpoint:http:get", async (cb) => { + cb(null, buildUrl("http", this.host, this.rpcPort, "rpc")); + }); } private get endpoint() { diff --git a/packages/stack/proxy/src/proxy.js b/packages/stack/proxy/src/proxy.js index c59162fcd..e405534b9 100644 --- a/packages/stack/proxy/src/proxy.js +++ b/packages/stack/proxy/src/proxy.js @@ -5,7 +5,7 @@ import cors from 'cors'; import { isDebug } from 'embark-utils'; const Web3RequestManager = require('web3-core-requestmanager'); -const ACTION_TIMEOUT = isDebug() ? 20000 : 5000; +const ACTION_TIMEOUT = isDebug() ? 30000 : 10000; export class Proxy { constructor(options) { @@ -143,7 +143,7 @@ export class Proxy { } try { - const modifiedResp = await this.emitActionsForResponse(modifiedRequest.request, response, transport); + const modifiedResp = await this.emitActionsForResponse(modifiedRequest, response, transport); // Send back to the client if (modifiedResp && modifiedResp.response && modifiedResp.response.error) { // error returned from the node and it wasn't stripped by our response actions @@ -209,7 +209,7 @@ export class Proxy { this.logger.debug(`Subscription data received from node and forwarded to originating socket client connection: ${JSON.stringify(subscriptionResponse)} `); // allow modification of the node subscription data sent to the client - subscriptionResponse = await this.emitActionsForResponse(subscriptionResponse, subscriptionResponse, clientSocket); + subscriptionResponse = await this.emitActionsForResponse({request: subscriptionResponse}, subscriptionResponse, clientSocket); this.respondWs(clientSocket, subscriptionResponse.response); }; @@ -308,9 +308,9 @@ export class Proxy { }); } - emitActionsForResponse(request, response, transport) { + emitActionsForResponse({originalRequest, request}, response, transport) { return new Promise((resolve, reject) => { - const data = { request, response, isWs: this.isWs, transport }; + const data = { originalRequest, request, response, isWs: this.isWs, transport }; let calledBack = false; setTimeout(() => { if (calledBack) { diff --git a/site/source/docs/blockchain_configuration.md b/site/source/docs/blockchain_configuration.md index 57052bbe2..904876194 100644 --- a/site/source/docs/blockchain_configuration.md +++ b/site/source/docs/blockchain_configuration.md @@ -43,7 +43,7 @@ Most of the options are self-explanatory, still, here are some brief description Option | Type: `default` | Value --- | --- | --- `enabled` | boolean: `true` | Whether or not to spawn an Ethereum node -`client` | string: `geth` | Client to use for the Ethereum node. Currently supported: `geth` and `parity` +`client` | string: `geth` | Client to use for the Ethereum node. Currently supported: `geth`, `parity`, `nethermind`, and `quorum`. Note: the corresponding plugin package must be installed in your dApp (ie `embark-quorum`) and the plugin must be enabled in your dApp's `embark.json`, ie `plugins: { embark-quorum: {} }` `miningMode` | string: `dev` | The mining mode to use for the node.
`dev`: This is a special mode where the node uses a development account as defaultAccount. This account is already funded and transactions are faster.
`auto`: Uses a mining script to mine only when needed.
`always`: Miner is always on.
`off`: Turns off the miner `endpoint` | string | Endpoint to connect to. Works for external endpoints (like Infura) and local ones too (only for nodes started by `embark run`) `accounts` | array | Accounts array for the node and to deploy. When no account is given, defaults to one node account. For more details, go [here](/docs/blockchain_accounts_configuration.html) @@ -74,6 +74,7 @@ Option | Type: `default` | Value `proxy` | boolean: `true` | Whether or not Embark should use a proxy to add functionalities. This proxy is used by Embark to see the different transactions that go through, for example, and shows them to you. `targetGasLimit` | number | Artificial target gas floor for the blocks to mine `genesisBlock` | string | The genesis file to use to create the network. This file is used when creating the network. It tells the client the parameters to initiate the node with. You can read more on [genesis blocks here](https://arvanaghi.com/blog/explaining-the-genesis-block-in-ethereum/) +`tesseraPrivateUrl` | string: `http://localhost:9081` | Endpoint of the Tessera private transaction manager when using `embark-quorum` as the dApp blockchain (`client: 'quorum'` must be set). {% notification info 'Using Parity and Metamask' %} diff --git a/site/source/docs/contracts_configuration.md b/site/source/docs/contracts_configuration.md index e41cdebce..0cb91bb95 100644 --- a/site/source/docs/contracts_configuration.md +++ b/site/source/docs/contracts_configuration.md @@ -483,6 +483,25 @@ tracking: 'path/to/some/file' Having the file referenced above under version control ensures that other users of our project don't redeploy the Smart Contracts on different platforms. +### Skipping bytecode check +By default, before deploying a contract, Embark will check if the bytecode for the contract already exists on-chain at the address that is tracked by Embark. If the bytecode does not exist, or is different, Embark will deploy the contract for you. We can skip a bytecode check, to ensure that Embark relies on the deployment tracker to determine if the contract has already been deployed or not. + +``` +environment: { + deploy: { + SimpleStorage: { + skipBytecodeCheck: true + } + } +}, +``` + +This becomes useful in cases of private multi-node environments, where we do not want Embark to deploy contracts to the current node when another node on the chain may already have the contract deployed to it. + +More to the point, when using Embark's Quorum plugin in a private multi-node private setup (ie using the [7nodes example](https://github.com/jpmorganchase/quorum-examples/tree/master/examples/7nodes)), we might want to deploy a contract privately between Node 1 and Node 7, for example. In this case, we deploy the contract by connecting Embark to the Node 1 environment we have set up in our blockchain config. Connecting to our Node 7 environment set up in our blockchain config, we should see values for our contracts. However, connecting to other Node environments in our blockchain config (ie Nodes 2 - 6), those contract values should not exist on the node. The only way to ensure that we do not deploy our contracts to Nodes 2 - 6 when they're not meant to exist there, is by setting the `skipBytecodeCheck: true` setting for the contract in the contracts config. + +The downside of this, is that if we perform an `embark reset` (effectively deleting our `chains.json`, and therefore losing track of which contracts we've previously deployed), we have no way to check if our chain has already deployed our contract or not. In this case, Embark will deploy the contracts to the current environment regardless. + ### Reducing contract size diff --git a/site/source/docs/plugin_reference.md b/site/source/docs/plugin_reference.md index ff4941372..769b0a17a 100644 --- a/site/source/docs/plugin_reference.md +++ b/site/source/docs/plugin_reference.md @@ -355,6 +355,9 @@ This call is used to request a certain resource from Embark * (`compiler:contracts`, contractFiles) - requests embark to compile a list of files, will return a compiled object in the callback * (`services:register`, serviceName, checkCallback) - requests embark to register a service, it will execute checkCallback every 5 seconds, the callback should return an object containing the service name and status (See embark.registerServiceCheck) * (`console:command`, cmd) - execute a command in the console + * (`proxy:endpoint:get`, endpoint) - retrieves the endpoint of Embark's proxy. This is the endpoint that should be used to connect to the blockchain node. The resulting protocol (HTTP or WS) is determined by the dApp's blockchain config. + * (`proxy:endpoint:ws:get`, endpoint) - retrieves the endpoint of Embark's WebSockets proxy, regardless of the configuration settings in the blockchain config. + * (`proxy:endpoint:http:get`, endpoint) - retrieves the endpoint of Embark's HTTP proxy, regardless of the configuration settings in the blockchain config. ``` module.exports = function(embark) { @@ -486,3 +489,8 @@ embark.registerActionForEvent("deployment:contract:beforeDeploy", async (params, - `response`: an object containing the response payload - `transport`: an object containing the client's websocket connection to the proxy - `isWs`: a boolean flag indicating if the request was performed using websockets +- `runcode:register:`: when variables are registered in the console using `runcode:register`, actions with the name of the variable (ie `runcode:register:web3`) will be run *before* the variable is actually registered in the console. This allows a variable to be modified by plugins before being registered in the console. Params are: + - ``: an object containing the variable registered in the console + - `callback`: callback to be called once the variable has been modified. Pass the following parameters to the callback: + - `error`: Any error that ocurred during the action + - ``: The modified variable to be registered in the console. diff --git a/tsconfig.json b/tsconfig.json index 6fe0f4573..03204a19e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -103,6 +103,9 @@ { "path": "packages/plugins/profiler" }, + { + "path": "packages/plugins/quorum" + }, { "path": "packages/plugins/rpc-manager" }, diff --git a/yarn.lock b/yarn.lock index e1d60f855..92813ce41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9600,6 +9600,21 @@ ethers@4.0.0-beta.3: uuid "2.0.1" xmlhttprequest "1.8.0" +ethers@4.0.40: + version "4.0.40" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.40.tgz#6e1963d10b5d336a13cd81b519c230cc17624653" + integrity sha512-MC9BtV7Hpq4dgFONEfanx9aU9GhhoWU270F+/wegHZXA7FR+2KXFdt36YIQYLmVY5ykUWswDxd+f9EVkIa7JOA== + dependencies: + aes-js "3.0.0" + bn.js "^4.4.0" + elliptic "6.5.2" + hash.js "1.1.3" + js-sha3 "0.5.7" + scrypt-js "2.0.4" + setimmediate "1.0.4" + uuid "2.0.1" + xmlhttprequest "1.8.0" + ethers@^4.0.40: version "4.0.42" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.42.tgz#7def83a1f770b84d44cf1c2dfc58b7fd29d1d45d" @@ -18259,6 +18274,19 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +quorum-js@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/quorum-js/-/quorum-js-0.3.4.tgz#8ad4bc9a15fe839f8d3bb33cebbf8817082c758c" + integrity sha512-1656I5tFsJ7bnVJMI69tQFO3EcBLrIw5YdO5lXfwq4+bUB5PP0ghpTehonMjzgQH3gMH5QETsqYEo4Bnrku7bQ== + dependencies: + dotenv "^6.2.0" + ethereumjs-tx "^2.1.1" + request-promise-native "^1.0.5" + rlp "^2.1.0" + underscore "^1.9.1" + utf8 "^3.0.0" + web3 "^1.2.0" + raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -19752,7 +19780,7 @@ rlp@^2.0.0, rlp@^2.2.2: bn.js "^4.11.1" safe-buffer "^5.1.1" -rlp@^2.2.1, rlp@^2.2.3: +rlp@^2.1.0, rlp@^2.2.1, rlp@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.4.tgz#d6b0e1659e9285fc509a5d169a9bd06f704951c1" integrity sha512-fdq2yYCWpAQBhwkZv+Z8o/Z4sPmYm1CUq6P7n6lVTOdb949CnqA0sndXal5C1NleSVSZm6q5F3iEbauyVln/iw== @@ -22115,6 +22143,11 @@ underscore@1.9.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== +underscore@^1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f" + integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ== + unfetch@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" @@ -23026,7 +23059,7 @@ web3-utils@1.2.6: underscore "1.9.1" utf8 "3.0.0" -web3@1.2.6: +web3@1.2.6, web3@^1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.6.tgz#c497dcb14cdd8d6d9fb6b445b3b68ff83f8ccf68" integrity sha512-tpu9fLIComgxGrFsD8LUtA4s4aCZk7px8UfcdEy6kS2uDi/ZfR07KJqpXZMij7Jvlq+cQrTAhsPSiBVvoMaivA==