Add init, deposit, send console commands to interact with the Plasma chain

This commit is contained in:
emizzle 2019-06-03 16:07:00 +10:00
parent 8036986f65
commit 1b34d68f70
No known key found for this signature in database
GPG Key ID: 1FD4BAB3C37EE9BA
6 changed files with 310 additions and 125 deletions

View File

@ -9,12 +9,12 @@
], ],
"scripts": { "scripts": {
"lint": "./node_modules/.bin/eslint src/", "lint": "./node_modules/.bin/eslint src/",
"babel": "npm run babel:node", "babel": "cross-env BABEL_ENV=node babel --out-dir dist src --source-maps",
"babel:node": "cross-env BABEL_ENV=node babel --out-dir dist src --source-maps", "build": "npm run babel",
"build": "npm run clean && npm run babel",
"clean": "rimraf dist embark-status-*.tgz package", "clean": "rimraf dist embark-status-*.tgz package",
"prepare": "npm run build", "prepare": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"watch": "npm run clean && npm run babel -- --verbose --watch"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -37,6 +37,7 @@
"@omisego/omg-js-childchain": "1.2.1", "@omisego/omg-js-childchain": "1.2.1",
"@omisego/omg-js-rootchain": "1.2.2", "@omisego/omg-js-rootchain": "1.2.2",
"@omisego/omg-js-util": "1.2.1", "@omisego/omg-js-util": "1.2.1",
"async": "3.0.1",
"embark-utils": "^4.1.0-beta.2", "embark-utils": "^4.1.0-beta.2",
"ethers": "4.0.28" "ethers": "4.0.28"
}, },

View File

@ -1,8 +1,11 @@
import { embarkPath, dappPath } from "embark-utils";
import { BigNumber } from "ethers/utils"; import { BigNumber } from "ethers/utils";
import ChildChain from "@omisego/omg-js-childchain"; import ChildChain from "@omisego/omg-js-childchain";
import EmbarkUtils from "./utils/embark";
import RootChain from "@omisego/omg-js-rootchain"; import RootChain from "@omisego/omg-js-rootchain";
import { selectUtxos } from "./utils/plasma";
import { transaction } from "@omisego/omg-js-util"; import { transaction } from "@omisego/omg-js-util";
import { waterfall } from "async";
// const WEB3_PROVIDER_URL = "https://rinkeby.infura.io/"; // const WEB3_PROVIDER_URL = "https://rinkeby.infura.io/";
// const WATCHER_URL = "https://watcher.ari.omg.network/"; // const WATCHER_URL = "https://watcher.ari.omg.network/";
@ -19,8 +22,11 @@ let globalKeystore;
let rootChain; let rootChain;
let childChain; let childChain;
const ADDRESS = "0x1e8df8b7d4212084bf5329fddc730b9e5aaba238"; // const ADDRESS = "0x1e8df8b7d4212084bf5329fddc730b9e5aaba238";
const ADDRESS_PK = "0x0f7aa58edd2758334a819516b3421953b6c453c3e8ed85b071ce1edf3aedfab8"; // const ADDRESS_PK = "0x0f7aa58edd2758334a819516b3421953b6c453c3e8ed85b071ce1edf3aedfab8";
const ACCOUNT_CONFIG_ERROR = "Blockchain accounts configuration is missing. To use the Embark-OMG plugin, you must configure blockchain accounts to use either a private key file, a private key, or a mnemonic.";
const ACCOUNT_BALANCE_ERROR = "The configured account does not have enough funds. Please make sure this account has Rinkeby ETH.";
/** /**
* Plugin that connects an Embark dApp to the Status app, and allows the dApp * Plugin that connects an Embark dApp to the Status app, and allows the dApp
@ -31,9 +37,14 @@ class EmbarkOmg {
this.embark = embark; this.embark = embark;
this.events = this.embark.events; this.events = this.embark.events;
this.pluginConfig = this.embark.pluginConfig; this.pluginConfig = this.embark.pluginConfig;
this.deviceIp = this.pluginConfig.deviceIp;
this.logger = this.embark.logger; this.logger = this.embark.logger;
this.fs = embark.fs; this.fs = embark.fs;
this.initing = false;
this.inited = false;
this.address = "";
this.addressPrivateKey = "";
this.maxDeposit = 0;
// plugin opts // plugin opts
this.plasmaContractAddress = this.pluginConfig.PLASMA_CONTRACT_ADDRESS; this.plasmaContractAddress = this.pluginConfig.PLASMA_CONTRACT_ADDRESS;
@ -41,128 +52,231 @@ class EmbarkOmg {
this.watcherUrl = this.pluginConfig.WATCHER_URL; this.watcherUrl = this.pluginConfig.WATCHER_URL;
this.childChainUrl = this.pluginConfig.CHILDCHAIN_URL; this.childChainUrl = this.pluginConfig.CHILDCHAIN_URL;
this.registerServiceCheck();
this.registerConsoleCommands();
this.webServerConfig = {};
this.blockchainConfig = {};
// gets hydrated webserver config from embark
this.events.on('config:load:webserver', webServerConfig => {
this.webServerConfig = webServerConfig;
});
// gets hydrated blockchain config from embark // gets hydrated blockchain config from embark
this.events.on('config:load:blockchain', blockchainConfig => { this.events.once('config:load:blockchain', (blockchainConfig) => {
this.blockchainConfig = blockchainConfig; this.logger.info("blockchain config loaded...");
this.embarkUtils = new EmbarkUtils({ events: this.events, logger: this.logger, blockchainConfig });
this.init();
}); });
// register service check
//this._registerServiceCheck();
this.init();
} }
async txChildChain() { async init() {
const val = "555"; try {
const toAddress = "0x38d5beb778b6e62d82e3ba4633e08987e6d0f990"; if (this.initing) {
const utxos = await this.childChain.getUtxos(ADDRESS); const message = "Already intializing the Plasma chain, please wait...";
const utxosToSpend = this.selectUtxos(utxos, val, transaction.ETH_CURRENCY); this.logger.error(message);
throw new Error(message);
}
this.initing = true;
// init account used for root and child chains
const accounts = await this.embarkUtils.accounts;
if (!(accounts && accounts.length)) {
this.logger.error(ACCOUNT_CONFIG_ERROR);
throw new Error(ACCOUNT_CONFIG_ERROR);
}
const { address, privateKey } = accounts[0];
this.address = address;
this.addressPrivateKey = privateKey;
// init Web3
const Web3 = await this.embarkUtils.web3;
this.web3 = new Web3();
const web3Provider = new Web3.providers.HttpProvider(this.web3ProviderUrl);
this.web3.setProvider(web3Provider);
// check account balance on the main chain
try {
this.maxDeposit = await this.web3.eth.getBalance(this.address);
if (!this.maxDeposit || new BigNumber(this.maxDeposit).lte(0)) {
this.logger.error(ACCOUNT_BALANCE_ERROR);
throw new Error(ACCOUNT_BALANCE_ERROR);
}
this.maxDeposit = new BigNumber(this.maxDeposit);
}
catch (e) {
this.logger.error(`Error getting balance for account ${this.address}: ${e}`);
}
// set up the Plasma chain
this.rootChain = new RootChain(this.web3, this.plasmaContractAddress);
this.childChain = new ChildChain(this.watcherUrl, this.childChainUrl);
// set lifecycle state vars
this.initing = false;
this.inited = true;
this.events.emit("embark-omg:init");
// await this.deposit();
// await this.txChildChain();
}
catch (e) {
const message = `Error initializing Plasma chain: ${e}`;
this.logger.error(message);
throw new Error(message);
}
}
async deposit(amount) {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
this.logger.error(message);
throw new Error(message);
}
amount = new BigNumber(amount);
if (!amount || amount.lte(0)) {
const message = "You must deposit more than 0 wei.";
this.logger.error(message);
throw new Error(message);
}
if (amount.gt(this.maxDeposit)) {
// recheck balance in case it was updated in a recent tx
this.maxDeposit = await this.web3.eth.getBalance(this.address);
if (amount.gt(this.maxDeposit)) {
const message = `You do not have enough funds for this deposit. Please deposit more funds in to ${this.address} and then try again.`;
this.logger.error(message);
throw new Error(message);
}
}
// const DEPOSIT_AMT = "100000";
this.logger.info(`Depositing ${amount} wei...`);
const depositTx = transaction.encodeDeposit(this.address, amount, transaction.ETH_CURRENCY);
try {
const receipt = await this.rootChain.depositEth(depositTx, amount, { from: this.address, privateKey: this.addressPrivateKey });
this.logger.trace(receipt);
const message = `Successfully deposited ${amount} wei in to the Plasma chain.\nView the transaction: https://rinkeby.etherscan.io/tx/${receipt.transactionHash}.`;
this.logger.info(message);
return message;
}
catch (e) {
const message = `Error depositing ${amount} wei: ${e}`;
this.logger.error(message);
throw new Error(message);
}
}
async txChildChain(toAddress, val) {
//const val = "555";
// const toAddress = "0x38d5beb778b6e62d82e3ba4633e08987e6d0f990";
const utxos = await this.childChain.getUtxos(this.address);
const utxosToSpend = selectUtxos(utxos, val, transaction.ETH_CURRENCY);
if (!utxosToSpend) { if (!utxosToSpend) {
return console.error(`No utxo big enough to cover the amount ${val}`); return this.logger.error(`No utxo big enough to cover the amount ${val}`);
}
val = new BigNumber(val);
if (!val || val.lte(0)) {
return this.logger.error("Transaction value must be more than 0 wei.");
} }
const txBody = { const txBody = {
inputs: utxosToSpend, inputs: utxosToSpend,
outputs: [{ outputs: [
owner: toAddress, {
currency: transaction.ETH_CURRENCY, owner: toAddress,
amount: Number(val) currency: transaction.ETH_CURRENCY,
}] amount: val
}
]
}; };
if (utxosToSpend[0].amount > val) { const utxoAmnt = new BigNumber(utxosToSpend[0].amount);
if (utxoAmnt.gt(val)) {
// specify the change amount back to yourself // specify the change amount back to yourself
const CHANGE_AMOUNT = utxosToSpend[0].amount - val; const changeAmnt = utxoAmnt.sub(val);
txBody.outputs.push({ txBody.outputs.push({
owner: ADDRESS, owner: this.address,
currency: transaction.ETH_CURRENCY, currency: transaction.ETH_CURRENCY,
amount: CHANGE_AMOUNT amount: changeAmnt
}); });
} }
try { try {
const unsignedTx = await this.childChain.createTransaction(txBody); const unsignedTx = await this.childChain.createTransaction(txBody);
const signatures = await this.childChain.signTransaction(unsignedTx, [ADDRESS_PK]); const signatures = await this.childChain.signTransaction(unsignedTx, [this.addressPrivateKey]);
const signedTx = await this.childChain.buildSignedTransaction(unsignedTx, signatures); const signedTx = await this.childChain.buildSignedTransaction(unsignedTx, signatures);
const result = await this.childChain.submitTransaction(signedTx); const result = await this.childChain.submitTransaction(signedTx);
console.log(`Submitted tx: ${JSON.stringify(result)}`); const message = `Successfully submitted tx on the child chain: ${JSON.stringify(result)}\nView the transaction: http://quest.ari.omg.network/transaction/${result.txhash}`;
this.logger.info(message);
return message;
} }
catch (e) { catch (e) {
console.error(e); this.logger.error(e);
throw e;
} }
} }
async deposit() { registerConsoleCommands() {
const DEPOSIT_AMT = "100000"; this.embark.registerConsoleCommand({
description: `Initialises the Plasma chain using the account configured in the DApp's blockchain configuration. All transactions on the child chain will use this as the 'from' account.`,
const depositTx = transaction.encodeDeposit(ADDRESS, DEPOSIT_AMT, transaction.ETH_CURRENCY); matches: ["plasma init", "plasma init --force"],
try { usage: "plasma init [--force]",
const receipt = await this.rootChain.depositEth(depositTx, DEPOSIT_AMT, { from: ADDRESS, privateKey: ADDRESS_PK }); process: (cmd, callback) => {
console.log(receipt); const force = cmd.endsWith("--force");
} if (this.inited && !force) {
catch (e) { return callback("The Plasma chain is already initialized. If you'd like to reinitialize the chain, use the --force option ('plasma init --force')."); // passes a message back to cockpit console
console.log(e);
}
}
async init() {
let web3Location = await this.getWeb3Location();
web3Location = web3Location.replace(/\\/g, '/');
const Web3 = require(web3Location);
this.web3 = new Web3;
const web3Provider = new Web3.providers.HttpProvider(this.web3ProviderUrl);
this.web3.setProvider(web3Provider);
this.rootChain = new RootChain(this.web3, this.plasmaContractAddress);
this.childChain = new ChildChain(this.watcherUrl, this.childChainUrl);
await this.deposit();
await this.txChildChain();
}
selectUtxos(utxos, amount, currency) {
const correctCurrency = utxos.filter(utxo => utxo.currency === currency)
// Just find the first utxo that can fulfill the amount
const selected = correctCurrency.find(utxo => new BigNumber(utxo.amount).gte(new BigNumber(amount)));
if (selected) {
return [selected];
}
}
getWeb3Location() {
return new Promise((resolve, reject) => {
this.events.request("version:get:web3", (web3Version) => {
if (web3Version === "1.0.0-beta") {
const nodePath = embarkPath('node_modules');
const web3Path = require.resolve("web3", { paths: [nodePath] });
return resolve(web3Path);
} }
this.events.request("version:getPackageLocation", "web3", web3Version, (err, location) => { this.init()
if (err) { .then((message) => {
return reject(err); callback(null, message);
} })
const locationPath = embarkPath(location); .catch(callback);
resolve(locationPath); }
}); });
});
const depositRegex = /^plasma[\s]+deposit[\s]+([0-9]+)$/;
this.embark.registerConsoleCommand({
description: "Deposits ETH from the root chain (Rinkeby) to the Plasma chain to be used for transacting on the Plasma chain.",
matches: (cmd) => {
return depositRegex.test(cmd);
},
usage: "plasma deposit [amount]",
process: (cmd, callback) => {
if (!this.inited) {
return callback("The Plasma chain has not been initialized. Please initialize the Plamsa chain using 'plasma init' before continuting."); // passes a message back to cockpit console
}
const matches = cmd.match(depositRegex) || [];
if (matches.length <= 1) {
return callback("Invalid command format, please use the format 'plasma deposit [amount]', ie 'plasma deposit 100000'");
}
this.deposit(matches[1])
.then((message) => {
callback(null, message);
})
.catch(callback);
}
});
const sendRegex = /^plasma[\s]+send[\s]+(0x[0-9,a-f,A-F]{40,40})[\s]+([0-9]+)$/;
this.embark.registerConsoleCommand({
description: "Sends an ETH tx on the Plasma chain from the account configured in the DApp's blockchain configuration to any other account on the Plasma chain.",
matches: (cmd) => {
return sendRegex.test(cmd);
},
usage: "plasma send [to_address] [amount]",
process: (cmd, callback) => {
if (!this.inited) {
return callback("The Plasma chain has not been initialized. Please initialize the Plamsa chain using 'plasma init' before continuting."); // passes a message back to cockpit console
}
const matches = cmd.match(sendRegex) || [];
if (matches.length <= 2) {
return callback("Invalid command format, please use the format 'plasma send [to_address] [amount]', ie 'plasma send 0x38d5beb778b6e62d82e3ba4633e08987e6d0f990 555'");
}
this.txChildChain(matches[1], matches[2])
.then((message) => {
callback(null, message);
})
.catch(callback);
}
}); });
} }
@ -172,29 +286,52 @@ class EmbarkOmg {
* *
* @returns {void} * @returns {void}
*/ */
_registerServiceCheck() { registerServiceCheck() {
// const serviceCheckQueue = queue((task, callback) => { const NO_NODE = "noNode";
// this.statusApi.ping((err, isOnline) => { const name = "OMG Plasma Chain";
// if (!err && isOnline) this.events.emit('embark-status:connect');
// const stateName = (isOnline ? SERVICE_CHECK_ON : SERVICE_CHECK_OFF); this.events.request("services:register", name, (cb) => {
// task.cb({ name: `Status.im (${this.deviceIp})`, status: stateName });
// }); waterfall([
// callback(); (next) => {
// }, 1); if (this.inited) {
this.embark.registerServiceCheck('OmiseGO', (cb) => { return next();
//serviceCheckQueue.push({ cb }); }
cb({ name: `OmiseGO network (${this.deviceIp})`, status: SERVICE_CHECK_ON }); this.events.once("embark-omg:init", next);
}); },
(next) => {
// TODO: web3_clientVersion method is currently not implemented in web3.js 1.0
this.web3._requestManager.send({ method: 'web3_clientVersion', params: [] }, (err, version) => {
if (err || !version) {
return next(null, { name: "Plasma chain not found", status: SERVICE_CHECK_OFF });
}
if (version.indexOf("/") < 0) {
return next(null, { name: version, status: SERVICE_CHECK_ON });
}
let nodeName = version.split("/")[0];
let versionNumber = version.split("/")[1].split("-")[0];
let name = nodeName + " " + versionNumber + " (Plasma)";
return next(null, { name: name, status: SERVICE_CHECK_ON });
});
}
], (err, statusObj) => {
if (err && err !== NO_NODE) {
return cb(err);
}
cb(statusObj);
});
}, 5000, 'off');
this.embark.events.on('check:backOnline:OmiseGO', () => { this.embark.events.on('check:backOnline:OmiseGO', () => {
this.logger.info("------------------"); this.logger.info("------------------");
this.logger.info("Connected to the OmiseGO network!"); this.logger.info("Connected to the Plama chain!");
this.logger.info("------------------"); this.logger.info("------------------");
}); });
this.embark.events.on('check:wentOffline:OmiseGO', () => { this.embark.events.on('check:wentOffline:OmiseGO', () => {
this.logger.error("------------------"); this.logger.error("------------------");
this.logger.error("Couldn't connect or lost connection to the OmiseGO network..."); this.logger.error("Couldn't connect or lost connection to the Plasma chain...");
this.logger.error("------------------"); this.logger.error("------------------");
}); });
} }

42
src/utils/embark.js Normal file
View File

@ -0,0 +1,42 @@
import { AccountParser, dappPath, embarkPath } from "embark-utils";
export default class EmbarkUtils {
constructor({ events, logger, blockchainConfig }) {
this.events = events;
this.logger = logger;
this.blockchainConfig = blockchainConfig;
}
get accounts() {
return new Promise((resolve, reject) => {
this.events.request("blockchain:ready", () => {
this.events.request("blockchain:get", (embarkWeb3) => {
try {
const accountsParsed = AccountParser.parseAccountsConfig(this.blockchainConfig.accounts, embarkWeb3, dappPath(), this.logger, []);
resolve(accountsParsed);
} catch (e) {
reject(e);
}
});
});
});
}
get web3() {
return new Promise((resolve, reject) => {
this.events.request("version:get:web3", (web3Version) => {
if (web3Version === "1.0.0-beta") {
const nodePath = embarkPath('node_modules');
const web3Path = require.resolve("web3", { paths: [nodePath] });
return resolve(require(web3Path));
}
this.events.request("version:getPackageLocation", "web3", web3Version, (err, location) => {
if (err) {
return reject(err);
}
const locationPath = embarkPath(location).replace(/\\/g, '/');
resolve(require(locationPath));
});
});
});
}
}

10
src/utils/plasma.js Normal file
View File

@ -0,0 +1,10 @@
import { BigNumber } from "ethers/utils";
export function selectUtxos(utxos, amount, currency) {
const correctCurrency = utxos.filter(utxo => utxo.currency === currency);
// Just find the first utxo that can fulfill the amount
const selected = correctCurrency.find(utxo => new BigNumber(utxo.amount).gte(new BigNumber(amount)));
if (selected) {
return [selected];
}
}

View File

@ -1,10 +0,0 @@
// import { BN } from "we";
// function selectUtxos (utxos, amount, currency) {
// const correctCurrency = utxos.filter(utxo => utxo.currency === currency)
// // Just find the first utxo that can fulfill the amount
// const selected = correctCurrency.find(utxo => new BigNumber(utxo.amount).gte(new BigNumber(amount)))
// if (selected) {
// return [selected]
// }
// }

View File

@ -629,7 +629,7 @@
lodash "^4.17.11" lodash "^4.17.11"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@omisego/omg-js-childchain@1.2.2", "@omisego/omg-js-childchain@^1.2.1": "@omisego/omg-js-childchain@1.2.1", "@omisego/omg-js-childchain@^1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/@omisego/omg-js-childchain/-/omg-js-childchain-1.2.1.tgz#7fd83af2e47ae7936fd9743c5a98244c89f64222" resolved "https://registry.yarnpkg.com/@omisego/omg-js-childchain/-/omg-js-childchain-1.2.1.tgz#7fd83af2e47ae7936fd9743c5a98244c89f64222"
integrity sha512-UuYc0w8xTZPceDUnuWXV46MhPMUmu9+ecTbEh0sd5YSx9N9ie+t2FRikvvTH9g4+GBmQRoI0XCkFBU/pW3SJkA== integrity sha512-UuYc0w8xTZPceDUnuWXV46MhPMUmu9+ecTbEh0sd5YSx9N9ie+t2FRikvvTH9g4+GBmQRoI0XCkFBU/pW3SJkA==
@ -648,7 +648,7 @@
"@omisego/omg-js-util" "^1.2.1" "@omisego/omg-js-util" "^1.2.1"
debug "^4.0.1" debug "^4.0.1"
"@omisego/omg-js-util@1.2.2", "@omisego/omg-js-util@^1.2.1": "@omisego/omg-js-util@1.2.1", "@omisego/omg-js-util@^1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/@omisego/omg-js-util/-/omg-js-util-1.2.1.tgz#7495f6835681a3096cc54dabb8c0495bc37d22ec" resolved "https://registry.yarnpkg.com/@omisego/omg-js-util/-/omg-js-util-1.2.1.tgz#7495f6835681a3096cc54dabb8c0495bc37d22ec"
integrity sha512-14YTB/hycMSgGnZ/SLKiu0KA9qqwc7mTvyGfO6VE2DonX6uDrYRqmbWX0wDnk3A1hWoVLPiqG7CLBNbkOnhsKg== integrity sha512-14YTB/hycMSgGnZ/SLKiu0KA9qqwc7mTvyGfO6VE2DonX6uDrYRqmbWX0wDnk3A1hWoVLPiqG7CLBNbkOnhsKg==
@ -872,6 +872,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
async@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/async/-/async-3.0.1.tgz#dfeb34657d1e63c94c0eee424297bf8a2c9a8182"
integrity sha512-ZswD8vwPtmBZzbn9xyi8XBQWXH3AvOQ43Za1KWYq7JeycrZuUYzx01KvHcVbXltjqH4y0MWrQ33008uLTqXuDw==
async@~1.5.2: async@~1.5.2:
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"