- {address &&
- routesWithSideBar.includes(location.pathname) && (
-
- )}
-
-
{children}
-
-
+
+
+ {address &&
+ routesWithSideBar.includes(location.pathname) && (
+
+ )}
+
-
+
)
}
}
diff --git a/app/containers/Buttons/RefreshButton/RefreshButton.jsx b/app/containers/Buttons/RefreshButton/RefreshButton.jsx
index 000e5b981..e6363074d 100644
--- a/app/containers/Buttons/RefreshButton/RefreshButton.jsx
+++ b/app/containers/Buttons/RefreshButton/RefreshButton.jsx
@@ -7,7 +7,7 @@ import RefreshIcon from '../../../assets/icons/refresh.svg'
import styles from './RefreshButton.scss'
type Props = {
- loadWalletData: () => void,
+ loadWalletData?: () => void,
loading?: boolean,
}
diff --git a/app/containers/TransactionHistory/TransactionHistory.jsx b/app/containers/TransactionHistory/TransactionHistory.jsx
index 2a8bb7323..4e28779c7 100644
--- a/app/containers/TransactionHistory/TransactionHistory.jsx
+++ b/app/containers/TransactionHistory/TransactionHistory.jsx
@@ -1,15 +1,168 @@
-import React from 'react'
+// @flow
+import React, { Component } from 'react'
+import fs from 'fs'
+import { Parser } from 'json2csv'
+import { api } from '@cityofzion/neon-js'
+import axios from 'axios'
+import { omit } from 'lodash-es'
+import moment from 'moment'
import HeaderBar from '../../components/HeaderBar'
import TransactionHistoryPanel from '../../components/TransactionHistory/TransactionHistoryPanel'
-
import styles from './TransactionHistory.scss'
+import RefreshButton from '../Buttons/RefreshButton'
+import ExportIcon from '../../assets/icons/export.svg'
+import PanelHeaderButton from '../../components/PanelHeaderButton/PanelHeaderButton'
+import { parseAbstractData } from '../../actions/transactionHistoryActions'
+
+const { dialog, app } = require('electron').remote
+
+type Props = {
+ showSuccessNotification: ({ message: string }) => string,
+ showErrorNotification: ({ message: string }) => string,
+ showInfoNotification: ({ message: string }) => string,
+ hideNotification: string => void,
+ net: string,
+ address: string,
+}
+
+type State = {
+ isExporting: boolean,
+}
-export default function TransactionHistory() {
- return (
-
-
-
+export default class TransactionHistory extends Component
{
+ state = {
+ isExporting: false,
+ }
+
+ render() {
+ return (
+
+ this.renderPanelHeaderContent()}
+ />
+
+
+ )
+ }
+
+ renderPanelHeaderContent = () => (
+
+
}
+ buttonText="Export"
+ />
+
)
+
+ fetchHistory = async () => {
+ const { showInfoNotification, net, address } = this.props
+ const infoNotification = showInfoNotification({
+ message: 'Fetching entire transaction history...',
+ })
+ const abstracts = []
+ const endpoint = api.neoscan.getAPIEndpoint(net)
+ let numberOfPages = 1
+ let currentPage = 1
+
+ while (currentPage !== numberOfPages || currentPage === 1) {
+ const { data } = await axios.get(
+ `${endpoint}/v1/get_address_abstracts/${address}/${currentPage}`,
+ )
+ abstracts.push(...data.entries)
+ numberOfPages = data.total_pages
+ currentPage += 1
+ }
+
+ const parsedAbstracts = await parseAbstractData(abstracts, address, net)
+ return { infoNotification, parsedAbstracts }
+ }
+
+ saveHistoryFile = async () => {
+ this.setState({
+ isExporting: true,
+ })
+ const {
+ showErrorNotification,
+ showSuccessNotification,
+ hideNotification,
+ } = this.props
+ const fields = [
+ 'to',
+ 'from',
+ 'txid',
+ 'time',
+ 'amount',
+ 'symbol',
+ 'type',
+ 'id',
+ ]
+ try {
+ const { infoNotification, parsedAbstracts } = await this.fetchHistory()
+ const parser = new Parser(fields)
+ const csv = parser.parse(
+ parsedAbstracts.map(abstract => {
+ const omitted = omit(abstract, [
+ 'isNetworkFee',
+ 'asset',
+ 'image',
+ 'label',
+ ])
+ omitted.time = moment
+ .unix(omitted.time)
+ .format('MM/DD/YYYY | HH:mm:ss')
+ return omitted
+ }),
+ )
+ hideNotification(infoNotification)
+ dialog.showSaveDialog(
+ {
+ defaultPath: `${app.getPath(
+ 'documents',
+ )}/neon-wallet-activity-${moment().unix()}.csv`,
+ filters: [
+ {
+ name: 'CSV',
+ extensions: ['csv'],
+ },
+ ],
+ },
+ fileName => {
+ this.setState({
+ isExporting: false,
+ })
+ if (fileName === undefined) {
+ return
+ }
+ // fileName is a string that contains the path and filename created in the save file dialog.
+ fs.writeFile(fileName, csv, errorWriting => {
+ if (errorWriting) {
+ showErrorNotification({
+ message: `An error occurred creating the file: ${
+ errorWriting.message
+ }`,
+ })
+ } else {
+ showSuccessNotification({
+ message: 'The file has been succesfully saved',
+ })
+ }
+ })
+ },
+ )
+ } catch (err) {
+ console.error(err)
+ this.setState({
+ isExporting: false,
+ })
+ showErrorNotification({
+ message: `An error occurred creating the file: ${err.message}`,
+ })
+ }
+ }
}
diff --git a/app/containers/TransactionHistory/TransactionHistory.scss b/app/containers/TransactionHistory/TransactionHistory.scss
index cf0da5226..9a215cf5d 100644
--- a/app/containers/TransactionHistory/TransactionHistory.scss
+++ b/app/containers/TransactionHistory/TransactionHistory.scss
@@ -7,3 +7,19 @@
flex: 1 1 auto;
}
}
+
+.panelHeaderButtons {
+ width: 225px;
+ display: flex;
+ justify-content: space-between;
+}
+
+.exportButton {
+ font-size: 16px !important;
+
+ svg {
+ path {
+ fill: var(--header-bar-default-icon-color);
+ }
+ }
+}
diff --git a/app/containers/TransactionHistory/index.js b/app/containers/TransactionHistory/index.js
index 8d3e92e06..5ab206231 100644
--- a/app/containers/TransactionHistory/index.js
+++ b/app/containers/TransactionHistory/index.js
@@ -1,6 +1,8 @@
// @flow
import { compose } from 'recompose'
import { withCall } from 'spunky'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
import TransactionHistory from './TransactionHistory'
import transactionHistoryActions from '../../actions/transactionHistoryActions'
@@ -10,8 +12,28 @@ import balancesActions from '../../actions/balancesActions'
import withLoadingProp from '../../hocs/withLoadingProp'
import withSuccessNotification from '../../hocs/withSuccessNotification'
import withFailureNotification from '../../hocs/withFailureNotification'
+import {
+ showErrorNotification,
+ showSuccessNotification,
+ showInfoNotification,
+ hideNotification,
+} from '../../modules/notifications'
+
+const actionCreators = {
+ showErrorNotification,
+ showSuccessNotification,
+ showInfoNotification,
+ hideNotification,
+}
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(actionCreators, dispatch)
export default compose(
+ connect(
+ null,
+ mapDispatchToProps,
+ ),
withNetworkData(),
withAuthData(),
withLoadingProp(balancesActions),
diff --git a/app/core/nep5.js b/app/core/nep5.js
index 54740b679..97e8f7db6 100644
--- a/app/core/nep5.js
+++ b/app/core/nep5.js
@@ -1,5 +1,6 @@
// @flow
-import { map } from 'lodash-es'
+import { map, isEmpty } from 'lodash-es'
+import axios from 'axios'
import { toBigNumber } from './math'
import { COIN_DECIMAL_LENGTH } from './formatters'
@@ -11,6 +12,8 @@ import {
} from './constants'
import { imageMap } from '../assets/nep5/png'
+let fetchedTokens
+
export const adjustDecimalAmountForTokenTransfer = (value: string): string =>
toBigNumber(value)
.times(10 ** COIN_DECIMAL_LENGTH)
@@ -19,36 +22,54 @@ export const adjustDecimalAmountForTokenTransfer = (value: string): string =>
const getTokenEntry = ((): Function => {
let id = 1
+
return (
symbol: string,
scriptHash: string,
networkId: string,
name: string,
decimals: number,
+ networkData: Object = {},
) => ({
id: `${id++}`, // eslint-disable-line no-plusplus
symbol,
scriptHash,
networkId,
isUserGenerated: false,
+ totalSupply: networkData.totalSupply,
+ decimals: networkData.decimals,
image: imageMap[symbol],
- name,
- decimals,
})
})()
export const getDefaultTokens = async (): Promise> => {
const tokens = []
+ // Prevent duplicate requests here
+ if (!fetchedTokens) {
+ const response = await axios
+ // use a time stamp query param to prevent caching
+ .get(
+ `https://raw.githubusercontent.com/CityOfZion/neo-tokens/master/tokenList.json?timestamp=${new Date().getTime()}`,
+ )
+ .catch(error => {
+ console.error('Falling back to hardcoded list of NEP5 tokens!', error)
+ // if request to gh fails use hardcoded list
+ fetchedTokens = TOKENS
+ })
+ if (response && response.data && !isEmpty(response.data)) {
+ fetchedTokens = response.data
+ } else {
+ fetchedTokens = TOKENS
+ }
+ }
tokens.push(
- ...map(TOKENS, tokenData =>
+ ...map(fetchedTokens, tokenData =>
getTokenEntry(
tokenData.symbol,
tokenData.networks['1'].hash,
MAIN_NETWORK_ID,
-
- tokenData.networks['1'].name,
- tokenData.networks['1'].decimals,
+ tokenData.networks['1'],
),
),
)
diff --git a/app/core/tokenList.json b/app/core/tokenList.json
index fd7fdaa04..51e4b7de3 100644
--- a/app/core/tokenList.json
+++ b/app/core/tokenList.json
@@ -784,16 +784,16 @@
"image":
"https://rawgit.com/CityOfZion/neo-tokens/master/assets/svg/tnc.svg"
},
- "TOLL": {
- "symbol": "TOLL",
+ "BRDG": {
+ "symbol": "BRDG",
"companyName": "Bridge Protocol",
"type": "NEP5",
"networks": {
"1": {
"name": "Bridge Protocol",
- "hash": "78fd589f7894bf9642b4a573ec0e6957dfd84c48",
+ "hash": "bac0d143a547dc66a1d6a2b7d66b06de42614971",
"decimals": 8,
- "totalSupply": 708097040
+ "totalSupply": 450000000
}
}
},
diff --git a/package.json b/package.json
index 7e389fe2f..671f1b03d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Neon",
- "version": "2.1.0",
+ "version": "2.2.0",
"main": "./main.js",
"description": "Light wallet for NEO blockchain",
"homepage": "https://github.com/CityOfZion/neon-wallet",
@@ -92,6 +92,7 @@
"howler": "2.0.15",
"instascan": "1.0.0",
"isomorphic-fetch": "2.2.1",
+ "json2csv": "^4.3.5",
"jsqr": "1.1.1",
"lodash-es": "4.17.11",
"moment": "2.22.2",
diff --git a/yarn.lock b/yarn.lock
index d1df16b48..4f641f038 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3325,7 +3325,7 @@ commander@2.17.x, commander@~2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
-commander@^2.11.0, commander@^2.15.0, commander@^2.9.0:
+commander@^2.11.0, commander@^2.15.0, commander@^2.15.1, commander@^2.9.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
@@ -7522,6 +7522,14 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+json2csv@^4.3.5:
+ version "4.3.5"
+ resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-4.3.5.tgz#859c82c361cb12cca64fc012be96bd2c33d4860e"
+ dependencies:
+ commander "^2.15.1"
+ jsonparse "^1.3.1"
+ lodash.get "^4.4.2"
+
json3@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
@@ -7558,6 +7566,10 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+jsonparse@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -7842,7 +7854,7 @@ lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
-lodash.get@^4.0.0:
+lodash.get@^4.0.0, lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"