From 7e71b910303686caaa66f2baf20fe040207c1613 Mon Sep 17 00:00:00 2001 From: alter-eggo Date: Thu, 2 Nov 2023 15:20:00 +0400 Subject: [PATCH] feat: process stacks ledger keys --- .../flows/jwt-signing/jwt-signing.utils.ts | 2 +- .../ledger-request-bitcoin-keys.tsx | 14 ++--- .../request-bitcoin-keys.utils.ts | 8 +-- .../ledger-request-stacks-keys.tsx | 10 +--- .../request-stacks-keys.utils.ts | 7 ++- .../ledger/utils/bitcoin-ledger-utils.ts | 1 + .../ledger/utils/stacks-ledger-utils.ts | 16 +----- .../ledger-chain-key-storage-generator.ts | 6 +- src/app/store/ledger/ledger.selectors.ts | 3 +- .../store/ledger/stacks/stacks-key.slice.ts | 2 +- src/shared/crypto/stacks/stacks.utils.ts | 13 +++++ src/shared/storage/redux-pesist.ts | 55 ++++++++++++++----- tests/page-object-models/onboarding.page.ts | 22 ++++++-- 13 files changed, 92 insertions(+), 67 deletions(-) create mode 100644 src/shared/crypto/stacks/stacks.utils.ts diff --git a/src/app/features/ledger/flows/jwt-signing/jwt-signing.utils.ts b/src/app/features/ledger/flows/jwt-signing/jwt-signing.utils.ts index af92b74d675..43d3a2a9153 100644 --- a/src/app/features/ledger/flows/jwt-signing/jwt-signing.utils.ts +++ b/src/app/features/ledger/flows/jwt-signing/jwt-signing.utils.ts @@ -3,7 +3,7 @@ import { bytesToHex } from '@stacks/common'; import StacksApp from '@zondax/ledger-stacks'; import ecdsaFormat from 'ecdsa-sig-formatter'; -import { getIdentityDerivationPath } from '../../utils/stacks-ledger-utils'; +import { getIdentityDerivationPath } from '@shared/crypto/stacks/stacks.utils'; function reformatDerSignatureToJose(derSignature: Uint8Array) { // Stacks authentication uses `ES256k`, however `ecdsa-sig-formatter` doesn't diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx index 6c783e43a67..c947f9c8f10 100644 --- a/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx +++ b/src/app/features/ledger/flows/request-bitcoin-keys/ledger-request-bitcoin-keys.tsx @@ -34,7 +34,10 @@ function LedgerRequestBitcoinKeys() { navigate('/', { replace: true }); }, async pullKeysFromDevice(app) { - const { keys } = await pullBitcoinKeysFromLedgerDevice(app)({ + const { keys } = await pullBitcoinKeysFromLedgerDevice( + app, + latestDeviceResponse?.targetId + )({ network: bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network), onRequestKey(index) { if (index <= 4) { @@ -47,15 +50,6 @@ function LedgerRequestBitcoinKeys() { }, }); dispatch(bitcoinKeysSlice.actions.addKeys(keys)); - const targetId = latestDeviceResponse?.targetId; - - if (targetId) { - dispatch( - bitcoinKeysSlice.actions.addTargetId({ - targetId, - }) - ); - } }, }); diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts index 636afcf5996..2198f5ec7c4 100644 --- a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts +++ b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts @@ -54,11 +54,11 @@ interface PullBitcoinKeysFromLedgerDeviceArgs { onRequestKey?(keyIndex: number): void; network: NetworkModes; } -export function pullBitcoinKeysFromLedgerDevice(bitcoinApp: BitcoinApp) { +export function pullBitcoinKeysFromLedgerDevice(bitcoinApp: BitcoinApp, targetId = '') { return async ({ onRequestKey, network }: PullBitcoinKeysFromLedgerDeviceArgs) => { const amountOfKeysToExtractFromDevice = 5; const fingerprint = await bitcoinApp.getMasterFingerprint(); - const keys: { id: string; path: string; policy: string }[] = []; + const keys: { id: string; path: string; policy: string; targetId: string }[] = []; for (let accountIndex = 0; accountIndex < amountOfKeysToExtractFromDevice; accountIndex++) { onRequestKey?.(accountIndex); const { path, policy } = await getNativeSegwitExtendedPublicKey({ @@ -67,7 +67,7 @@ export function pullBitcoinKeysFromLedgerDevice(bitcoinApp: BitcoinApp) { network, accountIndex, }); - keys.push({ id: createWalletIdDecoratedPath(path, 'default'), path, policy }); + keys.push({ id: createWalletIdDecoratedPath(path, 'default'), path, policy, targetId }); } for (let accountIndex = 0; accountIndex < amountOfKeysToExtractFromDevice; accountIndex++) { onRequestKey?.(accountIndex + 5); @@ -77,7 +77,7 @@ export function pullBitcoinKeysFromLedgerDevice(bitcoinApp: BitcoinApp) { network, accountIndex, }); - keys.push({ id: createWalletIdDecoratedPath(path, 'default'), path, policy }); + keys.push({ id: createWalletIdDecoratedPath(path, 'default'), path, policy, targetId }); } await delay(250); return { status: 'success', keys }; diff --git a/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx b/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx index eb272ca2f50..1a42825569b 100644 --- a/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx +++ b/src/app/features/ledger/flows/request-stacks-keys/ledger-request-stacks-keys.tsx @@ -54,18 +54,10 @@ function LedgerRequestStacksKeys() { resp.publicKeys.map(keys => ({ ...keys, id: keys.path.replace('m', defaultWalletKeyId), + targetId: latestDeviceResponse?.targetId || '', })) ) ); - const targetId = latestDeviceResponse?.targetId; - - if (targetId) { - dispatch( - stacksKeysSlice.actions.addTargetId({ - targetId, - }) - ); - } }, }); diff --git a/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts b/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts index 2de79a40545..317d59da44b 100644 --- a/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts +++ b/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts @@ -2,12 +2,15 @@ import { bytesToHex } from '@noble/hashes/utils'; import * as secp from '@noble/secp256k1'; import StacksApp from '@zondax/ledger-stacks'; +import { + getIdentityDerivationPath, + getStxDerivationPath, +} from '@shared/crypto/stacks/stacks.utils'; + import { delay } from '@app/common/utils'; import { StacksAppKeysResponseItem, - getIdentityDerivationPath, - getStxDerivationPath, requestPublicKeyForStxAccount, } from '../../utils/stacks-ledger-utils'; diff --git a/src/app/features/ledger/utils/bitcoin-ledger-utils.ts b/src/app/features/ledger/utils/bitcoin-ledger-utils.ts index 55bf2316cc4..a64ec49298e 100644 --- a/src/app/features/ledger/utils/bitcoin-ledger-utils.ts +++ b/src/app/features/ledger/utils/bitcoin-ledger-utils.ts @@ -9,6 +9,7 @@ export interface BitcoinLedgerAccountDetails { id: string; path: string; policy: string; + targetId: string; } export async function connectLedgerBitcoinApp() { diff --git a/src/app/features/ledger/utils/stacks-ledger-utils.ts b/src/app/features/ledger/utils/stacks-ledger-utils.ts index e2afe912955..4ddf2a9af8c 100644 --- a/src/app/features/ledger/utils/stacks-ledger-utils.ts +++ b/src/app/features/ledger/utils/stacks-ledger-utils.ts @@ -10,6 +10,7 @@ import { import StacksApp, { LedgerError, ResponseSign, ResponseVersion } from '@zondax/ledger-stacks'; import { compare } from 'compare-versions'; +import { getStxDerivationPath, stxDerivationWithAccount } from '@shared/crypto/stacks/stacks.utils'; import { RouteUrls } from '@shared/route-urls'; import { @@ -19,21 +20,6 @@ import { } from './generic-ledger-utils'; import { versionObjectToVersionString } from './generic-ledger-utils'; -const stxDerivationWithAccount = `m/44'/5757'/0'/0/{account}`; - -const stxIdentityDerivationWithAccount = `m/888'/0'/{account}'`; - -function getAccountIndexFromDerivationPathFactory(derivationPath: string) { - return (account: number) => derivationPath.replace('{account}', account.toString()); -} - -export const getStxDerivationPath = - getAccountIndexFromDerivationPathFactory(stxDerivationWithAccount); - -export const getIdentityDerivationPath = getAccountIndexFromDerivationPathFactory( - stxIdentityDerivationWithAccount -); - export function requestPublicKeyForStxAccount(app: StacksApp) { return async (index: number) => app.getAddressAndPubKey( diff --git a/src/app/store/ledger/ledger-chain-key-storage-generator.ts b/src/app/store/ledger/ledger-chain-key-storage-generator.ts index f66d5781138..359a42b4fe2 100644 --- a/src/app/store/ledger/ledger-chain-key-storage-generator.ts +++ b/src/app/store/ledger/ledger-chain-key-storage-generator.ts @@ -4,6 +4,7 @@ import { defaultWalletKeyId } from '@shared/utils'; interface RequiresId { id: string; + targetId: string; } export function generateLedgerChainKeyStorageSlice(name: string) { @@ -11,7 +12,7 @@ export function generateLedgerChainKeyStorageSlice(); - const initialState = { targetId: '', ...adapter.getInitialState() }; + const initialState = adapter.getInitialState(); const slice = createSlice({ name: name + 'Keys', @@ -24,9 +25,6 @@ export function generateLedgerChainKeyStorageSlice ({ ...key, walletId: defaultWalletKeyId })) ); }, - addTargetId(state, { payload }: PayloadAction<{ targetId: string }>) { - return { ...state, targetId: payload.targetId }; - }, signOut(state) { adapter.removeAll(state as any); }, diff --git a/src/app/store/ledger/ledger.selectors.ts b/src/app/store/ledger/ledger.selectors.ts index 0065466e52f..e72fd8302ca 100644 --- a/src/app/store/ledger/ledger.selectors.ts +++ b/src/app/store/ledger/ledger.selectors.ts @@ -22,6 +22,7 @@ export function useHasLedgerKeys() { export function useLedgerDeviceTargetId() { return useSelector( - (state: RootState) => state.ledger.stacks.targetId || state.ledger.bitcoin.targetId + (state: RootState) => + state.ledger.stacks.entities[0]?.targetId || state.ledger.bitcoin.entities[0]?.targetId || '' ); } diff --git a/src/app/store/ledger/stacks/stacks-key.slice.ts b/src/app/store/ledger/stacks/stacks-key.slice.ts index fcbc31bac79..bf82432e05c 100644 --- a/src/app/store/ledger/stacks/stacks-key.slice.ts +++ b/src/app/store/ledger/stacks/stacks-key.slice.ts @@ -9,7 +9,7 @@ function selectStacksKeysSlice(state: RootState) { // ts-unused-exports:disable-next-line export const { slice: stacksKeysSlice, adapter } = generateLedgerChainKeyStorageSlice< - StacksAppKeysResponseItem & { id: string } + StacksAppKeysResponseItem & { id: string; targetId: string } >('stacks'); const selectors = adapter.getSelectors(selectStacksKeysSlice); diff --git a/src/shared/crypto/stacks/stacks.utils.ts b/src/shared/crypto/stacks/stacks.utils.ts new file mode 100644 index 00000000000..b8bea027d17 --- /dev/null +++ b/src/shared/crypto/stacks/stacks.utils.ts @@ -0,0 +1,13 @@ +export const stxDerivationWithAccount = `m/44'/5757'/0'/0/{account}`; +const stxIdentityDerivationWithAccount = `m/888'/0'/{account}'`; + +function getAccountIndexFromDerivationPathFactory(derivationPath: string) { + return (account: number) => derivationPath.replace('{account}', account.toString()); +} + +export const getStxDerivationPath = + getAccountIndexFromDerivationPathFactory(stxDerivationWithAccount); + +export const getIdentityDerivationPath = getAccountIndexFromDerivationPathFactory( + stxIdentityDerivationWithAccount +); diff --git a/src/shared/storage/redux-pesist.ts b/src/shared/storage/redux-pesist.ts index 39341b7b415..66723f200af 100644 --- a/src/shared/storage/redux-pesist.ts +++ b/src/shared/storage/redux-pesist.ts @@ -1,6 +1,9 @@ import { PersistConfig, createMigrate, getStoredState } from 'redux-persist'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; +import { getStxDerivationPath } from '@shared/crypto/stacks/stacks.utils'; +import { defaultWalletKeyId } from '@shared/utils'; + import type { RootState } from '@app/store'; import { analytics } from '../utils/analytics'; @@ -97,22 +100,46 @@ async function migrateToUsingNoSerialization() { return storage; } +function processStacksLedgerKeys(stacksKeys: any) { + const newStacksKeys = stacksKeys.entities.default.publicKeys.reduce( + (acc: { ids: string[]; entities: Record }, item: any, index: number) => { + const path = getStxDerivationPath(index); + const id = path.replace('m', defaultWalletKeyId); + + acc.ids.push(id); + acc.entities[id] = { + ...item, + path, + id, + walletId: 'default', + targetId: '', + }; + + return acc; + }, + { ids: [], entities: {} } + ); + + return newStacksKeys; +} + async function migrateToRenameKeysStoreModule(state: Promise) { const resolvedState = await Promise.resolve(state); - const newStore = JSON.parse( - JSON.stringify({ - ...resolvedState, - softwareKeys: resolvedState.keys, - ledger: { - ...resolvedState.ledger, - }, - }) - ); + const newStore = { + ...resolvedState, + softwareKeys: resolvedState.keys, + ledger: { + ...resolvedState.ledger, + }, + }; // add stacks ledger keys to new place - if (resolvedState.keys.entities.default.type === 'ledger') { - newStore.ledger = { ...resolvedState.ledger, stacks: resolvedState.keys }; + if (resolvedState.keys?.entities.default.type === 'ledger') { + newStore.ledger = { + ...resolvedState.ledger, + stacks: processStacksLedgerKeys(resolvedState.keys), + }; } // add default bitcoin ledger state @@ -120,7 +147,6 @@ async function migrateToRenameKeysStoreModule(state: Promise) { newStore.ledger.bitcoin = { ids: [], entities: {}, - targetId: '', }; } @@ -129,7 +155,6 @@ async function migrateToRenameKeysStoreModule(state: Promise) { newStore.ledger.stacks = { ids: [], entities: {}, - targetId: '', }; } @@ -145,14 +170,14 @@ interface UntypedDeserializeOption { export const persistConfig: PersistConfig & UntypedDeserializeOption = { key: 'root', stateReconciler: autoMergeLevel2, - version: 1, + version: 2, storage, serialize: false, migrate: createMigrate({ 0: async () => { return migrateToUsingNoSerialization(); }, - 1: async (state: any) => { + 2: async (state: any) => { return migrateToRenameKeysStoreModule(state); }, debug: true, diff --git a/tests/page-object-models/onboarding.page.ts b/tests/page-object-models/onboarding.page.ts index 51816bf17ac..f230915b6bd 100644 --- a/tests/page-object-models/onboarding.page.ts +++ b/tests/page-object-models/onboarding.page.ts @@ -27,12 +27,10 @@ export const testSoftwareAccountDefaultWalletState = { bitcoin: { entities: {}, ids: [], - targetId: '', }, stacks: { entities: {}, ids: [], - targetId: '', }, }, networks: { ids: [], entities: {}, currentNetworkId: 'mainnet' }, @@ -46,11 +44,11 @@ export const testSoftwareAccountDefaultWalletState = { dismissedMessages: [], hasApprovedNewBrand: true, }, - _persist: { version: 1, rehydrated: true }, + _persist: { version: 2, rehydrated: true }, }; const testLedgerAccountDefaultWalletState = { - _persist: { rehydrated: true, version: 1 }, + _persist: { rehydrated: true, version: 2 }, analytics: { hasStxDeposits: { '1': false, '2147483648': true } }, chains: { stx: { default: { currentAccountIndex: 0, highestAccountIndex: 0 } } }, softwareKeys: { @@ -66,6 +64,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/84'/0'/0']xpub6BuKrNqTrGfsy8VAAdUW2KCxbHywuSKjg7hZuAXERXDv7GfuxUgUWdVRKNsgujcwdjEHCjaXWouPKi1m5gMgdWX8JpRcyMkrSxPe4Da3Lx8", walletId: 'default', + targetId: '', }, "default/84'/0'/1'": { id: "default/84'/0'/1'", @@ -73,6 +72,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/84'/0'/1']xpub6BuKrNqTrGft1dv2pR3Ey8VsBnSBkVVpehNsro8V8kaWMRGeUNv8yhJpTw62Ldqenm5kuVyC2bQqgc6yrKAruDKyzz18zi83Sg2FTwEHsrF", walletId: 'default', + targetId: '', }, "default/84'/0'/2'": { id: "default/84'/0'/2'", @@ -80,6 +80,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/84'/0'/2']xpub6BuKrNqTrGft5UhSiYcXtN1d9Cp8iwj9tBVLjfJtLUqUFYA2xjVmAiB4TbUP6uaX3qwNhrW3baGE1Fz49YNSFcEMTtcd4Uz25juszoCCy8w", walletId: 'default', + targetId: '', }, "default/84'/0'/3'": { id: "default/84'/0'/3'", @@ -87,6 +88,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/84'/0'/3']xpub6BuKrNqTrGft7h39ks3qJjcz3KusNtsDtr8t59t2MUneWoCqbGYLcqLeqRaXC5na2tWDDzncBBVNVPT55b6jLM4dT5f6aGvgaXEXV6VniL6", walletId: 'default', + targetId: '', }, "default/84'/0'/4'": { id: "default/84'/0'/4'", @@ -94,6 +96,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/84'/0'/4']xpub6BuKrNqTrGftAswPZxdCzxArCp1bsUh3JPizsMymSVanfVJqXR2wjsX7PBnwMXnXttiWU6pMdBgB82mR2BPDtSGcUfjD8QJTNca47iYkGD3", walletId: 'default', + targetId: '', }, "default/86'/0'/0'": { id: "default/86'/0'/0'", @@ -101,6 +104,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/86'/0'/0']xpub6C4MQD2bVDTfdnVe5AYKB6gE7BE4yQeKBRgukQ4Hi3phDB5fCYKEAdViQ2n7kZQ1t728QV4wKGgiR5qGigjNNrm5DCGWYUZDRVNWYb8ZWGK", walletId: 'default', + targetId: '', }, "default/86'/0'/1'": { id: "default/86'/0'/1'", @@ -108,6 +112,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/86'/0'/1']xpub6C4MQD2bVDTfgjjWZhmPMNDMFHFmrSmGzqJVpuf98XB8F5eNaQus6XmrcrTrTiiL2EscdC4cjztP5LfaW13vZ6eDuDHXXAq71W5KEHeEeKH", walletId: 'default', + targetId: '', }, "default/86'/0'/2'": { id: "default/86'/0'/2'", @@ -115,6 +120,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/86'/0'/2']xpub6C4MQD2bVDTfkGnARZXj6dRRF223bcyKAK2qCRKf9xyPQg7k4ZZc4FAHLcXhQ1NCVJCTVGEMd1YoRnBBDdgXKrmt4bm5XmF1ry9ox4Qsx3F", walletId: 'default', + targetId: '', }, "default/86'/0'/3'": { id: "default/86'/0'/3'", @@ -122,6 +128,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/86'/0'/3']xpub6C4MQD2bVDTfmbN4ZJfozbNRMqyD1jmMFcQTNRUNyjE2J6tdVggFoQ8KmxUpijsZX1E4iDciY5AmnHbq95BHMVGJAGZ1MAm7iupHkTBV6YE", walletId: 'default', + targetId: '', }, "default/86'/0'/4'": { id: "default/86'/0'/4'", @@ -129,6 +136,7 @@ const testLedgerAccountDefaultWalletState = { policy: "[e87a850b/86'/0'/4']xpub6C4MQD2bVDTfq9RLtYxmqJRNsiviyuM51CFE1qqQbE6o8QN9Uix47Kvj4fqKFX5f88DyhxaX93L4H1WdSZChMZUWGUzPm54N9VfvsYJBvi9", walletId: 'default', + targetId: '', }, }, ids: [ @@ -143,7 +151,6 @@ const testLedgerAccountDefaultWalletState = { "default/86'/0'/3'", "default/86'/0'/4'", ], - targetId: '', }, stacks: { entities: { @@ -154,6 +161,7 @@ const testLedgerAccountDefaultWalletState = { '04716759aa2d2ec9066ff699626c3404c5cc7e84e7295af6768a0fce2defcd1c50a9ee4b1fd1e63295abc47c81f602e77c497f4549fa68535c7abbe73854b62df7', id: "default/44'/5757'/0'/0/0", walletId: 'default', + targetId: '', }, "default/44'/5757'/0'/0/1": { path: "m/44'/5757'/0'/0/1", @@ -162,6 +170,7 @@ const testLedgerAccountDefaultWalletState = { '04c8fba749c7be4a817c1bee8c24b7464f3be6f7e78f5c9ab43a57710f703155e059ce8b5fcb33e8c8d0ff154e964f99c486eed8b8b19f108cf5137a07275a277f', id: "default/44'/5757'/0'/0/1", walletId: 'default', + targetId: '', }, "default/44'/5757'/0'/0/2": { path: "m/44'/5757'/0'/0/2", @@ -170,6 +179,7 @@ const testLedgerAccountDefaultWalletState = { '04614af2cb5b9a07fb9049713a860a09cd97549373e73104e32b814922392a97a3c6d938f2b7f6e771c5e6611be64b762919a435a242fa5796b5bb4b9728eb079e', id: "default/44'/5757'/0'/0/2", walletId: 'default', + targetId: '', }, "default/44'/5757'/0'/0/3": { path: "m/44'/5757'/0'/0/3", @@ -178,6 +188,7 @@ const testLedgerAccountDefaultWalletState = { '04e3c33077024159f2a1aa28e4e73811d477fac3303f6395bfb8937994bc61d1a3b762d52ea4a57d0f2ed36523a96ffec74d1f05676e4411601402013f16f16374', id: "default/44'/5757'/0'/0/3", walletId: 'default', + targetId: '', }, "default/44'/5757'/0'/0/4": { path: "m/44'/5757'/0'/0/4", @@ -186,6 +197,7 @@ const testLedgerAccountDefaultWalletState = { '04673e21fc8fb98131d843bcb10edb015dd3219bb1f730c81c6de13a9df91d5f1a709099cd0d41d535f45b3119d3458ccdc98614ee4833c99f09c7c62d654350fa', id: "default/44'/5757'/0'/0/4", walletId: 'default', + targetId: '', }, }, ids: [