Skip to content

Commit

Permalink
Merge pull request #220 from alephium/improve-creating-ledger-accounts
Browse files Browse the repository at this point in the history
Improve creating ledger accounts
  • Loading branch information
polarker authored Oct 22, 2024
2 parents c9fa389 + 200b0c9 commit cddbe9e
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 176 deletions.
2 changes: 2 additions & 0 deletions packages/extension/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@
"Export private key": "Export private key",
"Enter your password to export your private key.": "Enter your password to export your private key.",
"Click to copy public key": "Click to copy public key",
"Click to copy HD path": "Click to copy HD path",
"This is your private key (click to copy)": "This is your private key (click to copy)",
"Public Key": "Public Key",
"HD Path": "HD Path",
"NFTs": "NFTs",
"Error loading": "Error loading",
"Loading": "Loading",
Expand Down
6 changes: 3 additions & 3 deletions packages/extension/src/background/accountMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ export const handleAccountMessage: HandleMessage<AccountMessage> = async ({
throw Error("you need an open session")
}

const { account, hdIndex, networkId } = msg.data
const { account } = msg.data
try {
const baseAccount = await wallet.importLedgerAccount(account, hdIndex, networkId)
const baseAccount = await wallet.importLedgerAccount(account)
return sendMessageToUi({
type: "ALPH_NEW_LEDGER_ACCOUNT_RES",
data: {
Expand All @@ -105,7 +105,7 @@ export const handleAccountMessage: HandleMessage<AccountMessage> = async ({

analytics.track("createAccount", {
status: "failure",
networkId: networkId,
networkId: account.networkId,
errorMessage: error,
})

Expand Down
111 changes: 21 additions & 90 deletions packages/extension/src/background/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
publicKeyFromPrivateKey,
groupOfAddress,
KeyType,
Account,
ExplorerProvider,
TOTAL_NUMBER_OF_GROUPS
ExplorerProvider
} from "@alephium/web3"
import {
PrivateKeyWallet,
Expand All @@ -35,11 +33,10 @@ import {
} from "../shared/storage"
import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
import { accountsEqual } from "../shared/wallet.service"
import {
getNextPathIndex,
} from "./keys/keyDerivation"
import { getNextPathIndex } from "./keys/keyDerivation"
import backupSchema from "./schema/backup.schema"
import { BrowserStorage, walletEncrypt, walletOpen } from './utils/walletStore'
import { AccountDiscovery } from '../shared/discovery'

const isDev = process.env.NODE_ENV === "development"

Expand Down Expand Up @@ -81,13 +78,15 @@ export const sessionStore = new ObjectStorage<WalletSession | null>(null, {

export type GetNetwork = (networkId: string) => Promise<Network>

export class Wallet {
export class Wallet extends AccountDiscovery {
constructor(
private readonly store: IKeyValueStorage<WalletStorageProps>,
private readonly walletStore: IArrayStorage<WalletAccount>,
private readonly sessionStore: IObjectStorage<WalletSession | null>,
private readonly getNetwork: GetNetwork,
) { }
) {
super()
}

async signAndSubmitUnsignedTx(
account: WalletAccount,
Expand Down Expand Up @@ -258,19 +257,7 @@ export class Wallet {
}
}

public async importLedgerAccount(account: Account, hdIndex: number, networkId: string): Promise<BaseWalletAccount> {
const walletAccount: WalletAccount = {
address: account.address,
networkId: networkId,
signer: {
type: "ledger" as const,
publicKey: account.publicKey,
keyType: account.keyType,
derivationIndex: hdIndex,
group: groupOfAddress(account.address)
},
type: "alephium",
}
public async importLedgerAccount(walletAccount: WalletAccount): Promise<BaseWalletAccount> {
await this.walletStore.push([walletAccount])
await this.selectAccount(walletAccount)
return walletAccount
Expand Down Expand Up @@ -461,87 +448,31 @@ export class Wallet {
const accountsForNetwork = await this.walletStore.get(account => account.networkId == networkId)
const selectedAccount = await this.getSelectedAccount()

console.info(`start discovering active accounts for ${networkId}`)
const walletAccounts = await this.deriveActiveAccountsForNetwork(networkId)
const newDiscoveredAccounts = walletAccounts.filter(account => !accountsForNetwork.find(a => a.address === account.address))

if (newDiscoveredAccounts.length > 0) {
await this.walletStore.push(newDiscoveredAccounts)
if (selectedAccount === undefined) {
await this.selectAccount(newDiscoveredAccounts[0])
}
}
console.info(`Discovered ${newDiscoveredAccounts.length} new active accounts for ${networkId}`)
return newDiscoveredAccounts
}

public async deriveActiveAccountsForNetwork(networkId: string): Promise<WalletAccount[]> {
console.log(`derived active accounts for ${networkId}`)
const session = await this.sessionStore.get()
if (!(await this.isSessionOpen()) || !session) {
throw Error("no open session")
}

const network = await this.getNetwork(networkId)

const walletAccounts: WalletAccount[] = []

for (let group = 0; group < TOTAL_NUMBER_OF_GROUPS; group++) {
const walletAccountsForGroup = await this.deriveActiveAccountsForGroup(session.secret, network, 'default', group, [], [])
walletAccounts.push(...walletAccountsForGroup)
}

return walletAccounts
}

public async deriveActiveAccountsForGroup(
secret: string,
network: Network,
keyType: KeyType,
forGroup: number,
allWalletAccounts: { wallet: WalletAccount, active: boolean }[],
activeWalletAccounts: WalletAccount[]
): Promise<WalletAccount[]> {
const minGap = 5
const derivationBatchSize = 10
if (!network.explorerUrl) {
return []
}

const explorerService = new ExplorerProvider(network.explorerApiUrl)
const gapSatisfied = (allWalletAccounts.length >= minGap) && allWalletAccounts.slice(-minGap).every(item => !item.active);

if (gapSatisfied) {
return activeWalletAccounts
} else {
let startIndex = getNextPathIndex(allWalletAccounts.map(account => account.wallet.signer.derivationIndex))
const newWalletAccounts = []
for (let i = 0; i < derivationBatchSize; i++) {
const newWalletAccount = this.deriveAccount(secret, startIndex, network.id, keyType, forGroup)
newWalletAccounts.push(newWalletAccount)
startIndex = newWalletAccount.signer.derivationIndex + 1
}

const results = await explorerService.addresses.postAddressesUsed(newWalletAccounts.map(account => account.address))
console.info(`start discovering active accounts for ${networkId}`)
const explorerProvider = new ExplorerProvider(network.explorerApiUrl)
const discoverAccount = (startIndex: number): Promise<WalletAccount> => {
return Promise.resolve(this.deriveAccount(session.secret, startIndex, network.id, 'default'))
}
const walletAccounts = await this.deriveActiveAccountsForNetwork(explorerProvider, discoverAccount)
const newDiscoveredAccounts = walletAccounts.filter(account => !accountsForNetwork.find(a => a.address === account.address))

const updatedActiveWalletAccounts = activeWalletAccounts
for (let i = 0; i < derivationBatchSize; i++) {
const newWalletAccount = newWalletAccounts[i]
const result = results[i]
if (result) {
updatedActiveWalletAccounts.push(newWalletAccount)
}
allWalletAccounts.push({ wallet: newWalletAccount, active: result })
if (newDiscoveredAccounts.length > 0) {
await this.walletStore.push(newDiscoveredAccounts)
if (selectedAccount === undefined) {
await this.selectAccount(newDiscoveredAccounts[0])
}

return this.deriveActiveAccountsForGroup(
secret,
network,
keyType,
forGroup,
allWalletAccounts,
updatedActiveWalletAccounts
)
}
console.info(`Discovered ${newDiscoveredAccounts.length} new active accounts for ${networkId}`)
return newDiscoveredAccounts
}
}
54 changes: 54 additions & 0 deletions packages/extension/src/shared/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ExplorerProvider, TOTAL_NUMBER_OF_GROUPS } from '@alephium/web3'
import { WalletAccount } from './wallet.model'

const minGap = 5
const derivationBatchSize = 10

class DerivedAccountsPerGroup {
accounts: WalletAccount[]
gap: number

constructor() {
this.accounts = []
this.gap = 0
}

addAccount(account: WalletAccount, active: boolean) {
if (!this.isComplete()) {
if (!active) {
this.gap += 1
} else {
this.gap = 0
this.accounts.push(account)
}
}
}

isComplete(): boolean {
return this.gap >= minGap
}
}

export abstract class AccountDiscovery {
public async deriveActiveAccountsForNetwork(
explorerProvider: ExplorerProvider,
deriveAccount: (startIndex: number) => Promise<WalletAccount>
): Promise<WalletAccount[]> {
const allAccounts = Array.from(Array(TOTAL_NUMBER_OF_GROUPS)).map(() => new DerivedAccountsPerGroup())
let startIndex = 0
while (allAccounts.some((a) => !a.isComplete())) {
const newWalletAccounts = []
for (let i = 0; i < derivationBatchSize; i++) {
const newWalletAccount = await deriveAccount(startIndex + i)
newWalletAccounts.push(newWalletAccount)
}
startIndex += derivationBatchSize
const results = await explorerProvider.addresses.postAddressesUsed(newWalletAccounts.map(account => account.address))
newWalletAccounts.forEach((account, index) => {
const accountsPerGroup = allAccounts[account.signer.group]
accountsPerGroup.addAccount(account, results[index])
})
}
return allAccounts.flatMap((a) => a.accounts)
}
}
4 changes: 2 additions & 2 deletions packages/extension/src/shared/messages/AccountMessage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyType, Account } from "@alephium/web3";
import { KeyType } from "@alephium/web3";
import {
ArgentAccountType,
BaseWalletAccount,
Expand All @@ -15,7 +15,7 @@ export type AccountMessage =
}
}
| { type: "ALPH_NEW_ACCOUNT_REJ"; data: { error: string } }
| { type: "ALPH_NEW_LEDGER_ACCOUNT"; data: { account: Account; hdIndex: number, networkId: string } }
| { type: "ALPH_NEW_LEDGER_ACCOUNT"; data: { account: WalletAccount } }
| {
type: "ALPH_NEW_LEDGER_ACCOUNT_RES"
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useCurrentNetwork } from "../networks/useNetworks"
import { AccountEditName } from "./AccountEditName"
import { Button, CopyTooltip } from "@argent/ui"
import { useTranslation } from "react-i18next"
import { getHDWalletPath } from "@alephium/web3-wallet"

const { ExpandIcon, HideIcon, AlertIcon } = icons

Expand Down Expand Up @@ -145,6 +146,18 @@ export const AccountEditScreen: FC = () => {
<AddressCopyButton address={nip19.npubEncode(account?.publicKey)} type={t("nostr public key")} title="Nostr" />
</Center>
}
{ account !== undefined &&
<Center
border={"1px solid"}
borderColor={"border"}
borderTop={"none"}
borderBottomLeftRadius="lg"
borderBottomRightRadius="lg"
p={2}
>
<HDPathCopyButton hdPath={getHDWalletPath(account.signer.keyType, account.signer.derivationIndex)}/>
</Center>
}
</Flex>
<SpacerCell />
<ButtonCell
Expand Down Expand Up @@ -180,6 +193,22 @@ export const AccountEditScreen: FC = () => {
)
}

const HDPathCopyButton: FC<{ hdPath: string }> = ({ hdPath }) => {
const { t } = useTranslation()
return (
<CopyTooltip prompt={t('Click to copy HD path')} copyValue={hdPath}>
<Button
size="3xs"
color={"white50"}
bg={"transparent"}
_hover={{ bg: "neutrals.700", color: "text" }}
>
{`${t("HD Path")}: ${hdPath}`}
</Button>
</CopyTooltip>
)
}

const PublicKeyCopyButton: FC<{ publicKey: string }> = ({ publicKey }) => {
const { t } = useTranslation()
return (
Expand Down
9 changes: 5 additions & 4 deletions packages/extension/src/ui/features/accounts/useAddAccount.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyType, Account } from "@alephium/web3"
import { KeyType } from "@alephium/web3"
import { useCallback } from "react"
import { useNavigate } from "react-router-dom"

Expand All @@ -7,6 +7,7 @@ import { getAccounts, selectAccount } from "../../services/backgroundAccounts"
import { recover } from "../recovery/recovery.service"
import { createAccount } from "./accounts.service"
import { importNewLedgerAccount } from '../../services/backgroundAccounts'
import { WalletAccount } from "../../../shared/wallet.model"

export const useAddAccount = () => {
const navigate = useNavigate()
Expand All @@ -22,10 +23,10 @@ export const useAddAccount = () => {
return { addAccount }
}

export const addLedgerAccount = async (networkId: string, account: Account, hdIndex: number) => {
await importNewLedgerAccount(account, hdIndex, networkId)
export const addLedgerAccount = async (account: WalletAccount) => {
await importNewLedgerAccount(account)
// switch background wallet to the account that was selected
await selectAccount({ address: account.address, networkId: networkId })
await selectAccount({ address: account.address, networkId: account.networkId })
}

export const getAllLedgerAccounts = async (networkId: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AlephiumApp as LedgerApp } from "@alephium/ledger-app"
import { ALPH_TOKEN_ID, ONE_ALPH, prettifyTokenAmount, TransactionBuilder } from "@alephium/web3"
import { getHDWalletPath } from "@alephium/web3-wallet"
import { L1, icons } from "@argent/ui"
import { Flex, Text } from "@chakra-ui/react"
import { FC, useCallback, useEffect, useState } from "react"
Expand All @@ -17,7 +15,7 @@ import { usePageTracking } from "../../services/analytics"
import { rejectAction } from "../../services/backgroundActions"
import { Account } from "../accounts/Account"
import { useAllTokensWithBalance } from "../accountTokens/tokens.state"
import { getLedgerApp } from "../ledger/utils"
import { LedgerAlephium } from "../ledger/utils"
import { useNetwork } from "../networks/useNetworks"
import { ConfirmScreen } from "./ConfirmScreen"
import { ConfirmPageProps } from "./DeprecatedConfirmScreen"
Expand Down Expand Up @@ -214,7 +212,7 @@ export const ApproveTransactionScreen: FC<ApproveTransactionScreenProps> = ({
const [ledgerState, setLedgerState] = useState<
"detecting" | "notfound" | "signing" | "succeeded" | "failed"
>()
const [ledgerApp, setLedgerApp] = useState<LedgerApp>()
const [ledgerApp, setLedgerApp] = useState<LedgerAlephium>()
const { tokenDetails: allUserTokens, tokenDetailsIsInitialising } = useAllTokensWithBalance(selectedAccount)

// TODO: handle error
Expand Down Expand Up @@ -252,20 +250,15 @@ export const ApproveTransactionScreen: FC<ApproveTransactionScreenProps> = ({
setLedgerState(oldState => oldState === undefined ? "detecting" : oldState)

if (buildResult) {
let app: LedgerApp | undefined
let app: LedgerAlephium | undefined
try {
app = await getLedgerApp()
app = await LedgerAlephium.create()
setLedgerApp(app)
setLedgerState("signing")
const path = getHDWalletPath(
selectedAccount.signer.keyType,
selectedAccount.signer.derivationIndex,
)
const unsignedTx = Buffer.from(buildResult.result.unsignedTx, "hex")
const signature = await app.signUnsignedTx(path, unsignedTx)
const signature = await app.signUnsignedTx(selectedAccount, unsignedTx)
setLedgerState("succeeded")
onSubmit({ ...buildResult, signature })
await app.close()
} catch (e) {
if (app === undefined) {
setLedgerState(oldState => oldState === undefined || oldState === "detecting" ? "notfound" : oldState)
Expand Down
Loading

0 comments on commit cddbe9e

Please sign in to comment.