mirror of
https://github.com/alexgo-io/stacks.js.git
synced 2026-01-12 17:52:41 +08:00
feat: cli add custom derivation path option
This commit is contained in:
@@ -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 ' +
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
80
packages/cli/tests/derivation-path/keychain.ts
Normal file
80
packages/cli/tests/derivation-path/keychain.ts
Normal 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
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
32
packages/cli/tests/derivation-path/wallet.key.info.ts
Normal file
32
packages/cli/tests/derivation-path/wallet.key.info.ts
Normal 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 };
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user