diff --git a/package.json b/package.json index c750cf8..ad9526e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "eosjs": "^16.0.5", "eosjs-api": "^6.3.2", "eosjs-ecc": "^4.0.3", + "enujs": "^16.0.7", + "enujs-api": "^7.0.4", + "enujs-ecc": "^4.0.4", "ethereumjs-abi": "^0.6.5", "ethereumjs-tx": "^1.3.7", "ethereumjs-util": "^5.2.0", diff --git a/src/models/Blockchains.js b/src/models/Blockchains.js index 66b01ef..b37089f 100644 --- a/src/models/Blockchains.js +++ b/src/models/Blockchains.js @@ -1,8 +1,9 @@ export const Blockchains = { EOS:'eos', - ETH:'eth' + ETH:'eth', + ENU:'enu' }; export const BlockchainsArray = - Object.keys(Blockchains).map(key => ({key, value:Blockchains[key]})); \ No newline at end of file + Object.keys(Blockchains).map(key => ({key, value:Blockchains[key]})); diff --git a/src/models/KeyPair.js b/src/models/KeyPair.js index b0563b2..7fb8bf4 100644 --- a/src/models/KeyPair.js +++ b/src/models/KeyPair.js @@ -18,6 +18,7 @@ export default class KeyPair { static blockchain(publicKey){ if(publicKey.indexOf('EOS') !== -1) return Blockchains.EOS; + if(publicKey.indexOf('ENU') !== -1) return Blockchains.ENU; if(publicKey.indexOf('0x') !== -1 && publicKey.length === 42) return Blockchains.ETH; return null; } @@ -31,6 +32,8 @@ export default class KeyPair { case Blockchains.EOS: return this.privateKey.length > 51; // ETH private keys are 64 chars long case Blockchains.ETH: return this.privateKey.length > 64; + // ENU private keys are 51 chars long + case Blockchains.ENU: return this.privateKey.length > 51; }} /*** @@ -50,4 +53,4 @@ export default class KeyPair { if(this.isEncrypted()) this.privateKey = AES.decrypt(this.privateKey, seed); } -} \ No newline at end of file +} diff --git a/src/plugins/PluginRepository.js b/src/plugins/PluginRepository.js index f8a0a29..0b21f72 100644 --- a/src/plugins/PluginRepository.js +++ b/src/plugins/PluginRepository.js @@ -1,6 +1,7 @@ import * as PluginTypes from './PluginTypes'; import EOS from './defaults/eos'; import ETH from './defaults/eth'; +import ENU from './defaults/enu'; /*** * Setting up for plugin based generators, @@ -17,6 +18,7 @@ class PluginRepositorySingleton { loadPlugins(){ this.plugins.push(new EOS()); this.plugins.push(new ETH()); + this.plugins.push(new ENU()); } signatureProviders(){ @@ -37,4 +39,4 @@ class PluginRepositorySingleton { } const PluginRepository = new PluginRepositorySingleton(); -export default PluginRepository; \ No newline at end of file +export default PluginRepository; diff --git a/src/plugins/defaults/enu.js b/src/plugins/defaults/enu.js new file mode 100644 index 0000000..0874a81 --- /dev/null +++ b/src/plugins/defaults/enu.js @@ -0,0 +1,294 @@ +import Plugin from '../Plugin'; +import * as PluginTypes from '../PluginTypes'; +import {Blockchains} from '../../models/Blockchains' +import * as NetworkMessageTypes from '../../messages/NetworkMessageTypes' +import StringHelpers from '../../util/StringHelpers' +import Error from '../../models/errors/Error' +import Network from '../../models/Network' +import Account from '../../models/Account' +import AlertMsg from '../../models/alerts/AlertMsg' +import * as Actions from '../../store/constants'; +import Enu from 'enujs' +let {ecc, Fcbuffer} = Enu.modules; +import {IdentityRequiredFields} from '../../models/Identity'; +import ObjectHelpers from '../../util/ObjectHelpers' +import * as ricardianParser from 'eos-rc-parser'; +import StorageService from '../../services/StorageService' +import {strippedHost} from '../../util/GenericTools' + +let networkGetter = new WeakMap(); +let messageSender = new WeakMap(); +let throwIfNoIdentity = new WeakMap(); + +const proxy = (dummy, handler) => new Proxy(dummy, handler); + +export default class ENU extends Plugin { + + constructor(){ super(Blockchains.ENU, PluginTypes.BLOCKCHAIN_SUPPORT) } + accountFormatter(account){ return `${account.name}@${account.authority}` } + returnableAccount(account){ return { name:account.name, authority:account.authority }} + + async getEndorsedNetwork(){ + return new Promise((resolve, reject) => { + resolve(new Network( + 'ENU Mainnet', 'https', + 'rpc.enu.one', + 443, + Blockchains.ENU, + 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f' + )); + }); + } + + async isEndorsedNetwork(network){ + const endorsedNetwork = await this.getEndorsedNetwork(); + return network.hostport() === endorsedNetwork.hostport(); + } + + accountsAreImported(){ return true; } + importAccount(keypair, network, context, accountSelected){ + const getAccountsFromPublicKey = (publicKey, network) => { + return new Promise((resolve, reject) => { + const enu = Enu({httpEndpoint:`${network.protocol}://${network.hostport()}`}); + enu.getKeyAccounts(publicKey).then(res => { + if(!res || !res.hasOwnProperty('account_names')){ resolve([]); return false; } + + Promise.all(res.account_names.map(name => enu.getAccount(name).catch(e => resolve([])))).then(multires => { + let accounts = []; + multires.map(account => { + account.permissions.map(permission => { + accounts.push({name:account.account_name, authority:permission.perm_name}); + }); + }); + resolve(accounts) + }).catch(e => resolve([])); + }).catch(e => resolve([])); + }) + } + + getAccountsFromPublicKey(keypair.publicKey, network).then(accounts => { + switch(accounts.length){ + case 0: context[Actions.PUSH_ALERT](AlertMsg.NoAccountsFound()); reject(); return false; + // Only one account, so returning it + case 1: accountSelected(Account.fromJson({name:accounts[0].name, authority:accounts[0].authority, publicKey:keypair.publicKey, keypairUnique:keypair.unique() })); break; + // More than one account, prompting account selection + default: context[Actions.PUSH_ALERT](AlertMsg.SelectAccount(accounts)).then(res => { + if(!res || !res.hasOwnProperty('selected')) { reject(); return false; } + accountSelected(Account.fromJson(Object.assign(res.selected, {publicKey:keypair.publicKey, keypairUnique:keypair.unique()}))); + }) + } + }).catch(e => { + console.log('error', e); + accountSelected(null); + }); + } + + privateToPublic(privateKey){ return ecc.privateToPublic(privateKey); } + validPrivateKey(privateKey){ return ecc.isValidPrivate(privateKey); } + validPublicKey(publicKey){ return ecc.isValidPublic(publicKey); } + randomPrivateKey(){ return ecc.randomKey(); } + convertsTo(){ + return []; + } + from_eth(privateKey){ + return ecc.PrivateKey.fromHex(Buffer.from(privateKey, 'hex')).toString(); + } + + async getBalances(account, network, code = 'enu.token', table = 'accounts'){ + const enu = Enu({httpEndpoint:`${network.protocol}://${network.hostport()}`, chainId:network.chainId}); + const contract = await enu.contract(code); + return await enu.getTableRows({ + json: true, + code, + scope: account.name, + table, + limit: 5000 + }).then(res => res.rows.map(row => row.balance.split(' ').reverse())); + } + + actionParticipants(payload){ + return ObjectHelpers.flatten( + payload.messages + .map(message => message.authorization + .map(auth => `${auth.actor}@${auth.permission}`)) + ); + } + + signer(bgContext, payload, publicKey, callback, arbitrary = false, isHash = false){ + bgContext.publicToPrivate(privateKey => { + if(!privateKey){ + callback(null); + return false; + } + + let sig; + if(arbitrary && isHash) sig = ecc.Signature.signHash(payload.data, privateKey).toString(); + else sig = ecc.sign(Buffer.from(arbitrary ? payload.data : payload.buf.data, 'utf8'), privateKey); + + callback(sig); + }, publicKey) + } + + signatureProvider(...args){ + + messageSender = args[0]; + throwIfNoIdentity = args[1]; + + // Protocol will be deprecated. + return (network, _enu, _options = {}, protocol = 'http') => { + + + if(!['http', 'https', 'ws'].includes(protocol)) + throw new Error('Protocol must be either http, https, or ws'); + + // Backwards compatibility: Networks now have protocols, but some older dapps still use the argument + if(!network.hasOwnProperty('protocol') || !network.protocol.length) + network.protocol = protocol; + + network = Network.fromJson(network); + if(!network.isValid()) throw Error.noNetwork(); + const httpEndpoint = `${network.protocol}://${network.hostport()}`; + + const chainId = network.hasOwnProperty('chainId') && network.chainId.length ? network.chainId : _options.chainId; + network.chainId = chainId; + + // The proxy stands between the enujs object and scatter. + // This is used to add special functionality like adding `requiredFields` arrays to transactions + return proxy(_enu({httpEndpoint, chainId}), { + get(enuInstance, method) { + + let returnedFields = null; + + return (...args) => { + + if(args.find(arg => arg.hasOwnProperty('keyProvider'))) throw Error.usedKeyProvider(); + + let requiredFields = args.find(arg => arg.hasOwnProperty('requiredFields')); + requiredFields = IdentityRequiredFields.fromJson(requiredFields ? requiredFields.requiredFields : {}); + if(!requiredFields.isValid()) throw Error.malformedRequiredFields(); + + // The signature provider which gets elevated into the user's Scatter + const signProvider = async signargs => { + throwIfNoIdentity(); + + // Friendly formatting + signargs.messages = await requestParser(signargs, network); + + const payload = Object.assign(signargs, { domain:strippedHost(), network, requiredFields }); + const result = await messageSender(NetworkMessageTypes.REQUEST_SIGNATURE, payload); + + // No signature + if(!result) return null; + + if(result.hasOwnProperty('signatures')){ + // Holding onto the returned fields for the final result + returnedFields = result.returnedFields; + + // Grabbing buf signatures from local multi sig sign provider + let multiSigKeyProvider = args.find(arg => arg.hasOwnProperty('signProvider')); + if(multiSigKeyProvider){ + result.signatures.push(multiSigKeyProvider.signProvider(signargs.buf, signargs.sign)); + } + + // Returning only the signatures to enujs + return result.signatures; + } + + return result; + }; + + // TODO: We need to check about the implications of multiple enujs instances + return new Promise((resolve, reject) => { + _enu(Object.assign(_options, {httpEndpoint, signProvider, chainId}))[method](...args) + .then(result => { + + // Standard method ( ie. not contract ) + if(!result.hasOwnProperty('fc')){ + result = Object.assign(result, {returnedFields}); + resolve(result); + return; + } + + // Catching chained promise methods ( contract .then action ) + const contractProxy = proxy(result, { + get(instance,method){ + if(method === 'then') return instance[method]; + return (...args) => { + return new Promise(async (res, rej) => { + instance[method](...args).then(actionResult => { + res(Object.assign(actionResult, {returnedFields})); + }).catch(rej); + }) + + } + } + }); + + resolve(contractProxy); + }).catch(error => reject(error)) + }) + } + } + }); // Proxy + } + } +} + +const requestParser = async (signargs, network) => { + const enu = Enu({httpEndpoint:network.fullhost(), chainId:network.chainId}); + + const contracts = signargs.transaction.actions.map(action => action.account) + .reduce((acc, contract) => { + if(!acc.includes(contract)) acc.push(contract); + return acc; + }, []); + + const staleAbi = +new Date() - (1000 * 60 * 60 * 24 * 2); + const abis = {}; + + await Promise.all(contracts.map(async contractAccount => { + const cachedABI = await Promise.race([ + messageSender(NetworkMessageTypes.ABI_CACHE, {abiContractName:contractAccount, abiGet:true, chainId:network.chainId}), + new Promise(resolve => setTimeout(() => resolve('no cache'), 500)) + ]); + + if(cachedABI === 'object' && cachedABI.timestamp > +new Date((await enu.getAccount(contractAccount)).last_code_update)) + abis[contractAccount] = enu.fc.abiCache.abi(contractAccount, cachedABI.abi); + + else { + abis[contractAccount] = (await enu.contract(contractAccount)).fc; + const savableAbi = JSON.parse(JSON.stringify(abis[contractAccount])); + delete savableAbi.schema; + delete savableAbi.structs; + delete savableAbi.types; + savableAbi.timestamp = +new Date(); + + await messageSender(NetworkMessageTypes.ABI_CACHE, + {abiContractName: contractAccount, abi:savableAbi, abiGet: false, chainId:network.chainId}); + } + })); + + return await Promise.all(signargs.transaction.actions.map(async (action, index) => { + const contractAccountName = action.account; + + let abi = abis[contractAccountName]; + + const data = abi.fromBuffer(action.name, action.data); + const actionAbi = abi.abi.actions.find(fcAction => fcAction.name === action.name); + let ricardian = actionAbi ? actionAbi.ricardian_contract : null; + + if(ricardian){ + const htmlFormatting = {h1:'div class="ricardian-action"', h2:'div class="ricardian-description"'}; + const signer = action.authorization.length === 1 ? action.authorization[0].actor : null; + ricardian = ricardianParser.parse(action.name, data, ricardian, signer, htmlFormatting); + } + + return { + data, + code:action.account, + type:action.name, + authorization:action.authorization, + ricardian + }; + })); +}; diff --git a/src/services/KeyPairService.js b/src/services/KeyPairService.js index 276a7df..e086271 100644 --- a/src/services/KeyPairService.js +++ b/src/services/KeyPairService.js @@ -19,20 +19,13 @@ export default class KeyPairService { } let publicKey = ''; - - BlockchainsArray.map(blockchainKV => { - try { - if(!publicKey.length) { - const blockchain = blockchainKV.value; - - const plugin = PluginRepository.plugin(blockchain); - if (plugin && plugin.validPrivateKey(keypair.privateKey)) { - publicKey = plugin.privateToPublic(keypair.privateKey); - keypair.blockchain = blockchain; - } - } - } catch(e){} - }); + try{ + const plugin = PluginRepository.plugin(keypair.blockchain); + if (plugin && plugin.validPrivateKey(keypair.privateKey)) { + publicKey = plugin.privateToPublic(keypair.privateKey); + keypair.blockchain = blockchain; + } + } catch(e){} if(publicKey) keypair.publicKey = publicKey; resolve(true); @@ -68,4 +61,4 @@ export default class KeyPairService { context[Actions.UPDATE_STORED_SCATTER](scatter).then(() => callback()); } -} \ No newline at end of file +} diff --git a/src/util/ENUKeygen.js b/src/util/ENUKeygen.js new file mode 100644 index 0000000..ac09891 --- /dev/null +++ b/src/util/ENUKeygen.js @@ -0,0 +1,44 @@ +import KeyPair from '../models/KeyPair'; +import Mnemonic from './Mnemonic'; +import {PrivateKey} from 'enujs-ecc'; + +export default class ENUKeygen { + + /*** + * Generates a KeyPair + * @returns {KeyPair} + */ + static generateKeys(){ + let [mnemonic, seed] = Mnemonic.generateDanglingMnemonic(); + let privateKey = ENUKeygen.generatePrivateKey(seed); + let publicKey = ENUKeygen.privateToPublic(privateKey); + return KeyPair.fromJson({publicKey, privateKey}) + } + + /*** + * Generates only a private key + * @param seed - The seed to build the key from + * @returns {wif} + */ + static generatePrivateKey(seed) { + return PrivateKey.fromSeed(seed).toWif() + } + + /*** + * Converts a private key to a public key + * @param privateKey - The private key to convert + */ + static privateToPublic(privateKey) { + return PrivateKey.fromWif(privateKey).toPublic().toString() + } + + /*** + * Checks if a private key is a valid ENU private key + * @param privateKey - The private key to check + * @returns {boolean} + */ + static validPrivateKey(privateKey){ + return PrivateKey.isValid(privateKey); + } + +}