Skip to content

Commit

Permalink
Derive addresses synchronously
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterBenc committed Jan 14, 2021
1 parent 97523fb commit c7c60f9
Show file tree
Hide file tree
Showing 9 changed files with 44 additions and 67 deletions.
6 changes: 1 addition & 5 deletions app/frontend/wallet/account-manager.ts
Expand Up @@ -19,7 +19,7 @@ const AccountManager = ({config, cryptoProvider, blockchainExplorer}) => {
}

async function addNextAccount(account) {
await account.init() // To ensure user exported pubkey
await account.ensureXpubIsExported() // To ensure user exported pubkey
const isLastAccountUsed =
accounts.length > 0 ? await accounts[accounts.length - 1].isAccountUsed() : true
if (
Expand All @@ -33,8 +33,6 @@ const AccountManager = ({config, cryptoProvider, blockchainExplorer}) => {
}

async function discoverAccounts() {
// remove rejected promises from pubkey cache
cryptoProvider.cleanXpubCache()
const isBulkExportSupported = cryptoProvider.isFeatureSupported(
CryptoProviderFeatures.BULK_EXPORT
)
Expand All @@ -51,8 +49,6 @@ const AccountManager = ({config, cryptoProvider, blockchainExplorer}) => {
}

async function exploreNextAccount() {
// remove rejected promises from pubkey cache
cryptoProvider.cleanXpubCache()
const nextAccount = discoverNextAccount()
await addNextAccount(nextAccount)
return nextAccount
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/wallet/account.ts
Expand Up @@ -173,7 +173,7 @@ const Account = ({
blockchainExplorer,
})

async function init() {
async function ensureXpubIsExported() {
// get first address to ensure that public key was exported
await myAddresses.baseExtAddrManager._deriveAddress(0)
}
Expand Down Expand Up @@ -469,7 +469,7 @@ const Account = ({
accountIndex,
getPoolRecommendation,
isAccountUsed,
init,
ensureXpubIsExported,
}
}

Expand Down
7 changes: 5 additions & 2 deletions app/frontend/wallet/address-manager.ts
@@ -1,4 +1,3 @@
import range from './helpers/range'
import {toBip32StringPath} from './helpers/bip32'
import NamedError from '../helpers/NamedError'

Expand All @@ -20,7 +19,11 @@ const AddressManager = ({addressProvider, gapLimit, blockchainExplorer}) => {
}

async function deriveAddressesBlock(beginIndex: number, endIndex: number) {
return await Promise.all(range(beginIndex, endIndex).map(cachedDeriveAddress))
const derivedAddresses = []
for (let i = beginIndex; i < endIndex; i += 1) {
derivedAddresses.push(await cachedDeriveAddress(i))
}
return derivedAddresses
}

async function discoverAddresses() {
Expand Down
15 changes: 5 additions & 10 deletions app/frontend/wallet/byron/cardano-ledger-crypto-provider.ts
Expand Up @@ -28,15 +28,11 @@ const CardanoLedgerCryptoProvider = async ({config}) => {
const isHwWallet = () => true
const getHwWalletName = () => 'Ledger'

const {deriveXpub, cleanXpubCache} = CachedDeriveXpubFactory(
derivationScheme,
false,
async (absDerivationPath) => {
const response = await ledger.getExtendedPublicKeys(absDerivationPath)
const xpubHex = response.publicKeyHex + response.chainCodeHex
return Buffer.from(xpubHex, 'hex')
}
)
const deriveXpub = CachedDeriveXpubFactory(derivationScheme, false, async (absDerivationPath) => {
const response = await ledger.getExtendedPublicKeys(absDerivationPath)
const xpubHex = response.publicKeyHex + response.chainCodeHex
return Buffer.from(xpubHex, 'hex')
})

function deriveHdNode(childIndex) {
throw NamedError('UnsupportedOperationError', {
Expand Down Expand Up @@ -141,7 +137,6 @@ const CardanoLedgerCryptoProvider = async ({config}) => {
getHwWalletName,
_sign: sign,
_deriveHdNode: deriveHdNode,
cleanXpubCache,
}
}

Expand Down
Expand Up @@ -28,13 +28,9 @@ const CardanoWalletSecretCryptoProvider = ({
return derivationScheme
}

const {deriveXpub, cleanXpubCache} = CachedDeriveXpubFactory(
derivationScheme,
false,
(derivationPaths) => {
return derivationPaths.map((path) => deriveHdNode(path).extendedPublicKey)
}
)
const deriveXpub = CachedDeriveXpubFactory(derivationScheme, false, (derivationPaths) => {
return derivationPaths.map((path) => deriveHdNode(path).extendedPublicKey)
})

function getHdPassphrase() {
return xpubToHdPassphrase(masterHdNode.extendedPublicKey)
Expand Down Expand Up @@ -128,7 +124,6 @@ const CardanoWalletSecretCryptoProvider = ({
_deriveChildHdNode: deriveChildHdNode,
_signTxGetStructured: signTxGetStructured,
network,
cleanXpubCache,
}
}

Expand Down
59 changes: 25 additions & 34 deletions app/frontend/wallet/helpers/CachedDeriveXpubFactory.ts
Expand Up @@ -7,8 +7,8 @@ const BYRON_V2_PATH = [HARDENED_THRESHOLD + 44, HARDENED_THRESHOLD + 1815, HARDE

type BIP32Path = number[]

function CachedDeriveXpubFactory(derivationScheme, shouldExportPubKeyBulk, deriveXpubsFn) {
let derivedXpubs = {}
function CachedDeriveXpubFactory(derivationScheme, shouldExportPubKeyBulk, deriveXpubsHardenedFn) {
const derivedXpubs = {}

async function deriveXpub(absDerivationPath: BIP32Path) {
const memoKey = JSON.stringify(absDerivationPath)
Expand All @@ -23,22 +23,24 @@ function CachedDeriveXpubFactory(derivationScheme, shouldExportPubKeyBulk, deriv
*/

if (deriveHardened) {
const derivationPathsBulk =
const derivationPaths =
shouldExportPubKeyBulk && isShelleyPath(absDerivationPath)
? createPathBulk(absDerivationPath)
: [absDerivationPath]
const pubKeys = await deriveXpubsHardenedFn(derivationPathsBulk)
const pubKeys = await _deriveXpubsHardenedFn(derivationPaths)
Object.assign(derivedXpubs, pubKeys)
} else {
derivedXpubs[memoKey] = deriveXpubNonhardenedFn(absDerivationPath)
derivedXpubs[memoKey] = await deriveXpubNonhardenedFn(absDerivationPath)
}
}

/*
* the derivedXpubs map stores promises instead of direct results
* to deal with concurrent requests to derive the same xpub
* we await the derivation of the key so in case the derivation fails
* the key is not added to the cache
* this approach depends on the key derivation happening sychronously
*/
return await derivedXpubs[memoKey]

return derivedXpubs[memoKey]
}

async function deriveXpubNonhardenedFn(derivationPath: BIP32Path) {
Expand All @@ -52,47 +54,36 @@ function CachedDeriveXpubFactory(derivationScheme, shouldExportPubKeyBulk, deriv
const accountIndex = derivationPath[2] - HARDENED_THRESHOLD
const currentAccountPage = Math.floor(accountIndex / MAX_BULK_EXPORT_AMOUNT)

/*
* in case of the account 0 we append also the byron path
* since during byron era only the first account was used
*/
if (accountIndex === 0) paths.push(BYRON_V2_PATH)

for (let i = 0; i < MAX_BULK_EXPORT_AMOUNT; i += 1) {
const nextAccountIndex = currentAccountPage * MAX_BULK_EXPORT_AMOUNT + i + HARDENED_THRESHOLD
const nextAccountPath = [...derivationPath.slice(0, -1), nextAccountIndex]
paths.push(nextAccountPath)
}

/*
* in case of the account 0 we append also the byron path
* since during byron era only the first account was used
*/
if (accountIndex === 0 && !paths.includes(BYRON_V2_PATH)) paths.push(BYRON_V2_PATH)

return paths
}

async function deriveXpubsHardenedFn(derivationPaths: BIP32Path[]): Promise<any> {
const xPubBulk = await deriveXpubsFn(derivationPaths)
/*
* on top of the original deriveXpubHardenedFn this is priming
* the cache of derived keys to minimize the number of prompts on hardware wallets
*/
async function _deriveXpubsHardenedFn(derivationPaths: BIP32Path[]): Promise<any> {
const xPubBulk = await deriveXpubsHardenedFn(derivationPaths)
const _derivedXpubs = {}
xPubBulk.forEach((xpub: Buffer, i: number) => {
const memoKey = JSON.stringify(derivationPaths[i])
_derivedXpubs[memoKey] = Promise.resolve(xpub)
_derivedXpubs[memoKey] = xpub
})
return _derivedXpubs
}

/*
* in case a promise is rejected we need to remove this promise from
* the cache otherwise user would never be able to export the pubKey again
*/
function cleanXpubCache() {
const _derivedXpubs = {}
Object.entries(derivedXpubs).map(([memoKey, xpubPromise]: [string, Promise<Buffer>]) => {
xpubPromise
.then((xpub) => {
_derivedXpubs[memoKey] = Promise.resolve(xpub)
})
.catch((e) => null)
})
derivedXpubs = _derivedXpubs
}

return {deriveXpub, cleanXpubCache}
return deriveXpub
}

export default CachedDeriveXpubFactory
3 changes: 1 addition & 2 deletions app/frontend/wallet/shelley/shelley-js-crypto-provider.ts
Expand Up @@ -36,7 +36,7 @@ const ShelleyJsCryptoProvider = ({

const getDerivationScheme = () => derivationScheme

const {deriveXpub, cleanXpubCache} = CachedDeriveXpubFactory(
const deriveXpub = CachedDeriveXpubFactory(
derivationScheme,
config.shouldExportPubKeyBulk,
(derivationPaths) => {
Expand Down Expand Up @@ -151,7 +151,6 @@ const ShelleyJsCryptoProvider = ({
_deriveChildHdNode: deriveChildHdNode,
ensureFeatureIsSupported,
isFeatureSupported,
cleanXpubCache,
}
}

Expand Down
Expand Up @@ -84,7 +84,7 @@ const ShelleyLedgerCryptoProvider = async ({network, config, forceWebUsb}) => {
return response
}

const {deriveXpub, cleanXpubCache} = CachedDeriveXpubFactory(
const deriveXpub = CachedDeriveXpubFactory(
derivationScheme,
config.shouldExportPubKeyBulk && isFeatureSupported(CryptoProviderFeatures.BULK_EXPORT),
async (derivationPaths) => {
Expand Down Expand Up @@ -298,7 +298,6 @@ const ShelleyLedgerCryptoProvider = async ({network, config, forceWebUsb}) => {
getWalletName,
_sign: sign,
_deriveHdNode: deriveHdNode,
cleanXpubCache,
isFeatureSupported,
ensureFeatureIsSupported,
}
Expand Down
Expand Up @@ -21,7 +21,7 @@ const CardanoTrezorCryptoProvider = ({network, config}) => {
const isHwWallet = () => true
const getWalletName = () => 'Trezor'

const {deriveXpub, cleanXpubCache} = CachedDeriveXpubFactory(
const deriveXpub = CachedDeriveXpubFactory(
derivationScheme,
config.shouldExportPubKeyBulk,
async (absDerivationPaths: BIP32Path[]) => {
Expand Down Expand Up @@ -261,7 +261,6 @@ const CardanoTrezorCryptoProvider = ({network, config}) => {
}

return {
cleanXpubCache,
getWalletSecret,
getDerivationScheme,
signTx,
Expand Down

0 comments on commit c7c60f9

Please sign in to comment.