diff --git a/.eslintrc b/.eslintrc index f9a7bf6be..765a3afce 100644 --- a/.eslintrc +++ b/.eslintrc @@ -66,7 +66,8 @@ "react/jsx-one-expression-per-line": [0], "no-else-return": [0], "react/destructuring-assignment": [0], - "prefer-arrow-callback": [0] + "prefer-arrow-callback": [0], + "no-prototype-builtins": [0] }, "env": { "browser": true, diff --git a/shared/components/Form/TransferAssetsForm/index.native.jsx b/shared/components/Form/TransferAssetsForm/index.native.jsx index 0c8ce67f5..4a1da64ad 100644 --- a/shared/components/Form/TransferAssetsForm/index.native.jsx +++ b/shared/components/Form/TransferAssetsForm/index.native.jsx @@ -165,7 +165,7 @@ export default class TransferAssetsForm extends Component { componentDidMount() { const { activeAsset, eosAccountName } = this.props - this.props.actions.getEOSAssetBalanceRequested({ code: activeAsset.get('contract'), eosAccountName }) + this.props.actions.getEOSAssetBalanceRequested({ code: activeAsset.get('contract'), eosAccountName, symbol: activeAsset.get('symbol') }) } render() { diff --git a/shared/core/scatter/Blockchains.js b/shared/core/scatter/Blockchains.js new file mode 100644 index 000000000..a87c4ac63 --- /dev/null +++ b/shared/core/scatter/Blockchains.js @@ -0,0 +1,6 @@ +export const Blockchains = { + EOS: 'eos', + ETH: 'eth' +} + +export const BlockchainsArray = Object.keys(Blockchains).map(key => ({ key, value: Blockchains[key] })) diff --git a/shared/core/scatter/Error.js b/shared/core/scatter/Error.js new file mode 100644 index 000000000..f7750dd6f --- /dev/null +++ b/shared/core/scatter/Error.js @@ -0,0 +1,63 @@ +import * as ErrorTypes from './ErrorTypes' + +export const ErrorCodes = { + NO_SIGNATURE: 402, + FORBIDDEN: 403, + TIMED_OUT: 408, + LOCKED: 423, + UPGRADE_REQUIRED: 426, + TOO_MANY_REQUESTS: 429 +} + +export default class Error { + constructor(_type, _message, _code = ErrorCodes.LOCKED){ + this.type = _type + this.message = _message + this.code = _code + this.isError = true + } + + static locked(){ + return new Error(ErrorTypes.LOCKED, "The user's Scatter is locked. They have been notified and should unlock before continuing.") + } + + static promptClosedWithoutAction(){ + return new Error(ErrorTypes.PROMPT_CLOSED, 'The user closed the prompt without any action.', ErrorCodes.TIMED_OUT) + } + + static maliciousEvent(){ + return new Error(ErrorTypes.MALICIOUS, 'Malicious event discarded.', ErrorCodes.FORBIDDEN) + } + + static signatureError(_type, _message){ + return new Error(_type, _message, ErrorCodes.NO_SIGNATURE) + } + + static requiresUpgrade(){ + return new Error(ErrorTypes.UPGRADE_REQUIRED, "The required version is newer than the User's Scatter", ErrorCodes.UPGRADE_REQUIRED) + } + + static identityMissing(){ + return this.signatureError('identity_missing', "Identity no longer exists on the user's keychain") + } + + static signatureAccountMissing(){ + return this.signatureError('account_missing', 'Missing required accounts, repull the identity') + } + + static malformedRequiredFields(){ + return this.signatureError('malformed_requirements', 'The requiredFields you passed in were malformed') + } + + static noNetwork(){ + return this.signatureError('no_network', 'You must bind a network first') + } + + static usedKeyProvider(){ + return new Error( + ErrorTypes.MALICIOUS, + 'Do not use a `keyProvider` with a Scatter. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.', + ErrorCodes.NO_SIGNATURE + ) + } +} diff --git a/shared/core/scatter/ErrorTypes.js b/shared/core/scatter/ErrorTypes.js new file mode 100644 index 000000000..1000ec0ac --- /dev/null +++ b/shared/core/scatter/ErrorTypes.js @@ -0,0 +1,4 @@ +export const MALICIOUS = 'malicious'; +export const LOCKED = 'locked'; +export const PROMPT_CLOSED = 'prompt_closed'; +export const UPGRADE_REQUIRED = 'upgrade_required'; diff --git a/shared/core/scatter/Network.js b/shared/core/scatter/Network.js new file mode 100644 index 000000000..528c586d6 --- /dev/null +++ b/shared/core/scatter/Network.js @@ -0,0 +1,40 @@ +import { Blockchains } from './Blockchains' + +export default class Network { + constructor(_name = '', _protocol = 'https', _host = '', _port = 0, blockchain = Blockchains.EOS, chainId = ''){ + this.name = _name + this.protocol = _protocol + this.host = _host + this.port = _port + this.blockchain = blockchain + this.chainId = chainId.toString() + } + + static placeholder(){ return new Network() } + + static fromJson(json){ + const p = Object.assign(Network.placeholder(), json) + p.chainId = p.chainId ? p.chainId.toString() : '' + return p + } + + static fromUnique(netString){ + const blockchain = netString.split(':')[0] + if (netString.indexOf(':chain:') > -1) return new Network('', '', '', '', blockchain, netString.replace(`${blockchain}:chain:`, '')) + + const splits = netString.replace(`${blockchain}:`, '').split(':') + return new Network('', '', splits[0], parseInt(splits[1] || 80, 10), blockchain) + } + + unique(){ return (`${this.blockchain}:${this.chainId.length ? `chain:${this.chainId}` : `${this.host}:${this.port}`}`).toLowerCase() } + + hostport(){ return `${this.host}${this.port ? ':' : ''}${this.port}` } + + fullhost(){ return `${this.protocol}://${this.host}${this.port ? ':' : ''}${this.port}` } + + clone(){ return Network.fromJson(JSON.parse(JSON.stringify(this))) } + + isEmpty(){ return !this.host.length } + + isValid(){ return (this.host.length && this.port) || this.chainId.length } +} diff --git a/shared/core/scatter/NetworkMessageTypes.js b/shared/core/scatter/NetworkMessageTypes.js new file mode 100644 index 000000000..d83811a12 --- /dev/null +++ b/shared/core/scatter/NetworkMessageTypes.js @@ -0,0 +1,11 @@ +export const ERROR = 'error'; +export const PUSH_SCATTER = 'pushScatter'; +export const GET_OR_REQUEST_IDENTITY = 'getOrRequestIdentity'; +export const IDENTITY_FROM_PERMISSIONS = 'identityFromPermissions'; +export const FORGET_IDENTITY = 'forgetIdentity'; +export const REQUEST_SIGNATURE = 'requestSignature'; +export const ABI_CACHE = 'abiCache'; +export const REQUEST_ARBITRARY_SIGNATURE = 'requestArbitrarySignature'; +export const REQUEST_ADD_NETWORK = 'requestAddNetwork'; +export const REQUEST_VERSION_UPDATE = 'requestVersionUpdate'; +export const AUTHENTICATE = 'authenticate'; diff --git a/shared/core/scatter/ObjectHelpers.js b/shared/core/scatter/ObjectHelpers.js new file mode 100644 index 000000000..753d6843d --- /dev/null +++ b/shared/core/scatter/ObjectHelpers.js @@ -0,0 +1,85 @@ +/*** + * A set of helpers for Objects/Arrays + */ +export default class ObjectHelpers { + /*** + * Groups an array by key + * @param array + * @param key + * @returns {*} + */ + static groupBy(array, key){ + return array.reduce((acc, item) => { + (acc[item[key]] = acc[item[key]] || []).push(item); + return acc; + }, {}); + } + + /*** + * Makes a single level array distinct + * @param array + * @returns {*} + */ + static distinct(array){ + return array.reduce((a, b) => ((a.includes(b)) ? a : a.concat(b)), []); + } + + /*** + * Makes an object array distinct ( uses deep checking ) + * @param array + * @returns {*} + */ + static distinctObjectArray(array){ + return array.reduce((a, b) => ((a.find(x => this.deepEqual(x, b))) ? a : a.concat(b)), []); + } + + /*** + * Checks deep equality for objects + * @param objA + * @param objB + * @returns {boolean} + */ + static deepEqual(objA, objB) { + const keys = Object.keys + const typeA = typeof objA + const typeB = typeof objB + return objA && objB && typeA === 'object' && typeA === typeB ? ( + keys(objA).length === keys(objB).length + && keys(objA).every(key => this.deepEqual(objA[key], objB[key])) + ) : (objA === objB); + } + + /*** + * Flattens an array into a single dimension + * @param array + * @returns {*} + */ + static flatten(array){ + return array.reduce( + (a, b) => a.concat(Array.isArray(b) ? this.flatten(b) : b), [] + ); + } + + /*** + * Flattens an objects keys into a single dimension + * @param object + * @returns {*} + */ + static objectToFlatKeys(object){ + return this.flatten(Object.keys(object).map((key) => { + if (object[key] !== null && typeof object[key] === 'object') return this.objectToFlatKeys(object[key]) + else return key; + })) + } + + /*** + * Gets a field from an object by string dot notation, such as `location.country.code` + * @param object + * @param dotNotation + * @returns {*} + */ + static getFieldFromObjectByDotNotation(object, dotNotation){ + const props = dotNotation.split('.'); + return props.reduce((obj, key) => obj[key], object) + } +} diff --git a/shared/core/scatter/Plugin.js b/shared/core/scatter/Plugin.js new file mode 100644 index 000000000..fb1085986 --- /dev/null +++ b/shared/core/scatter/Plugin.js @@ -0,0 +1,6 @@ +export default class Plugin { + constructor(_name = '', _type = ''){ + this.name = _name; + this.type = _type; + } +} diff --git a/shared/core/scatter/PluginTypes.js b/shared/core/scatter/PluginTypes.js new file mode 100644 index 000000000..c5b79b550 --- /dev/null +++ b/shared/core/scatter/PluginTypes.js @@ -0,0 +1 @@ +export const BLOCKCHAIN_SUPPORT = 'blockchain_support'; diff --git a/shared/core/scatter/bridge.js b/shared/core/scatter/bridge.js new file mode 100644 index 000000000..590ab77f0 --- /dev/null +++ b/shared/core/scatter/bridge.js @@ -0,0 +1,225 @@ +const parseMessageId = function(text) { + const re = /BITPORAL_BRIDGE_MESSAGE@(\\d|\\w)+@/g + const found = text.match(re) + return found && found[0] +} + +const uuid = function() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) + } + return `BITPORAL_BRIDGE_MESSAGE@${s4()}_${s4()}_${s4()}_${s4()}_${s4()}_${new Date().getTime()}@` +} + +const callbacks = {} + +const sendMessage = (event, message) => { + document.dispatchEvent(new CustomEvent(`${event}_request`, { detail: { message } })) +} + +const sendRequest = function(type, payload, onSuccess, onError) { + onSuccess = onSuccess || function(){} + onError = onError || function(){} + const messageId = uuid() + callbacks[messageId] = { onSuccess, onError } + sendMessage(type, JSON.stringify({ messageId, type, payload })) +} + +const onMessage = (message) => { + let action + + try { + action = JSON.parse(message) + } catch (error) { + const messageId = parseMessageId(message) + if (messageId) { + callbacks[messageId].onError({ message: error.message }) + delete callbacks[messageId] + } + return + } + + const messageId = action.messageId + const payload = action.payload + + switch (action.type) { + case 'actionSucceeded': + callbacks[messageId].onSuccess(payload.data) + delete callbacks[messageId] + break + case 'actionFailed': + callbacks[messageId].onError(payload.error) + delete callbacks[messageId] + break + default: + break + } +} + +const supportedActions = [ + 'getEOSAccountInfo', + 'getEOSCurrencyBalance', + 'getEOSActions', + 'getEOSTransaction', + 'transferEOSAsset', + 'voteEOSProducers', + 'pushEOSAction', + 'eosAuthSign', + 'getCurrentWallet', + 'getAppInfo' +] + +supportedActions.forEach((action) => { + document.addEventListener(`${action}_response`, function(e) { + onMessage(e.detail.message) + }) +}) + +const bitportal = { + getEOSAccountInfo: function(params) { + if (!params.account) { + throw new Error('"account" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('getEOSAccountInfo', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + getEOSCurrencyBalance: function(params) { + if (!params.account) { + throw new Error('"account" is required') + } else if (!params.contract) { + throw new Error('"contract" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('getEOSCurrencyBalance', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + getEOSActions: function(params) { + if (!params.offset) { + throw new Error('"offset" is required') + } else if (!params.account) { + throw new Error('"account" is required') + } else if (!params.position) { + throw new Error('"position" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('getEOSActions', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + getEOSTransaction: function(params) { + if (!params.id) { + throw new Error('"offset" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('getEOSTransaction', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + transferEOSAsset: function(params) { + if (!params.amount) { + throw new Error('"amount" is required') + } else if (!params.precision) { + throw new Error('"precision" is required') + } else if (!params.symbol) { + throw new Error('"symbol" is required') + } else if (!params.contract) { + throw new Error('"contract" is required') + } else if (!params.from) { + throw new Error('"from" is required') + } else if (!params.to) { + throw new Error('"to" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('transferEOSAsset', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + voteEOSProducers: function(params) { + if (!params.voter) { + throw new Error('"voter" is required') + } else if (!params.producers) { + throw new Error('"producers" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('voteEOSProducers', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + pushEOSAction: function(params) { + if (!params.actions) { + throw new Error('"actions" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('pushEOSAction', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + eosAuthSign: function(params) { + if (!params.account) { + throw new Error('"account" is required') + } else if (!params.publicKey) { + throw new Error('"publicKey" is required') + } else if (!params.signData) { + throw new Error('"signData" is required') + } + + return new Promise(function(resolve, reject) { + sendRequest('eosAuthSign', params, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + getCurrentWallet: function() { + return new Promise(function(resolve, reject) { + sendRequest('getCurrentWallet', {}, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + }, + getAppInfo: function() { + return new Promise(function(resolve, reject) { + sendRequest('getAppInfo', {}, function(data) { + resolve(data) + }, function(error) { + reject(error) + }) + }) + } +} + +export default bitportal diff --git a/shared/core/scatter/eos-rc-parser.js b/shared/core/scatter/eos-rc-parser.js new file mode 100644 index 000000000..dd80b0708 --- /dev/null +++ b/shared/core/scatter/eos-rc-parser.js @@ -0,0 +1,107 @@ +/* eslint-disable */ + +// HELPERS ////////////////////////////////////////////////////// + +/*** + * Replaces curly brace placeholders + * @param ricardian + * @param placeholder + * @param replacement + * @returns {*} + */ +const replacePlaceholder = function replacePlaceholder(ricardian, placeholder, replacement) { + while (ricardian.indexOf('{{ ' + placeholder + ' }}') > -1) { + ricardian = ricardian.replace('{{ ' + placeholder + ' }}', '"' + replacement + '"'); + }return ricardian; +}; + +/*** + * Html can be either a boolean, null, or an object of {h1,h2} + * @param html + * @returns {*} + */ +const htmlDefaults = function htmlDefaults(html) { + if (html === null) return html; + if (html === false) return null; + + if (typeof html === "boolean") html = {}; + if (!html.hasOwnProperty('h1')) html.h1 = 'h1'; + if (!html.hasOwnProperty('h2')) html.h2 = 'h2'; + + return html; +}; + +// EXPORTED /////////////////////////////////////////////////////////// + +/*** + * Parses the constitution + * @param constitution + * @param signingAccount + * @param html + * @returns {*} + */ +export const constitution = function (constitution, signingAccount) { + var html = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + var getArticleTag = function getArticleTag() { + return constitution.match(new RegExp('#' + "(.*)" + '-')); + }; + + html = htmlDefaults(html); + + // Replacing signer + if (signingAccount) constitution = replacePlaceholder(constitution, "signer", signingAccount); + + // Optional HTML formatting. + if (html !== null) { + var articleTag = getArticleTag(); + + while (articleTag && articleTag[0].length) { + var strippedArticleTag = articleTag[0].replace('# ', '').replace(' -', ''); + constitution = constitution.replace(articleTag[0], '<' + html.h1 + '>' + strippedArticleTag + ''); + articleTag = getArticleTag(); + } + constitution = constitution.replace(/[\n\r]/g, '
'); + } + + return constitution; +}; + +/*** + * Parses arbitrary contract action ricardian contracts. + * @param actionName + * @param actionParameters + * @param ricardianContract + * @param signingAccount + * @param html + * @returns {*} + */ +export const parse = function (actionName, actionParameters, ricardianContract) { + var signingAccount = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + var html = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; + + html = htmlDefaults(html); + + // Stripping backticks + ricardianContract = ricardianContract.replace(/`/g, ''); + + // Replacing action name + ricardianContract = replacePlaceholder(ricardianContract, actionName, actionName); + + // Replacing action parameters + Object.keys(actionParameters).map(function (param) { + return ricardianContract = replacePlaceholder(ricardianContract, param, actionParameters[param]); + }); + + // Replacing signer + if (signingAccount) ricardianContract = replacePlaceholder(ricardianContract, "signer", signingAccount); + + // Optional HTML formatting. + if (html !== null) { + ricardianContract = ricardianContract.replace('# Action', '<' + html.h1 + '>Action'); + ricardianContract = ricardianContract.replace('## Description', '<' + html.h2 + '>Description'); + ricardianContract = ricardianContract.replace(/[\n\r]/g, '
'); + } + + return ricardianContract; +}; diff --git a/shared/core/scatter/eos.js b/shared/core/scatter/eos.js new file mode 100644 index 000000000..64dc807ae --- /dev/null +++ b/shared/core/scatter/eos.js @@ -0,0 +1,353 @@ +import Eos from 'eosjs' +import * as ricardianParser from './eos-rc-parser' +import Plugin from './Plugin' +import * as PluginTypes from './PluginTypes' +import * as NetworkMessageTypes from './NetworkMessageTypes' +import { Blockchains } from './Blockchains' +import Error from './Error' +import Network from './Network' +import ObjectHelpers from './ObjectHelpers' + +const { ecc } = Eos.modules + +const strippedHost = () => { + let host = location.hostname; + + // Replacing www. only if the domain starts with it. + if (host.indexOf('www.') === 0) host = host.replace('www.', '') + + return host +} + +const PersonalFields = { + firstname: 'firstname', + lastname: 'lastname', + email: 'email', + birthdate: 'birthdate' +} + +const LocationFields = { + phone: 'phone', + address: 'address', + city: 'city', + state: 'state', + country: 'country', + zipcode: 'zipcode' +} + +const IdentityBaseFields = { + account: 'accounts' +} + +class IdentityRequiredFields { + constructor(){ + this.accounts = [] + this.personal = [] + this.location = [] + } + + static placeholder(){ return new IdentityRequiredFields() } + + static fromJson(json){ + const p = Object.assign(new IdentityRequiredFields(), json) + p.accounts = json.hasOwnProperty('accounts') ? json.accounts.map(Network.fromJson) : [] + return p + } + + isEmpty(){ + return !this.accounts.length + && !this.personal.length + && !this.location.length + } + + isValid(){ + if (JSON.stringify(Object.keys(new IdentityRequiredFields())) !== JSON.stringify(Object.keys(this))) return false + if (!this.personal.every(field => Object.keys(PersonalFields).includes(field))) return false + if (!this.location.every(field => Object.keys(LocationFields).includes(field))) return false + if (this.accounts.length && !this.accounts.every(network => network.isValid())) return false + return true + } + + toFieldsArray(){ + const fields = [] + Object.keys(this).map((key) => { + if (key === IdentityBaseFields.account) return this[key].map(network => fields.push(`ACCOUNT: ${network.unique()}`)) + else return this[key].map(subKey => fields.push(subKey)) + }) + return fields + } +} + +// const networkGetter = new WeakMap() +let messageSender = new WeakMap() +let throwIfNoIdentity = new WeakMap() + +const proxy = (dummy, handler) => new Proxy(dummy, handler) + + +const requestParser = async (signargs, network) => { + const eos = Eos({ 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 eos.getAccount(contractAccount)).last_code_update)) abis[contractAccount] = eos.fc.abiCache.abi(contractAccount, cachedABI.abi) + + else { + abis[contractAccount] = (await eos.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 Promise.all(signargs.transaction.actions.map(async (action) => { + const contractAccountName = action.account + + const abi = abis[contractAccountName] + + const typeName = abi.abi.actions.find(x => x.name === action.name).type + const data = abi.fromBuffer(typeName, 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 + } + })) +} + +export default class EOS extends Plugin { + constructor(){ super(Blockchains.EOS, 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) => { + resolve(new Network( + 'EOS Mainnet', 'https', + 'nodes.get-scatter.com', + 443, + Blockchains.EOS, + 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906' + )) + }) + } + + 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 eos = Eos({httpEndpoint:`${network.protocol}://${network.hostport()}`}) + // eos.getKeyAccounts(publicKey).then(res => { + // if(!res || !res.hasOwnProperty('account_names')){ resolve([]) return false } + + // Promise.all(res.account_names.map(name => eos.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 = 'eosio.token', table = 'accounts'){ + const eos = Eos({ httpEndpoint: `${network.protocol}://${network.hostport()}`, chainId: network.chainId }) + await eos.contract(code) + return eos.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, _eos, _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 eosjs object and scatter. + // This is used to add special functionality like adding `requiredFields` arrays to transactions + return proxy(_eos({ httpEndpoint, chainId }), { + get(eosInstance, 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 + const multiSigKeyProvider = args.find(arg => arg.hasOwnProperty('signProvider')) + if (multiSigKeyProvider){ + result.signatures.push(multiSigKeyProvider.signProvider(signargs.buf, signargs.sign)) + } + + // Returning only the signatures to eosjs + return result.signatures + } + + return result + } + + // TODO: We need to check about the implications of multiple eosjs instances + return new Promise((resolve, reject) => { + _eos(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) => new Promise(async (res, rej) => { + instance[method](...args).then((actionResult) => { + res(Object.assign(actionResult, { returnedFields })) + }).catch(rej) + }) + } + }) + + resolve(contractProxy) + }).catch(error => reject(error)) + }) + } + } + }) // Proxy + } + } +} diff --git a/shared/core/scatter/scatterdapp.js b/shared/core/scatter/scatterdapp.js new file mode 100644 index 000000000..b264805f8 --- /dev/null +++ b/shared/core/scatter/scatterdapp.js @@ -0,0 +1,202 @@ +// import NetworkMessage from './messages/NetworkMessage' +// import * as NetworkMessageTypes from './messages/NetworkMessageTypes' +// import * as PairingTags from './messages/PairingTags' +// import Error from './models/errors/Error' +// import Network from './models/Network' +// import IdGenerator from './util/IdGenerator' +// import PluginRepository from './plugins/PluginRepository' +// const ecc = require('eosjs-ecc') +// import {strippedHost} from './util/GenericTools' + +// const throws = (msg) => { +// throw new Error(msg) +// } + +// /*** +// * This is just a helper to manage resolving fake-async +// * requests using browser messaging. +// */ +// class DanglingResolver { +// constructor(_id, _resolve, _reject){ +// this.id = _id +// this.resolve = _resolve +// this.reject = _reject +// } +// } + +// // Removing properties from exposed scatterdapp object +// // Pseudo privacy +// let provider = new WeakMap() +// let stream = new WeakMap() +// let resolvers = new WeakMap() +// let network = new WeakMap() +// let publicKey = new WeakMap() +// let currentVersion = new WeakMap() +// let requiredVersion = new WeakMap() + +// const throwIfNoIdentity = () => { +// if(!publicKey) throws('There is no identity with an account set on your Scatter instance.') +// } + + +// const locationHost = () => strippedHost() + +// /*** +// * Messages do not come back on the same thread. +// * To accomplish a future promise structure this method +// * catches all incoming messages and dispenses +// * them to the open promises. */ +// const _subscribe = () => { +// stream.listenWith(msg => { +// if(!msg || !msg.hasOwnProperty('type')) return false +// for(let i=0 i < resolvers.length i++) { +// if (resolvers[i].id === msg.resolver) { +// if(msg.type === 'error') resolvers[i].reject(msg.payload) +// else resolvers[i].resolve(msg.payload) +// resolvers = resolvers.slice(i, 1) +// } +// } +// }) +// } + +// /*** +// * Turns message sending between the application +// * and the content script into async promises +// * @param _type +// * @param _payload +// */ +// const _send = (_type, _payload) => { +// return new Promise((resolve, reject) => { + +// // Version requirements +// if(!!requiredVersion && requiredVersion > currentVersion){ +// let message = new NetworkMessage(NetworkMessageTypes.REQUEST_VERSION_UPDATE, {}, -1) +// stream.send(message, PairingTags.SCATTER) +// reject(Error.requiresUpgrade()) +// return false +// } + +// let id = IdGenerator.numeric(24) +// let message = new NetworkMessage(_type, _payload, id) +// resolvers.push(new DanglingResolver(id, resolve, reject)) +// stream.send(message, PairingTags.SCATTER) +// }) +// } + +// const setupSigProviders = context => { +// PluginRepository.signatureProviders().map(sigProvider => { +// context[sigProvider.name] = sigProvider.signatureProvider(_send, throwIfNoIdentity) +// }) +// } + +// /*** +// * Scatterdapp is the object injected into the web application that +// * allows it to interact with Scatter. Without using this the web application +// * has no access to the extension. +// */ +// export default class Scatterdapp { + +// constructor(_stream, _options){ +// currentVersion = parseFloat(_options.version) +// this.useIdentity(_options.identity) +// stream = _stream +// resolvers = [] + +// setupSigProviders(this) + +// _subscribe() +// } + +// useIdentity(identity){ +// this.identity = identity +// publicKey = identity ? identity.publicKey : '' +// } + +// /*** +// * Suggests the set network to the user's Scatter. +// */ +// suggestNetwork(network){ +// if(!Network.fromJson(network).isValid()) throws('The provided network is invalid.') +// return _send(NetworkMessageTypes.REQUEST_ADD_NETWORK, { +// network:network +// }) +// } + +// /*** +// * Gets an Identity from the user to use. +// * @param fields - You can specify required fields such as ['email', 'country', 'firstname'] +// */ +// getIdentity(fields = {}){ +// return _send(NetworkMessageTypes.GET_OR_REQUEST_IDENTITY, { +// network:network, +// fields +// }).then(async (identity) => { +// this.useIdentity(identity) +// return identity +// }) +// } + +// /*** +// * Authenticates the identity on scope +// * Returns a signature which can be used to self verify against the domain name +// * @returns {Promise.} +// */ +// async authenticate(){ +// throwIfNoIdentity() + +// // TODO: Verify identity matches RIDL registration + +// const signature = await _send(NetworkMessageTypes.AUTHENTICATE, { +// publicKey +// }, true).catch(err => err) + +// // If the `signature` is an object, it's an error message +// if(typeof signature === 'object') return signature + +// try { if(ecc.verify(signature, strippedHost(), publicKey)) return signature } +// catch (e) { +// this.identity = null +// publicKey = '' +// throws('Could not authenticate identity') +// } +// } + +// /*** +// * Signs out the identity. +// * @returns {Promise.} +// */ +// forgetIdentity(){ +// throwIfNoIdentity() +// return _send(NetworkMessageTypes.FORGET_IDENTITY, {}).then(() => { +// this.identity = null +// publicKey = null +// return true +// }) +// } + +// /*** +// * Sets a version requirement. If the version is not met all +// * scatter requests will fail and notify the user of the reason. +// * @param _version +// */ +// requireVersion(_version){ +// requiredVersion = _version +// } + +// /*** +// * Requests a signature for arbitrary data. +// * @param publicKey +// * @param data - The data to be signed +// * @param whatfor +// * @param isHash - True if the data requires a hash signature +// */ +// getArbitrarySignature(publicKey, data, whatfor = '', isHash = false){ +// return _send(NetworkMessageTypes.REQUEST_ARBITRARY_SIGNATURE, { +// publicKey, +// data, +// whatfor, +// isHash +// }, true) +// } + +// } diff --git a/shared/reducers/balance.ts b/shared/reducers/balance.ts index ea223cffa..31f391642 100644 --- a/shared/reducers/balance.ts +++ b/shared/reducers/balance.ts @@ -32,7 +32,7 @@ export default handleActions({ const balanceInfo = action.payload.balanceInfo if (v.has(eosAccountName)) { return v.update(eosAccountName, (v: any) => { - const index = v.findIndex((v: any) => v.get('contract') === balanceInfo.contract) + const index = v.findIndex((v: any) => v.get('contract') === balanceInfo.contract && v.get('symbol') === balanceInfo.symbol) return index === -1 ? v.push(Immutable.fromJS(balanceInfo)) : v.set(index, Immutable.fromJS(balanceInfo)) }) } diff --git a/shared/sagas/balance.ts b/shared/sagas/balance.ts index e2682c333..44f756a6d 100644 --- a/shared/sagas/balance.ts +++ b/shared/sagas/balance.ts @@ -38,16 +38,18 @@ function* getEOSAssetBalanceRequested(action: Action) { try { const eosAccountName = action.payload.eosAccountName const code = action.payload.code + const symbol = action.payload.symbol const eosAccountCreationInfo = yield select((state: RootState) => state.eosAccount.get('eosAccountCreationInfo')) const useCreationServer = eosAccountCreationInfo.get('transactionId') && eosAccountCreationInfo.get('eosAccountName') === eosAccountName && !eosAccountCreationInfo.get('irreversible') const eos = yield call(initEOS, useCreationServer ? { httpEndpoint: BITPORTAL_API_EOS_URL } : {}) const data = yield call(eos.getCurrencyBalance, { code, account: eosAccountName }) - assert(data && data[0] && typeof data[0] === 'string', 'No balance!') + assert(data && data.length, 'No balance!') + const balanceData = data.filter((item: string) => item.split(' ')[1] === symbol) + assert(balanceData.length, 'No balance!') - const symbol = data[0].split(' ')[1] - const balance = data[0].split(' ')[0] + const balance = balanceData[0].split(' ')[0] const blockchain = 'EOS' const contract = code const balanceInfo = { symbol, balance, contract, blockchain } @@ -70,7 +72,7 @@ function* getEOSAssetBalanceListRequested(action: Action selectedEOSAssetSelector(state)) const selectedEOSAssetListArray = selectedEOSAssetList.toJS() for (const asset of selectedEOSAssetListArray) { - yield put(actions.getEOSAssetBalanceRequested({ eosAccountName, code: asset.contract })) + yield put(actions.getEOSAssetBalanceRequested({ eosAccountName, code: asset.contract, symbol: asset.symbol })) } // const data = yield all(selectedEOSAssetList.toJS().map((selectedEOSAsset: { contract: string }) => call(eos.getCurrencyBalance, { code: selectedEOSAsset.contract, account: eosAccountName }))) diff --git a/shared/sagas/transfer.ts b/shared/sagas/transfer.ts index 2613a624a..009101ab2 100644 --- a/shared/sagas/transfer.ts +++ b/shared/sagas/transfer.ts @@ -38,7 +38,7 @@ function* transfer(action: Action) { if (contract === 'eosio.token') { yield put(getEOSBalanceRequested({ eosAccountName: fromAccount })) } else { - yield put(getEOSAssetBalanceRequested({ code: contract, eosAccountName: fromAccount })) + yield put(getEOSAssetBalanceRequested({ symbol, code: contract, eosAccountName: fromAccount })) } } catch (e) { yield put(actions.transferFailed(getEOSErrorMessage(e))) diff --git a/shared/screens/Assets/AssetChart/index.jsx b/shared/screens/Assets/AssetChart/index.jsx index 6554a669e..fe097b0bc 100644 --- a/shared/screens/Assets/AssetChart/index.jsx +++ b/shared/screens/Assets/AssetChart/index.jsx @@ -158,7 +158,7 @@ export default class AssetChart extends Component { componentDidMount() { this.onRefresh() const { activeAsset, eosAccountName } = this.props - this.props.actions.getEOSAssetBalanceRequested({ code: activeAsset.get('contract'), eosAccountName }) + this.props.actions.getEOSAssetBalanceRequested({ code: activeAsset.get('contract'), eosAccountName, symbol: activeAsset.get('symbol') }) } render() { diff --git a/shared/selectors/balance.ts b/shared/selectors/balance.ts index f23a9be6e..a5de1f536 100644 --- a/shared/selectors/balance.ts +++ b/shared/selectors/balance.ts @@ -39,7 +39,8 @@ export const selectedEOSTokenBalanceSelector = createSelector( (eosTokenBalance: any, selectedEOSAsset: any) => selectedEOSAsset.map((v: any) => { if (eosTokenBalance) { const contract = v.get('contract') - const index = eosTokenBalance.findIndex((v: any) => v.get('contract') === contract) + const symbol = v.get('symbol') + const index = eosTokenBalance.findIndex((v: any) => v.get('contract') === contract && v.get('symbol') === symbol) return index !== -1 ? v.set('balance', eosTokenBalance.getIn([index, 'balance'])) : v.set('balance', '0.0000') } @@ -67,10 +68,12 @@ export const eosTotalAssetBalanceSelector = createSelector( export const activeAssetBalanceSelector = createSelector( eosAssetBalanceSelector, - activeAssetContractSelector, - (eosAssetBalance: any, contract: any) => { + activeAssetSelector, + (eosAssetBalance: any, activeAsset: any) => { if (eosAssetBalance) { - const index = eosAssetBalance.findIndex((v: any) => v.get('contract') === contract) + const contract = activeAsset.get('contract') + const symbol = activeAsset.get('symbol') + const index = eosAssetBalance.findIndex((v: any) => v.get('contract') === contract && v.get('symbol') === symbol) return index !== -1 ? eosAssetBalance.getIn([index, 'balance']) : 0 } else { return 0 diff --git a/shared/selectors/transaction.ts b/shared/selectors/transaction.ts index 87c229d3f..e9751a501 100644 --- a/shared/selectors/transaction.ts +++ b/shared/selectors/transaction.ts @@ -1,10 +1,15 @@ import { createSelector } from 'reselect' -import { activeAssetContractSelector } from 'selectors/balance' +import { activeAssetSelector } from 'selectors/balance' export const transferTransactionsSelector = (state: RootState) => state.transaction.get('data').filter((transaction: any) => transaction.getIn(['action_trace', 'act', 'name']) === 'transfer') export const activeAssetTransactionsSelector = createSelector( - activeAssetContractSelector, + activeAssetSelector, transferTransactionsSelector, - (contract: string, transactions: any) => transactions.filter((v: any) => v.getIn(['action_trace', 'act', 'account']) === contract) + (activeAsset: any, transactions: any) => { + const contract = activeAsset.get('contract') + const symbol = activeAsset.get('symbol') + + return transactions.filter((v: any) => v.getIn(['action_trace', 'act', 'account']) === contract && v.getIn(['action_trace', 'act', 'data', 'quantity']) && v.getIn(['action_trace', 'act', 'data', 'quantity']).indexOf(symbol) !== -1) + } ) diff --git a/shared/types/balance.d.ts b/shared/types/balance.d.ts index 9d1c6f182..f6beedc2f 100644 --- a/shared/types/balance.d.ts +++ b/shared/types/balance.d.ts @@ -10,6 +10,7 @@ declare interface GetEOSBalanceResult { declare interface GetAssetBalanceParams { eosAccountName: string code: string + symbol: string } declare interface GetAssetBalanceResult {