Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/next/src/localization/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@
"Provider": "Provider",
"Public key not found": "Public key not found",
"Quote includes an {{coreFee}} Core fee": "Quote includes an {{coreFee}} Core fee",
"QR code (Supports: Essential, Pro, 3 Pro)": "QR code (Supports: Essential, Pro, 3 Pro)",
"RPC URL": "RPC URL",
"RPC URL reset successfully": "RPC URL reset successfully",
"Rate": "Rate",
Expand Down Expand Up @@ -872,6 +873,7 @@
"User declined the transaction": "User declined the transaction",
"User rejected the request": "User rejected the request",
"Users may not use the Bridge if they are on the Specially Designated Nationals (SDN) List of the Office of Foreign Assets Control (OFAC) or any other sanctions or are otherwise a sanctioned person or from a sanctioned jurisdiction": "Users may not use the Bridge if they are on the Specially Designated Nationals (SDN) List of the Office of Foreign Assets Control (OFAC) or any other sanctions or are otherwise a sanctioned person or from a sanctioned jurisdiction",
"USB (Keystone 3 Pro only)": "USB (Keystone 3 Pro only)",
"Value": "Value",
"Verify": "Verify",
"Verify Code": "Verify Code",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ export const ConnectKeystoneFlow = () => {
const { isFlagEnabled } = useFeatureFlagContext();

const isKeystoneUsbSupported = isFlagEnabled(FeatureGates.KEYSTONE_3);
const [device, setDevice] = useState<Device>(
isKeystoneUsbSupported ? 'keystone-usb' : 'keystone-qr',
);
const [device, setDevice] = useState<Device>('keystone-qr');
const [derivedKeys, setDerivedKeys] = useState<DerivedKeys>();
const [addresses, setAddresses] = useState<string[]>([]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export const KeystoneDeviceSelect = ({
onChange={(e) => setDevice(e.target.value as Device)}
sx={{ py: 0.75 }}
>
<MenuItem value="keystone-usb" disabled={!isKeystoneUsbSupported}>
USB (Keystone 3 Pro)
</MenuItem>
<MenuItem value="keystone-qr">
QR code (Keystone Essential/Pro)
{t('QR code (Supports: Essential, Pro, 3 Pro)')}
</MenuItem>
<MenuItem value="keystone-usb" disabled={!isKeystoneUsbSupported}>
{t('USB (Keystone 3 Pro only)')}
</MenuItem>
</Select>
</Section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { FC, useState, useEffect, useCallback, useRef } from 'react';
import { getAddressPublicKeyFromXPub } from '@avalabs/core-wallets-sdk';

import { useCameraPermissions } from '@core/ui';
import { EVM_BASE_DERIVATION_PATH } from '@core/types';
import { EVM_BASE_DERIVATION_PATH, ExtendedPublicKey } from '@core/types';
import { getAvalancheExtendedKeyPath } from '@core/common';

import { VideoFeedCrosshair } from '@/components/keystone';

Expand Down Expand Up @@ -68,6 +69,30 @@ export const KeystoneQRConnector: FC<KeystoneQRConnectorProps> = ({
[minNumberOfKeys],
);

const getAvmAddressPublicKeys = useCallback(
async (extendedPublicKeyHex: string) => {
const keys: PublicKey[] = [];
const startingIndexes = Array.from(
{ length: minNumberOfKeys },
(_, i) => i,
);
for (const index of startingIndexes) {
const avmKey = await getAddressPublicKeyFromXPub(
extendedPublicKeyHex,
index,
);
keys.push({
index,
vm: 'AVM',
key: buildAddressPublicKey(avmKey, index, 'AVM'),
});
}
Copy link
Member

Choose a reason for hiding this comment

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

In case of EVM this would be correct - you'd be creating multiple accounts from a single XPUB (m/44'/60'/0').

In case of X/P chains, though, we want to import minNumberOfKeys extended public keys (one for each account). What you're doing here is creating multiple (minNumberOfKeys) addresses for a single X/P account (i.e. extended public key (`m/44'/9000'/0')).

In Core, the account model for X/P chains looks as follows:

  • Account 1: gets the extended public key for m/44'/9000'/0' and from that we derive a single receive address to display in the UI (m/44'/9000'/0'/0/0).
  • Account 2: gets the extended public key for m/44'/9000'/1' and from that we derive a single receive address to display in the UI (m/44'/9000'/1'/0/0).
  • etc..

However, if the user has funds on other addresses for a given account (e.g. has funds spread across m/44'/9000'/0'/0/0, m/44'/9000'/0'/0/1, ``m/44'/9000'/0'/0/2`) - those funds would still be usable (given an XPUB for the account, we try to discover all addresses with activity/funds on them and make them spendable).

So given an account N, you want to import the following to make X/P chains work:

  • m/44'/9000'/N' (to be able to find all funds under this xpub)
  • m/44'/9000'/N'/0/0 (to be able to receive funds)

Copy link
Author

Choose a reason for hiding this comment

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

Addressed in d259e24


return keys;
},
[minNumberOfKeys],
);

const handleUnreadableQRCode = useCallback(
(isDimensionsError: boolean) => {
setStatus('error');
Expand All @@ -84,24 +109,46 @@ export const KeystoneQRConnector: FC<KeystoneQRConnectorProps> = ({
const cryptoMultiAccounts = CryptoMultiAccounts.fromCBOR(buffer);

const masterFingerprint = cryptoMultiAccounts.getMasterFingerprint();
const [key] = cryptoMultiAccounts.getKeys();
const allKeys = cryptoMultiAccounts.getKeys();

const extendedPublicKeys: ExtendedPublicKey[] = [];
let evmAddressPublicKeys: PublicKey[] = [];
let avmAddressPublicKeys: PublicKey[] = [];

for (const key of allKeys) {
const path = key.getOrigin()?.getPath();
const xpub = key.getBip32Key();

if (path?.includes("44'/60'")) {
extendedPublicKeys.push(
buildExtendedPublicKey(xpub, EVM_BASE_DERIVATION_PATH),
);
evmAddressPublicKeys = await getAddressPublicKeys(xpub);
} else if (path?.includes("44'/9000'")) {
Copy link
Member

Choose a reason for hiding this comment

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

Please:

  1. use constants for the derivation path prefixes
  2. create a util function to recognize keys based on their derivation paths

such that when I read the if statement, I immediately know which key I'm processing without having to memorize the derivation paths for given chains. Example:

if (isEvmXpub(key)) {
  // ...
} else if (isAvalancheXpub(key)) {
  // ...
}
// etc.

Copy link
Author

Choose a reason for hiding this comment

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

Addressed in bbadb7b, with additional cleanup:

  • 2754146: extracted predicates to @core/common
  • a55b65f: return path from getter to avoid non-null assertions
  • 971860a: added unit tests for the shared predicates

extendedPublicKeys.push(
buildExtendedPublicKey(xpub, getAvalancheExtendedKeyPath(0)),
);
avmAddressPublicKeys = await getAvmAddressPublicKeys(xpub);
}
}

if (key) {
if (extendedPublicKeys.length > 0) {
onQRCodeScanned({
extendedPublicKeys: [
buildExtendedPublicKey(key.getBip32Key(), EVM_BASE_DERIVATION_PATH),
],
addressPublicKeys: await getAddressPublicKeys(key.getBip32Key()),
extendedPublicKeys,
addressPublicKeys: [...evmAddressPublicKeys, ...avmAddressPublicKeys],
masterFingerprint: masterFingerprint.toString('hex'),
});
} else {
console.error(
'[Keystone] Invalid QR code: missing extended public key',
);
console.error('[Keystone] Invalid QR code: no valid keys found');
handleUnreadableQRCode(false);
}
},
[onQRCodeScanned, getAddressPublicKeys, handleUnreadableQRCode],
[
onQRCodeScanned,
getAddressPublicKeys,
getAvmAddressPublicKeys,
handleUnreadableQRCode,
],
);

const handleError = useCallback(
Expand Down