Skip to content

Commit

Permalink
fix: import all accounts for wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
pete-watters committed Nov 12, 2024
1 parent 9b342af commit 06aaadf
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 139 deletions.
108 changes: 54 additions & 54 deletions apps/mobile/ios/Podfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"@stacks/common": "6.13.0",
"@stacks/stacks-blockchain-api-types": "7.8.2",
"@stacks/transactions": "6.17.0",
"@stacks/wallet-sdk": "6.15.0",
"@stacks/wallet-sdk": "7.0.0",
"@tanstack/react-query": "5.59.16",
"bignumber.js": "9.1.2",
"buffer": "6.0.3",
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/src/hooks/api-clients.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// from extension/src/app/store/common/api-clients.ts
import { useMemo } from 'react';

import { stacksClient, useCurrentNetworkState } from '@leather.io/query';

export function useStacksClient(): ReturnType<typeof stacksClient> {
const network = useCurrentNetworkState();

return useMemo(() => {
return stacksClient(network.chain.stacks.url);
}, [network.chain.stacks.url]);
}
10 changes: 10 additions & 0 deletions apps/mobile/src/store/accounts/accounts.write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export const accountsSlice = createSlice({
})
);
})
.addCase(userAddsAccounts, (state, action) => {
const accounts = action.payload.map((payload, index) =>
addAccountDefaults({
account: payload.account,
accountIdx: index + 1,
})
);
return accountsAdapter.addMany(state, accounts);
})

.addCase(
userTogglesHideAccount,
Expand Down Expand Up @@ -127,6 +136,7 @@ interface AddAccountPayload {
};
}
export const userAddsAccount = createAction<AddAccountPayload>('accounts/userAddsAccount');
export const userAddsAccounts = createAction<AddAccountPayload[]>('accounts/userAddsAccounts');

interface ToggleHideAccountPayload {
accountId: string;
Expand Down
89 changes: 86 additions & 3 deletions apps/mobile/src/store/key-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
// useStacksClient should be in the query package
import { useStacksClient } from '@/hooks/api-clients.hooks';
import { AddressVersion } from '@stacks/transactions';

import {
deriveAddressIndexZeroFromAccount,
deriveNativeSegwitAccountFromRootKeychain,
getNativeSegwitPaymentFromAddressIndex,
makeNativeSegwitAccountDerivationPath,
makeTaprootAccountDerivationPath,
} from '@leather.io/bitcoin';
Expand All @@ -8,12 +15,19 @@ import {
deriveRootBip32Keychain,
generateMnemonic,
getMnemonicRootKeyFingerprint,
recurseAccountsForActivity,
} from '@leather.io/crypto';
import { useBitcoinClient } from '@leather.io/query';
import { stacksRootKeychainToAccountDescriptor } from '@leather.io/stacks';

import { userAddsAccount, userTogglesHideAccount } from './accounts/accounts.write';
import {
userAddsAccount,
userAddsAccounts,
userTogglesHideAccount,
} from './accounts/accounts.write';
import { useBitcoinAccounts } from './keychains/bitcoin/bitcoin-keychains.read';
import { findHighestAccountIndexOfFingerprint } from './keychains/keychains';
import { getStacksAddressByIndex } from './keychains/stacks/utils';
import { mnemonicStore } from './storage-persistors';
import { makeAccountIdentifer, useAppDispatch } from './utils';
import { useWallets } from './wallets/wallets.read';
Expand All @@ -36,6 +50,9 @@ export function useKeyStore() {
const wallets = useWallets();
const bitcoinKeychains = useBitcoinAccounts();

const stxClient = useStacksClient();
const btcClient = useBitcoinClient();

return {
async createTemporarySoftwareWallet() {
const mnemonic = generateMnemonic();
Expand All @@ -50,7 +67,6 @@ export function useKeyStore() {
async isWalletInKeychain({ fingerprint }: { fingerprint: string }) {
return !!wallets.list.find(wallet => wallet.fingerprint === fingerprint);
},

async restoreWalletFromMnemonic({
biometrics,
mnemonic,
Expand All @@ -61,6 +77,8 @@ export function useKeyStore() {
passphrase?: string;
}) {
const fingerprint = await getMnemonicRootKeyFingerprint(mnemonic, passphrase);

// checks if the wallet exists
if (await this.isWalletInKeychain({ fingerprint })) {
keychainErrorHandlers.throwKeyExistsError();
return;
Expand All @@ -74,7 +92,6 @@ export function useKeyStore() {
withKeychains: { bitcoin: bitcoinKeychains, stacks: stacksKeychains },
});
},

async createNewAccountOfWallet(fingerprint: string) {
const { accountIndex, bitcoinKeychains, stacksKeychains } =
await this.deriveNextAccountKeychainsFrom(fingerprint);
Expand All @@ -89,6 +106,24 @@ export function useKeyStore() {
})
);
},
async createNewAccountsOfWallet(fingerprint: string, activeAccounts: number) {
const { bitcoinKeychains, stacksKeychains } =
await this.deriveNextAccountKeychainsFrom(fingerprint);

dispatch(
userAddsAccounts(
Array.from({ length: activeAccounts }, (_, i) => ({
account: {
id: makeAccountIdentifer(fingerprint, i),
},
withKeychains: {
bitcoin: bitcoinKeychains,
stacks: stacksKeychains,
},
}))
)
);
},

async deriveNextAccountKeychainsFrom(fingerprint: string) {
const { mnemonic, passphrase } = await mnemonicStore(fingerprint).getMnemonic();
Expand All @@ -109,6 +144,54 @@ export function useKeyStore() {
const nextAccountIndex =
fingerprintAccounts.length === 0 ? 0 : highestKeychainAccountIndex + 1;

// in extension secretKey is the mnemonic
const secretKey = mnemonic;

async function doesStacksAddressHaveBalance(address: string) {
const controller = new AbortController();
const resp = await stxClient.getAccountBalance(address, controller.signal);
return Number(resp.stx.balance) > 0;
}
async function doesBitcoinAddressHaveBalance(address: string) {
const resp = await btcClient.addressApi.getUtxosByAddress(address);
return resp.length > 0;
}

function getNativeSegwitMainnetAddressFromMnemonic() {
return (accountIndex: number) => {
const account = deriveNativeSegwitAccountFromRootKeychain(
rootKeychain,
'mainnet'
)(accountIndex);

return getNativeSegwitPaymentFromAddressIndex(
deriveAddressIndexZeroFromAccount(account.keychain),
'mainnet'
);
};
}

try {
void recurseAccountsForActivity({
async doesAddressHaveActivityFn(index: number) {
// seems like it could be better to do this with useQueries for batches of accountIndexes
const stxAddress = getStacksAddressByIndex(
secretKey,
AddressVersion.MainnetSingleSig
)(index);
// here we call doesStacksAddressHaveBalance which calls stacks client directly not using react query
const hasStxBalance = await doesStacksAddressHaveBalance(stxAddress);

// TODO - refactor this to use new queries
const btcAddress = getNativeSegwitMainnetAddressFromMnemonic()(index);
const hasBtcBalance = await doesBitcoinAddressHaveBalance(btcAddress.address!);
return hasStxBalance || hasBtcBalance;
},
}).then((activeAccounts: number) => {
this.createNewAccountsOfWallet(fingerprint, activeAccounts);

Check warning on line 191 in apps/mobile/src/store/key-store.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
});
} catch {}

const stacksKeychainDescriptors = [
{ descriptor: stacksRootKeychainToAccountDescriptor(rootKeychain, nextAccountIndex) },
];
Expand Down
21 changes: 21 additions & 0 deletions apps/mobile/src/store/keychains/stacks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { entitySchema } from '@/store/utils';
import {
AddressVersion,
createStacksPrivateKey,
getPublicKey,
publicKeyToAddress,
} from '@stacks/transactions';
import { deriveStxPrivateKey } from '@stacks/wallet-sdk';
import z from 'zod';

// todo fix this deprecated import
import { mnemonicToRootNode } from '@leather.io/bitcoin';

const stacksKeychainSchema = z.object({
// Stacks doesn't use the concept of BIP-380 Descriptors the same way Bitcoin
// does. However, we need to store the same data. Reusing this structure
Expand All @@ -12,3 +22,14 @@ const stacksKeychainSchema = z.object({
export type StacksKeychain = z.infer<typeof stacksKeychainSchema>;
export const stacksKeychainStoreSchema = entitySchema(stacksKeychainSchema);
export type StacksKeychainStore = z.infer<typeof stacksKeychainStoreSchema>;

// seems like secretKey is the mnemonic in this code??
export function getStacksAddressByIndex(secretKey: string, addressVersion: AddressVersion) {
return (index: number) => {
const accountPrivateKey = createStacksPrivateKey(
deriveStxPrivateKey({ rootNode: mnemonicToRootNode(secretKey) as any, index })
);
const pubKey = getPublicKey(accountPrivateKey);
return publicKeyToAddress(addressVersion, pubKey);
};
}
1 change: 1 addition & 0 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './derivation-path-utils';
export * from './keychain';
export * from './signer/signer';
export * from './recurse-accounts';

Check warning on line 4 in packages/crypto/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/index.ts#L4

Added line #L4 was not covered by tests
77 changes: 77 additions & 0 deletions packages/crypto/src/recurse-accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// from extension src/app/common/account-restoration/account-restore.ts

Check warning on line 1 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L1

Added line #L1 was not covered by tests
import { createCounter, fibonacciGenerator } from '@leather.io/utils';

const numOfEmptyAccountsToCheck = 20;

Check warning on line 4 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L4

Added line #L4 was not covered by tests

interface AccountIndexActivityCheckHistory {
index: number;
hasActivity: boolean;
}

function minNumberOfAccountsNotChecked(num: number) {
return num < numOfEmptyAccountsToCheck;
}

Check warning on line 13 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L11-L13

Added lines #L11 - L13 were not covered by tests

function anyOfLastCheckedAccountsHaveActivity(arr: AccountIndexActivityCheckHistory[]) {
return arr.slice(arr.length - numOfEmptyAccountsToCheck).some(check => check.hasActivity);
}

Check warning on line 17 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L15-L17

Added lines #L15 - L17 were not covered by tests

function returnHighestIndex(arr: AccountIndexActivityCheckHistory[]) {
return Math.max(0, ...arr.filter(check => check.hasActivity).map(check => check.index));
}

Check warning on line 21 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L19-L21

Added lines #L19 - L21 were not covered by tests

async function recurseUntilGeneratorDone(generator: AsyncGenerator): Promise<any> {
const result = await generator.next();
if (result.done) return result.value;
return recurseUntilGeneratorDone(generator);
}

Check warning on line 27 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L23-L27

Added lines #L23 - L27 were not covered by tests

interface RecurseAccountsForActivityArgs {
doesAddressHaveActivityFn(index: number): Promise<boolean>;
}

/**
* Used to recursively look for account activity. The use case is that, when
* restoring an account, we want to know how many accounts to generate. This
* function makes no assumption as to what constitutes an active account. It
* takes a function that returns a boolean. If true, it means that the account
* at the given index is considered to have activity.
*
* Original PR was https://github.com/leather-io/extension/pull/3026
*/
export async function recurseAccountsForActivity({
doesAddressHaveActivityFn,
}: RecurseAccountsForActivityArgs): Promise<number> {
async function* findHighestAddressIndexExponent() {
const fibonacci = fibonacciGenerator(2);
const activity: AccountIndexActivityCheckHistory[] = [];

Check warning on line 47 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L42-L47

Added lines #L42 - L47 were not covered by tests

while (activity.length === 0 || activity[activity.length - 1]?.hasActivity) {
const index = fibonacci.next().value;
const hasActivity = await doesAddressHaveActivityFn(index);
activity.push({ index, hasActivity });
yield;
}
return returnHighestIndex(activity);
}

Check warning on line 56 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L49-L56

Added lines #L49 - L56 were not covered by tests

const knownActivityAtIndex = await recurseUntilGeneratorDone(findHighestAddressIndexExponent());
async function* checkForMostRecentAccount() {
const indexCounter = createCounter(knownActivityAtIndex + 1);
const activity: AccountIndexActivityCheckHistory[] = [];

Check warning on line 61 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L58-L61

Added lines #L58 - L61 were not covered by tests

while (
minNumberOfAccountsNotChecked(activity.length) ||
anyOfLastCheckedAccountsHaveActivity(activity)
) {
const hasActivity = await doesAddressHaveActivityFn(indexCounter.getValue());
activity.push({ index: indexCounter.getValue(), hasActivity });
indexCounter.increment();
yield;
}
return returnHighestIndex(activity);
}
const mostRecentAccount = await recurseUntilGeneratorDone(checkForMostRecentAccount());
const accountsToRestore = mostRecentAccount;
return accountsToRestore;
}

Check warning on line 77 in packages/crypto/src/recurse-accounts.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/recurse-accounts.ts#L63-L77

Added lines #L63 - L77 were not covered by tests
Loading

0 comments on commit 06aaadf

Please sign in to comment.