mirror of
https://github.com/alexgo-io/stacks.js.git
synced 2026-01-12 22:52:34 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
18
packages/wallet-sdk/src/usernames.ts
Normal file
18
packages/wallet-sdk/src/usernames.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
48
packages/wallet-sdk/tests/models/profile.test.ts
Normal file
48
packages/wallet-sdk/tests/models/profile.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
55
packages/wallet-sdk/tests/models/wallet.test.ts
Normal file
55
packages/wallet-sdk/tests/models/wallet.test.ts
Normal 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")
|
||||
});
|
||||
Reference in New Issue
Block a user