You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
9.2 KiB
366 lines
9.2 KiB
7 years ago
|
// @flow
|
||
|
|
||
|
// Scan accounts on device
|
||
|
// -----------------------
|
||
|
//
|
||
|
// _ ,--()
|
||
|
// ( )-'-.------|>
|
||
|
// " `--[]
|
||
|
//
|
||
|
|
||
7 years ago
|
import Btc from '@ledgerhq/hw-app-btc'
|
||
7 years ago
|
import { withDevice } from 'helpers/deviceAccess'
|
||
7 years ago
|
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
|
||
7 years ago
|
|
||
7 years ago
|
import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types'
|
||
7 years ago
|
import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc'
|
||
7 years ago
|
|
||
7 years ago
|
type Props = {
|
||
|
devicePath: string,
|
||
|
currencyId: string,
|
||
7 years ago
|
onAccountScanned: AccountRaw => *,
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
|
||
7 years ago
|
const { devicePath, currencyId, onAccountScanned } = props
|
||
|
|
||
7 years ago
|
return withDevice(devicePath)(async transport => {
|
||
|
const hwApp = new Btc(transport)
|
||
7 years ago
|
|
||
7 years ago
|
const commonParams = {
|
||
|
hwApp,
|
||
|
currencyId,
|
||
|
onAccountScanned,
|
||
|
devicePath,
|
||
|
}
|
||
7 years ago
|
|
||
7 years ago
|
// scan segwit AND non-segwit accounts
|
||
|
const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({
|
||
|
...commonParams,
|
||
|
isSegwit: false,
|
||
|
})
|
||
7 years ago
|
const segwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, isSegwit: true })
|
||
7 years ago
|
|
||
7 years ago
|
const accounts = [...nonSegwitAccounts, ...segwitAccounts]
|
||
7 years ago
|
|
||
7 years ago
|
return accounts
|
||
|
})
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
export async function getWalletIdentifier({
|
||
7 years ago
|
hwApp,
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
currencyId,
|
||
|
devicePath,
|
||
7 years ago
|
}: {
|
||
|
hwApp: Object,
|
||
|
isSegwit: boolean,
|
||
|
currencyId: string,
|
||
|
devicePath: string,
|
||
7 years ago
|
}): Promise<string> {
|
||
7 years ago
|
const isVerify = false
|
||
|
const deviceIdentifiers = await hwApp.getWalletPublicKey(devicePath, isVerify, isSegwit)
|
||
7 years ago
|
const { publicKey } = deviceIdentifiers
|
||
7 years ago
|
const WALLET_IDENTIFIER = `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}`
|
||
7 years ago
|
return WALLET_IDENTIFIER
|
||
|
}
|
||
|
|
||
|
async function scanAccountsOnDeviceBySegwit({
|
||
|
hwApp,
|
||
|
currencyId,
|
||
|
onAccountScanned,
|
||
|
devicePath,
|
||
|
isSegwit,
|
||
7 years ago
|
}: {
|
||
|
hwApp: Object,
|
||
|
currencyId: string,
|
||
|
onAccountScanned: AccountRaw => void,
|
||
|
devicePath: string,
|
||
|
isSegwit: boolean,
|
||
|
}): Promise<AccountRaw[]> {
|
||
7 years ago
|
// compute wallet identifier
|
||
|
const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath })
|
||
7 years ago
|
|
||
|
// retrieve or create the wallet
|
||
7 years ago
|
const wallet = await getOrCreateWallet(WALLET_IDENTIFIER, currencyId, isSegwit)
|
||
7 years ago
|
const accountsCount = await wallet.getAccountCount()
|
||
|
|
||
|
// recursively scan all accounts on device on the given app
|
||
|
// new accounts will be created in sqlite, existing ones will be updated
|
||
|
const accounts = await scanNextAccount({
|
||
|
wallet,
|
||
|
hwApp,
|
||
|
currencyId,
|
||
|
accountsCount,
|
||
|
accountIndex: 0,
|
||
|
accounts: [],
|
||
|
onAccountScanned,
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
})
|
||
|
|
||
|
return accounts
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
async function scanNextAccount(props: {
|
||
|
// $FlowFixMe
|
||
|
wallet: NJSWallet,
|
||
|
hwApp: Object,
|
||
|
currencyId: string,
|
||
|
accountsCount: number,
|
||
|
accountIndex: number,
|
||
|
accounts: AccountRaw[],
|
||
|
onAccountScanned: AccountRaw => void,
|
||
|
isSegwit: boolean,
|
||
|
}): Promise<AccountRaw[]> {
|
||
7 years ago
|
const {
|
||
|
wallet,
|
||
|
hwApp,
|
||
|
currencyId,
|
||
|
accountsCount,
|
||
|
accountIndex,
|
||
|
accounts,
|
||
|
onAccountScanned,
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
} = props
|
||
|
|
||
|
// TODO: investigate why importing it on file scope causes trouble
|
||
7 years ago
|
const core = require('init-ledger-core')()
|
||
7 years ago
|
|
||
7 years ago
|
console.log(`>> Scanning account ${accountIndex} - isSegwit: ${isSegwit.toString()}`) // eslint-disable-line no-console
|
||
7 years ago
|
|
||
7 years ago
|
// create account only if account has not been scanned yet
|
||
|
// if it has already been created, we just need to get it, and sync it
|
||
|
const hasBeenScanned = accountIndex < accountsCount
|
||
|
|
||
7 years ago
|
const njsAccount = hasBeenScanned
|
||
7 years ago
|
? await wallet.getAccount(accountIndex)
|
||
|
: await core.createAccount(wallet, hwApp)
|
||
|
|
||
7 years ago
|
if (!hasBeenScanned) {
|
||
|
await core.syncAccount(njsAccount)
|
||
|
}
|
||
7 years ago
|
|
||
7 years ago
|
const query = njsAccount.queryOperations()
|
||
7 years ago
|
const ops = await query.complete().execute()
|
||
7 years ago
|
|
||
7 years ago
|
const account = await buildAccountRaw({
|
||
7 years ago
|
njsAccount,
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
accountIndex,
|
||
|
wallet,
|
||
|
currencyId,
|
||
|
core,
|
||
|
hwApp,
|
||
7 years ago
|
ops,
|
||
7 years ago
|
})
|
||
|
|
||
7 years ago
|
const isEmpty = ops.length === 0
|
||
7 years ago
|
|
||
7 years ago
|
// trigger event
|
||
|
onAccountScanned(account)
|
||
|
|
||
|
accounts.push(account)
|
||
|
|
||
7 years ago
|
// returns if the current index points on an account with no ops
|
||
|
if (isEmpty) {
|
||
|
return accounts
|
||
|
}
|
||
|
|
||
7 years ago
|
return scanNextAccount({ ...props, accountIndex: accountIndex + 1 })
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
async function getOrCreateWallet(
|
||
|
WALLET_IDENTIFIER: string,
|
||
|
currencyId: string,
|
||
|
isSegwit: boolean,
|
||
|
): NJSWallet {
|
||
7 years ago
|
// TODO: investigate why importing it on file scope causes trouble
|
||
7 years ago
|
const core = require('init-ledger-core')()
|
||
7 years ago
|
try {
|
||
|
const wallet = await core.getWallet(WALLET_IDENTIFIER)
|
||
|
return wallet
|
||
|
} catch (err) {
|
||
|
const currency = await core.getCurrency(currencyId)
|
||
7 years ago
|
const walletConfig = isSegwit
|
||
|
? {
|
||
|
KEYCHAIN_ENGINE: 'BIP49_P2SH',
|
||
|
KEYCHAIN_DERIVATION_SCHEME: "49'/<coin_type>'/<account>'/<node>/<address>",
|
||
|
}
|
||
|
: undefined
|
||
|
const njsWalletConfig = core.createWalletConfig(walletConfig)
|
||
|
const wallet = await core.createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig)
|
||
7 years ago
|
return wallet
|
||
7 years ago
|
}
|
||
7 years ago
|
}
|
||
7 years ago
|
|
||
7 years ago
|
async function buildAccountRaw({
|
||
7 years ago
|
njsAccount,
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
wallet,
|
||
|
currencyId,
|
||
7 years ago
|
core,
|
||
7 years ago
|
hwApp,
|
||
|
accountIndex,
|
||
7 years ago
|
ops,
|
||
7 years ago
|
}: {
|
||
|
njsAccount: NJSAccount,
|
||
7 years ago
|
isSegwit: boolean,
|
||
7 years ago
|
// $FlowFixMe
|
||
7 years ago
|
wallet: NJSWallet,
|
||
|
currencyId: string,
|
||
|
accountIndex: number,
|
||
|
core: Object,
|
||
|
hwApp: Object,
|
||
7 years ago
|
// $FlowFixMe
|
||
7 years ago
|
ops: NJSOperation[],
|
||
7 years ago
|
}): Promise<AccountRaw> {
|
||
7 years ago
|
/*
|
||
7 years ago
|
const balanceByDay = ops.length
|
||
|
? await getBalanceByDaySinceOperation({
|
||
|
njsAccount,
|
||
|
njsOperation: ops[0],
|
||
|
core,
|
||
|
})
|
||
|
: {}
|
||
7 years ago
|
*/
|
||
7 years ago
|
|
||
|
const njsBalance = await njsAccount.getBalance()
|
||
|
const balance = njsBalance.toLong()
|
||
|
|
||
7 years ago
|
const jsCurrency = getCryptoCurrencyById(currencyId)
|
||
|
|
||
|
// retrieve xpub
|
||
|
const { derivations } = await wallet.getAccountCreationInfo(accountIndex)
|
||
|
const [walletPath, accountPath] = derivations
|
||
7 years ago
|
|
||
7 years ago
|
const isVerify = false
|
||
7 years ago
|
const { bitcoinAddress } = await hwApp.getWalletPublicKey(accountPath, isVerify, isSegwit)
|
||
7 years ago
|
|
||
7 years ago
|
const xpub = bitcoinAddress
|
||
7 years ago
|
|
||
7 years ago
|
// blockHeight
|
||
7 years ago
|
const { height: blockHeight } = await njsAccount.getLastBlock()
|
||
|
|
||
7 years ago
|
// get a bunch of fresh addresses
|
||
7 years ago
|
const rawAddresses = await njsAccount.getFreshPublicAddresses()
|
||
7 years ago
|
|
||
|
const addresses = rawAddresses.map(njsAddress => ({
|
||
|
str: njsAddress.toString(),
|
||
|
path: `${accountPath}/${njsAddress.getDerivationPath()}`,
|
||
7 years ago
|
}))
|
||
7 years ago
|
|
||
7 years ago
|
if (addresses.length === 0) {
|
||
|
throw new Error('no addresses found')
|
||
|
}
|
||
|
|
||
|
const { str: freshAddress, path: freshAddressPath } = addresses[0]
|
||
|
|
||
7 years ago
|
const operations = ops.map(op => buildOperationRaw({ core, op, xpub }))
|
||
7 years ago
|
|
||
7 years ago
|
const rawAccount: AccountRaw = {
|
||
7 years ago
|
id: xpub, // FIXME for account id you might want to prepend the crypto currency id to this because it's not gonna be unique.
|
||
7 years ago
|
xpub,
|
||
7 years ago
|
path: walletPath,
|
||
7 years ago
|
name: `${operations.length === 0 ? 'New ' : ''}Account ${accountIndex}${
|
||
|
isSegwit ? ' (segwit)' : ''
|
||
|
}`, // TODO: placeholder name?
|
||
7 years ago
|
isSegwit,
|
||
7 years ago
|
freshAddress,
|
||
|
freshAddressPath,
|
||
7 years ago
|
balance,
|
||
7 years ago
|
blockHeight,
|
||
7 years ago
|
archived: true,
|
||
7 years ago
|
index: accountIndex,
|
||
7 years ago
|
operations,
|
||
7 years ago
|
pendingOperations: [],
|
||
7 years ago
|
currencyId,
|
||
|
unitMagnitude: jsCurrency.units[0].magnitude,
|
||
|
lastSyncDate: new Date().toISOString(),
|
||
|
}
|
||
|
|
||
|
return rawAccount
|
||
|
}
|
||
|
|
||
7 years ago
|
function buildOperationRaw({
|
||
|
core,
|
||
|
op,
|
||
|
xpub,
|
||
|
}: {
|
||
|
core: Object,
|
||
|
op: NJSOperation,
|
||
|
xpub: string,
|
||
|
}): OperationRaw {
|
||
7 years ago
|
const id = op.getUid()
|
||
7 years ago
|
const bitcoinLikeOperation = op.asBitcoinLikeOperation()
|
||
|
const bitcoinLikeTransaction = bitcoinLikeOperation.getTransaction()
|
||
|
const hash = bitcoinLikeTransaction.getHash()
|
||
7 years ago
|
const operationType = op.getOperationType()
|
||
7 years ago
|
const value = op.getAmount().toLong()
|
||
|
|
||
|
const OperationTypeMap: { [_: $Keys<typeof core.OPERATION_TYPES>]: OperationType } = {
|
||
|
[core.OPERATION_TYPES.SEND]: 'OUT',
|
||
|
[core.OPERATION_TYPES.RECEIVE]: 'IN',
|
||
|
}
|
||
7 years ago
|
|
||
|
// if transaction is a send, amount becomes negative
|
||
7 years ago
|
const type = OperationTypeMap[operationType]
|
||
7 years ago
|
|
||
7 years ago
|
return {
|
||
7 years ago
|
id,
|
||
7 years ago
|
hash,
|
||
7 years ago
|
type,
|
||
|
value,
|
||
7 years ago
|
senders: op.getSenders(),
|
||
|
recipients: op.getRecipients(),
|
||
|
blockHeight: op.getBlockHeight(),
|
||
7 years ago
|
blockHash: null,
|
||
7 years ago
|
accountId: xpub,
|
||
|
date: op.getDate().toISOString(),
|
||
|
}
|
||
|
}
|
||
|
|
||
7 years ago
|
/*
|
||
7 years ago
|
async function getBalanceByDaySinceOperation({
|
||
|
njsAccount,
|
||
|
njsOperation,
|
||
|
core,
|
||
|
}: {
|
||
|
njsAccount: NJSAccount,
|
||
|
njsOperation: NJSOperation,
|
||
|
core: Object,
|
||
|
}) {
|
||
|
const startDate = njsOperation.getDate()
|
||
|
// set end date to tomorrow
|
||
|
const endDate = new Date()
|
||
|
endDate.setDate(endDate.getDate() + 1)
|
||
|
const njsBalanceHistory = await njsAccount.getBalanceHistory(
|
||
|
startDate.toISOString(),
|
||
|
endDate.toISOString(),
|
||
|
core.TIME_PERIODS.DAY,
|
||
|
)
|
||
|
let i = 0
|
||
|
const res = {}
|
||
|
while (!areSameDay(startDate, endDate)) {
|
||
|
const dateSQLFormatted = startDate.toISOString().substr(0, 10)
|
||
|
const balanceDay = njsBalanceHistory[i]
|
||
|
if (balanceDay) {
|
||
|
res[dateSQLFormatted] = njsBalanceHistory[i].toLong()
|
||
|
} else {
|
||
|
console.warn(`No balance for day ${dateSQLFormatted}. This is a bug.`) // eslint-disable-line no-console
|
||
|
}
|
||
|
startDate.setDate(startDate.getDate() + 1)
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return res
|
||
|
}
|
||
|
|
||
7 years ago
|
function areSameDay(date1: Date, date2: Date): boolean {
|
||
7 years ago
|
return (
|
||
7 years ago
|
date1.getFullYear() === date2.getFullYear() &&
|
||
7 years ago
|
date1.getMonth() === date2.getMonth() &&
|
||
|
date1.getDate() === date2.getDate()
|
||
|
)
|
||
7 years ago
|
}
|
||
7 years ago
|
*/
|