diff --git a/package.json b/package.json
index a11d765..1597eb2 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@coingecko/cryptoformat": "^0.3.1",
"@oceanprotocol/typographies": "^0.1.0",
"@reach/router": "^1.2.1",
+ "ethereum-address": "0.0.4",
"ms": "^2.1.1",
"react": "^16.8.6",
"react-blockies": "^1.4.1",
diff --git a/src/App.css b/src/App.css
index 1097e88..105dfc8 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,5 +1,3 @@
-@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
-
*,
*::before,
*::after {
@@ -54,23 +52,16 @@ html.fullscreen {
h1,
h2,
h3,
-h4 {
- font-family: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
- 'Segoe UI', Helvetica, Arial, sans-serif;
- font-weight: 600;
-}
-
-button {
- font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
- Helvetica, Arial, sans-serif;
- font-weight: 600;
+h4,
+h5 {
+ font-weight: 700;
}
.app {
margin-top: 35px;
padding: 5% 7%;
cursor: default;
- height: 100vh;
+ height: calc(100vh - 5%);
transition: .15s ease-out;
width: 100%;
overflow-y: auto;
diff --git a/src/components/Balance.css b/src/components/Balance.css
new file mode 100644
index 0000000..d9a1e3a
--- /dev/null
+++ b/src/components/Balance.css
@@ -0,0 +1,35 @@
+.number {
+ margin: 0;
+ transition: .15s ease-out;
+ -webkit-app-region: no-drag;
+ -webkit-user-select: text;
+ font-size: 1rem;
+ display: inline-block;
+ padding: 0 .3rem;
+ animation: fadeIn .5s ease-out;
+ border-radius: 4px;
+}
+
+.updated {
+ animation: updated .5s ease-out;
+}
+
+@keyframes updated {
+ 0% {
+ background: rgba(255, 255, 255, .2);
+ }
+
+ 100% {
+ background: rgba(255, 255, 255, 0);
+ }
+}
+
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/components/Balance.jsx b/src/components/Balance.jsx
index b3741dc..09ab47c 100644
--- a/src/components/Balance.jsx
+++ b/src/components/Balance.jsx
@@ -1,24 +1,28 @@
-import React from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { AppContext } from '../store/createContext'
import { locale } from '../util/moneyFormatter'
import { formatCurrency } from '@coingecko/cryptoformat'
+import './Balance.css'
-const Balance = ({ balance }) => (
-
-
- {({ currency }) =>
- formatCurrency(balance[currency], currency.toUpperCase(), locale)
+export default class Balance extends PureComponent {
+ static contextType = AppContext
+
+ static propTypes = {
+ balance: PropTypes.object.isRequired
+ }
+
+ render() {
+ const { currency } = this.context
+ const { balance } = this.props
+
+ return (
+
+ {formatCurrency(balance[currency], currency.toUpperCase(), locale)
.replace(/BTC/, 'Ƀ')
.replace(/ETH/, 'Ξ')
- .replace(/OCEAN/, 'Ọ')
- }
-
-
-)
-
-Balance.propTypes = {
- balance: PropTypes.object.isRequired
+ .replace(/OCEAN/, 'Ọ')}
+
+ )
+ }
}
-
-export default Balance
diff --git a/src/components/Total.jsx b/src/components/Total.jsx
index cadd4b8..3430ed6 100644
--- a/src/components/Total.jsx
+++ b/src/components/Total.jsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { PureComponent } from 'react'
import { AppContext } from '../store/createContext'
import Balance from './Balance'
import { prices } from '../../config'
@@ -19,26 +19,26 @@ const calculateTotalBalance = (accounts, currency) => {
return balanceTotal
}
-const Total = () => (
-
-
- {({ accounts }) => {
- const conversions = Object.assign(
- ...prices.map(key => ({
- [key]: calculateTotalBalance(accounts, key)
- }))
- )
+export default class Total extends PureComponent {
+ static contextType = AppContext
- const balanceNew = {
- ocean: calculateTotalBalance(accounts, 'ocean'),
- ...conversions
- }
+ render() {
+ const conversions = Object.assign(
+ ...prices.map(key => ({
+ [key]: calculateTotalBalance(this.context.accounts, key)
+ }))
+ )
- return
- }}
-
-
Total Balance
-
-)
+ const balanceNew = {
+ ocean: calculateTotalBalance(this.context.accounts, 'ocean'),
+ ...conversions
+ }
-export default Total
+ return (
+
+
+ Total Balance
+
+ )
+ }
+}
diff --git a/src/screens/Home.css b/src/screens/Home.css
index 80c1937..b23ead5 100644
--- a/src/screens/Home.css
+++ b/src/screens/Home.css
@@ -64,23 +64,6 @@
color: #f6388a;
}
-.number {
- margin: 0;
- transition: .15s ease-out;
- font-weight: 400;
- -webkit-app-region: no-drag;
- -webkit-user-select: text;
- font-size: 1rem;
- display: inline-block;
- padding: 0 .3rem;
- animation: fadeIn .5s ease-out;
- border-radius: 4px;
-}
-
-.updated {
- animation: updated .5s ease-out;
-}
-
.number-unit-wrap--accounts {
min-height: 55px;
}
@@ -102,16 +85,6 @@
font-size: 2.5rem;
}
-@keyframes updated {
- 0% {
- background: rgba(255, 255, 255, .2);
- }
-
- 100% {
- background: rgba(255, 255, 255, 0);
- }
-}
-
@keyframes fadeIn {
0% {
opacity: 0;
diff --git a/src/screens/Preferences.css b/src/screens/Preferences.css
index f566a8c..202ec7f 100644
--- a/src/screens/Preferences.css
+++ b/src/screens/Preferences.css
@@ -7,17 +7,14 @@
}
.preferences__title {
- font-size: 2rem;
+ font-size: 2.2rem;
margin-top: -1rem;
margin-bottom: 3rem;
}
.preferences__close {
text-decoration: none;
- font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
- Helvetica, Arial, sans-serif;
- font-weight: 600;
- font-size: 2.5rem;
+ font-size: 2rem;
position: absolute;
top: -1.5rem;
right: 0;
@@ -37,13 +34,17 @@
border-top-color: #303030;
}
-.preference__list li {
- list-style: none;
+.preference__list li,
+.preference__list li > div {
display: flex;
align-items: center;
+}
+
+.preference__list li {
+ list-style: none;
justify-content: space-between;
border-bottom: 1px solid #e2e2e2;
- padding-top: .3rem;
+ padding-top: .25rem;
padding-bottom: .25rem;
}
@@ -56,14 +57,16 @@
border: 0;
box-shadow: none;
margin: 0;
+ padding: 0;
outline: 0;
color: #f6388a;
font-size: 1rem;
- text-transform: uppercase;
}
button.delete {
+ position: relative;
font-size: 2rem;
+ top: -.2rem;
color: #41474e;
transition: color .5s ease-out;
}
@@ -77,18 +80,27 @@ button.delete:hover {
-webkit-user-select: text;
}
+.preference__title,
+.preference__help {
+ display: inline-block;
+ margin-top: 0;
+ margin-bottom: .5rem;
+}
+
.preference__title {
- font-size: 1rem;
+ font-size: 1.2rem;
+}
+
+.preference__help {
color: #8b98a9;
+ margin-left: .5rem;
}
.preference .identicon {
width: 1.5rem !important;
height: 1.5rem !important;
border-radius: 50%;
- vertical-align: middle;
- margin-top: -.2rem;
- margin-right: .5rem;
+ margin-right: .75rem;
}
.preference__input {
@@ -105,3 +117,7 @@ button.delete:hover {
.dark .preference__input {
color: #fff;
}
+
+.preference__error {
+ font-size: .9rem;
+}
diff --git a/src/screens/Preferences.jsx b/src/screens/Preferences.jsx
index 2534d75..98c1960 100644
--- a/src/screens/Preferences.jsx
+++ b/src/screens/Preferences.jsx
@@ -2,15 +2,16 @@ import React, { PureComponent } from 'react'
import { Link } from '@reach/router'
import Store from 'electron-store'
import Blockies from 'react-blockies'
-import './Preferences.css'
+import ethereum_address from 'ethereum-address'
import { AppContext } from '../store/createContext'
+import './Preferences.css'
export default class Preferences extends PureComponent {
static contextType = AppContext
store = new Store()
- state = { accounts: [], input: '' }
+ state = { accounts: [], input: '', error: '' }
componentDidMount() {
if (this.store.has('accounts')) {
@@ -25,15 +26,27 @@ export default class Preferences extends PureComponent {
handleSave = e => {
e.preventDefault()
- if (
- this.state.input !== '' &&
- !this.state.accounts.includes(this.state.input) // duplication check
- ) {
- const joined = [...this.state.accounts, this.state.input]
+ const { accounts, input } = this.state
+
+ const isEmpty = input === ''
+ const isDuplicate = accounts.includes(input)
+ const isAddress = ethereum_address.isAddress(input)
+
+ if (isEmpty) {
+ this.setState({ error: 'Please enter an address.' })
+ return
+ } else if (isDuplicate) {
+ this.setState({ error: 'Address already added. Try another one.' })
+ return
+ } else if (!isAddress) {
+ this.setState({ error: 'Not an Ethereum address. Try another one.' })
+ return
+ } else {
+ const joined = [...accounts, input]
this.store.set('accounts', joined)
- this.setState({ accounts: joined, input: '' })
- this.context.setBalances(joined)
+ this.setState({ accounts: joined, input: '', error: '' })
+ this.context.setBalances()
}
}
@@ -50,10 +63,12 @@ export default class Preferences extends PureComponent {
this.store.set('accounts', array)
this.setState({ accounts: array })
- this.context.setBalances(array)
+ this.context.setBalances()
}
render() {
+ const { accounts, input, error } = this.state
+
return (
Preferences
{' '}
@@ -62,9 +77,12 @@ export default class Preferences extends PureComponent {
Accounts
+
+ Add Ethereum account addresses holding Ocean Tokens.
+
- {this.state.accounts &&
- this.state.accounts.map(account => (
+ {accounts &&
+ accounts.map(account => (
-
@@ -84,7 +102,7 @@ export default class Preferences extends PureComponent {
@@ -96,6 +114,7 @@ export default class Preferences extends PureComponent {
+ {error !== '' &&
{error}
}
)
diff --git a/src/store/AppProvider.jsx b/src/store/AppProvider.jsx
index 1bbd8d5..4189794 100644
--- a/src/store/AppProvider.jsx
+++ b/src/store/AppProvider.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import ms from 'ms'
import Store from 'electron-store'
import { AppContext } from './createContext'
+import fetchData from '../util/fetch'
import { refreshInterval, prices, oceanTokenContract } from '../../config'
export default class AppProvider extends PureComponent {
@@ -19,24 +20,19 @@ export default class AppProvider extends PureComponent {
needsConfig: false,
prices: Object.assign(...prices.map(key => ({ [key]: 0 }))),
toggleCurrencies: currency => this.setState({ currency }),
- setBalances: account => this.setBalances(account)
+ setBalances: () => this.setBalances()
}
async componentDidMount() {
- const { accountsPref } = await this.getAccounts()
await this.fetchAndSetPrices()
- await this.setBalances(accountsPref)
+ await this.setBalances()
- await setInterval(this.fetchAndSetPrices, ms(refreshInterval))
- await setInterval(this.setBalances, ms(refreshInterval))
+ setInterval(this.fetchAndSetPrices, ms(refreshInterval))
+ setInterval(this.setBalances, ms(refreshInterval))
this.setState({ isLoading: false })
}
- componentWillUnmount() {
- this.clearAccounts()
- }
-
getAccounts() {
let accountsPref
@@ -50,64 +46,41 @@ export default class AppProvider extends PureComponent {
accountsPref = []
}
- return { accountsPref }
+ return accountsPref
}
- clearAccounts() {
- this.setState({ accounts: [] })
- }
-
- async fetch(url) {
- try {
- const response = await fetch(url)
-
- if (response.status !== 200) {
- return console.log('Non-200 response: ' + response.status) // eslint-disable-line
- }
-
- const json = await response.json()
- if (!json) return
-
- return json
- } catch (error) {
- console.log('Error parsing json:' + error) // eslint-disable-line
- }
- }
-
- async fetchBalance(account) {
- const json = await this.fetch(
+ async getBalance(account) {
+ const json = await fetchData(
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
)
- const balance = (json.result /= 1000000000000000000) // Convert from wei 10^18
+ const balance = (json.result /= 1000000000000000000) // Convert from vodka 10^18
return balance
}
- async fetchAndSetPrices() {
+ fetchAndSetPrices = async () => {
const currencies = prices.join(',')
- const json = await this.fetch(
+ const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}`
)
- await this.setState({
- prices: Object.assign(
- ...prices.map(key => ({
- ocean: 1,
- [key]: json['ocean-protocol'][key]
- }))
- )
- })
+ const newPrizes = Object.assign(
+ ...prices.map(key => ({
+ ocean: 1,
+ [key]: json['ocean-protocol'][key]
+ }))
+ )
+
+ this.setState({ prices: newPrizes })
}
- setBalances(accounts) {
- // TODO: make this less lazy and update numbers in place
- // when they are changed instead of resetting all to 0 here
- this.clearAccounts()
+ setBalances = async () => {
+ const accountsPref = await this.getAccounts()
- const accountsArray = accounts ? accounts : this.state.accounts
+ let newAccounts = []
- accountsArray.map(async account => {
- const oceanBalance = await this.fetchBalance(account)
+ for (const account of accountsPref) {
+ const oceanBalance = await this.getBalance(account)
const conversions = Object.assign(
...prices.map(key => ({
@@ -118,15 +91,17 @@ export default class AppProvider extends PureComponent {
const newAccount = {
address: account,
balance: {
- ocean: oceanBalance || 0,
+ ocean: oceanBalance,
...conversions
}
}
- await this.setState(prevState => ({
- accounts: [...prevState.accounts, newAccount]
- }))
- })
+ newAccounts.push(newAccount)
+ }
+
+ if (newAccounts !== this.state.accounts) {
+ this.setState({ accounts: newAccounts })
+ }
}
render() {
diff --git a/src/store/createContext.jsx b/src/store/createContext.jsx
index 75ba03c..10b0812 100644
--- a/src/store/createContext.jsx
+++ b/src/store/createContext.jsx
@@ -1,5 +1,5 @@
import { createContext } from 'react'
-const AppContext = createContext({})
+const AppContext = createContext()
export { AppContext }
diff --git a/src/util/fetch.js b/src/util/fetch.js
new file mode 100644
index 0000000..d0f33c3
--- /dev/null
+++ b/src/util/fetch.js
@@ -0,0 +1,18 @@
+const fetchData = async url => {
+ try {
+ const response = await fetch(url)
+
+ if (response.status !== 200) {
+ return console.log('Non-200 response: ' + response.status) // eslint-disable-line
+ }
+
+ const json = await response.json()
+ if (!json) return
+
+ return json
+ } catch (error) {
+ console.log('Error parsing json:' + error) // eslint-disable-line
+ }
+}
+
+export default fetchData