diff --git a/packages/cli/src/auth.ts b/packages/cli/src/auth.ts index 1a6cfe5f..6fca0dc9 100644 --- a/packages/cli/src/auth.ts +++ b/packages/cli/src/auth.ts @@ -420,7 +420,7 @@ export async function handleAuth( res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': authPage.length }); res.write(authPage); res.end(); - } catch (e) { + } catch (e: any) { if (!errorMsg) { errorMsg = e.message; } diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index dd0f0e06..61faa529 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -545,7 +545,7 @@ export function mkdirs(path: string): void { if ((statInfo.mode & fs.constants.S_IFDIR) === 0) { throw new Error(`Not a directory: ${tmpPath}`); } - } catch (e) { + } catch (e: any) { if (e.code === 'ENOENT') { // need to create fs.mkdirSync(tmpPath); diff --git a/packages/stacking/src/utils.ts b/packages/stacking/src/utils.ts index 06ef2d3d..890ecf58 100644 --- a/packages/stacking/src/utils.ts +++ b/packages/stacking/src/utils.ts @@ -72,7 +72,7 @@ export function getAddressHashMode(btcAddress: string) { try { const { version } = address.fromBase58Check(btcAddress); return btcAddressVersionToHashMode(version); - } catch (error) { + } catch (error: any) { throw new InvalidAddressError(btcAddress, error); } } @@ -81,7 +81,7 @@ export function decodeBtcAddress(btcAddress: string) { let b58Result: address.Base58CheckResult; try { b58Result = address.fromBase58Check(btcAddress); - } catch (error) { + } catch (error: any) { throw new InvalidAddressError(btcAddress, error); } const hashMode = btcAddressVersionToHashMode(b58Result.version); diff --git a/packages/wallet-sdk/src/derive.ts b/packages/wallet-sdk/src/derive.ts index 550b8fcd..e015a8f4 100644 --- a/packages/wallet-sdk/src/derive.ts +++ b/packages/wallet-sdk/src/derive.ts @@ -4,8 +4,10 @@ import { ECPair } from 'bitcoinjs-lib'; import { createSha2Hash, ecPairToHexString } from '@stacks/encryption'; import { assertIsTruthy } from './utils'; -import { Account } from './models/common'; -import { WalletKeys } from './models/common'; +import { Account, WalletKeys } from './models/common'; +import { StacksNetwork } from '@stacks/network'; +import { getAddressFromPrivateKey } from '@stacks/transactions'; +import { fetchFirstName } from './usernames'; const DATA_DERIVATION_PATH = `m/888'/0'`; const WALLET_CONFIG_PATH = `m/44/5757'/0'/1`; @@ -71,19 +73,182 @@ export const deriveSalt = async (rootNode: BIP32Interface) => { return salt; }; +/** + * Determines how a private key is derived for an account: + * + * Wallet => STX_DERIVATION_PATH + * Data => DATA_DERIVATION_PATH + */ +export enum DerivationType { + Wallet, + Data, + Unknown, +} + +/** + * Tries to find a derivation path for the stxPrivateKey for the account + * defined by rootNode and index that respects the username of that account. + * + * The stxPrivateKey is used to sign the profile of the account, therefore, + * a username must be owned by the stxPrivateKey. + * + * If a username is provided, a lookup for the owner address + * on the provided network is done. + * + * If no username is provided, a lookup for names owned + * by the stx derivation path and by the data derivation path is done. + * @param selectionOptions + * @returns username and derivation type + */ +export const selectStxDerivation = async ({ + username, + rootNode, + index, + network, +}: { + username?: string; + rootNode: BIP32Interface; + index: number; + network?: StacksNetwork; +}): Promise<{ username: string | undefined; stxDerivationType: DerivationType }> => { + if (username) { + // Based on username, determine the derivation path for the stx private key + const stxDerivationTypeForUsername = await selectDerivationTypeForUsername({ + username, + rootNode, + index, + network, + }); + return { username, stxDerivationType: stxDerivationTypeForUsername }; + } else { + const { username, derivationType } = await selectUsernameForAccount({ + rootNode, + index, + network, + }); + return { username, stxDerivationType: derivationType }; + } +}; + +const selectDerivationTypeForUsername = async ({ + username, + rootNode, + index, + network, +}: { + username: string; + rootNode: BIP32Interface; + index: number; + network?: StacksNetwork; +}): Promise => { + if (network) { + const nameInfo = await network.getNameInfo(username); + let stxPrivateKey = deriveStxPrivateKey({ rootNode, index }); + let derivedAddress = getAddressFromPrivateKey(stxPrivateKey); + if (derivedAddress !== nameInfo.address) { + // try data private key + stxPrivateKey = deriveDataPrivateKey({ + rootNode, + index, + }); + derivedAddress = getAddressFromPrivateKey(stxPrivateKey); + if (derivedAddress !== nameInfo.address) { + return DerivationType.Unknown; + } else { + return DerivationType.Data; + } + } else { + return DerivationType.Wallet; + } + } else { + // no network to determine the derivation path + return DerivationType.Unknown; + } +}; + +const selectUsernameForAccount = async ({ + rootNode, + index, + network, +}: { + rootNode: BIP32Interface; + index: number; + network?: StacksNetwork; +}): Promise<{ username: string | undefined; derivationType: DerivationType }> => { + // try to find existing usernames owned by stx derivation path + const address = deriveStxPrivateKey({ rootNode, index }); + if (network) { + let username = await fetchFirstName(address, network); + if (username) { + return { username, derivationType: DerivationType.Wallet }; + } else { + // try to find existing usernames owned by data derivation path + const address = deriveDataPrivateKey({ rootNode, index }); + username = await fetchFirstName(address, network); + if (username) { + return { username, derivationType: DerivationType.Data }; + } + } + } + // use wallet derivation for accounts without username + return { username: undefined, derivationType: DerivationType.Wallet }; +}; + +export const derivePrivateKeyByType = ({ + rootNode, + index, + derivationType, +}: { + rootNode: BIP32Interface; + index: number; + derivationType: DerivationType; +}): string => { + return derivationType === DerivationType.Wallet + ? deriveStxPrivateKey({ rootNode, index }) + : deriveDataPrivateKey({ rootNode, index }); +}; + +export const deriveStxPrivateKey = ({ + rootNode, + index, +}: { + rootNode: BIP32Interface; + index: number; +}) => { + const childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index); + assertIsTruthy(childKey.privateKey); + const ecPair = ECPair.fromPrivateKey(childKey.privateKey); + return ecPairToHexString(ecPair); +}; + +export const deriveDataPrivateKey = ({ + rootNode, + index, +}: { + rootNode: BIP32Interface; + index: number; +}) => { + const childKey = rootNode.derivePath(DATA_DERIVATION_PATH).deriveHardened(index); + assertIsTruthy(childKey.privateKey); + const ecPair = ECPair.fromPrivateKey(childKey.privateKey); + return ecPairToHexString(ecPair); +}; + export const deriveAccount = ({ rootNode, index, salt, + stxDerivationType, }: { rootNode: BIP32Interface; index: number; salt: string; + stxDerivationType: DerivationType.Wallet | DerivationType.Data; }): Account => { - const childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index); - assertIsTruthy(childKey.privateKey); - const ecPair = ECPair.fromPrivateKey(childKey.privateKey); - const stxPrivateKey = ecPairToHexString(ecPair); + const stxPrivateKey = + stxDerivationType === DerivationType.Wallet + ? deriveStxPrivateKey({ rootNode, index }) + : deriveDataPrivateKey({ rootNode, index }); const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH); const identityKeychain = identitiesKeychain.deriveHardened(index); diff --git a/packages/wallet-sdk/src/generate.ts b/packages/wallet-sdk/src/generate.ts index 16752688..6d2746ca 100644 --- a/packages/wallet-sdk/src/generate.ts +++ b/packages/wallet-sdk/src/generate.ts @@ -4,6 +4,7 @@ import randomBytes from 'randombytes'; import { Wallet, getRootNode } from './models/common'; import { encrypt } from './encryption'; import { deriveAccount, deriveWalletKeys } from './derive'; +import { DerivationType } from '.'; export type AllowedKeyEntropyBits = 128 | 256; @@ -49,6 +50,7 @@ export const generateNewAccount = (wallet: Wallet) => { rootNode: getRootNode(wallet), index: accountIndex, salt: wallet.salt, + stxDerivationType: DerivationType.Wallet, }); return { diff --git a/packages/wallet-sdk/src/models/profile.ts b/packages/wallet-sdk/src/models/profile.ts index cf510471..64aed72c 100644 --- a/packages/wallet-sdk/src/models/profile.ts +++ b/packages/wallet-sdk/src/models/profile.ts @@ -50,8 +50,10 @@ export const fetchAccountProfileUrl = async ({ }; export function signProfileForUpload({ profile, account }: { profile: Profile; account: Account }) { - const privateKey = account.dataPrivateKey; - const publicKey = getPublicKeyFromPrivate(privateKey); + // the profile is always signed with the stx private key + // because a username (if any) is owned by the stx private key + const privateKey = account.stxPrivateKey; + const publicKey = getPublicKeyFromPrivate(privateKey.slice(0, 64)); const token = signProfileToken(profile, privateKey, { publicKey }); const tokenRecord = wrapProfileToken(token); diff --git a/packages/wallet-sdk/src/models/wallet.ts b/packages/wallet-sdk/src/models/wallet.ts index d8b78d8f..882df989 100644 --- a/packages/wallet-sdk/src/models/wallet.ts +++ b/packages/wallet-sdk/src/models/wallet.ts @@ -1,3 +1,5 @@ +import { StacksNetwork } from '@stacks/network'; +import { DerivationType, derivePrivateKeyByType, selectStxDerivation } from '..'; import { deriveAccount, deriveLegacyConfigPrivateKey } from '../derive'; import { connectToGaiaHubWithConfig, getHubInfo } from '../utils'; import { Wallet, getRootNode } from './common'; @@ -18,9 +20,11 @@ export interface LockedWallet { export async function restoreWalletAccounts({ wallet, gaiaHubUrl, + network, }: { wallet: Wallet; gaiaHubUrl: string; + network?: StacksNetwork; }): Promise { const hubInfo = await getHubInfo(gaiaHubUrl); const rootNode = getRootNode(wallet); @@ -39,26 +43,49 @@ export async function restoreWalletAccounts({ fetchWalletConfig({ wallet, gaiaHubConfig: currentGaiaConfig }), fetchLegacyWalletConfig({ wallet, gaiaHubConfig: legacyGaiaConfig }), ]); - // Restore from existing config if ( walletConfig && walletConfig.accounts.length >= (legacyWalletConfig?.identities.length || 0) ) { - const newAccounts = walletConfig.accounts.map((account, index) => { - let existingAccount = wallet.accounts[index]; - if (!existingAccount) { - existingAccount = deriveAccount({ + const newAccounts = await Promise.all( + walletConfig.accounts.map(async (account, index) => { + let existingAccount = wallet.accounts[index]; + const { username, stxDerivationType } = await selectStxDerivation({ + username: account.username, rootNode, index, - salt: wallet.salt, + network, }); - } - return { - ...existingAccount, - username: account.username, - }; - }); + if (stxDerivationType === DerivationType.Unknown) { + // This account index has a username + // that is not owned by stx derivation path or data derivation path + // we can't determine the stx private key :-/ + return Promise.reject(`Username ${username} is owned by unknown private key`); + } + if (!existingAccount) { + existingAccount = deriveAccount({ + rootNode, + index, + salt: wallet.salt, + stxDerivationType, + }); + } else { + existingAccount = { + ...existingAccount, + stxPrivateKey: derivePrivateKeyByType({ + rootNode, + index, + derivationType: stxDerivationType, + }), + }; + } + return { + ...existingAccount, + username, + }; + }) + ); return { ...wallet, @@ -68,20 +95,44 @@ export async function restoreWalletAccounts({ // Restore from legacy config, and upload a new one if (legacyWalletConfig) { - const newAccounts = legacyWalletConfig.identities.map((identity, index) => { - let existingAccount = wallet.accounts[index]; - if (!existingAccount) { - existingAccount = deriveAccount({ + const newAccounts = await Promise.all( + legacyWalletConfig.identities.map(async (identity, index) => { + let existingAccount = wallet.accounts[index]; + const { username, stxDerivationType } = await selectStxDerivation({ + username: identity.username, rootNode, index, - salt: wallet.salt, + network, }); - } - return { - ...existingAccount, - username: identity.username, - }; - }); + if (stxDerivationType === DerivationType.Unknown) { + // This account index has a username + // that is not owned by stx derivation path or data derivation path + // we can't determine the stx private key :-/ + return Promise.reject(`Username ${username} is owned by unknown private key`); + } + if (!existingAccount) { + existingAccount = deriveAccount({ + rootNode, + index, + salt: wallet.salt, + stxDerivationType, + }); + } else { + existingAccount = { + ...existingAccount, + stxPrivateKey: derivePrivateKeyByType({ + rootNode, + index, + derivationType: stxDerivationType, + }), + }; + } + return { + ...existingAccount, + username, + }; + }) + ); const meta: Record = {}; if (legacyWalletConfig.hideWarningForReusingIdentity) { diff --git a/packages/wallet-sdk/src/usernames.ts b/packages/wallet-sdk/src/usernames.ts new file mode 100644 index 00000000..ae411d61 --- /dev/null +++ b/packages/wallet-sdk/src/usernames.ts @@ -0,0 +1,18 @@ +import { fetchPrivate } from '@stacks/common'; +import { StacksNetwork } from '@stacks/network'; + +export const fetchFirstName = async ( + address: string, + network: StacksNetwork +): Promise => { + try { + const namesResponse = await fetchPrivate( + `${network.bnsLookupUrl}/v1/addresses/stacks/${address}` + ); + const namesJson = await namesResponse.json(); + if ((namesJson.names.length || 0) > 0) { + return namesJson.names[0]; + } + } catch (e) {} + return undefined; +}; diff --git a/packages/wallet-sdk/tests/derive.test.ts b/packages/wallet-sdk/tests/derive.test.ts index a2efcbed..07521003 100644 --- a/packages/wallet-sdk/tests/derive.test.ts +++ b/packages/wallet-sdk/tests/derive.test.ts @@ -3,33 +3,150 @@ import { deriveAccount, getStxAddress, deriveLegacyConfigPrivateKey, + DerivationType, + selectStxDerivation, } from '../src'; import { mnemonicToSeed } from 'bip39'; import { fromBase58, fromSeed } from 'bip32'; import { TransactionVersion } from '@stacks/transactions'; +import { StacksMainnet } from '@stacks/network'; +import fetchMock from 'jest-fetch-mock'; -test('keys are serialized, and can be deserialized properly', async () => { - const secretKey = - '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 rootPrivateKey = await mnemonicToSeed(secretKey); +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 = fromSeed(rootPrivateKey); const derived = await deriveWalletKeys(rootNode1); const rootNode = fromBase58(derived.rootKey); - const account = deriveAccount({ rootNode, index: 0, salt: derived.salt }); + const account = deriveAccount({ rootNode, index: 0, salt: derived.salt, stxDerivationType: DerivationType.Wallet }); expect(getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet })).toEqual( - 'SP384CVPNDTYA0E92TKJZQTYXQHNZSWGCAG7SAPVB' + 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 = fromSeed(rootPrivateKey); + const derived = await deriveWalletKeys(rootNode1); + const rootNode = fromBase58(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 secretKey = - '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 rootPrivateKey = await mnemonicToSeed(secretKey); + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); const rootNode = fromSeed(rootPrivateKey); const legacyKey = deriveLegacyConfigPrivateKey(rootNode); expect(legacyKey).toEqual('767b51d866d068b02ce126afe3737896f4d0c486263d9b932f2822109565a3c6'); }); + + +test('derive derivation path without username', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = fromSeed(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 = fromSeed(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 = fromSeed(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 = fromSeed(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 = fromSeed(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); +}) + + + +test('derive derivation path with new username owned by address of data derivation path', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = fromSeed(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); +}) + + +test('derive derivation path with username and without network', async () => { + const rootPrivateKey = await mnemonicToSeed(SECRET_KEY); + const rootNode = fromSeed(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 = fromSeed(rootPrivateKey); + + const { username, stxDerivationType } = await selectStxDerivation({ username: undefined, rootNode, index: 0 }); + expect(username).toEqual(undefined); + expect(stxDerivationType).toEqual(DerivationType.Wallet); +}) diff --git a/packages/wallet-sdk/tests/models/profile.test.ts b/packages/wallet-sdk/tests/models/profile.test.ts new file mode 100644 index 00000000..4c5b6271 --- /dev/null +++ b/packages/wallet-sdk/tests/models/profile.test.ts @@ -0,0 +1,48 @@ +import { getPublicKeyFromPrivate } from "@stacks/encryption"; +import { makeRandomPrivKey, privateKeyToString } from "@stacks/transactions"; +import { TokenVerifier } from "jsontokens"; +import { DEFAULT_PROFILE, signProfileForUpload } from "../../src/models/profile"; +import { mockAccount } from "../mocks"; + +describe(signProfileForUpload, () => { + test('sign with the stx private key', () => { + const account = mockAccount; + const profile = DEFAULT_PROFILE + + const signedProfileString = signProfileForUpload({profile, account}); + const signedProfileToken = JSON.parse(signedProfileString)[0] + + const tokenVerifierData = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.dataPrivateKey.slice(0,64))); + expect(tokenVerifierData.verify(signedProfileToken.token)).toEqual(false); + const tokenVerifierStx = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.stxPrivateKey.slice(0,64))); + expect(tokenVerifierStx.verify(signedProfileToken.token)).toEqual(true); + }); + + + test('sign with the data private key', () => { + const account = mockAccount; + account.stxPrivateKey = account.dataPrivateKey; + const profile = DEFAULT_PROFILE + + const signedProfileString = signProfileForUpload({profile, account}); + const signedProfileToken = JSON.parse(signedProfileString)[0] + const tokenVerifierData = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.dataPrivateKey.slice(0,64))); + expect(tokenVerifierData.verify(signedProfileToken.token)).toEqual(true); + const tokenVerifierStx = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.stxPrivateKey.slice(0,64))); + expect(tokenVerifierStx.verify(signedProfileToken.token)).toEqual(true); + }); + + + test('sign with unknown private key', () => { + const account = mockAccount; + account.stxPrivateKey = privateKeyToString(makeRandomPrivKey()); + const profile = DEFAULT_PROFILE + + const signedProfileString = signProfileForUpload({profile, account}); + const signedProfileToken = JSON.parse(signedProfileString)[0] + const tokenVerifierData = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.dataPrivateKey.slice(0,64))); + expect(tokenVerifierData.verify(signedProfileToken.token)).toEqual(false); + const tokenVerifierStx = new TokenVerifier('ES256k', getPublicKeyFromPrivate(account.stxPrivateKey.slice(0,64))); + expect(tokenVerifierStx.verify(signedProfileToken.token)).toEqual(true); + }); +}); diff --git a/packages/wallet-sdk/tests/models/wallet.test.ts b/packages/wallet-sdk/tests/models/wallet.test.ts new file mode 100644 index 00000000..296ca43e --- /dev/null +++ b/packages/wallet-sdk/tests/models/wallet.test.ts @@ -0,0 +1,55 @@ +import { StacksMainnet } from '@stacks/network'; +import fetchMock from 'jest-fetch-mock'; + +import { generateWallet, restoreWalletAccounts } from "../../src"; +import { mockGaiaHubInfo } from "../mocks"; + +beforeEach(() => { + fetchMock.resetMocks(); +}); + +test("restore wallet with username", async () => { + const secretKey = + '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 baseWallet = await generateWallet({ secretKey, password: 'password' }); + const ownerPrivateKey = baseWallet.accounts[0].dataPrivateKey.slice(0, 64) + + fetchMock + .once(mockGaiaHubInfo) + .once(JSON.stringify("no found"), {status: 404}) // TODO mock fetch legacy wallet config + .once(JSON.stringify({address: "SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K"})) + .once(JSON.stringify("ok")); // updateWalletConfig + + const wallet = await restoreWalletAccounts({ + wallet: baseWallet, gaiaHubUrl: "https://hub.gaia.com", + network: new StacksMainnet() + }) + expect(wallet?.accounts[0]?.username).toEqual("public_profile_for_testing.id.blockstack") + expect(wallet?.accounts[0]?.stxPrivateKey.slice(0,64)).toEqual(ownerPrivateKey) +}); + + +test("restore wallet with username not owned by derived address", async () => { + const secretKey = + '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 baseWallet = await generateWallet({ secretKey, password: 'password' }); + + fetchMock + .once(mockGaiaHubInfo) + .once(JSON.stringify("no found"), {status: 404}) // TODO mock fetch legacy wallet config + .once(JSON.stringify({address: "SP000000000000000000002Q6VF78"})) + .once(JSON.stringify("ok")); // updateWalletConfig + const error = await restoreWalletAccounts({ + wallet: baseWallet, gaiaHubUrl: "https://hub.gaia.com", + network: new StacksMainnet() + }).catch((e:string) => { + return e + }) + expect(error).toEqual("Username public_profile_for_testing.id.blockstack is owned by unknown private key") +});