diff --git a/package.json b/package.json index 745d3ba1..40eb5d5a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@ledgerhq/hw-transport-node-hid": "^4.13.0", "@ledgerhq/ledger-core": "1.4.5", "@ledgerhq/live-common": "2.29.0", + "async": "^2.6.1", "axios": "^0.18.0", "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", diff --git a/src/actions/bridgeSync.js b/src/actions/bridgeSync.js index d41f40b8..3b55459e 100644 --- a/src/actions/bridgeSync.js +++ b/src/actions/bridgeSync.js @@ -6,9 +6,3 @@ export const setAccountSyncState = (accountId: string, state: AsyncState) => ({ accountId, state, }) - -export const setAccountPullMoreState = (accountId: string, state: AsyncState) => ({ - type: 'SET_ACCOUNT_PULL_MORE_STATE', - accountId, - state, -}) diff --git a/src/bridge/BridgeSyncContext.js b/src/bridge/BridgeSyncContext.js index e0ddf61b..c469e20a 100644 --- a/src/bridge/BridgeSyncContext.js +++ b/src/bridge/BridgeSyncContext.js @@ -1,28 +1,23 @@ // @flow +// Unify the synchronization management for bridges with the redux store +// it handles automatically re-calling synchronize +// this is an even high abstraction than the bridge + import logger from 'logger' +import shuffle from 'lodash/shuffle' import React, { Component } from 'react' +import priorityQueue from 'async/priorityQueue' import { connect } from 'react-redux' import type { Account } from '@ledgerhq/live-common/lib/types' import { createStructuredSelector } from 'reselect' import { updateAccountWithUpdater } from 'actions/accounts' -import { setAccountSyncState, setAccountPullMoreState } from 'actions/bridgeSync' -import { - bridgeSyncSelector, - syncStateLocalSelector, - pullMoreStateLocalSelector, -} from 'reducers/bridgeSync' +import { setAccountSyncState } from 'actions/bridgeSync' +import { bridgeSyncSelector, syncStateLocalSelector } from 'reducers/bridgeSync' import type { BridgeSyncState } from 'reducers/bridgeSync' import { accountsSelector } from 'reducers/accounts' -import { SYNC_BOOT_DELAY, SYNC_INTERVAL } from 'config/constants' +import { SYNC_BOOT_DELAY, SYNC_ALL_INTERVAL } from 'config/constants' import { getBridgeForCurrency } from '.' -// Unify the synchronization management for bridges with the redux store -// it handles automatically re-calling synchronize -// this is an even high abstraction than the bridge - -// $FlowFixMe can't wait flow implement createContext -const BridgeSyncContext = React.createContext(() => {}) - type BridgeSyncProviderProps = { children: *, } @@ -32,7 +27,6 @@ type BridgeSyncProviderOwnProps = BridgeSyncProviderProps & { accounts: Account[], updateAccountWithUpdater: (string, (Account) => Account) => void, setAccountSyncState: (string, AsyncState) => *, - setAccountPullMoreState: (string, AsyncState) => *, } type AsyncState = { @@ -40,15 +34,15 @@ type AsyncState = { error: ?Error, } -type BridgeSync = { - synchronize: (accountId: string) => Promise, +export type BehaviorAction = + | { type: 'BACKGROUND_TICK' } + | { type: 'SET_SKIP_UNDER_PRIORITY', priority: number } + | { type: 'SYNC_ONE_ACCOUNT', accountId: string, priority: number } + | { type: 'SYNC_ALL_ACCOUNTS', priority: number } - // sync for all accounts (if there were errors it stopped) - syncAll: () => {}, +export type Sync = (action: BehaviorAction) => void - // - pullMoreOperations: (accountId: string, count: number) => Promise, -} +const BridgeSyncContext = React.createContext((_: BehaviorAction) => {}) const mapStateToProps = createStructuredSelector({ accounts: accountsSelector, @@ -58,110 +52,104 @@ const mapStateToProps = createStructuredSelector({ const actions = { updateAccountWithUpdater, setAccountSyncState, - setAccountPullMoreState, } -class Provider extends Component { +class Provider extends Component { constructor() { super() - const syncPromises = {} - const syncSubs = {} - const pullMorePromises = {} - const getSyncState = accountId => syncStateLocalSelector(this.props.bridgeSync, { accountId }) - - const getPullMoreOperationsState = accountId => - pullMoreStateLocalSelector(this.props.bridgeSync, { accountId }) + const synchronize = (accountId: string, next: () => void) => { + const state = syncStateLocalSelector(this.props.bridgeSync, { accountId }) + if (state.pending) { + next() + return + } + const account = this.props.accounts.find(a => a.id === accountId) + if (!account) throw new Error('account not found') - const getAccountById = accountId => { - const a = this.props.accounts.find(a => a.id === accountId) - if (!a) throw new Error('account not found') - return a - } + const bridge = getBridgeForCurrency(account.currency) - const getBridgeForAccountId = accountId => - getBridgeForCurrency(getAccountById(accountId).currency) + this.props.setAccountSyncState(accountId, { pending: true, error: null }) - const pullMoreOperations = (accountId, count) => { - const state = getPullMoreOperationsState(accountId) - if (state.pending) { - return ( - pullMorePromises[accountId] || Promise.reject(new Error('no pullMore started. (bug)')) - ) - } - this.props.setAccountPullMoreState(accountId, { pending: true, error: null }) - const bridge = getBridgeForAccountId(accountId) - const p = bridge.pullMoreOperations(getAccountById(accountId), count).then( - accountUpdater => { - this.props.setAccountPullMoreState(accountId, { - pending: false, - error: null, - }) + // TODO use Subscription to unsubscribe at relevant time + bridge.synchronize(account).subscribe({ + next: accountUpdater => { this.props.updateAccountWithUpdater(accountId, accountUpdater) }, - error => { - this.props.setAccountPullMoreState(accountId, { - pending: false, - error, - }) + complete: () => { + this.props.setAccountSyncState(accountId, { pending: false, error: null }) + next() }, - ) - pullMorePromises[accountId] = p - return p + error: error => { + this.props.setAccountSyncState(accountId, { pending: false, error }) + next() + }, + }) } - const synchronize = accountId => { - const state = getSyncState(accountId) - if (state.pending) { - return syncPromises[accountId] || Promise.reject(new Error('no sync started. (bug)')) - } + const syncQueue = priorityQueue(synchronize, 2) - this.props.setAccountSyncState(accountId, { pending: true, error: null }) - const bridge = getBridgeForAccountId(accountId) - const p = new Promise((resolve, reject) => { - const subscription = bridge.synchronize(getAccountById(accountId), { - next: accountUpdater => { - this.props.updateAccountWithUpdater(accountId, accountUpdater) - }, - complete: () => { - this.props.setAccountSyncState(accountId, { pending: false, error: null }) - resolve() - }, - error: error => { - this.props.setAccountSyncState(accountId, { pending: false, error }) - reject(error) - }, - }) - syncSubs[accountId] = subscription - }) - syncPromises[accountId] = p - return p + let skipUnderPriority: number = -1 + + const schedule = (ids: string[], priority: number) => { + if (priority < skipUnderPriority) return + // by convention we remove concurrent tasks with same priority + syncQueue.remove(o => priority === o.priority) + syncQueue.push(ids, -priority) } - const syncAll = () => Promise.all(this.props.accounts.map(account => synchronize(account.id))) + // don't always sync in same order to avoid potential "never account never reached" + const shuffledAccountIds = () => shuffle(this.props.accounts.map(a => a.id)) + + const handlers = { + BACKGROUND_TICK: () => { + if (syncQueue.idle()) { + schedule(shuffledAccountIds(), -1) + } + }, + + SET_SKIP_UNDER_PRIORITY: ({ priority }) => { + if (priority === skipUnderPriority) return + skipUnderPriority = priority + syncQueue.remove(({ priority }) => priority < skipUnderPriority) + }, + + SYNC_ALL_ACCOUNTS: ({ priority }) => { + schedule(shuffledAccountIds(), priority) + }, + + SYNC_ONE_ACCOUNT: ({ accountId, priority }) => { + schedule([accountId], priority) + }, + } - this.api = { - synchronize, - syncAll, - pullMoreOperations, + const sync = (action: BehaviorAction) => { + const handler = handlers[action.type] + if (handler) { + // $FlowFixMe + handler(action) + } else { + logger.warn('BridgeSyncContext unsupported action', action) + } } + + this.api = sync } componentDidMount() { const syncLoop = async () => { - try { - await this.api.syncAll() - } catch (e) { - logger.error('sync issues', e) - } - setTimeout(syncLoop, SYNC_INTERVAL) + this.api({ type: 'BACKGROUND_TICK' }) + this.syncTimeout = setTimeout(syncLoop, SYNC_ALL_INTERVAL) } - setTimeout(syncLoop, SYNC_BOOT_DELAY) + this.syncTimeout = setTimeout(syncLoop, SYNC_BOOT_DELAY) } - // TODO we might want to call sync straight away when new accounts got added (it will happen every 10s anyway) + componentWillUnmount() { + clearTimeout(this.syncTimeout) + } - api: BridgeSync + syncTimeout: * + api: Sync render() { return ( diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index db28ce4a..8bf3006e 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -268,69 +268,68 @@ const EthereumBridge: WalletBridge = { return { unsubscribe } }, - synchronize({ freshAddress, blockHeight, currency, operations }, { next, complete, error }) { - let unsubscribed = false - const api = apiForCurrency(currency) - async function main() { - try { - const block = await fetchCurrentBlock(currency) - if (unsubscribed) return - if (block.height === blockHeight) { - complete() - } else { - const filterConfirmedOperations = o => - o.blockHeight && blockHeight - o.blockHeight > SAFE_REORG_THRESHOLD - - operations = operations.filter(filterConfirmedOperations) - const blockHash = operations.length > 0 ? operations[0].blockHash : undefined - const { txs } = await api.getTransactions(freshAddress, blockHash) - if (unsubscribed) return - if (txs.length === 0) { - next(a => ({ - ...a, - blockHeight: block.height, - lastSyncDate: new Date(), - })) - complete() - return - } - const balance = await api.getAccountBalance(freshAddress) - if (unsubscribed) return - const nonce = await api.getAccountNonce(freshAddress) + synchronize: ({ freshAddress, blockHeight, currency, operations }) => + Observable.create(o => { + let unsubscribed = false + const api = apiForCurrency(currency) + async function main() { + try { + const block = await fetchCurrentBlock(currency) if (unsubscribed) return - next(a => { - const currentOps = a.operations.filter(filterConfirmedOperations) - const newOps = flatMap(txs, txToOps(a)) - const operations = mergeOps(currentOps, newOps) - const pendingOperations = a.pendingOperations.filter( - o => - o.transactionSequenceNumber && - o.transactionSequenceNumber >= nonce && - !operations.some(op => o.hash === op.hash), - ) - return { - ...a, - pendingOperations, - operations, - balance, - blockHeight: block.height, - lastSyncDate: new Date(), + if (block.height === blockHeight) { + o.complete() + } else { + const filterConfirmedOperations = o => + o.blockHeight && blockHeight - o.blockHeight > SAFE_REORG_THRESHOLD + + operations = operations.filter(filterConfirmedOperations) + const blockHash = operations.length > 0 ? operations[0].blockHash : undefined + const { txs } = await api.getTransactions(freshAddress, blockHash) + if (unsubscribed) return + if (txs.length === 0) { + o.next(a => ({ + ...a, + blockHeight: block.height, + lastSyncDate: new Date(), + })) + o.complete() + return } - }) - complete() + const balance = await api.getAccountBalance(freshAddress) + if (unsubscribed) return + const nonce = await api.getAccountNonce(freshAddress) + if (unsubscribed) return + o.next(a => { + const currentOps = a.operations.filter(filterConfirmedOperations) + const newOps = flatMap(txs, txToOps(a)) + const operations = mergeOps(currentOps, newOps) + const pendingOperations = a.pendingOperations.filter( + o => + o.transactionSequenceNumber && + o.transactionSequenceNumber >= nonce && + !operations.some(op => o.hash === op.hash), + ) + return { + ...a, + pendingOperations, + operations, + balance, + blockHeight: block.height, + lastSyncDate: new Date(), + } + }) + o.complete() + } + } catch (e) { + o.error(e) } - } catch (e) { - error(e) } - } - main() + main() - return { - unsubscribe() { + return () => { unsubscribed = true - }, - } - }, + } + }), pullMoreOperations: () => Promise.resolve(a => a), // NOT IMPLEMENTED @@ -359,10 +358,8 @@ const EthereumBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, - // $FlowFixMe EditFees, - // $FlowFixMe EditAdvancedOptions, canBeSpent: (a, t) => Promise.resolve(t.amount <= a.balance), diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index f077ce4c..ffe8f8b5 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -1,6 +1,7 @@ // @flow import logger from 'logger' import React from 'react' +import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import type { Account } from '@ledgerhq/live-common/lib/types' import { decodeAccount, encodeAccount } from 'reducers/accounts' @@ -54,53 +55,56 @@ const LibcoreBridge: WalletBridge = { .subscribe(observer) }, - synchronize(account, { next, complete, error }) { - // FIXME TODO: - // - when you implement addPendingOperation you also here need to: - // - if there were pendingOperations that are now in operations, remove them as well. - // - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically) - // then we probably should trash them out? it's a complex question for UI - ;(async () => { - try { - const rawAccount = encodeAccount(account) - const rawSyncedAccount = await libcoreSyncAccount.send({ rawAccount }).toPromise() - const syncedAccount = decodeAccount(rawSyncedAccount) - next(account => { - const accountOps = account.operations - const syncedOps = syncedAccount.operations - const patch: $Shape = { - freshAddress: syncedAccount.freshAddress, - freshAddressPath: syncedAccount.freshAddressPath, - balance: syncedAccount.balance, - blockHeight: syncedAccount.blockHeight, - lastSyncDate: new Date(), - } - - const hasChanged = - accountOps.length !== syncedOps.length || // size change, we do a full refresh for now... - (accountOps.length > 0 && syncedOps.length > 0 && accountOps[0].id !== syncedOps[0].id) // if same size, only check if the last item has changed. - - if (hasChanged) { - patch.operations = syncedAccount.operations - patch.pendingOperations = [] // For now, we assume a change will clean the pendings. - } - - return { - ...account, - ...patch, - } - }) - complete() - } catch (e) { - error(e) + synchronize: account => + Observable.create(o => { + // FIXME TODO: + // - when you implement addPendingOperation you also here need to: + // - if there were pendingOperations that are now in operations, remove them as well. + // - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically) + // then we probably should trash them out? it's a complex question for UI + ;(async () => { + try { + const rawAccount = encodeAccount(account) + const rawSyncedAccount = await libcoreSyncAccount.send({ rawAccount }).toPromise() + const syncedAccount = decodeAccount(rawSyncedAccount) + o.next(account => { + const accountOps = account.operations + const syncedOps = syncedAccount.operations + const patch: $Shape = { + freshAddress: syncedAccount.freshAddress, + freshAddressPath: syncedAccount.freshAddressPath, + balance: syncedAccount.balance, + blockHeight: syncedAccount.blockHeight, + lastSyncDate: new Date(), + } + + const hasChanged = + accountOps.length !== syncedOps.length || // size change, we do a full refresh for now... + (accountOps.length > 0 && + syncedOps.length > 0 && + accountOps[0].id !== syncedOps[0].id) // if same size, only check if the last item has changed. + + if (hasChanged) { + patch.operations = syncedAccount.operations + patch.pendingOperations = [] // For now, we assume a change will clean the pendings. + } + + return { + ...account, + ...patch, + } + }) + o.complete() + } catch (e) { + o.error(e) + } + })() + return { + unsubscribe() { + logger.warn('LibcoreBridge: unsub sync not implemented') + }, } - })() - return { - unsubscribe() { - logger.warn('LibcoreBridge: unsub sync not implemented') - }, - } - }, + }), pullMoreOperations: () => Promise.reject(notImplemented), diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index b34faf83..fbe57a0d 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -340,85 +340,86 @@ const RippleJSBridge: WalletBridge = { return { unsubscribe } }, - synchronize({ currency, freshAddress, blockHeight }, { next, error, complete }) { - let finished = false - const unsubscribe = () => { - finished = true - } - - async function main() { - const api = apiForCurrency(currency) - try { - await api.connect() - if (finished) return - const serverInfo = await getServerInfo(currency) - if (finished) return - const ledgers = serverInfo.completeLedgers.split('-') - const minLedgerVersion = Number(ledgers[0]) - const maxLedgerVersion = Number(ledgers[1]) + synchronize: ({ currency, freshAddress, blockHeight }) => + Observable.create(o => { + let finished = false + const unsubscribe = () => { + finished = true + } - let info + async function main() { + const api = apiForCurrency(currency) try { - info = await api.getAccountInfo(freshAddress) - } catch (e) { - if (e.message !== 'actNotFound') { - throw e + await api.connect() + if (finished) return + const serverInfo = await getServerInfo(currency) + if (finished) return + const ledgers = serverInfo.completeLedgers.split('-') + const minLedgerVersion = Number(ledgers[0]) + const maxLedgerVersion = Number(ledgers[1]) + + let info + try { + info = await api.getAccountInfo(freshAddress) + } catch (e) { + if (e.message !== 'actNotFound') { + throw e + } } - } - if (finished) return - - if (!info) { - // account does not exist, we have nothing to sync - complete() - return - } + if (finished) return - const balance = parseAPIValue(info.xrpBalance) - if (isNaN(balance) || !isFinite(balance)) { - throw new Error(`Ripple: invalid balance=${balance} for address ${freshAddress}`) - } + if (!info) { + // account does not exist, we have nothing to sync + o.complete() + return + } - next(a => ({ ...a, balance })) - - const transactions = await api.getTransactions(freshAddress, { - minLedgerVersion: Math.max(blockHeight, minLedgerVersion), - maxLedgerVersion, - }) - - if (finished) return - - next(a => { - const newOps = transactions.map(txToOperation(a)) - const operations = mergeOps(a.operations, newOps) - const [last] = operations - const pendingOperations = a.pendingOperations.filter( - o => - last && - last.transactionSequenceNumber && - o.transactionSequenceNumber && - o.transactionSequenceNumber > last.transactionSequenceNumber, - ) - return { - ...a, - operations, - pendingOperations, - blockHeight: maxLedgerVersion, - lastSyncDate: new Date(), + const balance = parseAPIValue(info.xrpBalance) + if (isNaN(balance) || !isFinite(balance)) { + throw new Error(`Ripple: invalid balance=${balance} for address ${freshAddress}`) } - }) - complete() - } catch (e) { - error(e) - } finally { - api.disconnect() + o.next(a => ({ ...a, balance })) + + const transactions = await api.getTransactions(freshAddress, { + minLedgerVersion: Math.max(blockHeight, minLedgerVersion), + maxLedgerVersion, + }) + + if (finished) return + + o.next(a => { + const newOps = transactions.map(txToOperation(a)) + const operations = mergeOps(a.operations, newOps) + const [last] = operations + const pendingOperations = a.pendingOperations.filter( + o => + last && + last.transactionSequenceNumber && + o.transactionSequenceNumber && + o.transactionSequenceNumber > last.transactionSequenceNumber, + ) + return { + ...a, + operations, + pendingOperations, + blockHeight: maxLedgerVersion, + lastSyncDate: new Date(), + } + }) + + o.complete() + } catch (e) { + o.error(e) + } finally { + api.disconnect() + } } - } - main() + main() - return { unsubscribe } - }, + return unsubscribe + }), pullMoreOperations: () => Promise.resolve(a => a), // FIXME not implemented diff --git a/src/bridge/UnsupportedBridge.js b/src/bridge/UnsupportedBridge.js index 2f1bbce5..a317731b 100644 --- a/src/bridge/UnsupportedBridge.js +++ b/src/bridge/UnsupportedBridge.js @@ -5,10 +5,10 @@ import type { WalletBridge } from './types' const genericError = new Error('UnsupportedBridge') const UnsupportedBridge: WalletBridge<*> = { - synchronize(initialAccount, { error }) { - Promise.resolve(genericError).then(error) - return { unsubscribe() {} } - }, + synchronize: () => + Observable.create(o => { + o.error(genericError) + }), scanAccountsOnDevice(currency, deviceId, { error }) { Promise.resolve(genericError).then(error) diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 9e0b6c69..9762c46e 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -43,38 +43,37 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { const syncTimeouts = {} return { - synchronize(initialAccount, { error, next, complete }) { - const accountId = initialAccount.id - if (syncTimeouts[accountId]) { - // this is just for tests. we'll assume impl don't need to handle race condition on this function. - logger.warn('synchronize was called multiple pending time for same accounts!!!') - } - syncTimeouts[accountId] = setTimeout(() => { - if (Math.random() < syncSuccessRate) { - const ops = broadcasted[accountId] || [] - broadcasted[accountId] = [] - next(account => { - account = { ...account } - account.blockHeight++ - for (const op of ops) { - account.balance += getOperationAmountNumber(op) - } - return account - }) - complete() - } else { - error(new Error('Sync Failed')) + synchronize: initialAccount => + Observable.create(o => { + const accountId = initialAccount.id + if (syncTimeouts[accountId]) { + // this is just for tests. we'll assume impl don't need to handle race condition on this function. + logger.warn('synchronize was called multiple pending time for same accounts!!!') } - syncTimeouts[accountId] = null - }, 20000) + syncTimeouts[accountId] = setTimeout(() => { + if (Math.random() < syncSuccessRate) { + const ops = broadcasted[accountId] || [] + broadcasted[accountId] = [] + o.next(account => { + account = { ...account } + account.blockHeight++ + for (const op of ops) { + account.balance += getOperationAmountNumber(op) + } + return account + }) + o.complete() + } else { + o.error(new Error('Sync Failed')) + } + syncTimeouts[accountId] = null + }, 20000) - return { - unsubscribe() { + return () => { clearTimeout(syncTimeouts[accountId]) syncTimeouts[accountId] = null - }, - } - }, + } + }), scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { let unsubscribed = false diff --git a/src/bridge/types.js b/src/bridge/types.js index c84758de..75bbe777 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -32,6 +32,7 @@ export interface WalletBridge { // observer is an Observer of Account object. Account are expected to be `archived` by default because we want to import all and opt-in on what account to use. // the scan can stop once all accounts are discovered. // the function returns a Subscription and you MUST stop everything if it is unsubscribed. + // TODO return Observable scanAccountsOnDevice( currency: Currency, deviceId: DeviceId, @@ -46,7 +47,7 @@ export interface WalletBridge { // operations if there are new ones (prepended), balance, blockHeight, ... // the synchronize can stop once everything is up to date. it is the user side responsability to start it again. // we should be able to interrupt the Subscription but we'll leave this usecase for later. if you don't support interruption, please `console.warn` - synchronize(initialAccount: Account, observer: Observer<(Account) => Account>): Subscription; + synchronize(initialAccount: Account): Observable<(Account) => Account>; // for a given account, UI wants to load more operations in the account.operations // if you can't do it or there is no more things to load, just return account, diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index c7f6b139..95080626 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -7,6 +7,7 @@ import { translate } from 'react-i18next' import { Redirect } from 'react-router' import styled from 'styled-components' import type { Currency, Account } from '@ledgerhq/live-common/lib/types' +import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' @@ -97,6 +98,7 @@ class AccountPage extends PureComponent { return ( // Force re-render account page, for avoid animation + diff --git a/src/components/PollCounterValuesOnMount.js b/src/components/PollCounterValuesOnMount.js new file mode 100644 index 00000000..9a2e5da1 --- /dev/null +++ b/src/components/PollCounterValuesOnMount.js @@ -0,0 +1,21 @@ +// @flow + +import React, { Component } from 'react' +import CounterValues from 'helpers/countervalues' + +class Effect extends Component<{ cvPolling: * }> { + componentDidMount() { + this.props.cvPolling.poll() + } + render() { + return null + } +} + +const PollCounterValuesOnMount = () => ( + + {cvPolling => } + +) + +export default PollCounterValuesOnMount diff --git a/src/components/SyncOneAccountOnMount.js b/src/components/SyncOneAccountOnMount.js new file mode 100644 index 00000000..f8483047 --- /dev/null +++ b/src/components/SyncOneAccountOnMount.js @@ -0,0 +1,39 @@ +// @flow + +import React, { Component } from 'react' +import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' +import type { Sync } from 'bridge/BridgeSyncContext' + +export class Effect extends Component<{ + sync: Sync, + accountId: string, + priority: number, +}> { + componentDidMount() { + const { sync, accountId, priority } = this.props + sync({ type: 'SYNC_ONE_ACCOUNT', accountId, priority }) + } + componentDidUpdate(prevProps: *) { + const { sync, accountId, priority } = this.props + if (accountId !== prevProps.accountId) { + sync({ type: 'SYNC_ONE_ACCOUNT', accountId, priority }) + } + } + render() { + return null + } +} + +const SyncOneAccountOnMount = ({ + accountId, + priority, +}: { + accountId: string, + priority: number, +}) => ( + + {sync => } + +) + +export default SyncOneAccountOnMount diff --git a/src/components/SyncSkipUnderPriority.js b/src/components/SyncSkipUnderPriority.js new file mode 100644 index 00000000..d36a002c --- /dev/null +++ b/src/components/SyncSkipUnderPriority.js @@ -0,0 +1,41 @@ +// @flow + +import React, { PureComponent } from 'react' +import { BridgeSyncConsumer } from 'bridge/BridgeSyncContext' +import type { Sync } from 'bridge/BridgeSyncContext' + +const instances = [] + +export class Effect extends PureComponent<{ + sync: Sync, + priority: number, // eslint-disable-line +}> { + componentDidMount() { + instances.push(this) + this.check() + } + componentDidUpdate() { + this.check() + } + componentWillUnmount() { + const i = instances.indexOf(this) + if (i !== -1) { + instances.splice(i, 1) + this.check() + } + } + check() { + const { sync } = this.props + const priority = instances.length === 0 ? -1 : Math.max(...instances.map(i => i.props.priority)) + sync({ type: 'SET_SKIP_UNDER_PRIORITY', priority }) + } + render() { + return null + } +} + +const SyncSkipUnderPriority = ({ priority }: { priority: number }) => ( + {sync => } +) + +export default SyncSkipUnderPriority diff --git a/src/components/TopBar/ActivityIndicator.js b/src/components/TopBar/ActivityIndicator.js index e88e900b..8f9fd5eb 100644 --- a/src/components/TopBar/ActivityIndicator.js +++ b/src/components/TopBar/ActivityIndicator.js @@ -124,7 +124,7 @@ class ActivityIndicatorInner extends Component { const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState, t: T }) => ( - {bridgeSync => ( + {setSyncBehavior => ( {cvPolling => { const isPending = cvPolling.pending || globalSyncState.pending @@ -137,7 +137,7 @@ const ActivityIndicator = ({ globalSyncState, t }: { globalSyncState: AsyncState isError={!!isError && !isPending} onClick={() => { cvPolling.poll() - bridgeSync.syncAll() + setSyncBehavior({ type: 'SYNC_ALL_ACCOUNTS', priority: 5 }) }} /> ) diff --git a/src/components/modals/ImportAccounts/index.js b/src/components/modals/ImportAccounts/index.js index 7f6b464d..968e0bfd 100644 --- a/src/components/modals/ImportAccounts/index.js +++ b/src/components/modals/ImportAccounts/index.js @@ -6,6 +6,8 @@ import { connect } from 'react-redux' import { translate } from 'react-i18next' import { createStructuredSelector } from 'reselect' +import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' + import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import type { T, Device } from 'types/common' @@ -199,6 +201,7 @@ class ImportAccounts extends PureComponent { onHide={() => this.setState({ ...INITIAL_STATE })} render={({ onClose }) => ( + onBack(stepProps) : void 0}> {t('importAccounts:title')} diff --git a/src/components/modals/Receive/index.js b/src/components/modals/Receive/index.js index f3a402cb..2b643d6b 100644 --- a/src/components/modals/Receive/index.js +++ b/src/components/modals/Receive/index.js @@ -9,6 +9,8 @@ import type { T, Device } from 'types/common' import { MODAL_RECEIVE } from 'config/constants' import getAddress from 'commands/getAddress' +import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' +import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' import Box from 'components/base/Box' import Breadcrumb from 'components/Breadcrumb' @@ -300,7 +302,7 @@ class ReceiveModal extends PureComponent { render() { const { t } = this.props - const { stepsErrors, stepsDisabled, stepIndex } = this.state + const { stepsErrors, stepsDisabled, stepIndex, account } = this.state const canClose = this.canClose() const canPrev = this.canPrev() @@ -313,6 +315,8 @@ class ReceiveModal extends PureComponent { preventBackdropClick={!canClose} render={({ onClose }) => ( + + {account && } {canPrev && } {t('receive:title')} diff --git a/src/components/modals/Send/SendModalBody.js b/src/components/modals/Send/SendModalBody.js index 445555de..c50cd67c 100644 --- a/src/components/modals/Send/SendModalBody.js +++ b/src/components/modals/Send/SendModalBody.js @@ -16,11 +16,15 @@ import { getBridgeForCurrency } from 'bridge' import { accountsSelector } from 'reducers/accounts' import { updateAccountWithUpdater } from 'actions/accounts' +import PollCounterValuesOnMount from 'components/PollCounterValuesOnMount' + import Breadcrumb from 'components/Breadcrumb' import { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' import PrevButton from 'components/modals/PrevButton' import StepConnectDevice from 'components/modals/StepConnectDevice' import ChildSwitch from 'components/base/ChildSwitch' +import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' +import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount' import Footer from './Footer' import ConfirmationFooter from './ConfirmationFooter' @@ -271,6 +275,10 @@ class SendModalBody extends PureComponent> { return ( + + + {account && } + {canPrev && } {t('send:title')} diff --git a/src/config/constants.js b/src/config/constants.js index 54beedfe..90be4ff9 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -1,7 +1,7 @@ // @flow export const SYNC_BOOT_DELAY = 2 * 1000 -export const SYNC_INTERVAL = 30 * 1000 +export const SYNC_ALL_INTERVAL = 60 * 1000 export const CHECK_APP_INTERVAL_WHEN_INVALID = 600 export const CHECK_APP_INTERVAL_WHEN_VALID = 1200 export const CHECK_UPDATE_DELAY = 5e3 diff --git a/src/reducers/bridgeSync.js b/src/reducers/bridgeSync.js index 8f32aedd..b7895a4f 100644 --- a/src/reducers/bridgeSync.js +++ b/src/reducers/bridgeSync.js @@ -12,12 +12,10 @@ export type AsyncState = { export type BridgeSyncState = { syncs: { [accountId: string]: AsyncState }, - pullMores: { [accountId: string]: AsyncState }, } -const state: BridgeSyncState = { +const initialState: BridgeSyncState = { syncs: {}, - pullMores: {}, } const handlers: Object = { @@ -33,16 +31,6 @@ const handlers: Object = { [action.accountId]: action.state, }, }), - - SET_ACCOUNT_PULL_MORE_STATE: ( - state: BridgeSyncState, - action: { accountId: string, state: AsyncState }, - ) => ({ - pullMores: { - ...state.pullMores, - [action.accountId]: action.state, - }, - }), } // Selectors @@ -56,11 +44,6 @@ export const syncStateLocalSelector = ( { accountId }: { accountId: string }, ) => bridgeSync.syncs[accountId] || nothingState -export const pullMoreStateLocalSelector = ( - bridgeSync: BridgeSyncState, - { accountId }: { accountId: string }, -) => bridgeSync.pullMores[accountId] || nothingState - export const globalSyncStateSelector = createSelector( accountsSelector, bridgeSyncSelector, @@ -78,4 +61,4 @@ export const globalSyncStateSelector = createSelector( }, ) -export default handleActions(handlers, state) +export default handleActions(handlers, initialState) diff --git a/yarn.lock b/yarn.lock index f9e155fe..30615f37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2352,7 +2352,7 @@ async@^1.4.0, async@^1.5.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.1.2, async@^2.1.4, async@^2.6.0: +async@^2.1.2, async@^2.1.4, async@^2.6.0, async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: