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.
 
 
 
 

544 lines
15 KiB

// @flow
// TODO split these into many files
import logger from 'logger'
import { BigNumber } from 'bignumber.js'
import Btc from '@ledgerhq/hw-app-btc'
import { withDevice } from 'helpers/deviceAccess'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { SHOW_LEGACY_NEW_ACCOUNT } from 'config/constants'
import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types'
import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc'
import { isSegwitAccount, isUnsplitAccount } from 'helpers/bip32'
import * as accountIdHelper from 'helpers/accountId'
import { createCustomErrorClass, deserializeError } from './errors'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName'
const NoAddressesFound = createCustomErrorClass('NoAddressesFound')
// TODO: put that info inside currency itself
const SPLITTED_CURRENCIES = {
bitcoin_cash: {
coinType: 0,
},
bitcoin_gold: {
coinType: 0,
},
}
export function isValidAddress(core: *, currency: *, address: string): boolean {
const addr = new core.NJSAddress(address, currency)
return addr.isValid(address, currency)
}
type Props = {
core: *,
devicePath: string,
currencyId: string,
onAccountScanned: AccountRaw => void,
isUnsubscribed: () => boolean,
}
export async function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const { devicePath, currencyId, onAccountScanned, core, isUnsubscribed } = props
const currency = getCryptoCurrencyById(currencyId)
const commonParams = {
core,
currencyId,
onAccountScanned,
devicePath,
isUnsubscribed,
}
let allAccounts = []
const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: !!SHOW_LEGACY_NEW_ACCOUNT || !currency.supportsSegwit,
isSegwit: false,
isUnsplit: false,
})
allAccounts = allAccounts.concat(nonSegwitAccounts)
if (currency.supportsSegwit) {
const segwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: true,
isSegwit: true,
isUnsplit: false,
})
allAccounts = allAccounts.concat(segwitAccounts)
}
// TODO: put that info inside currency itself
if (currencyId in SPLITTED_CURRENCIES) {
const splittedAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
isSegwit: false,
showNewAccount: false,
isUnsplit: true,
})
allAccounts = allAccounts.concat(splittedAccounts)
if (currency.supportsSegwit) {
const segwitAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
showNewAccount: false,
isUnsplit: true,
isSegwit: true,
})
allAccounts = allAccounts.concat(segwitAccounts)
}
}
return allAccounts
}
function encodeWalletName({
publicKey,
currencyId,
isSegwit,
isUnsplit,
}: {
publicKey: string,
currencyId: string,
isSegwit: boolean,
isUnsplit: boolean,
}) {
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
return `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}${splitConfig ? '_unsplit' : ''}`
}
async function scanAccountsOnDeviceBySegwit({
core,
devicePath,
currencyId,
onAccountScanned,
isUnsubscribed,
isSegwit,
isUnsplit,
showNewAccount,
}: {
core: *,
devicePath: string,
currencyId: string,
onAccountScanned: AccountRaw => void,
isUnsubscribed: () => boolean,
isSegwit: boolean, // FIXME all segwit to change to 'purpose'
showNewAccount: boolean,
isUnsplit: boolean,
}): Promise<AccountRaw[]> {
const customOpts =
isUnsplit && SPLITTED_CURRENCIES[currencyId] ? SPLITTED_CURRENCIES[currencyId] : null
const { coinType } = customOpts ? customOpts.coinType : getCryptoCurrencyById(currencyId)
const path = `${isSegwit ? '49' : '44'}'/${coinType}'`
const { publicKey } = await withDevice(devicePath)(async transport =>
new Btc(transport).getWalletPublicKey(path, false, isSegwit),
)
if (isUnsubscribed()) return []
const walletName = encodeWalletName({ publicKey, currencyId, isSegwit, isUnsplit })
// retrieve or create the wallet
const wallet = await getOrCreateWallet(core, walletName, currencyId, isSegwit, isUnsplit)
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({
core,
wallet,
devicePath,
currencyId,
accountsCount,
accountIndex: 0,
accounts: [],
onAccountScanned,
isSegwit,
isUnsplit,
showNewAccount,
isUnsubscribed,
})
return accounts
}
const hexToBytes = str => Array.from(Buffer.from(str, 'hex'))
const createAccount = async (wallet, devicePath) => {
const accountCreationInfos = await wallet.getNextAccountCreationInfo()
await accountCreationInfos.derivations.reduce(
(promise, derivation) =>
promise.then(async () => {
const { publicKey, chainCode } = await withDevice(devicePath)(async transport =>
new Btc(transport).getWalletPublicKey(derivation),
)
accountCreationInfos.publicKeys.push(hexToBytes(publicKey))
accountCreationInfos.chainCodes.push(hexToBytes(chainCode))
}),
Promise.resolve(),
)
return wallet.newAccountWithInfo(accountCreationInfos)
}
function createEventReceiver(core, cb) {
return new core.NJSEventReceiver({
onEvent: event => cb(event),
})
}
function subscribeToEventBus(core, eventBus, receiver) {
eventBus.subscribe(core.getSerialExecutionContext('main'), receiver)
}
const coreSyncAccount = (core, account) =>
new Promise((resolve, reject) => {
const eventReceiver = createEventReceiver(core, e => {
const code = e.getCode()
if (code === core.EVENT_CODE.UNDEFINED || code === core.EVENT_CODE.SYNCHRONIZATION_FAILED) {
const payload = e.getPayload()
const message = (
(payload && payload.getString('EV_SYNC_ERROR_MESSAGE')) ||
'Sync failed'
).replace(' (EC_PRIV_KEY_INVALID_FORMAT)', '')
try {
reject(deserializeError(JSON.parse(message)))
} catch (e) {
reject(message)
}
return
}
if (
code === core.EVENT_CODE.SYNCHRONIZATION_SUCCEED ||
code === core.EVENT_CODE.SYNCHRONIZATION_SUCCEED_ON_PREVIOUSLY_EMPTY_ACCOUNT
) {
resolve(() => {
eventBus.unsubscribe(eventReceiver)
})
}
})
const eventBus = account.synchronize()
subscribeToEventBus(core, eventBus, eventReceiver)
})
async function scanNextAccount(props: {
// $FlowFixMe
wallet: NJSWallet,
core: *,
devicePath: string,
currencyId: string,
accountsCount: number,
accountIndex: number,
accounts: AccountRaw[],
onAccountScanned: AccountRaw => void,
isSegwit: boolean,
isUnsplit: boolean,
showNewAccount: boolean,
isUnsubscribed: () => boolean,
}): Promise<AccountRaw[]> {
const {
core,
wallet,
devicePath,
currencyId,
accountsCount,
accountIndex,
accounts,
onAccountScanned,
isSegwit,
isUnsplit,
showNewAccount,
isUnsubscribed,
} = props
// 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
const njsAccount = hasBeenScanned
? await wallet.getAccount(accountIndex)
: await createAccount(wallet, devicePath)
if (isUnsubscribed()) return []
const shouldSyncAccount = true // TODO: let's sync everytime. maybe in the future we can optimize.
if (shouldSyncAccount) {
await coreSyncAccount(core, njsAccount)
}
if (isUnsubscribed()) return []
const query = njsAccount.queryOperations()
const ops = await query.complete().execute()
const account = await buildAccountRaw({
njsAccount,
isSegwit,
isUnsplit,
accountIndex,
wallet,
currencyId,
core,
ops,
})
if (isUnsubscribed()) return []
const isEmpty = ops.length === 0
if (!isEmpty || showNewAccount) {
onAccountScanned(account)
accounts.push(account)
}
// returns if the current index points on an account with no ops
if (isEmpty) {
return accounts
}
return scanNextAccount({ ...props, accountIndex: accountIndex + 1 })
}
const createWalletConfig = (core, configMap = {}) => {
const config = new core.NJSDynamicObject()
for (const i in configMap) {
if (configMap.hasOwnProperty(i)) {
config.putString(i, configMap[i])
}
}
return config
}
async function getOrCreateWallet(
core: *,
WALLET_IDENTIFIER: string,
currencyId: string,
isSegwit: boolean,
isUnsplit: boolean,
): NJSWallet {
const pool = core.getPoolInstance()
try {
const wallet = await pool.getWallet(WALLET_IDENTIFIER)
return wallet
} catch (err) {
const currency = await pool.getCurrency(currencyId)
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const coinType = splitConfig ? splitConfig.coinType : '<coin_type>'
const walletConfig = isSegwit
? {
KEYCHAIN_ENGINE: 'BIP49_P2SH',
KEYCHAIN_DERIVATION_SCHEME: `49'/${coinType}'/<account>'/<node>/<address>`,
}
: splitConfig
? {
KEYCHAIN_DERIVATION_SCHEME: `44'/${coinType}'/<account>'/<node>/<address>`,
}
: undefined
const njsWalletConfig = createWalletConfig(core, walletConfig)
const wallet = await core
.getPoolInstance()
.createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig)
return wallet
}
}
async function buildAccountRaw({
njsAccount,
isSegwit,
isUnsplit,
wallet,
currencyId,
core,
accountIndex,
ops,
}: {
njsAccount: NJSAccount,
isSegwit: boolean,
isUnsplit: boolean,
wallet: NJSWallet,
currencyId: string,
accountIndex: number,
core: *,
ops: NJSOperation[],
}): Promise<AccountRaw> {
const njsBalance = await njsAccount.getBalance()
const balance = njsBalance.toLong()
const jsCurrency = getCryptoCurrencyById(currencyId)
const { derivations } = await wallet.getAccountCreationInfo(accountIndex)
const [walletPath, accountPath] = derivations
// retrieve xpub
const xpub = njsAccount.getRestoreKey()
// blockHeight
const { height: blockHeight } = await njsAccount.getLastBlock()
// get a bunch of fresh addresses
const rawAddresses = await njsAccount.getFreshPublicAddresses()
const addresses = rawAddresses.map(njsAddress => ({
str: njsAddress.toString(),
path: `${accountPath}/${njsAddress.getDerivationPath()}`,
}))
if (addresses.length === 0) {
throw new NoAddressesFound()
}
const { str: freshAddress, path: freshAddressPath } = addresses[0]
ops.sort((a, b) => b.getDate() - a.getDate())
const operations = ops.map(op => buildOperationRaw({ core, op, xpub }))
const currency = getCryptoCurrencyById(currencyId)
const name =
operations.length === 0
? getNewAccountPlaceholderName(currency, accountIndex)
: getAccountPlaceholderName(
currency,
accountIndex,
(currency.supportsSegwit && !isSegwit) || false,
isUnsplit,
)
const rawAccount: AccountRaw = {
id: accountIdHelper.encode({
type: 'libcore',
version: '1',
xpub,
walletName: wallet.getName(),
}),
xpub,
path: walletPath,
name,
isSegwit,
freshAddress,
freshAddressPath,
balance,
blockHeight,
archived: false,
index: accountIndex,
operations,
pendingOperations: [],
currencyId,
unitMagnitude: jsCurrency.units[0].magnitude,
lastSyncDate: new Date().toISOString(),
}
return rawAccount
}
function buildOperationRaw({
core,
op,
xpub,
}: {
core: *,
op: NJSOperation,
xpub: string,
}): OperationRaw {
const bitcoinLikeOperation = op.asBitcoinLikeOperation()
const bitcoinLikeTransaction = bitcoinLikeOperation.getTransaction()
const hash = bitcoinLikeTransaction.getHash()
const operationType = op.getOperationType()
let value = op.getAmount().toLong()
const fee = op.getFees().toLong()
const OperationTypeMap: { [_: $Keys<typeof core.OPERATION_TYPES>]: OperationType } = {
[core.OPERATION_TYPES.SEND]: 'OUT',
[core.OPERATION_TYPES.RECEIVE]: 'IN',
}
// if transaction is a send, amount becomes negative
const type = OperationTypeMap[operationType]
if (type === 'OUT') {
value += fee
}
const id = `${xpub}-${hash}-${type}`
return {
id,
hash,
type,
value,
fee,
senders: op.getSenders(),
recipients: op.getRecipients(),
blockHeight: op.getBlockHeight(),
blockHash: null,
accountId: xpub, // FIXME accountId: xpub !?
date: op.getDate().toISOString(),
}
}
export async function syncAccount({ rawAccount, core }: { core: *, rawAccount: AccountRaw }) {
const decodedAccountId = accountIdHelper.decode(rawAccount.id)
const isSegwit = isSegwitAccount(rawAccount)
const isUnsplit = isUnsplitAccount(rawAccount, SPLITTED_CURRENCIES[rawAccount.currencyId])
let njsWallet
try {
njsWallet = await core.getPoolInstance().getWallet(decodedAccountId.walletName)
} catch (e) {
logger.warn(`Have to reimport the account... (${e})`)
njsWallet = await getOrCreateWallet(
core,
decodedAccountId.walletName,
rawAccount.currencyId,
isSegwit,
isUnsplit,
)
}
let njsAccount
try {
njsAccount = await njsWallet.getAccount(rawAccount.index)
} catch (e) {
logger.warn(`Have to recreate the account... (${e.message})`)
const extendedInfos = await njsWallet.getExtendedKeyAccountCreationInfo(rawAccount.index)
extendedInfos.extendedKeys.push(decodedAccountId.xpub)
njsAccount = await njsWallet.newAccountWithExtendedKeyInfo(extendedInfos)
}
const unsub = await coreSyncAccount(core, njsAccount)
unsub()
const query = njsAccount.queryOperations()
const ops = await query.complete().execute()
const njsBalance = await njsAccount.getBalance()
const syncedRawAccount = await buildAccountRaw({
njsAccount,
isSegwit,
isUnsplit,
accountIndex: rawAccount.index,
wallet: njsWallet,
currencyId: rawAccount.currencyId,
core,
ops,
})
syncedRawAccount.balance = njsBalance.toLong()
logger.log(`Synced account [${syncedRawAccount.name}]: ${syncedRawAccount.balance}`)
return syncedRawAccount
}
export function libcoreAmountToBigNumber(njsAmount: *): BigNumber {
return BigNumber(njsAmount.toBigInt().toString(10))
}
export function bigNumberToLibcoreAmount(core: *, njsWalletCurrency: *, bigNumber: BigNumber) {
return new core.NJSAmount(njsWalletCurrency, 0).fromHex(njsWalletCurrency, bigNumber.toString(16))
}