Merge branch 'feat/remove-pk-add-metamask'

This commit is contained in:
emizzle 2019-06-17 12:38:32 +10:00
commit b5187f4a8e
No known key found for this signature in database
GPG Key ID: 1FD4BAB3C37EE9BA
4 changed files with 988 additions and 205 deletions

View File

@ -1,7 +1,7 @@
{
"name": "embarkjs-omg",
"name": "embarkjs-plasma",
"version": "1.0.0",
"description": "EmbarkJS library for the OmiseGO plugin for Embark",
"description": "EmbarkJS library for the OmiseGO Plasma plugin for Embark",
"main": "dist/index.js",
"browser": {
"./dist/index.js": "./dist/browser/index.js",
@ -13,6 +13,7 @@
"> 0.2%"
],
"files": [
"src",
"dist"
],
"scripts": {
@ -49,13 +50,12 @@
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "7.2.0",
"@babel/runtime-corejs2": "7.3.1",
"@omisego/omg-js": "1.2.2",
"@omisego/omg-js-childchain": "1.2.1",
"@omisego/omg-js-rootchain": "1.2.2",
"@omisego/omg-js-util": "1.2.1",
"async": "3.0.1",
"axios": "0.19.0",
"ethers": "4.0.28"
"@omisego/omg-js": "2.0.0-v0.2",
"@omisego/omg-js-childchain": "2.0.0-v0.2",
"@omisego/omg-js-rootchain": "2.0.0-v0.2",
"@omisego/omg-js-util": "2.0.0-v0.2",
"human-standard-token-abi": "2.0.0",
"web3": "1.0.0-beta.55"
},
"devDependencies": {
"@babel/cli": "7.2.3",

View File

@ -1,129 +1,157 @@
/* global Web3 */
import {
confirmTransaction,
normalizeUrl,
selectUtxos,
signTypedData
} from "./utils";
import BigNumber from "bn.js";
import ChildChain from "@omisego/omg-js-childchain";
import RootChain from "@omisego/omg-js-rootchain";
import Web3 from "web3";
import {transaction} from "@omisego/omg-js-util";
const ERC20_ABI = require("human-standard-token-abi");
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.";
const web3Options = {transactionConfirmationBlocks: 1};
export default class BaseEmbarkOmg {
export default class EmbarkJSPlasma {
constructor({pluginConfig, logger}) {
this.logger = logger;
this.initing = false;
this.inited = false;
this.address = "";
this.addressPrivateKey = "";
this.currentAddress = "";
this.maxDeposit = 0;
this.state = {
account: {
address: "",
rootBalance: 0,
childBalance: 0
},
transactions: [],
utxos: []
};
this.rootChain = null;
this.childChain = null;
// plugin opts
this.plasmaContractAddress = pluginConfig.PLASMA_CONTRACT_ADDRESS;
this.web3ProviderUrl = pluginConfig.WEB3_PROVIDER_URL;
this.watcherUrl = pluginConfig.WATCHER_URL;
this.childChainUrl = pluginConfig.CHILDCHAIN_URL;
this.config = {
plasmaContractAddress: pluginConfig.PLASMA_CONTRACT_ADDRESS || "0x740ecec4c0ee99c285945de8b44e9f5bfb71eea7",
watcherUrl: normalizeUrl(pluginConfig.WATCHER_URL || "https://watcher.samrong.omg.network/"),
childChainUrl: normalizeUrl(pluginConfig.CHILDCHAIN_URL || "https://samrong.omg.network/"),
childChainExplorerUrl: normalizeUrl(pluginConfig.CHILDCHAIN_EXPLORER_URL || "https://quest.samrong.omg.network")
};
}
async init(accounts, web3Path) {
async init(web3, useGivenWeb3 = false) {
try {
if (this.initing) {
const message = "Already intializing the Plasma chain, please wait...";
this.logger.error(message);
throw new Error(message);
}
this.initing = true;
if (!(accounts && accounts.length)) {
this.logger.error(ACCOUNT_CONFIG_ERROR);
throw new Error(ACCOUNT_CONFIG_ERROR);
if (useGivenWeb3) {
this.web3 = web3;
}
const { address, privateKey } = accounts[0];
this.address = address;
this.addressPrivateKey = privateKey;
// init Web3
const web3Lib = web3Path ? require(web3Path) : Web3;
this.web3 = new web3Lib();
const web3Provider = new web3Lib.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}`);
else {
this.web3 = new Web3(web3.currentProvider, null, web3Options);
}
// set up the Plasma chain
this.rootChain = new RootChain(this.web3, this.plasmaContractAddress);
this.childChain = new ChildChain(this.watcherUrl, this.childChainUrl);
this.rootChain = new RootChain(this.web3, this.config.plasmaContractAddress);
this.childChain = new ChildChain(this.config.watcherUrl); //, this.config.childChainUrl);
let accounts = await this.web3.eth.getAccounts();
const address = accounts.length > 1 ? accounts[1] : accounts[0]; // ignore the first account because it is our deployer account, we want the manually added account
this.currentAddress = address;
// check account balance on the main chain
// try {
// this.maxDeposit = await this.web3.eth.getBalance(this.currentAddress);
// if (!this.maxDeposit || new BigNumber(this.maxDeposit).lte(0)) {
// throw new Error("The configured account does not have enough funds. Please make sure this account has Rinkeby ETH.");
// }
// this.maxDeposit = new BigNumber(this.maxDeposit);
// }
// catch (e) {
// this.logger.warn(`Error getting balance for account ${this.currentAddress}: ${e}`);
// }
// set lifecycle state vars
this.initing = false;
this.inited = true;
}
catch (e) {
await this.updateState();
} catch (e) {
const message = `Error initializing Plasma chain: ${e}`;
this.logger.error(message);
throw new Error(message);
}
}
async deposit(amount) {
async deposit(amount, currency = transaction.ETH_CURRENCY, approveDeposit = false) {
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";
// if (amount.gt(this.maxDeposit) && this.maxDeposit.gt(0)) {
// // recheck balance in case it was updated in a recent tx
// this.maxDeposit = await this.web3.eth.getBalance(this.currentAddress);
// if (amount.gt(this.maxDeposit)) {
// const message = `You do not have enough funds for this deposit. Please deposit more funds in to ${this.currentAddress} and then try again.`;
// throw new Error(message);
// }
// }
// Create the deposit transaction
const depositTx = transaction.encodeDeposit(this.currentAddress, amount, currency);
if (currency === transaction.ETH_CURRENCY) {
this.logger.info(`Depositing ${amount} wei...`);
const depositTx = transaction.encodeDeposit(this.address, amount, transaction.ETH_CURRENCY);
// ETH deposit
try {
const receipt = await this.rootChain.depositEth(depositTx, amount, { from: this.address, privateKey: this.addressPrivateKey });
const receipt = await this.rootChain.depositEth(depositTx, amount, {from: this.currentAddress});
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);
const message = `Successfully deposited ${amount} ${currency === transaction.ETH_CURRENCY ? "wei" : currency} in to the Plasma chain.\nView the transaction: https://rinkeby.etherscan.io/tx/${receipt.transactionHash}`;
return message;
}
catch (e) {
} catch (e) {
const message = `Error depositing ${amount} wei: ${e}`;
this.logger.error(message);
throw new Error(message);
}
}
async send(toAddress, val) {
//const val = "555";
// const toAddress = "0x38d5beb778b6e62d82e3ba4633e08987e6d0f990";
const utxos = await this.childChain.getUtxos(this.address);
const utxosToSpend = this.selectUtxos(utxos, val, transaction.ETH_CURRENCY);
if (!utxosToSpend) {
return this.logger.error(`No utxo big enough to cover the amount ${val}`);
// ERC20 token deposit
if (approveDeposit) {
// First approve the plasma contract on the erc20 contract
const erc20 = new this.web3.eth.Contract(ERC20_ABI, currency);
// const approvePromise = Promise.promisify(erc20.approve.sendTransaction)
// TODO
const gasPrice = 1000000;
const receipt = await erc20.methods
.approve(this.rootChain.plasmaContractAddress, amount)
.send({from: this.currentAddress, gasPrice, gas: 2000000});
// Wait for the approve tx to be mined
this.logger.info(`${amount} erc20 approved: ${receipt.transactionHash}. Waiting for confirmation...`);
await confirmTransaction(this.web3, receipt.transactionHash);
this.logger.info(`... ${receipt.transactionHash} confirmed.`);
}
val = new BigNumber(val);
if (!val || val.lte(0)) {
return this.logger.error("Transaction value must be more than 0 wei.");
return this.rootChain.depositToken(depositTx, {from: this.currentAddress});
}
async transfer(toAddress, amount, currency = transaction.ETH_CURRENCY) {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
throw new Error(message);
}
const verifyingContract = this.config.plasmaContractAddress;
const utxosToSpend = await this.selectUtxos(amount, currency);
if (!utxosToSpend) {
throw new Error(`No utxo big enough to cover the amount ${amount}`);
}
const txBody = {
@ -131,59 +159,80 @@ export default class BaseEmbarkOmg {
outputs: [
{
owner: toAddress,
currency: transaction.ETH_CURRENCY,
amount: val
currency,
amount: amount.toString()
}
]
};
const utxoAmnt = new BigNumber(utxosToSpend[0].amount);
if (utxoAmnt.gt(val)) {
// specify the change amount back to yourself
const changeAmnt = utxoAmnt.sub(val);
const bnAmount = new BigNumber(utxosToSpend[0].amount);
if (bnAmount.gt(new BigNumber(amount))) {
// Need to add a 'change' output
const CHANGE_AMOUNT = bnAmount.sub(new BigNumber(amount));
txBody.outputs.push({
owner: this.address,
currency: transaction.ETH_CURRENCY,
amount: changeAmnt
owner: this.currentAddress,
currency,
amount: CHANGE_AMOUNT
});
}
try {
const unsignedTx = await this.childChain.createTransaction(txBody);
if (currency !== transaction.ETH_CURRENCY && utxosToSpend.length > 1) {
// The fee input can be returned
txBody.outputs.push({
owner: this.currentAddress,
currency: utxosToSpend[utxosToSpend.length - 1].currency,
amount: utxosToSpend[utxosToSpend.length - 1].amount
});
}
const signatures = await this.childChain.signTransaction(unsignedTx, [this.addressPrivateKey]);
// Get the transaction data
const typedData = transaction.getTypedData(txBody, verifyingContract);
const signedTx = await this.childChain.buildSignedTransaction(unsignedTx, signatures);
// We should really sign each input separately but in this we know that they're all
// from the same address, so we can sign once and use that signature for each input.
//
// const sigs = await Promise.all(utxosToSpend.map(input => signTypedData(web3, web3.utils.toChecksumAddress(from), typedData)))
//
const signature = await signTypedData(
this.web3,
this.web3.utils.toChecksumAddress(this.currentAddress),
JSON.stringify(typedData)
);
const sigs = new Array(utxosToSpend.length).fill(signature);
// Build the signed transaction
const signedTx = this.childChain.buildSignedTransaction(typedData, sigs);
// Submit the signed transaction to the childchain
const result = await this.childChain.submitTransaction(signedTx);
const message = `Successfully submitted tx on the child chain: ${JSON.stringify(result)}\nView the transaction: http://quest.ari.omg.network/transaction/${result.txhash}`;
const message = `Successfully submitted tx on the child chain: ${JSON.stringify(
result
)}\nView the transaction: ${this.config.childChainExplorerUrl}transaction/${
result.txhash
}`;
this.logger.info(message);
return message;
}
catch (e) {
this.logger.error(e);
throw e;
}
async exitAllUtxos(fromAddress) {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
throw new Error(message);
}
async exit(fromAddress) {
const utxos = await this.childChain.getUtxos(fromAddress);
if (utxos.length <= 0) {
const message = `No UTXOs found on the Plasma chain for ${fromAddress}.`;
this.logger.error(message);
throw new Error(message);
}
// NB This only exits the first UTXO.
// Selecting _which_ UTXO to exit is left as an exercise for the reader...
const errors = [];
utxos.forEach(async (utxo) => {
utxos.forEach(async utxo => {
const exitData = await this.childChain.getExitData(utxo);
try {
let receipt = await this.rootChain.startStandardExit(
exitData.utxo_pos.toString(),
Number(exitData.utxo_pos.toString()),
exitData.txbytes,
exitData.proof,
{
@ -192,13 +241,15 @@ export default class BaseEmbarkOmg {
}
);
const message = `Exited UTXO from address ${fromAddress} with value ${utxo.amount}. View the transaction: https://rinkeby.etherscan.io/tx/${receipt.transactionHash}`;
this.logger.info(message);
return message;
}
catch (e) {
const message = `Error exiting the Plasma chain for UTXO ${JSON.stringify(utxo)}: ${e}`;
this.logger.error(message);
return `Exited UTXO from address ${fromAddress} with value ${
utxo.amount
}. View the transaction: https://rinkeby.etherscan.io/tx/${
receipt.transactionHash
}`;
} catch (e) {
const message = `Error exiting the Plasma chain for UTXO ${JSON.stringify(
utxo
)}: ${e}`;
errors.push(message);
}
});
@ -207,12 +258,71 @@ export default class BaseEmbarkOmg {
}
}
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];
async exitUtxo(from, utxoToExit) {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
throw new Error(message);
}
const exitData = await this.childChain.getExitData(utxoToExit);
return this.rootChain.startStandardExit(
Number(exitData.utxo_pos.toString()),
exitData.txbytes,
exitData.proof,
{from}
);
}
async selectUtxos(amount, currency) {
const transferZeroFee = currency !== transaction.ETH_CURRENCY;
const utxos = await this.childChain.getUtxos(this.currentAddress);
return selectUtxos(utxos, amount, currency, transferZeroFee);
}
async balances() {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
throw new Error(message);
}
const rootBalance = await this.web3.eth.getBalance(this.currentAddress);
const childchainBalances = await this.childChain.getBalance(this.currentAddress);
const childBalances = await Promise.all(childchainBalances.map(
async (balance) => {
if (balance.currency === transaction.ETH_CURRENCY) {
balance.symbol = 'wei';
} else {
const tokenContract = new this.web3.eth.Contract(ERC20_ABI, balance.currency);
try {
balance.symbol = await tokenContract.methods.symbol().call();
} catch (err) {
balance.symbol = 'Unknown ERC20';
}
}
return balance;
}
));
return {
rootBalance,
childBalances
};
}
async updateState() {
if (!this.inited) {
const message = "Please wait for the Plasma chain to initialize...";
throw new Error(message);
}
const {rootBalance, childBalances} = await this.balances();
this.state.account.address = this.currentAddress;
this.state.account.rootBalance = rootBalance;
this.state.account.childBalances = childBalances;
this.state.transactions = await this.childChain.getTransactions({address: this.currentAddress});
this.state.utxos = await this.childChain.getUtxos(this.currentAddress);
}
}

106
src/utils.js Normal file
View File

@ -0,0 +1,106 @@
import BigNumber from "bn.js";
import { transaction } from "@omisego/omg-js-util";
const DEFAULT_INTERVAL = 1000;
const DEFAULT_BLOCKS_TO_WAIT = 13;
export function confirmTransaction(web3, txnHash, options) {
const interval = options && options.interval ? options.interval : DEFAULT_INTERVAL;
const blocksToWait = options && options.blocksToWait ? options.blocksToWait : DEFAULT_BLOCKS_TO_WAIT;
const transactionReceiptAsync = async function (txnHash, resolve, reject) {
try {
const receipt = await web3.eth.getTransactionReceipt(txnHash);
if (!receipt) {
return setTimeout(function () {
transactionReceiptAsync(txnHash, resolve, reject);
}, interval);
}
if (blocksToWait > 0) {
const resolvedReceipt = await receipt;
if (!resolvedReceipt || !resolvedReceipt.blockNumber) {
return setTimeout(function () {
transactionReceiptAsync(txnHash, resolve, reject);
}, interval);
}
try {
const block = await web3.eth.getBlock(resolvedReceipt.blockNumber);
const current = await web3.eth.getBlock('latest');
if (current.number - block.number >= blocksToWait) {
const txn = await web3.eth.getTransaction(txnHash);
// eslint-disable-next-line max-depth
if (txn.blockNumber !== null) {
return resolve(resolvedReceipt);
}
return reject(new Error('Transaction with hash: ' + txnHash + ' ended up in an uncle block.'));
}
return setTimeout(function () {
transactionReceiptAsync(txnHash, resolve, reject);
}, interval);
} catch (e) {
setTimeout(function () {
transactionReceiptAsync(txnHash, resolve, reject);
}, interval);
}
} else resolve(receipt);
} catch (e) {
reject(e);
}
};
if (Array.isArray(txnHash)) {
const promises = [];
txnHash.forEach(function (oneTxHash) {
promises.push(confirmTransaction(web3, oneTxHash, options));
});
return Promise.all(promises);
}
return new Promise(function (resolve, reject) {
transactionReceiptAsync(txnHash, resolve, reject);
});
}
export function selectUtxos(utxos, amount, currency, includeFee) {
// Filter by desired currency and sort in descending order
const sorted = utxos
.filter(utxo => utxo.currency === currency)
.sort((a, b) => new BigNumber(b.amount).sub(new BigNumber(a.amount)));
if (sorted) {
const selected = [];
let currentBalance = new BigNumber(0);
for (let i = 0; i < Math.min(sorted.length, 4); i++) {
selected.push(sorted[i]);
currentBalance.iadd(new BigNumber(sorted[i].amount));
if (currentBalance.gte(new BigNumber(amount))) {
break;
}
}
if (currentBalance.gte(new BigNumber(amount))) {
if (includeFee) {
// Find the first ETH utxo (that's not selected)
const ethUtxos = utxos.filter(
utxo => utxo.currency === transaction.ETH_CURRENCY
);
const feeUtxo = ethUtxos.find(utxo => utxo !== selected);
if (!feeUtxo) {
throw new Error(`Can't find a fee utxo for transaction`);
} else {
selected.push(feeUtxo);
}
}
return selected;
}
}
}
export function signTypedData(web3, signer, data) {
return web3.currentProvider.send('eth_signTypedData_v3', [signer, data]);
}
export function normalizeUrl(url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
}

709
yarn.lock

File diff suppressed because it is too large Load Diff