feat: cli add custom derivation path option

This commit is contained in:
AhsanJavaid
2021-10-14 20:41:25 +05:00
committed by kyranjamie
parent bb1db2bb57
commit 9ba53be4dd
8 changed files with 184 additions and 7 deletions

View File

@@ -1362,9 +1362,14 @@ export const CLI_ARGS = {
type: 'string',
realtype: '24_words_or_ciphertext',
},
{
name: 'derivation_path',
type: 'string',
realtype: 'custom_derivation_path_string',
},
],
minItems: 1,
maxItems: 1,
maxItems: 2,
help:
'Get the payment private key from a 24-word backup phrase used by the Stacks wallet. If you provide an ' +
'encrypted backup phrase, you will be asked for your password to decrypt it. This command ' +
@@ -1474,9 +1479,14 @@ export const CLI_ARGS = {
type: 'string',
realtype: '12_words_or_ciphertext',
},
{
name: 'derivation_path',
type: 'string',
realtype: 'custom_derivation_path_string',
},
],
minItems: 0,
maxItems: 1,
maxItems: 2,
help:
'Generate the owner and payment private keys, optionally from a given 12-word ' +
'backup phrase. If no backup phrase is given, a new one will be generated. If you provide ' +

View File

@@ -331,8 +331,9 @@ async function getPaymentKey(network: CLINetworkAdapter, args: string[]): Promis
*/
async function getStacksWalletKey(network: CLINetworkAdapter, args: string[]): Promise<string> {
const mnemonic = await getBackupPhrase(args[0]);
const derivationPath: string | undefined = args[1] || undefined;
// keep the return value consistent with getOwnerKeys
const keyObj = await getStacksWalletKeyInfo(network, mnemonic);
const keyObj = await getStacksWalletKeyInfo(network, mnemonic, derivationPath);
const keyInfo: StacksKeyInfoType[] = [];
keyInfo.push(keyObj);
return JSONStringify(keyInfo);
@@ -355,7 +356,8 @@ async function makeKeychain(network: CLINetworkAdapter, args: string[]): Promise
);
}
const stacksKeyInfo = await getStacksWalletKeyInfo(network, mnemonic);
const derivationPath: string | undefined = args[1] || undefined;
const stacksKeyInfo = await getStacksWalletKeyInfo(network, mnemonic, derivationPath);
return JSONStringify({
mnemonic: mnemonic,
keyInfo: stacksKeyInfo,
@@ -1965,5 +1967,7 @@ export const testables =
? {
addressConvert,
contractFunctionCall,
makeKeychain,
getStacksWalletKey,
}
: undefined;

View File

@@ -134,11 +134,12 @@ export async function getPaymentKeyInfo(
*/
export async function getStacksWalletKeyInfo(
network: CLINetworkAdapter,
mnemonic: string
mnemonic: string,
derivationPath = DERIVATION_PATH
): Promise<StacksKeyInfoType> {
const seed = await bip39.mnemonicToSeed(mnemonic);
const master = bip32.fromSeed(seed);
const child = master.derivePath("m/44'/5757'/0'/0/0"); // taken from stacks-wallet. See https://github.com/blockstack/stacks-wallet
const child = master.derivePath(derivationPath); // taken from stacks-wallet. See https://github.com/blockstack/stacks-wallet
const ecPair = bitcoin.ECPair.fromPrivateKey(child.privateKey!);
const privkey = blockstack.ecPairToHexString(ecPair);
const wif = child.toWIF();

View File

@@ -675,6 +675,9 @@ export async function getBackupPhrase(
if (!process.stdin.isTTY && !password) {
// password must be given
reject(new Error('Password argument required in non-interactive mode'));
} else if (process.env.password) {
// Do not prompt password for unit tests
resolve(process.env.password);
} else {
// prompt password
getpass('Enter password: ', p => {

View File

@@ -8,11 +8,12 @@ import {ClarityAbi} from '@stacks/transactions';
import {readFileSync} from 'fs';
import path from 'path';
import fetchMock from 'jest-fetch-mock';
import { makekeychainTests, keyInfoTests, MakeKeychainResult, WalletKeyInfoResult } from './derivation-path/keychain';
const TEST_ABI: ClarityAbi = JSON.parse(readFileSync(path.join(__dirname, './abi/test-abi.json')).toString());
jest.mock('inquirer');
const { addressConvert, contractFunctionCall } = testables as any;
const { addressConvert, contractFunctionCall, makeKeychain, getStacksWalletKey } = testables as any;
const mainnetNetwork = new CLINetworkAdapter(
getNetwork({} as CLI_CONFIG_TYPE, false),
@@ -179,3 +180,39 @@ describe('Contract function call', () => {
expect(result.txid).toEqual(txid);
});
});
describe('Keychain custom derivation path', () => {
test.each(makekeychainTests)('Make keychain using custom derivation path %#', async (derivationPath: string, keyChainResult: MakeKeychainResult) => {
const encrypted = 'vim+XrRNSm+SqSn0MyWNEi/e+UK5kX8WGCLE/sevT6srZG+quzpp911sWP0CcvsExCH1M4DgOfOldMitLdkq1b6rApDwtAcOWdAqiaBk37M=';
const args = [encrypted, derivationPath];
// Mock TTY
process.stdin.isTTY = true;
process.env.password = 'supersecret';
const keyChain = await makeKeychain(testnetNetwork, args);
const result = JSON.parse(keyChain);
expect(result).toEqual(keyChainResult);
// Unmock TTY
process.stdin.isTTY = false;
process.env.password = undefined;
});
test.each(keyInfoTests)('Make keychain using custom derivation path %#', async (derivationPath: string, walletInfoResult: WalletKeyInfoResult ) => {
const encrypted = 'vim+XrRNSm+SqSn0MyWNEi/e+UK5kX8WGCLE/sevT6srZG+quzpp911sWP0CcvsExCH1M4DgOfOldMitLdkq1b6rApDwtAcOWdAqiaBk37M=';
const args = [encrypted, derivationPath];
// Mock TTY
process.stdin.isTTY = true;
process.env.password = 'supersecret';
const walletKey = await getStacksWalletKey(testnetNetwork, args);
const result = JSON.parse(walletKey);
expect(result).toEqual([
walletInfoResult
]);
// Unmock TTY
process.stdin.isTTY = false;
process.env.password = undefined;
});
});

View File

@@ -0,0 +1,80 @@
export type MakeKeychainResult = {
mnemonic: string,
keyInfo: {
privateKey: string;
address: string;
btcAddress: string;
wif: string;
index: number;
};
};
export type WalletKeyInfoResult = {
privateKey: string;
address: string;
btcAddress: string;
wif: string;
index: number;
};
export const makekeychainTests: Array<[string, MakeKeychainResult]> = [
[
// Derivation Path
"m/44'/5757'/0'/0/0",
// Expected result
{
mnemonic: 'vivid oxygen neutral wheat find thumb cigar wheel board kiwi portion business',
keyInfo: {
privateKey: 'd1124855494c883c5e1df0201be40a835f08ae5fc3a6520224b2239db94a818001',
address: 'ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS',
btcAddress: 'mpeSzfUTBba7qzKNcg8ojNm4GAfwmNPX8X',
wif: 'L4E7pXmqdm8C8TakpX7YDDmFopaQw32Ak6V5BpRFNDJmo7wjGVqc',
index: 0
}
}
],
[
// Derivation Path
"m/888'/0'/0",
// Expected result
{
mnemonic: 'vivid oxygen neutral wheat find thumb cigar wheel board kiwi portion business',
keyInfo: {
privateKey: 'd4d30d4fdaa59e166865b836548015c2780063b82e7b2a364c8a2e32df7139ce01',
address: 'ST1WT20920NVRQ892MS535R7XEMV6KD6M6X2HQPK3',
btcAddress: 'mrc4w3oQZ39Yvkimk9DDJQnHFjv1e336mg',
wif: 'L4MQx6c6ZmoiwFYUHnmt39THRGeQnPmfA2AFobwWmssZJabi3qXm',
index: 0
}
}
]
];
export const keyInfoTests: Array<[string, WalletKeyInfoResult]> = [
[
// Derivation Path
"m/44'/5757'/0'/0/0",
// Expected result
{
privateKey: 'd1124855494c883c5e1df0201be40a835f08ae5fc3a6520224b2239db94a818001',
address: 'ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS',
btcAddress: 'mpeSzfUTBba7qzKNcg8ojNm4GAfwmNPX8X',
wif: 'L4E7pXmqdm8C8TakpX7YDDmFopaQw32Ak6V5BpRFNDJmo7wjGVqc',
index: 0
}
],
[
// Derivation Path
"m/888'/0'/0",
// Expected result
{
privateKey: 'd4d30d4fdaa59e166865b836548015c2780063b82e7b2a364c8a2e32df7139ce01',
address: 'ST1WT20920NVRQ892MS535R7XEMV6KD6M6X2HQPK3',
btcAddress: 'mrc4w3oQZ39Yvkimk9DDJQnHFjv1e336mg',
wif: 'L4MQx6c6ZmoiwFYUHnmt39THRGeQnPmfA2AFobwWmssZJabi3qXm',
index: 0
}
]
];

View File

@@ -0,0 +1,32 @@
import { WalletKeyInfoResult } from './keychain';
export const keyInfoTests: Array<[string, WalletKeyInfoResult]> = [
[
// Derivation path
"m/44'/5757'/0'/0/0",
// Expected result
{
privateKey: '25899fab1b9b95cc2d1692529f00fb788e85664df3d14db1a660f33c5f96d8ab01',
address: 'SP3RBZ4TZ3EK22SZRKGFZYBCKD7WQ5B8FFS0AYVF7',
btcAddress: '1Nwxfx7VoYAg2mEN35dTRw4H7gte8ajFki',
wif: 'KxUgLbeVeFZEUUQpc3ncYn5KFB3WH5MVRv3SJ2g5yPwkrXs3QRaP',
index: 0
}
],
[
// Derivation path
"m/888'/0'/0",
// Expected result
{
privateKey: '0f0936f59a7d55be6bcd1820f798460ac4b3aa50f26c8fa76beb82a19af5110901',
address: 'SPGJAPK47Z9XY7E7BCEJFAEX9C7WGB0YB74A54MA',
btcAddress: '142G3fnfn1WZPtnYLYiVGt8aU55GZYxeVP',
wif: 'KwiwQgTK2412XSdBfcRWJ4xQFbevUHCwGnRCuvjeHjSqceNwS1wW',
index: 0
}
]
];
export { WalletKeyInfoResult };

View File

@@ -1,6 +1,7 @@
import { getStacksWalletKeyInfo, getOwnerKeyInfo, findIdentityIndex } from '../src/keys';
import { getNetwork, CLINetworkAdapter, CLI_NETWORK_OPTS } from '../src/network';
import { CLI_CONFIG_TYPE } from '../src/argparse';
import { keyInfoTests, WalletKeyInfoResult } from './derivation-path/wallet.key.info';
import * as fixtures from './fixtures/keys.fixture';
@@ -23,6 +24,15 @@ test('getStacksWalletKeyInfo', async () => {
});
});
describe('getStacksWalletKeyInfo custom derivation path', () => {
test.each(keyInfoTests)('%#', async (derivationPath: string, keyInfoResult: WalletKeyInfoResult) => {
const mnemonic = 'apart spin rich leader siren foil dish sausage fee pipe ethics bundle';
const info = await getStacksWalletKeyInfo(mainnetNetwork, mnemonic, derivationPath);
expect(info).toEqual(keyInfoResult);
});
});
describe('getOwnerKeyInfo', () => {
test.each(fixtures.getOwnerKeyInfo)('%#', async (mnemonic, index, version, result) => {
const info = await getOwnerKeyInfo(mainnetNetwork, mnemonic, index, version);