Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: import all accounts for wallet #605

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

pete-watters
Copy link
Contributor

@pete-watters pete-watters commented Nov 7, 2024

This PR contains a working draft of importing multiple accounts from a seed.

It is based on previous extension code but some more work is needed.

When importing the dev wallet account it takes quite a while and the resulting UI is very laggy (maybe as we have no controls in place on the max limits of the accounts)

Copy link

linear bot commented Nov 7, 2024

@@ -61,14 +61,15 @@
"@rnx-kit/metro-config": "1.3.14",
"@rnx-kit/metro-resolver-symlinks": "0.1.35",
"@scure/bip32": "1.5.0",
"@scure/bip39": "1.4.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this for mnemonicToSeedSync in mnemonicToRootNode but maybe we have a different way on mobile?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both of these methods exist in @leather.io/crypto

Copy link

codecov bot commented Nov 7, 2024

Codecov Report

Attention: Patch coverage is 97.91667% with 1 line in your changes missing coverage. Please review.

Project coverage is 24.65%. Comparing base (9b342af) to head (eea0493).
Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
packages/crypto/src/index.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #605      +/-   ##
==========================================
+ Coverage   24.07%   24.65%   +0.57%     
==========================================
  Files         164      165       +1     
  Lines        6085     6133      +48     
  Branches      333      346      +13     
==========================================
+ Hits         1465     1512      +47     
- Misses       4620     4621       +1     
Files with missing lines Coverage Δ
packages/crypto/src/recurse-accounts.ts 100.00% <100.00%> (ø)
packages/crypto/src/index.ts 0.00% <0.00%> (ø)
Components Coverage Δ
bitcoin 62.04% <ø> (ø)
query 12.94% <ø> (ø)
utils 48.31% <ø> (ø)
crypto 76.27% <97.91%> (+8.05%) ⬆️
stacks 54.71% <ø> (ø)

console.info('Found account activity at higher index', { recursiveActivityIndex });
// dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex));
console.log('recursiveActivityIndex', recursiveActivityIndex);
this.createNewAccountOfWallet(fingerprint, recursiveActivityIndex);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the extension stx-chain.slice.ts for stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex) it is using highestAccountIndex which gets incremented in createNewAccount so maybe that's where I am going wrong.

Copy link
Collaborator

@kyranjamie kyranjamie Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be side effects in this callback.

The idea of this recursive look up is that we give it a callback to determine whether there's activity or not. Then, the return value (which isn't used here because of void recurseAccountsForActivity) tells you the highest account index that has activity.

Later, you use that index to know how many accounts should be created.

Copy link
Collaborator

@kyranjamie kyranjamie Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see here you're try to recurse as well, but that isn't needed because all the recursion is handled internally to this function, it's way easier that way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based this code on the extension code commented out above it. That was using dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex)) to restore accounts.

I could be mistaken, but when I debug this, it seems like in .then the recursiveActivityIndex is always 0 so this code is never reached (on extension or copied version). See extension draft .

I think accounts are actually created further on by:

 if (legacyAccountActivityLookup !== 0)
        // dispatch(stxChainSlice.actions.restoreAccountIndex(legacyAccountActivityLookup)); -> extension
        this.createNewAccountOfWallet(fingerprint, legacyAccountActivityLookup);

legacyAccountActivityLookup is determined by checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex which I think is actually what is determining how many accounts to create.

What I think I can get to work is to run this.createNewAccountOfWallet inside of the recursive function if there is a balance

Copy link
Collaborator

@kyranjamie kyranjamie Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it always return 0? Does the account have activity you're testing with? It's quite slow, so are you waiting until the end?

image

What I think I can get to work is to run this.createNewAccountOfWallet inside of the recursive function if there is a balance

I really don't think this is the way to do it. The purpose of recurseAccountsForActivity is to handle the recursion of the look up. If you have to do additional recursion outside that fn, then either your implementation misses the point of the function, or the implementation of recurseAccountsForActivity doesn't work and needs fixing. It's been a while since I wrote this, and can likely be improved, it's v slow for example.

Happy to run through in more detail the gist of this function, but really its just a look up function that stops after n false values and returns a number.

Also, let's remove all the gaia stuff so we don't need to worry about legacy account look up code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Kyran. I will do more testing and then implement the best solution using your recommendations.

I'm not sure if it's working as we think in the extension right now. I am testing with my personal wallet and the leather dev wallet and in both cases accounts are created only if if (legacyAccountActivityLookup !== 0).

We have two calls to add accounts:

  • in recurseAccountsForActivity.then
    dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex));

  • after a check for if (legacyAccountActivityLookup !== 0):
    dispatch(stxChainSlice.actions.restoreAccountIndex(legacyAccountActivityLookup));

  • If I remove the dispatch after the legacyAccountActivityLookup check, no accounts are created apart from the first one.

  • If I add a debugger in recurseAccountsForActivity.then it never gets hit.

Here's the full code.

 try {
      void recurseAccountsForActivity({
        async doesAddressHaveActivityFn(index) {
          ....
          return hasStxBalance || hasNames || hasBtcBalance;
        },
      }).then(recursiveActivityIndex => {
        if (recursiveActivityIndex <= legacyAccountActivityLookup) return;
        logger.info('Found account activity at higher index', { recursiveActivityIndex });
        dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex));
      });
    } catch (e) {
      // Errors during account restore are non-critical and can fail silently
    }

 ....
    if (legacyAccountActivityLookup !== 0)
      dispatch(stxChainSlice.actions.restoreAccountIndex(legacyAccountActivityLookup));
  };

If I change this code to be:

const accountsWithBalances = recurseAccountsForActivity({
        async doesAddressHaveActivityFn(index) {
          ....
          return hasStxBalance || hasNames || hasBtcBalance;
        },
      })

accountsWithBalances is always 0. If I add logs inside of there I can see it finding accounts with balances but I think it's not working as we expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further investigation into this, it seems like the recursion we are using does not generate any accounts. They are in fact all generated initially based on the count returned from checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex

Here is a demo PR from the extension showing that: leather-io/extension#5945

@@ -61,14 +61,15 @@
"@rnx-kit/metro-config": "1.3.14",
"@rnx-kit/metro-resolver-symlinks": "0.1.35",
"@scure/bip32": "1.5.0",
"@scure/bip39": "1.4.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both of these methods exist in @leather.io/crypto

@@ -142,7 +143,7 @@
"babel-plugin-macros": "3.1.0",
"babel-preset-expo": "11.0.6",
"concurrently": "8.2.2",
"eslint": "8.53.0",
"eslint": "9.14.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unintended I imagine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw some warnings about it being out of date so updated it. I should have removed from here

@@ -0,0 +1,79 @@
// from extension src/app/common/account-restoration/account-restore.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be added directly to mobile, instead though of as a distinct module that I think should like in /crypto pkg, the refactored out to extension

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do this once I can get it to work properly

Comment on lines 139 to 140
const legacyAccountActivityLookup =
legacyAccountActivityLookupGaia === 0 ? 123 : legacyAccountActivityLookupGaia;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest we don't add any of the gaia checks here, resolving the import issue, check stacks/bitcoin balances only

console.info('Found account activity at higher index', { recursiveActivityIndex });
// dispatch(stxChainSlice.actions.restoreAccountIndex(recursiveActivityIndex));
console.log('recursiveActivityIndex', recursiveActivityIndex);
this.createNewAccountOfWallet(fingerprint, recursiveActivityIndex);
Copy link
Collaborator

@kyranjamie kyranjamie Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be side effects in this callback.

The idea of this recursive look up is that we give it a callback to determine whether there's activity or not. Then, the return value (which isn't used here because of void recurseAccountsForActivity) tells you the highest account index that has activity.

Later, you use that index to know how many accounts should be created.

@kyranjamie
Copy link
Collaborator

kyranjamie commented Nov 7, 2024

pseudo code we need here

// user restores mnemonic
const mnemonic = 'abandon …';

async function doesStacksAddressHaveBalance (index) {
  // derive address
  // look up api for balances, return true if found
}
async  function doesNativeSegwitAddressHaveBalance (index) {
  // derive address
  // look up api for balances, return true if found
}

const highestAccountIndex = await recurseAccountsForActivity({
  async doesAddressHaveActivityFn(index) {
    const hasStacksBal = await doesStacksAddressHaveBalance(index);
    const hasNsBal = await doesNativeSegwitAddressHaveBalance(index)
    // this tells the inner lookup function to keep looking, or not
    // using callbacks like this means that the inner fn isn't aware of any specific chain related 
    // details, all it is told is whether it needs to keep looking or not
    return hasStacksBal ?? hasNsBal;
  }
})

// now we know that `highestAccountIndex` is the account with the farthest known balance

const newAccountsToAdd = createNullArrayOf(highestAccountIndex).map(index => {
  return generateTheAccountOfThisIndex();
})

// redux store method to add all of these to the store
// should be single action with many, not many actions with a single item
dispatch(etc)

This look up can take some time though, so ideally we want some way of handling this in the background. I would first get the slow version implemented before tackling that though.

}

// on the extension dev wallet this is where most accounts are created
if (legacyAccountActivityLookup !== 0)
Copy link
Contributor Author

@pete-watters pete-watters Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I try restore one of my personal wallets on the extension the accounts also get generated here.

That account is from 2023 at the earliest so I don't think it should be considered legacy?

@pete-watters
Copy link
Contributor Author

pseudo code we need here

// user restores mnemonic
const mnemonic = 'abandon …';

async function doesStacksAddressHaveBalance (index) {
  // derive address
  // look up api for balances, return true if found
}
async  function doesNativeSegwitAddressHaveBalance (index) {
  // derive address
  // look up api for balances, return true if found
}

const highestAccountIndex = await recurseAccountsForActivity({
  async doesAddressHaveActivityFn(index) {
    const hasStacksBal = await doesStacksAddressHaveBalance(index);
    const hasNsBal = await doesNativeSegwitAddressHaveBalance(index)
    // this tells the inner lookup function to keep looking, or not
    // using callbacks like this means that the inner fn isn't aware of any specific chain related 
    // details, all it is told is whether it needs to keep looking or not
    return hasStacksBal ?? hasNsBal;
  }
})

// now we know that `highestAccountIndex` is the account with the farthest known balance

const newAccountsToAdd = createNullArrayOf(highestAccountIndex).map(index => {
  return generateTheAccountOfThisIndex();
})

// redux store method to add all of these to the store
// should be single action with many, not many actions with a single item
dispatch(etc)

This look up can take some time though, so ideally we want some way of handling this in the background. I would first get the slow version implemented before tackling that though.

Thanks!

@pete-watters pete-watters force-pushed the fix/LEA-1579/restore-wallet branch 2 times, most recently from c4bf59e to ea86391 Compare November 12, 2024 09:51
Comment on lines 118 to 119
// not sure why this is needed?
// don't think I need to pass in the same keychain for each account
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the keychain is different for every account

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kyranjamie 👍

I realised I have more work to do as I was hitting issues in bitcoin-keychains.read.ts with undefined nativeSegwit taproot.

@@ -32,6 +32,12 @@ export const bitcoinKeychainSlice = createSlice({
handleEntityActionWith(adapter.addMany, payload => payload.withKeychains.bitcoin)
)

// NOTE: need to adapt this to add multiple accounts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this comment. This adapter already uses addMany, not addOne

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new action userAddsAccounts to add many accounts at once so I think I need to adapt this to also handle their keychains as that's not working fully and the accounts seem incomplete.

I am hitting issues with undefined keyOrigin so I think I missed some things during account creation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved this. I'm passing an array of account creation data so needed a flatMap here

Comment on lines 74 to 76
const mostRecentAccount = await recurseUntilGeneratorDone(checkForMostRecentAccount());
const accountsToRestore = mostRecentAccount;
return accountsToRestore;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd reassignments here

Suggested change
const mostRecentAccount = await recurseUntilGeneratorDone(checkForMostRecentAccount());
const accountsToRestore = mostRecentAccount;
return accountsToRestore;
const accountsToRestore = await recurseUntilGeneratorDone(checkForMostRecentAccount());
return accountsToRestore;

import z from 'zod';

// FIXME fix this deprecated import
import { mnemonicToRootNode } from '@leather.io/bitcoin';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is in crypto pkg

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be removing this whole file in favour of the newer balance queries we have

@kyranjamie
Copy link
Collaborator

I wonder how we're going to manage this functionality, because in the case of lots of accounts, it's going to take ages

@pete-watters
Copy link
Contributor Author

pete-watters commented Nov 13, 2024

I wonder how we're going to manage this functionality, because in the case of lots of accounts, it's going to take ages

I will think about this a bit more. The latest version of this is still messy but it's working at least.

If I try and restore the Leather dev wallet it takes a while (I'll get stats) and although it creates the accounts, the resulting UI is very laggy. Probably as we have no controls to limit how many things to show etc.

I had a call with @alexp3y about this yesterday in relation to the balances service in case he has some ideas there too.

The flow for determining active keychains and creating accounts now is to:

  • iterate over them recursively and check the balance
  • repeat until we have no more balances
  • batch create accounts

I will be looking into activity soon so I wonder if there is a better way of determining active addresses using activity instead of balances?

Or if on first load when we are trying to calculate the totalBalance, we could also check for the highest index active keychain in a worker?

I'll keep thinking about this. Also wondering if we should add the keychain to state for each account index

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants