Fix/1105 to handle usernames registered with wallet key (#1117)

* lookup username before signing profile

* add usernameOwnerAddress to Account

* remove unused imports

* add test for usernames owned by third private key

* use derivation type for stx key derivation

* lookup usernames during restoring wallet accounts

* fix name lookup, refactor derivation path selection, add docs

* add test for unknown username owners

* make network optional

* set stxPrivateKey on existing accounts

* add test for stx private key after recovery
This commit is contained in:
Friedger Müffke
2021-12-14 14:24:42 +01:00
committed by GitHub
parent 02e70000b2
commit 3f8fa11bde
11 changed files with 506 additions and 48 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<DerivationType> => {
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);

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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<Wallet> {
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<string, boolean> = {};
if (legacyWalletConfig.hideWarningForReusingIdentity) {

View File

@@ -0,0 +1,18 @@
import { fetchPrivate } from '@stacks/common';
import { StacksNetwork } from '@stacks/network';
export const fetchFirstName = async (
address: string,
network: StacksNetwork
): Promise<string | undefined> => {
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;
};

View File

@@ -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);
})

View File

@@ -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);
});
});

View File

@@ -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")
});