Skip to content

Commit 4c44e6f

Browse files
authored
Merge pull request #702 from nos/develop
[Release] 0.4.5
2 parents a317f74 + 71bfbb8 commit 4c44e6f

File tree

15 files changed

+170
-57
lines changed

15 files changed

+170
-57
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "nOS",
33
"description": "nOS: NEO Operating System",
44
"author": "nOS",
5-
"version": "0.4.4",
5+
"version": "0.4.5",
66
"private": true,
77
"main": "dist/main/main.js",
88
"license": "MIT",

src/common/util/getRPCEndpoint.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default async function getRPCEndpoint(net) {
6666
throw new Error('No eligible nodes found!');
6767
}
6868

69-
const heightThreshold = nodes[0].height - 1;
69+
const heightThreshold = goodNodes[0].height - 1;
7070
const highestNodes = goodNodes.filter((n) => n.height >= heightThreshold);
7171
const urls = highestNodes.map((n) => n.url);
7272

src/renderer/account/components/Account/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import currencyActions from 'settings/actions/currencyActions';
1010
import authActions from 'login/actions/authActions';
1111
import withInitialCall from 'shared/hocs/withInitialCall';
1212
import withNetworkData from 'shared/hocs/withNetworkData';
13-
import { ASSETS } from 'shared/values/assets';
13+
import { ASSETS, NOS } from 'shared/values/assets';
1414

1515
import Account from './Account';
1616
import pricesActions from '../../actions/pricesActions';
@@ -23,7 +23,7 @@ const mapCurrencyDataToProps = (currency) => ({ currency });
2323

2424
const mapBalancesDataToProps = (balances) => ({
2525
balances: pickBy(balances, ({ scriptHash, balance }) => {
26-
return keys(ASSETS).includes(scriptHash) || balance !== '0';
26+
return keys(ASSETS).includes(scriptHash) || scriptHash === NOS || balance !== '0';
2727
})
2828
});
2929

src/renderer/account/components/AccountPanel/AccountPanel.scss

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
$border-width: 1px;
44

55
display: flex;
6+
align-items: flex-start;
67

78
.summary {
89
flex: 1 1 auto;
Original file line numberDiff line numberDiff line change
@@ -1 +1,28 @@
1-
export { default } from './Holdings';
1+
import { withProps } from 'recompose';
2+
import { sortBy } from 'lodash';
3+
4+
import { NOS, NEO, GAS } from 'shared/values/assets';
5+
6+
import Holdings from './Holdings';
7+
8+
/**
9+
* Sort by:
10+
* 1. preferred tokens
11+
* 2. token symbol (ascending)
12+
*/
13+
const customSort = (balances, preferredOrder) => {
14+
return sortBy(balances, (token) => {
15+
const precidence = preferredOrder.findIndex((v) => v === token.scriptHash);
16+
17+
return [
18+
precidence < 0 ? Infinity : precidence,
19+
token.symbol
20+
];
21+
});
22+
};
23+
24+
const sortBalances = ({ balances }) => ({
25+
balances: customSort(balances, [NOS, NEO, GAS])
26+
});
27+
28+
export default withProps(sortBalances)(Holdings);

src/renderer/account/components/AccountPanel/TokenBalance/TokenBalance.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import TokenIcon from 'shared/components/TokenIcon';
77

88
import balanceShape from '../../../shapes/balanceShape';
99
import formatCurrency from '../../../util/formatCurrency';
10+
import formatBalance from '../../../util/formatBalance';
1011
import ClaimButton from '../ClaimButton';
1112
import styles from './TokenBalance.scss';
1213

@@ -40,7 +41,9 @@ export default class TokenBalance extends React.PureComponent {
4041
<div className={styles.token}>
4142
{this.renderImage()}
4243
<div className={styles.detail}>
43-
<div className={styles.balance}>{token.balance} {token.symbol}</div>
44+
<div className={styles.balance}>
45+
{formatBalance(token.balance, token.decimals)} {token.symbol}
46+
</div>
4447
<div className={styles.currency}>
4548
<span className={styles.tokenValue}>
4649
{formatCurrency(price, currency)}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import BigNumber from 'bignumber.js';
2+
3+
export default function formatBalance(balance, decimals) {
4+
const value = new BigNumber(balance).toFormat(decimals);
5+
const [integer, fraction] = value.split('.');
6+
7+
return [
8+
integer,
9+
`.${fraction || 0}`.replace(/\.?0+$/, '')
10+
].join('');
11+
}
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,51 @@
11
import { createActions } from 'spunky';
2-
import { rpc, wallet } from '@cityofzion/neon-js';
2+
import { rpc } from '@cityofzion/neon-js';
33
import { isArray } from 'lodash';
44

55
import getRPCEndpoint from 'util/getRPCEndpoint';
66

77
import createScript from 'shared/util/createScript';
88

99
import generateDAppActionId from './generateDAppActionId';
10+
import createArrayScript from '../util/createArrayScript';
11+
import validateScriptArgs from '../util/validateScriptArgs';
1012

1113
export const ID = 'testInvoke';
1214

13-
const testInvoke = async ({ net, scriptHash, operation, args, encodeArgs }) => {
14-
if (!wallet.isScriptHash(scriptHash)) {
15-
throw new Error(`Invalid script hash: "${scriptHash}"`);
16-
}
17-
18-
if (typeof operation !== 'string') {
19-
throw new Error(`Invalid operation: "${operation}"`);
20-
}
21-
22-
if (!isArray(args)) {
23-
throw new Error(`Invalid arguments: "${args}"`);
15+
const testInvoke = async ({ net, scriptHash, operation, args, script, encodeArgs }) => {
16+
let invokeScript;
17+
18+
if (scriptHash && operation && args) {
19+
validateScriptArgs({ scriptHash, operation, args });
20+
invokeScript = createScript(scriptHash, operation, args, encodeArgs);
21+
} else if (script) {
22+
if (typeof script !== 'string' && !isArray(script)) {
23+
throw new Error(`Invalid script: "${script}"`);
24+
}
25+
26+
if (typeof script === 'string') {
27+
invokeScript = script;
28+
} else if (isArray(script)) {
29+
script.forEach(validateScriptArgs);
30+
invokeScript = createArrayScript(script, encodeArgs);
31+
}
32+
} else {
33+
throw new Error('Invalid config!');
2434
}
2535

2636
const endpoint = await getRPCEndpoint(net);
27-
const script = createScript(scriptHash, operation, args, encodeArgs);
28-
const { result } = await rpc.Query.invokeScript(script).execute(endpoint);
37+
const { result } = await rpc.Query.invokeScript(invokeScript).execute(endpoint);
2938

3039
return result;
3140
};
3241

3342
export default function makeTestInvokeActions(sessionId, requestId) {
3443
const id = generateDAppActionId(sessionId, `${ID}-${requestId}`);
3544

36-
return createActions(id, ({ net, scriptHash, operation, args, encodeArgs = true }) => () => {
37-
return testInvoke({ net, scriptHash, operation, args, encodeArgs });
38-
});
45+
return createActions(
46+
id,
47+
({ net, scriptHash, operation, args, script, encodeArgs = true }) => () => {
48+
return testInvoke({ net, scriptHash, operation, args, script, encodeArgs });
49+
}
50+
);
3951
}

src/renderer/browser/components/RequestsProcessor/TestInvoke/index.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import withRejectMessage from '../../../hocs/withRejectMessage';
1212

1313
const mapInvokeDataToProps = (result) => ({ result });
1414

15-
const CONFIG_KEYS = ['scriptHash', 'operation', 'args', 'encodeArgs'];
15+
const CONFIG_KEYS = ['scriptHash', 'operation', 'args', 'script', 'encodeArgs'];
1616

1717
export default function makeTestInvoke(testInvokeActions) {
1818
return compose(
@@ -26,13 +26,17 @@ export default function makeTestInvoke(testInvokeActions) {
2626
withNetworkData(),
2727

2828
// Run the test invoke & wait for success or failure
29-
withInitialCall(testInvokeActions, ({ net, scriptHash, operation, args, encodeArgs }) => ({
30-
net,
31-
scriptHash,
32-
operation,
33-
args,
34-
encodeArgs
35-
})),
29+
withInitialCall(
30+
testInvokeActions,
31+
({ net, scriptHash, operation, args, script, encodeArgs }) => ({
32+
net,
33+
scriptHash,
34+
operation,
35+
args,
36+
script,
37+
encodeArgs
38+
})
39+
),
3640
withNullLoader(testInvokeActions),
3741
withRejectMessage(testInvokeActions, ({ operation, scriptHash, error }) => (
3842
`Invocation failed for operation "${operation}" on "${scriptHash}": ${error}`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Neon from '@cityofzion/neon-js';
2+
3+
import encode from 'shared/util/encodeArgs';
4+
5+
const createArrayScript = (scripts, encodeArgs) => {
6+
const invokeScripts = scripts.map(({ args, ...restProps }) => {
7+
return { ...restProps, args: encodeArgs ? encode(args) : args };
8+
});
9+
10+
// Create script
11+
return Neon.create.script(invokeScripts);
12+
};
13+
14+
export default createArrayScript;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { wallet } from '@cityofzion/neon-js';
2+
import { isArray } from 'lodash';
3+
4+
export default ({ scriptHash, operation, args }) => {
5+
if (!wallet.isScriptHash(scriptHash)) {
6+
throw new Error(`Invalid script hash: "${scriptHash}"`);
7+
}
8+
if (typeof operation !== 'string') {
9+
throw new Error(`Invalid operation: "${operation}"`);
10+
}
11+
if (!isArray(args)) {
12+
throw new Error(`Invalid arguments: "${args}"`);
13+
}
14+
};

src/renderer/root/components/AuthenticatedLayout/AuthenticatedLayout.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Navigation from './Navigation';
1313
import AddressBar from './AddressBar';
1414
import styles from './AuthenticatedLayout.scss';
1515

16-
const POLL_FREQUENCY = 10000; // 10 seconds
16+
const POLL_FREQUENCY = 14000; // milliseconds
1717

1818
export default class AuthenticatedLayout extends React.PureComponent {
1919
static propTypes = {
+48-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,54 @@
1-
import { extend, get, find } from 'lodash';
2-
import { api, rpc, wallet } from '@cityofzion/neon-js';
1+
import { get, find, map, mapValues, chunk, filter, extend } from 'lodash';
2+
import { u, sc, rpc, wallet } from '@cityofzion/neon-js';
33

44
import getRPCEndpoint from 'util/getRPCEndpoint';
55

66
import getTokens from './getTokens';
77
import { GAS, NEO, ASSETS } from '../values/assets';
88

9-
async function getTokenBalance(endpoint, token, address) {
10-
try {
11-
const response = await api.nep5.getToken(endpoint, token.scriptHash, address);
12-
const balance = (response.balance || 0).toString();
13-
14-
return {
15-
[token.scriptHash]: { ...token, ...response, balance }
16-
};
17-
} catch (err) {
18-
// invalid scriptHash
19-
return {};
20-
}
9+
const CHUNK_SIZE = 18;
10+
11+
function parseHexNum(hex) {
12+
return hex ? parseInt(u.reverseHex(hex), 16) : 0;
13+
}
14+
15+
function getRawTokenBalances(url, tokens, address) {
16+
const addrScriptHash = u.reverseHex(wallet.getScriptHashFromAddress(address));
17+
const sb = new sc.ScriptBuilder();
18+
19+
tokens.forEach(({ scriptHash }) => {
20+
sb.emitAppCall(scriptHash, 'balanceOf', [addrScriptHash]);
21+
});
22+
23+
return rpc.Query.invokeScript(sb.str, false)
24+
.execute(url)
25+
.then((res) => {
26+
const tokenList = {};
27+
28+
if (res && res.result && res.result.stack && res.result.stack.length >= 1) {
29+
for (let i = 0; i < res.result.stack.length; i += 1) {
30+
const { scriptHash, decimals } = tokens[i];
31+
const value = parseHexNum(res.result.stack[i].value);
32+
tokenList[scriptHash] = new u.Fixed8(value).div(10 ** decimals).toString();
33+
}
34+
}
35+
return tokenList;
36+
});
37+
}
38+
39+
async function getTokenBalances(endpoint, address) {
40+
const tokens = await getTokens();
41+
const chunks = chunk(map(tokens, 'scriptHash'), CHUNK_SIZE);
42+
43+
const balances = extend({}, ...await Promise.all(chunks.map((scriptHashes) => {
44+
const filteredTokens = filter(tokens, (token) => scriptHashes.includes(token.scriptHash));
45+
return getRawTokenBalances(endpoint, filteredTokens, address);
46+
})));
47+
48+
return mapValues(balances, (balance, scriptHash) => ({
49+
...find(tokens, { scriptHash }),
50+
balance
51+
}));
2152
}
2253

2354
async function getAssetBalances(endpoint, address) {
@@ -40,13 +71,8 @@ export default async function getBalances({ net, address }) {
4071
throw new Error(`Invalid address: "${address}"`);
4172
}
4273

43-
// token balances - // TODO use getTokenBalances to avoid multiple api calls
44-
const promises = (await getTokens(net)).map((token) => {
45-
return getTokenBalance(endpoint, token, address);
46-
});
47-
48-
// asset balances
49-
promises.unshift(getAssetBalances(endpoint, address));
74+
const assets = await getAssetBalances(endpoint, address);
75+
const tokens = await getTokenBalances(endpoint, address);
5076

51-
return extend({}, ...await Promise.all(promises));
77+
return { ...assets, ...tokens };
5278
}

src/renderer/shared/util/getTokens.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ function normalizeImage(src) {
1111
return trim(src) === '' ? null : src;
1212
}
1313

14-
export default async function getTokens(net) {
14+
export default async function getTokens(net = 'MainNet') {
1515
const networkKey = NETWORK_MAP[net];
1616

1717
const response = await fetch(TOKENS_URL);
1818
const tokens = values(await response.json());
1919
const networkTokens = filter(tokens, (token) => token.networks[networkKey]);
2020
const sortedTokens = sortBy(networkTokens, 'symbol');
2121

22-
return sortedTokens.map(({ image, networks }) => {
22+
return sortedTokens.map(({ image, symbol, networks }) => {
2323
const { name, hash: scriptHash, decimals, totalSupply } = networks[networkKey];
24-
return { name, scriptHash, decimals, totalSupply, image: normalizeImage(image) };
24+
return { name, symbol, scriptHash, decimals, totalSupply, image: normalizeImage(image) };
2525
});
2626
}

src/renderer/shared/values/assets.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export const NOS = 'c9c0fc5a2b66a29d6b14601e752e6e1a445e088d';
12
export const NEO = 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b';
23
export const GAS = '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7';
34

0 commit comments

Comments
 (0)