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 + '' + html.h1.split(' ')[0] + '>');
+ 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' + html.h1.split(' ')[0] + '>');
+ ricardianContract = ricardianContract.replace('## Description', '<' + html.h2 + '>Description' + html.h2.split(' ')[0] + '>');
+ 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 {