diff --git a/packages/transactions/src/utils.ts b/packages/transactions/src/utils.ts index a3193cc4d..a777a7e55 100644 --- a/packages/transactions/src/utils.ts +++ b/packages/transactions/src/utils.ts @@ -8,6 +8,7 @@ import fetch from 'cross-fetch'; import { c32addressDecode } from 'c32check'; import lodashCloneDeep from 'lodash.clonedeep'; import { with0x } from '@stacks/common'; +import { bytesToHex } from '@noble/hashes/utils'; /** * Use utils.randomBytes to replace randombytes dependency @@ -17,6 +18,8 @@ import { with0x } from '@stacks/common'; */ export const randomBytes = (bytesLength?: number) => Buffer.from(utils.randomBytes(bytesLength)); +export { bytesToHex }; + export class BufferArray { _value: Buffer[] = []; get value() { diff --git a/packages/wallet-sdk/package.json b/packages/wallet-sdk/package.json index 2fe3f8534..c3e4e7f69 100644 --- a/packages/wallet-sdk/package.json +++ b/packages/wallet-sdk/package.json @@ -36,6 +36,7 @@ "@types/jest": "^26.0.22", "@types/node": "^14.14.43", "assert": "^2.0.0", + "bip32": "^2.0.6", "crypto-browserify": "^3.12.0", "jest": "^26.6.3", "jest-fetch-mock": "^3.0.3", @@ -51,7 +52,7 @@ "@stacks/profile": "^3.3.0", "@stacks/storage": "^3.3.0", "@stacks/transactions": "^3.3.0", - "bip32": "^2.0.6", + "@scure/bip32": "^1.0.1", "bip39": "^3.0.2", "bitcoinjs-lib": "^5.2.0", "bn.js": "^5.2.0", diff --git a/packages/wallet-sdk/src/derive.ts b/packages/wallet-sdk/src/derive.ts index 1c04a67d6..50fd03327 100644 --- a/packages/wallet-sdk/src/derive.ts +++ b/packages/wallet-sdk/src/derive.ts @@ -1,23 +1,27 @@ -import { BIP32Interface } from 'bip32'; +// https://github.com/paulmillr/scure-bip32 +// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets. +import { HDKey } from '@scure/bip32'; import { Buffer, ChainID, TransactionVersion } from '@stacks/common'; +import { HARDENED_OFFSET } from './models/common'; import { ECPair } from 'bitcoinjs-lib'; import { createSha2Hash, ecPairToHexString } from '@stacks/encryption'; import { assertIsTruthy, whenChainId } from './utils'; -import { Account, WalletKeys } from './models/common'; +import { Account, WalletKeys, BIP32Interface } from './models/common'; import { StacksMainnet, StacksNetwork } from '@stacks/network'; -import { getAddressFromPrivateKey } from '@stacks/transactions'; +import { getAddressFromPrivateKey, bytesToHex } from '@stacks/transactions'; import { fetchFirstName } from './usernames'; const DATA_DERIVATION_PATH = `m/888'/0'`; const WALLET_CONFIG_PATH = `m/44/5757'/0'/1`; const STX_DERIVATION_PATH = `m/44'/5757'/0'/0`; -export const deriveWalletKeys = async (rootNode: BIP32Interface): Promise => { +export const deriveWalletKeys = async (rootNode: HDKey | BIP32Interface): Promise => { + // Keep BIP32Interface for backward compatibility with bip32 assertIsTruthy(rootNode.privateKey); const derived: WalletKeys = { salt: await deriveSalt(rootNode), - rootKey: rootNode.toBase58(), + rootKey: rootNode instanceof HDKey ? rootNode.privateExtendedKey : rootNode.toBase58(), // Backward compatibility with bip32 configPrivateKey: deriveConfigPrivateKey(rootNode).toString('hex'), }; return derived; @@ -34,11 +38,21 @@ export const deriveWalletKeys = async (rootNode: BIP32Interface): Promise { - const derivedConfigKey = rootNode.derivePath(WALLET_CONFIG_PATH).privateKey; +export const deriveConfigPrivateKey = (rootNode: HDKey | BIP32Interface): Buffer => { + // Keep BIP32Interface for backward compatibility with bip32 + let derivedConfigKey; + if (rootNode instanceof HDKey) { + derivedConfigKey = rootNode.derive(WALLET_CONFIG_PATH).privateKey; + } else { + // Backward compatibility with bip32 + derivedConfigKey = rootNode.derivePath(WALLET_CONFIG_PATH).privateKey; + } if (!derivedConfigKey) { throw new TypeError('Unable to derive config key for wallet identities'); } + if (derivedConfigKey instanceof Uint8Array) { + derivedConfigKey = Buffer.from(derivedConfigKey); + } return derivedConfigKey; }; @@ -49,22 +63,42 @@ export const deriveConfigPrivateKey = (rootNode: BIP32Interface): Buffer => { * The path for this key is `m/45'` * @param rootNode A keychain that was created using the wallet's seed phrase */ -export const deriveLegacyConfigPrivateKey = (rootNode: BIP32Interface): string => { - const derivedLegacyKey = rootNode.deriveHardened(45).privateKey; +export const deriveLegacyConfigPrivateKey = (rootNode: HDKey | BIP32Interface): string => { + // Keep BIP32Interface for backward compatibility with bip32 + let derivedLegacyKey; + if (rootNode instanceof HDKey) { + derivedLegacyKey = rootNode.deriveChild(45 + HARDENED_OFFSET).privateKey; + } else { + // Backward compatibility with bip32 + derivedLegacyKey = rootNode.deriveHardened(45).privateKey; + } if (!derivedLegacyKey) { throw new TypeError('Unable to derive config key for wallet identities'); } - const configPrivateKey = derivedLegacyKey.toString('hex'); - return configPrivateKey; + if (derivedLegacyKey instanceof Buffer) { + return derivedLegacyKey.toString('hex'); + } else { + return bytesToHex(derivedLegacyKey); + } }; /** * Generate a salt, which is used for generating an app-specific private key * @param rootNode */ -export const deriveSalt = async (rootNode: BIP32Interface) => { - const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH); - const publicKeyHex = Buffer.from(identitiesKeychain.publicKey.toString('hex')); +export const deriveSalt = async (rootNode: HDKey | BIP32Interface) => { + // Keep BIP32Interface for backward compatibility with bip32 + let identitiesKeychain; + let publicKeyHex; + + if (rootNode instanceof HDKey) { + identitiesKeychain = rootNode.derive(DATA_DERIVATION_PATH); + publicKeyHex = Buffer.from(bytesToHex(identitiesKeychain.publicKey as Uint8Array)); + } else { + // Backward compatibility with bip32 + identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH); + publicKeyHex = Buffer.from(identitiesKeychain.publicKey.toString('hex')); + } const sha2Hash = await createSha2Hash(); const saltData = await sha2Hash.digest(publicKeyHex, 'sha256'); @@ -111,7 +145,7 @@ export const selectStxDerivation = async ({ network, }: { username?: string; - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; network?: StacksNetwork; }): Promise<{ username: string | undefined; stxDerivationType: DerivationType }> => { @@ -141,7 +175,7 @@ const selectDerivationTypeForUsername = async ({ network, }: { username: string; - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; network?: StacksNetwork; }): Promise => { @@ -175,7 +209,7 @@ const selectUsernameForAccount = async ({ index, network, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; network?: StacksNetwork; }): Promise<{ username: string | undefined; derivationType: DerivationType }> => { @@ -211,7 +245,7 @@ export const fetchUsernameForAccountByDerivationType = async ({ derivationType, network, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; derivationType: DerivationType.Wallet | DerivationType.Data; network?: StacksNetwork; @@ -235,7 +269,7 @@ export const derivePrivateKeyByType = ({ index, derivationType, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; derivationType: DerivationType; }): string => { @@ -248,12 +282,22 @@ export const deriveStxPrivateKey = ({ rootNode, index, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; }) => { - const childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index); + let childKey; + if (rootNode instanceof HDKey) { + childKey = rootNode.derive(STX_DERIVATION_PATH).deriveChild(index); + } else { + // Backward compatibility with bip32 + childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index); + } assertIsTruthy(childKey.privateKey); - const ecPair = ECPair.fromPrivateKey(childKey.privateKey); + const privateKey = + childKey.privateKey instanceof Uint8Array + ? Buffer.from(childKey.privateKey) + : childKey.privateKey; + const ecPair = ECPair.fromPrivateKey(privateKey); return ecPairToHexString(ecPair); }; @@ -261,12 +305,22 @@ export const deriveDataPrivateKey = ({ rootNode, index, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; }) => { - const childKey = rootNode.derivePath(DATA_DERIVATION_PATH).deriveHardened(index); + let childKey; + if (rootNode instanceof HDKey) { + childKey = rootNode.derive(DATA_DERIVATION_PATH).deriveChild(index + HARDENED_OFFSET); + } else { + // Backward compatibility with bip32 + childKey = rootNode.derivePath(DATA_DERIVATION_PATH).deriveHardened(index); + } assertIsTruthy(childKey.privateKey); - const ecPair = ECPair.fromPrivateKey(childKey.privateKey); + const privateKey = + childKey.privateKey instanceof Uint8Array + ? Buffer.from(childKey.privateKey) + : childKey.privateKey; + const ecPair = ECPair.fromPrivateKey(privateKey); return ecPairToHexString(ecPair); }; @@ -276,7 +330,7 @@ export const deriveAccount = ({ salt, stxDerivationType, }: { - rootNode: BIP32Interface; + rootNode: HDKey | BIP32Interface; // Keep BIP32Interface for backward compatibility with bip32 index: number; salt: string; stxDerivationType: DerivationType.Wallet | DerivationType.Data; @@ -285,13 +339,25 @@ export const deriveAccount = ({ stxDerivationType === DerivationType.Wallet ? deriveStxPrivateKey({ rootNode, index }) : deriveDataPrivateKey({ rootNode, index }); - const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH); + let dataPrivateKey; + let appsKey; + if (rootNode instanceof HDKey) { + const identitiesKeychain = rootNode.derive(DATA_DERIVATION_PATH); + const identityKeychain = identitiesKeychain.deriveChild(index + HARDENED_OFFSET); + if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities'); + dataPrivateKey = bytesToHex(identityKeychain.privateKey); + + appsKey = identityKeychain.deriveChild(0 + HARDENED_OFFSET).privateExtendedKey; + } else { + // Backward compatibility with bip32 + const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH); - const identityKeychain = identitiesKeychain.deriveHardened(index); - if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities'); - const dataPrivateKey = identityKeychain.privateKey.toString('hex'); + const identityKeychain = identitiesKeychain.deriveHardened(index); + if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities'); + dataPrivateKey = identityKeychain.privateKey.toString('hex'); - const appsKey = identityKeychain.deriveHardened(0).toBase58(); + appsKey = identityKeychain.deriveHardened(0).toBase58(); + } return { stxPrivateKey, diff --git a/packages/wallet-sdk/src/generate.ts b/packages/wallet-sdk/src/generate.ts index 6ca6053a5..e55d9f3ff 100644 --- a/packages/wallet-sdk/src/generate.ts +++ b/packages/wallet-sdk/src/generate.ts @@ -1,5 +1,7 @@ import { generateMnemonic, mnemonicToSeed } from 'bip39'; -import { fromSeed } from 'bip32'; +// https://github.com/paulmillr/scure-bip32 +// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets. +import { HDKey } from '@scure/bip32'; import { randomBytes } from '@stacks/encryption'; import { Wallet, getRootNode } from './models/common'; import { encrypt } from './encryption'; @@ -29,7 +31,7 @@ export const generateWallet = async ({ const encryptedSecretKey = ciphertextBuffer.toString('hex'); const rootPrivateKey = await mnemonicToSeed(secretKey); - const rootNode = fromSeed(rootPrivateKey); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); const walletKeys = await deriveWalletKeys(rootNode); const wallet = { diff --git a/packages/wallet-sdk/src/models/account.ts b/packages/wallet-sdk/src/models/account.ts index 083331b9c..2fd404725 100644 --- a/packages/wallet-sdk/src/models/account.ts +++ b/packages/wallet-sdk/src/models/account.ts @@ -5,15 +5,17 @@ import { hashSha256Sync, } from '@stacks/encryption'; import { makeAuthResponse as _makeAuthResponse } from '@stacks/auth'; -import { TransactionVersion, getAddressFromPrivateKey } from '@stacks/transactions'; -import { fromBase58 } from 'bip32'; +import { TransactionVersion, getAddressFromPrivateKey, bytesToHex } from '@stacks/transactions'; +// https://github.com/paulmillr/scure-bip32 +// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets. +import { HDKey } from '@scure/bip32'; import { DEFAULT_PROFILE, fetchAccountProfileUrl, fetchProfileFromUrl, signAndUploadProfile, } from './profile'; -import { Account } from './common'; +import { Account, HARDENED_OFFSET } from './common'; import { ECPair } from 'bitcoinjs-lib'; import { connectToGaiaHubWithConfig, getHubInfo, makeGaiaAssociationToken } from '../utils'; import { Buffer } from '@stacks/common'; @@ -54,10 +56,10 @@ export const getAppPrivateKey = ({ const hashBuffer = hashSha256Sync(Buffer.from(`${appDomain}${account.salt}`)); const hash = hashBuffer.toString('hex'); const appIndex = hashCode(hash); - const appsNode = fromBase58(account.appsKey); - const appKeychain = appsNode.deriveHardened(appIndex); + const appsNode = HDKey.fromExtendedKey(account.appsKey); + const appKeychain = appsNode.deriveChild(appIndex + HARDENED_OFFSET); if (!appKeychain.privateKey) throw 'Needs private key'; - return appKeychain.privateKey.toString('hex'); + return bytesToHex(appKeychain.privateKey); }; export const makeAuthResponse = async ({ diff --git a/packages/wallet-sdk/src/models/common.ts b/packages/wallet-sdk/src/models/common.ts index 4c7d8a016..1a296c277 100644 --- a/packages/wallet-sdk/src/models/common.ts +++ b/packages/wallet-sdk/src/models/common.ts @@ -1,5 +1,9 @@ +// @ts-ignore +import { Buffer } from '@stacks/common'; import { getPublicKeyFromPrivate, publicKeyToAddress } from '@stacks/encryption'; -import { fromBase58 } from 'bip32'; +// https://github.com/paulmillr/scure-bip32 +// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets. +import { HDKey } from '@scure/bip32'; export interface Account { /** The private key used for STX payments */ @@ -18,6 +22,50 @@ export interface Account { index: number; } +// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L102 +// Used to replicate deriveHardened bip32 method using deriveChild of scure-bip32 to offload old bip32 library +export const HARDENED_OFFSET = 0x80000000; + +// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L7-L17 +// Used inside BIP32Interface for backward compatibility with offloaded bip32 dependency +interface Network { + wif: number; + bip32: { + public: number; + private: number; + }; + messagePrefix?: string; + bech32?: string; + pubKeyHash?: number; + scriptHash?: number; +} + +// Reference: https://github.com/bitcoinjs/bip32/blob/79c6dedb3edfdc8505fe74d9f34c115c33e8a2da/ts-src/bip32.ts#L19-L41 +// Using BIP32Interface for backward compatibility with offloaded bip32 dependency +export interface BIP32Interface { + chainCode: Buffer; + network: Network; + lowR: boolean; + depth: number; + index: number; + parentFingerprint: number; + publicKey: Buffer; + privateKey?: Buffer; + identifier: Buffer; + fingerprint: Buffer; + isNeutered(): boolean; + neutered(): BIP32Interface; + toBase58(): string; + toWIF(): string; + derive(index: number): BIP32Interface; + deriveHardened(index: number): BIP32Interface; + derivePath(path: string): BIP32Interface; + sign(hash: Buffer, lowR?: boolean): Buffer; + verify(hash: Buffer, signature: Buffer): boolean; + signSchnorr?(hash: Buffer): Buffer; + verifySchnorr?(hash: Buffer, signature: Buffer): boolean; +} + const PERSON_TYPE = 'Person'; const CONTEXT = 'http://schema.org'; const IMAGE_TYPE = 'ImageObject'; @@ -72,5 +120,5 @@ export const getGaiaAddress = (account: Account) => { }; export const getRootNode = (wallet: Wallet) => { - return fromBase58(wallet.rootKey); + return HDKey.fromExtendedKey(wallet.rootKey); }; diff --git a/packages/wallet-sdk/tests/derive-keychain.test.ts b/packages/wallet-sdk/tests/derive-keychain.test.ts new file mode 100644 index 000000000..2b8688932 --- /dev/null +++ b/packages/wallet-sdk/tests/derive-keychain.test.ts @@ -0,0 +1,272 @@ +import { + deriveWalletKeys, + deriveAccount, + getStxAddress, + deriveLegacyConfigPrivateKey, + DerivationType, + selectStxDerivation, + fetchUsernameForAccountByDerivationType, +} from '../src'; +import { mnemonicToSeed } from 'bip39'; +import { BIP32Interface, fromBase58 } from 'bip32'; +import { HDKey } from '@scure/bip32'; +import { TransactionVersion, bytesToHex } from '@stacks/transactions'; +import { StacksMainnet } from '@stacks/network'; +import fetchMock from 'jest-fetch-mock'; + +const SECRET_KEY = + 'sound idle panel often situate develop unit text design antenna ' + + 'vendor screen opinion balcony share trigger accuse scatter visa uniform brass ' + + 'update opinion media'; +const WALLET_ADDRESS = 'SP384CVPNDTYA0E92TKJZQTYXQHNZSWGCAG7SAPVB'; +const DATA_ADDRESS = 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K'; + +test('keys are serialized, and can be deserialized properly using wallet private key for stx', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode1 = HDKey.fromMasterSeed(rootPrivateKey); + const derived = await deriveWalletKeys(rootNode1); + const rootNode = HDKey.fromExtendedKey(derived.rootKey); + const account = deriveAccount({ + rootNode, + index: 0, + salt: derived.salt, + stxDerivationType: DerivationType.Wallet, + }); + expect(getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet })).toEqual( + WALLET_ADDRESS + ); +}); + +test('keys are serialized, and can be deserialized properly using data private key for stx', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode1 = HDKey.fromMasterSeed(rootPrivateKey); + const derived = await deriveWalletKeys(rootNode1); + const rootNode = HDKey.fromExtendedKey(derived.rootKey); + const account = deriveAccount({ + rootNode, + index: 0, + salt: derived.salt, + stxDerivationType: DerivationType.Data, + }); + expect(getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet })).toEqual( + DATA_ADDRESS + ); +}); + +test('backwards compatible legacy config private key derivation', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const legacyKey = deriveLegacyConfigPrivateKey(rootNode); + expect(legacyKey).toEqual('767b51d866d068b02ce126afe3737896f4d0c486263d9b932f2822109565a3c6'); +}); + +test('derive derivation path without username', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + const { username, stxDerivationType } = await selectStxDerivation({ + username: undefined, + rootNode, + index: 0, + network, + }); + expect(username).toEqual(undefined); + expect(stxDerivationType).toEqual(DerivationType.Wallet); +}); + +test('derive derivation path with username owned by address of stx derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + + fetchMock.once(JSON.stringify({ address: DATA_ADDRESS })); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: 'public_profile_for_testing.id.blockstack', + rootNode, + index: 0, + network, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Data); +}); + +test('derive derivation path with username owned by address of unknown derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + + fetchMock.once(JSON.stringify({ address: 'SP000000000000000000002Q6VF78' })); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: 'public_profile_for_testing.id.blockstack', + rootNode, + index: 0, + network, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Unknown); +}); + +test('derive derivation path with username owned by address of data derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + + fetchMock.once(JSON.stringify({ address: 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K' })); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: 'public_profile_for_testing.id.blockstack', + rootNode, + index: 0, + network, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Data); +}); + +test('derive derivation path with new username owned by address of stx derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + + fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] })); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: undefined, + rootNode, + index: 0, + network, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Wallet); + expect(fetchMock.mock.calls[0][0]).toEqual( + `https://stacks-node-api.mainnet.stacks.co/v1/addresses/stacks/${WALLET_ADDRESS}` + ); +}); + +test('derive derivation path with new username owned by address of data derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + const network = new StacksMainnet(); + + fetchMock + .once(JSON.stringify({ names: [] })) // no names on stx derivation path + .once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] })); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: undefined, + rootNode, + index: 0, + network, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Data); + expect(fetchMock.mock.calls[0][0]).toEqual( + `https://stacks-node-api.mainnet.stacks.co/v1/addresses/stacks/${WALLET_ADDRESS}` + ); + expect(fetchMock.mock.calls[1][0]).toEqual( + `https://stacks-node-api.mainnet.stacks.co/v1/addresses/stacks/${DATA_ADDRESS}` + ); +}); + +test('derive derivation path with username and without network', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: 'public_profile_for_testing.id.blockstack', + rootNode, + index: 0, + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); + expect(stxDerivationType).toEqual(DerivationType.Unknown); +}); + +test('derive derivation path without username and without network', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + + const { username, stxDerivationType } = await selectStxDerivation({ + username: undefined, + rootNode, + index: 0, + }); + expect(username).toEqual(undefined); + expect(stxDerivationType).toEqual(DerivationType.Wallet); +}); + +test('fetch username owned by derivation type', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + + fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] })); + + const { username } = await fetchUsernameForAccountByDerivationType({ + rootNode, + index: 0, + derivationType: DerivationType.Wallet, + network: new StacksMainnet(), + }); + expect(username).toEqual('public_profile_for_testing.id.blockstack'); +}); + +test('fetch username owned by different derivation type', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + + fetchMock.once(JSON.stringify({ names: [] })); + + const { username } = await fetchUsernameForAccountByDerivationType({ + rootNode, + index: 0, + derivationType: DerivationType.Wallet, + network: new StacksMainnet(), + }); + expect(username).toEqual(undefined); +}); + +test('fetch username defaults to mainnet', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = HDKey.fromMasterSeed(rootPrivateKey); + + fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] })); + + await fetchUsernameForAccountByDerivationType({ + rootNode, + index: 0, + derivationType: DerivationType.Wallet, + }); + expect(fetchMock.mock.calls[0][0]).toContain('stacks-node-api.mainnet'); +}); + +test('Verify compatibility between @scure/bip32 and bip32 dependency',() => { + // Consider a root key in base58 format + const root = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; + const bip32Node: BIP32Interface = fromBase58(root); + + // Use same root key to create node using @scure/bip32 + const keychainNode = HDKey.fromExtendedKey(root); + + if (bip32Node.privateKey && keychainNode.privateKey) { + expect(bip32Node.privateKey.toString('hex')).toEqual(bytesToHex(keychainNode.privateKey)) + } else { + // Reject test case + fail('No private keys: failed to verify compatibility between @scure/bip32 and bip32 dependency'); + } + + const derivationPath = 'm/0/0'; + + // Derive a child from bip32Node at given derivation path + const childBip32Node: BIP32Interface = bip32Node.derivePath(derivationPath); + + // Derive a child from keychainNode at given derivation path + const childKeychainNode = keychainNode.derive(derivationPath); + + if (childBip32Node.privateKey && childKeychainNode.privateKey) { + expect(childBip32Node.privateKey.toString('hex')).toEqual(bytesToHex(childKeychainNode.privateKey)) + } else { + // Reject test case + fail('No private keys: failed to verify compatibility between @scure/bip32 and bip32 dependency'); + } +});