Browse Source

Merge pull request #46 from loeck/master

Store lastAdress and lastIndex for better sync
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
8b76004610
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .prettierignore
  2. 7
      package.json
  3. 5
      src/actions/accounts.js
  4. 2
      src/components/AccountPage.js
  5. 1
      src/components/AppRegionDrag.js
  6. 2
      src/components/base/Modal/index.js
  7. 9
      src/components/modals/AddAccount.js
  8. 90
      src/helpers/btc.js
  9. 4
      src/internals/accounts/sync.js
  10. 9
      src/internals/usb/wallet/accounts.js
  11. 42
      src/reducers/accounts.js
  12. 16
      src/renderer/events.js
  13. 2
      src/types/common.js
  14. 58
      yarn.lock

1
.prettierignore

@ -0,0 +1 @@
package.json

7
package.json

@ -20,7 +20,11 @@
"storybook": "start-storybook -p 4444"
},
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write", "git add"]
"*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
},
"electronWebpack": {
"renderer": {
@ -42,6 +46,7 @@
"axios": "^0.17.1",
"bcryptjs": "^2.4.3",
"bitcoinjs-lib": "^3.3.2",
"blockchain.info": "^2.11.0",
"bs58check": "^2.1.1",
"color": "^2.0.1",
"downshift": "^1.25.0",

5
src/actions/accounts.js

@ -20,7 +20,10 @@ export const fetchAccounts: FetchAccounts = () => ({
payload: db('accounts'),
})
const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data }))
const setAccountData = createAction('DB:SET_ACCOUNT_DATA', (accountID, data) => ({
accountID,
data,
}))
export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => {
const { id, ...data } = account

2
src/components/AccountPage.js

@ -39,7 +39,7 @@ class AccountPage extends PureComponent<Props> {
<Card title="Balance">{format(accountData.balance)}</Card>
</Box>
<Box flex={1}>
<Card title="Receive" />
<Card title="Receive">{accountData.address}</Card>
</Box>
</Box>
<Card title="Last operations">

1
src/components/AppRegionDrag.js

@ -6,4 +6,5 @@ export default styled.div`
-webkit-app-region: drag;
background: ${p => p.theme.colors.white};
height: 40px;
z-index: 21;
`

2
src/components/base/Modal/index.js

@ -51,7 +51,7 @@ const Container = styled(Box).attrs({
})`
overflow: hidden;
position: fixed;
z-index: 1;
z-index: 20;
`
const Backdrop = styled(Box).attrs({

9
src/components/modals/AddAccount.js

@ -133,7 +133,7 @@ class AddAccountModal extends PureComponent<Props, State> {
componentWillReceiveProps(nextProps) {
const { currentDevice } = nextProps
if (this.state.step !== 'createAccount') {
if (this.props.currentDevice === null && this.state.step !== 'createAccount') {
this.setState({
step: currentDevice !== null ? 'startWallet' : 'connectDevice',
})
@ -219,16 +219,13 @@ class AddAccountModal extends PureComponent<Props, State> {
const { inputValue, accounts } = this.state
const { addAccount, closeModal } = this.props
const { id, balance, transactions } = accounts[index]
const { id, ...data } = accounts[index]
addAccount({
id,
name: inputValue.accountName,
type: inputValue.wallet,
data: {
balance,
transactions,
},
data,
})
closeModal('add-account')

90
src/helpers/btc.js

@ -1,9 +1,11 @@
// @flow
import axios from 'axios'
// import axios from 'axios'
import bitcoin from 'bitcoinjs-lib'
import { formatCurrencyUnit } from '@ledgerhq/common/lib/data/currency'
const blockexplorer = require('blockchain.info/blockexplorer').usingNetwork(3)
export function format(v: string | number, options: Object = { alwaysShowSign: true }) {
return formatCurrencyUnit(
{
@ -30,13 +32,26 @@ export const networks = [
]
export function computeTransaction(addresses: Array<*>) {
// return (transaction: Object) => {
// const outputVal = transaction.outputs
// .filter(o => addresses.includes(o.address))
// .reduce((acc, cur) => acc + cur.value, 0)
// const inputVal = transaction.inputs
// .filter(i => addresses.includes(i.address))
// .reduce((acc, cur) => acc + cur.value, 0)
// const balance = outputVal - inputVal
// return {
// ...transaction,
// balance,
// }
// }
return (transaction: Object) => {
const outputVal = transaction.outputs
.filter(o => addresses.includes(o.address))
const outputVal = transaction.out
.filter(o => addresses.includes(o.addr))
.reduce((acc, cur) => acc + cur.value, 0)
const inputVal = transaction.inputs
.filter(i => addresses.includes(i.address))
.reduce((acc, cur) => acc + cur.value, 0)
.filter(i => addresses.includes(i.prev_out.addr))
.reduce((acc, cur) => acc + cur.prev_out.value, 0)
const balance = outputVal - inputVal
return {
...transaction,
@ -46,11 +61,12 @@ export function computeTransaction(addresses: Array<*>) {
}
export function getTransactions(addresses: Array<string>) {
return axios.get(
`http://api.ledgerwallet.com/blockchain/v2/btc_testnet/addresses/${addresses.join(
',',
)}/transactions?noToken=true`,
)
// return axios.get(
// `http://api.ledgerwallet.com/blockchain/v2/btc_testnet/addresses/${addresses.join(
// ',',
// )}/transactions?noToken=true`,
// )
return blockexplorer.getMultiAddress(addresses)
}
export async function getAccount({
@ -64,9 +80,11 @@ export async function getAccount({
segwit: boolean,
network: Object,
}) {
const gapLimit = 20
const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10)
let transactions = []
let lastAddress = null
const pubKeyToSegwitAddress = (pubKey, scriptVersion) => {
const script = [0x00, 0x14].concat(Array.from(bitcoin.crypto.hash160(pubKey)))
@ -82,26 +100,47 @@ export async function getAccount({
return pubKeyToSegwitAddress(hdnode.getPublicKeyBuffer(), script)
}
const nextPath = (index = 0) => {
const count = 20
const getAddress = path => getPublicAddress({ hdnode, path, script, segwit })
const getAddress = ({ type, index }) => ({
type,
index,
address: getPublicAddress({
hdnode,
path: `${type === 'external' ? 0 : 1}/${index}`,
script,
segwit,
}),
})
const getLastAddress = (addresses, lastTx) => {
const address = addresses
.filter(a => a.type === 'external')
.find(a => a.address === lastTx.addr) || { index: 0 }
return getAddress({ type: 'external', index: address.index + 1 })
}
return Promise.all(
Array.from(Array(count).keys()).map(v =>
const nextPath = (index = 0) =>
Promise.all(
Array.from(new Array(gapLimit).keys()).map(v =>
Promise.all([
getAddress(`0/${v + index}`), // external chain
getAddress(`1/${v + index}`), // internal chain
getAddress({ type: 'external', index: v + index }),
getAddress({ type: 'internal', index: v + index }),
]),
),
).then(async results => {
const currentAddresses = results.reduce((result, v) => [...result, ...v], [])
const addresses = results.reduce((result, v) => [...result, ...v], [])
const { data: { txs } } = await getTransactions(currentAddresses)
const listAddresses = addresses.map(a => a.address)
transactions = [...transactions, ...txs.map(computeTransaction(currentAddresses))]
const { txs } = await getTransactions(listAddresses)
if (txs.length > 0) {
return nextPath(index + (count - 1))
const hasTransactions = txs.length > 0
transactions = [...transactions, ...txs.map(computeTransaction(listAddresses))]
lastAddress = hasTransactions ? getLastAddress(addresses, txs[0].out[0]) : lastAddress
if (hasTransactions) {
return nextPath(index + (gapLimit - 1))
}
return {
@ -110,9 +149,14 @@ export async function getAccount({
return result
}, 0),
transactions,
...(lastAddress !== null
? {
currentIndex: lastAddress.index,
address: lastAddress.address,
}
: {}),
}
})
}
return nextPath(currentIndex)
}

4
src/internals/accounts/sync.js

@ -8,9 +8,9 @@ export default (send: Function) => ({
send('accounts.sync.progress', null, { kill: false })
const syncAccount = ({ id }) => {
const syncAccount = ({ id, currentIndex }) => {
const hdnode = getHDNode({ xpub58: id, network })
return getAccount({ hdnode, network, segwit: true }).then(account => ({
return getAccount({ currentIndex, hdnode, network, segwit: true }).then(account => ({
id,
...account,
}))

9
src/internals/usb/wallet/accounts.js

@ -113,21 +113,20 @@ export default async ({
}
const hdnode = getHDNode({ xpub58, network })
const { transactions, balance } = await getAccount({ hdnode, network, segwit })
const account = await getAccount({ hdnode, network, segwit })
onProgress({
account: currentAccount,
transactions: transactions.length,
transactions: account.transactions.length,
})
const hasTransactions = transactions.length > 0
const hasTransactions = account.transactions.length > 0
// If the first account is empty we still add it
if (currentAccount === 0 || hasTransactions) {
accounts[currentAccount] = {
id: xpub58,
balance,
transactions,
...account,
}
}

42
src/reducers/accounts.js

@ -11,24 +11,46 @@ export type AccountsState = Accounts
const state: AccountsState = {}
function setAccount(account: Account) {
return {
...account,
data: {
...account.data,
transactions: get(account.data, 'transactions', []).reverse(),
},
}
}
const handlers: Object = {
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({
...state,
[account.id]: {
...account,
},
[account.id]: setAccount(account),
}),
FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => accounts,
SET_ACCOUNT_DATA: (
state: AccountsState,
{ payload: { accountID, data } }: { payload: { accountID: string, data: AccountData } },
): AccountsState => ({
...state,
[accountID]: {
...state[accountID],
data,
},
}),
): AccountsState => {
const account = state[accountID]
const { data: accountData } = account
const balance = get(accountData, 'balance', 0)
const transactions = get(accountData, 'transactions', [])
const currentIndex = data.currentIndex ? data.currentIndex : get(accountData, 'currentIndex', 0)
account.data = {
...accountData,
...data,
balance: balance + data.balance,
currentIndex,
transactions: [...transactions, ...data.transactions],
}
return {
...state,
[accountID]: setAccount(account),
}
},
}
// Selectors

16
src/renderer/events.js

@ -6,6 +6,7 @@ import objectPath from 'object-path'
import { updateDevices, addDevice, removeDevice } from 'actions/devices'
import { syncAccount } from 'actions/accounts'
import { setUpdateStatus } from 'reducers/update'
import { getAccounts } from 'reducers/accounts'
type MsgPayload = {
type: string,
@ -14,7 +15,7 @@ type MsgPayload = {
// wait a bit before launching update check
const CHECK_UPDATE_TIMEOUT = 3e3
const SYNC_ACCOUNT_TIMEOUT = 1e3
const SYNC_ACCOUNT_TIMEOUT = 5e3
export function sendEvent(channel: string, msgType: string, data: any) {
ipcRenderer.send(channel, {
@ -30,10 +31,13 @@ export function sendSyncEvent(channel: string, msgType: string, data: any): any
})
}
function syncAccounts(accounts) {
function startSyncAccounts(store) {
const accounts = getAccounts(store.getState())
sendEvent('accounts', 'sync.all', {
accounts: Object.entries(accounts).map(([id]: [string, any]) => ({
accounts: Object.entries(accounts).map(([id, account]: [string, any]) => ({
id,
currentIndex: account.data.currentIndex,
})),
})
}
@ -44,7 +48,7 @@ export default (store: Object) => {
sync: {
success: accounts => {
accounts.forEach(account => store.dispatch(syncAccount(account)))
setTimeout(() => syncAccounts(store.getState().accounts), SYNC_ACCOUNT_TIMEOUT)
setTimeout(() => startSyncAccounts(store), SYNC_ACCOUNT_TIMEOUT)
},
},
},
@ -76,8 +80,6 @@ export default (store: Object) => {
handler(data)
})
const state = store.getState()
// First time, we get all devices
sendEvent('usb', 'devices.all')
@ -85,7 +87,7 @@ export default (store: Object) => {
sendEvent('usb', 'devices.listen')
// Start accounts sync
syncAccounts(state.accounts)
startSyncAccounts(store)
if (__PROD__) {
// Start check of eventual updates

2
src/types/common.js

@ -18,7 +18,9 @@ export type Transaction = {
// -------------------- Accounts
export type AccountData = {
address: string,
balance: number,
currentIndex: number,
transactions: Array<Transaction>,
}

58
yarn.lock

@ -1776,12 +1776,28 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
blockchain.info@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/blockchain.info/-/blockchain.info-2.11.0.tgz#63b46617e194164d377e183e6c667d3ef38ad5b6"
dependencies:
q "^1.4.1"
request-promise "^0.4.3"
url-join "0.0.1"
url-parse "^1.0.5"
url-pattern "^0.10.2"
optionalDependencies:
ws "^1.1.2"
bluebird-lst@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.5.tgz#bebc83026b7e92a72871a3dc599e219cbfb002a9"
dependencies:
bluebird "^3.5.1"
bluebird@^2.3:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@ -2168,7 +2184,7 @@ chalk@0.5.1:
strip-ansi "^0.3.0"
supports-color "^0.2.0"
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@ -5621,7 +5637,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^3.10.1:
lodash@^3.10.0, lodash@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@ -6322,6 +6338,10 @@ optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
options@>=0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
ora@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
@ -7091,7 +7111,7 @@ pushdata-bitcoin@^1.0.1:
dependencies:
bitcoin-ops "^1.3.0"
q@^1.1.2:
q@^1.1.2, q@^1.4.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -7718,6 +7738,15 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request-promise@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-0.4.3.tgz#3c8ddc82f06f8908d720aede1d6794258e22121c"
dependencies:
bluebird "^2.3"
chalk "^1.1.0"
lodash "^3.10.0"
request "^2.34"
request@2.81.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@ -7745,7 +7774,7 @@ request@2.81.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
request@^2.45.0, request@^2.81.0, request@^2.83.0:
request@^2.34, request@^2.45.0, request@^2.81.0, request@^2.83.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
dependencies:
@ -8859,6 +8888,10 @@ uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
union-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@ -8956,6 +8989,10 @@ urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
url-join@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8"
url-loader@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
@ -8977,13 +9014,17 @@ url-parse@1.0.x:
querystringify "0.0.x"
requires-port "1.0.x"
url-parse@^1.1.8:
url-parse@^1.0.5, url-parse@^1.1.8:
version "1.2.0"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986"
dependencies:
querystringify "~1.0.0"
requires-port "~1.0.0"
url-pattern@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-0.10.2.tgz#e9f07104982b72312db4473dd86a527b580015da"
url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
@ -9307,6 +9348,13 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
ws@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51"
dependencies:
options ">=0.0.5"
ultron "1.0.x"
xdg-basedir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"

Loading…
Cancel
Save