diff --git a/accounts.example.json b/accounts.example.json new file mode 100644 index 0000000..dee00d1 --- /dev/null +++ b/accounts.example.json @@ -0,0 +1,7 @@ +{ + "accounts": [ + { + "mnemonic": "add mnemonic here" + } + ] +} diff --git a/package.json b/package.json index 13f53d7..8f99b89 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "homepage": ".", "dependencies": { "@aragon/os": "3.1.9", + "async": "^3.0.0", "clear": "^0.1.0", "cli-spinner": "^0.2.10", "cli-table": "^0.3.1", diff --git a/src/actions.js b/src/actions.js index becf8c3..4b93f01 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,11 +1,10 @@ var inquirer = require('inquirer'); -const Web3 = require("web3"); const PledgeAdminUtils = require('./pledgeadmin-utils'); const PledgeUtils = require('./pledge-utils'); const TrxUtils = require('./trx-utils'); const Contracts = require("./contracts.js"); - -const web3 = new Web3(); +const Provider = require("./provider.js"); +const Web3 = require('web3'); function doAction(actionText, action) { console.dir(actionText) @@ -34,21 +33,36 @@ function doAction(actionText, action) { class Actions { - constructor(chain) { + constructor(chain, accounts) { this.chain = chain || "development"; + this.accounts = accounts || []; } - connect(url, cb) { + connect(options, cb) { + const url = options.url; + console.dir("connecting to: " + url); - web3.setProvider(url); + + if (this.accounts.length > 0) { + this.provider = new Provider(); + this.provider.initAccounts(this.accounts); + this.provider.startWeb3Provider("ws", url) + } else { + this.web3 = new Web3(); + this.web3.setProvider(url); + } setTimeout(async () => { - let accounts = await web3.eth.getAccounts(); + if (this.accounts.length > 0) { + this.web3 = this.provider.web3; + } + + let accounts = await this.web3.eth.getAccounts(); console.dir("== accounts"); console.dir(accounts); - web3.eth.defaultAccount = accounts[0] + this.web3.eth.defaultAccount = accounts[0] - let contracts = new Contracts(this.chain, web3); + let contracts = new Contracts(this.chain, this.web3); contracts.loadContracts(); this.contracts = contracts.contracts; @@ -56,15 +70,15 @@ class Actions { }, 1000); } - web3() { - return web3; + web3Object() { + return this.web3; } async addProject(params) { - let text = `await LiquidPledging.methods.addProject(\"${params.name}\", \"${params.url}\", \"${params.account}\", ${params.parentProject}, ${params.commitTime}, \"${params.plugin}\").send({from: \"${web3.eth.defaultAccount}\", gas: 2000000})` + let text = `await LiquidPledging.methods.addProject(\"${params.name}\", \"${params.url}\", \"${params.account}\", ${params.parentProject}, ${params.commitTime}, \"${params.plugin}\").send({gas: 2000000})` return doAction(text, async () => { const toSend = this.contracts.LiquidPledging.methods.addProject(params.name, params.url, params.account, params.parentProject, params.commitTime, params.plugin); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); const projectId = receipt.events.ProjectAdded.returnValues.idProject; console.log("Project ID: " , projectId); @@ -100,8 +114,8 @@ class Actions { async withdraw(params) { let text = `await LiquidPledging.methods.withdraw(\"${params.id}\", web3.utils.toWei(\"${params.amount}\", "ether")).send({gas: 2000000})`; return doAction(text, async () => { - const toSend = this.contracts.LiquidPledging.methods.withdraw(params.id.toString(), web3.utils.toWei(params.amount.toString(), "ether")); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const toSend = this.contracts.LiquidPledging.methods.withdraw(params.id.toString(), this.web3.utils.toWei(params.amount.toString(), "ether")); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); const paymentId = receipt.events.AuthorizePayment.returnValues.idPayment; console.log("Payment ID: " , paymentId); @@ -112,7 +126,7 @@ class Actions { return new Promise(async (resolve, reject) => { try { const pledges = await PledgeUtils.getPledges(this.contracts.LiquidPledging); - PledgeUtils.printTable(pledges, web3); + PledgeUtils.printTable(pledges, this.web3); } catch(error){ console.log(error); console.log("Couldn't obtain the list of pledges: ", error.message); @@ -135,10 +149,10 @@ class Actions { } async addGiver(params) { - let text = `await LiquidPledging.methods.addGiver(\"${params.name}\", \"${params.url}\", ${params.commitTime}, \"${params.plugin}\").send({from: \"${web3.eth.defaultAccount}\", gas: 2000000})` + let text = `await LiquidPledging.methods.addGiver(\"${params.name}\", \"${params.url}\", ${params.commitTime}, \"${params.plugin}\").send({gas: 2000000})` return doAction(text, async () => { const toSend = this.contracts.LiquidPledging.methods.addGiver(params.name, params.url, params.commitTime, params.plugin); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); const funderId = receipt.events.GiverAdded.returnValues.idGiver; console.log("Funder ID: " , funderId); @@ -148,8 +162,8 @@ class Actions { async mintToken(params) { let text = `await StandardToken.methods.mint(\"${params.account}\", web3.utils.toWei(\"${params.amount}\", \"ether\")).send({gas: 2000000})` return doAction(text, async () => { - const toSend = this.contracts.StandardToken.methods.mint(params.account, web3.utils.toWei(params.amount.toString(), "ether")); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const toSend = this.contracts.StandardToken.methods.mint(params.account, this.web3.utils.toWei(params.amount.toString(), "ether")); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); }); } @@ -157,8 +171,8 @@ class Actions { async approveToken(params) { let text = `await StandardToken.methods.approve(\"${this.contracts.LiquidPledging.options.address}\", web3.utils.toWei(\"${params.amount}\", \"ether\")).send({gas: 2000000})` return doAction(text, async () => { - const toSend = this.contracts.StandardToken.methods.approve(this.contracts.LiquidPledging.options.address, web3.utils.toWei(params.amount.toString(), "ether")); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const toSend = this.contracts.StandardToken.methods.approve(this.contracts.LiquidPledging.options.address, this.web3.utils.toWei(params.amount.toString(), "ether")); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); }); } @@ -166,8 +180,8 @@ class Actions { async donate(params) { let text = `await LiquidPledging.methods.donate(${params.funderId}, ${params.projectId}, \"${params.tokenAddress}\", web3.utils.toWei(\"${params.amount}\", \"ether\")).send({gas: 2000000});` return doAction(text, async () => { - const toSend = this.contracts.LiquidPledging.methods.donate(params.funderId, params.projectId, params.tokenAddress, web3.utils.toWei(params.amount.toString(), "ether")); - const receipt = await TrxUtils.executeAndWait(toSend, web3.eth.defaultAccount); + const toSend = this.contracts.LiquidPledging.methods.donate(params.funderId, params.projectId, params.tokenAddress, this.web3.utils.toWei(params.amount.toString(), "ether")); + const receipt = await TrxUtils.executeAndWait(toSend, this.web3.eth.defaultAccount); console.dir("txHash: " + receipt.transactionHash); }); } diff --git a/src/cmd.js b/src/cmd.js index c017937..d0f1878 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -11,18 +11,18 @@ function mainMenu(actions) { if (subAction === 'List Projects') { await actions.listProjects(); - } - + } + if (subAction === 'Create Project') { - let params = (await menus.createProject(actions.web3().eth.defaultAccount)) + let params = (await menus.createProject(actions.web3Object().eth.defaultAccount)) await actions.addProject(params); - } - + } + if (subAction === 'View Project') { let params = (await menus.viewProject()) await actions.viewProject(params); - } - + } + if (subAction === 'Fund a Project') { let params = (await menus.donate()) await actions.donate(params); @@ -37,7 +37,7 @@ function mainMenu(actions) { if (subAction === 'Withdraw') { let params = (await menus.withdraw()) await actions.withdraw(params); - } + } } else if (action === 'Funders') { subAction = (await menus.funders()).action diff --git a/src/index.js b/src/index.js index add2fb8..e429409 100644 --- a/src/index.js +++ b/src/index.js @@ -5,12 +5,20 @@ const Actions = require('./actions.js'); program .version('0.1.0') .option('-u, --url [url]', "host to connect to (default: ws://localhost:8556)") + .option('-a, --accounts [accounts]', "accounts file, if not defined uses accounts in the connecting node") .option('-c, --chain [chain]', "environment to run, can be mainnet, ropsten, development (default: development)") .parse(process.argv); -const actions = new Actions(program.chain || "development"); +let accounts = []; +if (program.accounts) { + accounts = require(process.cwd() + "/" + program.accounts).accounts; +} -actions.connect(program.url || "ws://localhost:8556", async () => { +const actions = new Actions(program.chain || "development", accounts || []); + +actions.connect({ + url: (program.url || "ws://localhost:8556") +}, async () => { cmd(actions) }); diff --git a/src/provider.js b/src/provider.js new file mode 100644 index 0000000..b947645 --- /dev/null +++ b/src/provider.js @@ -0,0 +1,134 @@ +const async = require('async'); +const Web3 = require('web3'); +const bip39 = require("bip39"); +const hdkey = require('ethereumjs-wallet/hdkey'); +const ethUtil = require('ethereumjs-util'); +const Transaction = require('ethereumjs-tx'); + +class Provider { + + constructor() { + this.accounts = [] + this.addresses = [] + this.nonceCache = {}; + this.web3 = new Web3(); + } + + initAccounts(accounts) { + for (let accountConfig of accounts) { + if (accountConfig.mnemonic) { + const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(accountConfig.mnemonic.trim())); + + const addressIndex = accountConfig.addressIndex || 0; + const numAddresses = accountConfig.numAddresses || 1; + const wallet_hdpath = accountConfig.hdpath || "m/44'/60'/0'/0/"; + + for (let i = addressIndex; i < addressIndex + numAddresses; i++) { + const wallet = hdwallet.derivePath(wallet_hdpath + i).getWallet(); + //if (returnAddress) { + // this.accounts.push(wallet.getAddressString()); + //} else { + this.accounts.push(this.web3.eth.accounts.privateKeyToAccount('0x' + wallet.getPrivateKey().toString('hex'))); + //} + } + } + } + } + + startWeb3Provider(type, web3Endpoint) { + const self = this; + + if (type === 'rpc') { + self.provider = new this.web3.providers.HttpProvider(web3Endpoint); + } else if (type === 'ws') { + // Note: don't pass to the provider things like {headers: {Origin: "embark"}}. Origin header is for browser to fill + // to protect user, it has no meaning if it is used server-side. See here for more details: https://github.com/ethereum/go-ethereum/issues/16608 + // Moreover, Parity reject origins that are not urls so if you try to connect with Origin: "embark" it gives the following error: + // << Blocked connection to WebSockets server from untrusted origin: Some("embark") >> + // The best choice is to use void origin, BUT Geth rejects void origin, so to keep both clients happy we can use http://embark + self.provider = new this.web3.providers.WebsocketProvider(web3Endpoint, { + headers: {Origin: "http://embark"}, + // TODO remove this when Geth fixes this: https://github.com/ethereum/go-ethereum/issues/16846 + clientConfig: { + fragmentationThreshold: 81920 + } + }); + self.provider.on('error', () => console.error('Websocket Error')); + self.provider.on('end', () => console.error('Websocket connection ended')); + } else { + } + + self.web3.setProvider(self.provider); + + self.accounts.forEach(account => { + self.addresses.push(account.address || account); + if (account.privateKey) { + self.web3.eth.accounts.wallet.add(account); + } + }); + self.addresses = [...new Set(self.addresses)]; // Remove duplicates + + if (self.accounts.length) { + self.web3.eth.defaultAccount = self.addresses[0]; + } + + const realSend = self.provider.send.bind(self.provider); + + self.runTransaction = async.queue(({payload}, callback) => { + const rawTx = payload.params[0]; + const rawData = Buffer.from(ethUtil.stripHexPrefix(rawTx), 'hex'); + const tx = new Transaction(rawData, 'hex'); + const address = '0x' + tx.getSenderAddress().toString('hex').toLowerCase(); + + self.getNonce(address, (newNonce) => { + tx.nonce = newNonce; + const key = ethUtil.stripHexPrefix(self.web3.eth.accounts.wallet[address].privateKey); + const privKey = Buffer.from(key, 'hex'); + tx.sign(privKey); + payload.params[0] = '0x' + tx.serialize().toString('hex'); + return realSend(payload, (error, result) => { + self.web3.eth.getTransaction(result.result, () => { + callback(error, result); + }); + }); + }); + }, 1); + + self.provider.send = function(payload, cb) { + if (payload.method === 'eth_accounts') { + return realSend(payload, function(err, result) { + if (err) { + return cb(err); + } + if (self.accounts.length) { + result.result = self.addresses; + } + cb(null, result); + }); + } else if (payload.method === "eth_sendRawTransaction") { + return self.runTransaction.push({payload}, cb); + } + + realSend(payload, cb); + }; + } + + getNonce(address, callback) { + this.web3.eth.getTransactionCount(address, (_error, transactionCount) => { + if(this.nonceCache[address] === undefined) { + this.nonceCache[address] = -1; + } + + if (transactionCount > this.nonceCache[address]) { + this.nonceCache[address] = transactionCount; + return callback(this.nonceCache[address]); + } + + this.nonceCache[address]++; + callback(this.nonceCache[address]); + }); + } + +} + +module.exports = Provider; diff --git a/src/trx-utils.js b/src/trx-utils.js index 67eb640..c439224 100644 --- a/src/trx-utils.js +++ b/src/trx-utils.js @@ -1,20 +1,23 @@ const Spinner = require('cli-spinner').Spinner; const executeAndWait = async (toSend, account) => { - const spinner = new Spinner('%s'); - spinner.setSpinnerString(18); - spinner.start(); + return new Promise(async (resolve, reject) => { + const spinner = new Spinner('%s'); + spinner.setSpinnerString(18); + spinner.start(); - try { - const estimatedGas = await toSend.estimateGas({from: account}); - const receipt = await toSend.send({from: account, gas: estimatedGas + 10000}); + try { + const estimatedGas = await toSend.estimateGas({from: account}); + const receipt = await toSend.send({from: account, gas: estimatedGas + 10000}); - spinner.stop(true); - return receipt; - } catch(error) { - console.log("Error minting tokens: ", error.message); - spinner.stop(true); - } + spinner.stop(true); + return resolve(receipt); + } catch(error) { + console.log("Error: ", error.message); + spinner.stop(true); + } + resolve(); + }); } module.exports = { diff --git a/yarn.lock b/yarn.lock index 3199eea..9e12246 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1351,6 +1351,11 @@ async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0, async@^2.6.0, async@^2.6 dependencies: lodash "^4.17.11" +async@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.0.0.tgz#4c959b37d8c477dc189f2efb9340847f7ad7f785" + integrity sha512-LNZ6JSpKraIia6VZKKbKxmX6nWIdfsG7WqrOvKpCuDjH7BnGyQRFMTSXEe8to2WF/rqoAKgZvj+L5nnxe0suAg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"