From 2167fd5c747c9fab0eda705d654fc993bab25303 Mon Sep 17 00:00:00 2001 From: Wizz Wallet <153743376+wizz-wallet-dev@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:28:11 +0800 Subject: [PATCH] Support `--passphrase` with `wallet-init` (#9) --- lib/cli.ts | 24 ++++++++-- lib/commands/wallet-init-command.ts | 20 ++++++-- lib/commands/wallet-phrase-decode-command.ts | 6 +-- lib/index.ts | 10 ++-- lib/utils/address-helpers.ts | 6 ++- lib/utils/address-keypair-path.ts | 8 +++- lib/utils/create-key-pair.ts | 49 +++++++++++++------- lib/utils/decode-mnemonic-phrase.ts | 6 +-- lib/utils/validate-wallet-storage.ts | 18 ++----- 9 files changed, 91 insertions(+), 56 deletions(-) diff --git a/lib/cli.ts b/lib/cli.ts index d5d15b2..406a2c9 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -6,7 +6,13 @@ import { ElectrumApi } from './api/electrum-api'; import { validateCliInputs } from './utils/validate-cli-inputs'; import { IValidatedWalletInfo, IWalletRecord, validateWalletStorage } from './utils/validate-wallet-storage'; import * as qrcode from 'qrcode-terminal'; -import { detectAddressTypeToScripthash, detectAddressTypeToScripthash2, detectScriptToAddressType, performAddressAliasReplacement } from './utils/address-helpers'; +import { + defaultDerivedPath, + detectAddressTypeToScripthash, + detectAddressTypeToScripthash2, + detectScriptToAddressType, + performAddressAliasReplacement +} from './utils/address-helpers'; import { AtomicalsGetFetchType } from './commands/command.interface'; import { fileReader, jsonFileReader, jsonFileWriter } from './utils/file-utils'; import * as cbor from 'borc'; @@ -233,10 +239,12 @@ program.command('wallet-create') program.command('wallet-decode') .description('Decode secret mnemonic phrase to display derive address and key at provided path') .argument('', 'string') - .option('-p, --path ', 'Derivation path to use', `m/44'/0'/0'/0/0`) + .option('-p, --path ', 'Derivation path to use', defaultDerivedPath) + .option('--passphrase ', 'Passphrase for the wallet') .action(async (phrase, options) => { let path = options.path; - const result = await Atomicals.walletPhraseDecode(phrase, path); + let passphrase = options.passphrase; + const result = await Atomicals.walletPhraseDecode(phrase, path, passphrase); console.log('Provided mnemonic phrase:'); console.log(`phrase: ${result.data.phrase}`); console.log(`Requested Derivation Path: ${path}`); @@ -250,11 +258,17 @@ program.command('wallet-decode') program.command('wallet-init') .description('Initializes a new wallet at wallet.json') .option('--phrase ', 'Provide a wallet phrase') - .option('--path ', 'Provide a path base', `m/86'/0'/0'`) + .option('--path ', 'Provide a path base', defaultDerivedPath.substring(0, 11)) + .option('--passphrase ', 'Provide a passphrase for the wallet') .option('--n ', 'Provider number of alias') .action(async (options) => { try { - const result = await Atomicals.walletInit(options.phrase, options.path, options.n ? parseInt(options.n, 10) : undefined); + const result = await Atomicals.walletInit( + options.phrase, + options.path, + options.passphrase, + options.n ? parseInt(options.n, 10) : undefined + ); console.log('Wallet created at wallet.json'); console.log(`phrase: ${result.data.phrase}`); console.log(`Primary address (P2TR): ${result.data.primary.address}`); diff --git a/lib/commands/wallet-init-command.ts b/lib/commands/wallet-init-command.ts index 40195d7..447aba3 100644 --- a/lib/commands/wallet-init-command.ts +++ b/lib/commands/wallet-init-command.ts @@ -8,21 +8,35 @@ import * as fs from 'fs'; const walletPath = walletPathResolver(); export class WalletInitCommand implements CommandInterface { - constructor(private phrase: string | undefined, private path: string, private n?: number) { - + constructor( + private phrase: string | undefined, + private path: string, + private passphrase?: string, + private n?: number + ) { } + async run(): Promise { if (await this.walletExists()) { throw "wallet.json exists, please remove it first to initialize another wallet. You may also use 'wallet-create' command to generate a new wallet." } - const { wallet, imported } = await createPrimaryAndFundingImportedKeyPairs(this.phrase, this.path, this.n); + const { + wallet, + imported + } = await createPrimaryAndFundingImportedKeyPairs( + this.phrase, + this.path, + this.passphrase, + this.n + ); const walletDir = `wallets/`; if (!fs.existsSync(walletDir)) { fs.mkdirSync(walletDir); } const created = { phrase: wallet.phrase, + passphrase: wallet.passphrase, primary: { address: wallet.primary.address, path: wallet.primary.path, diff --git a/lib/commands/wallet-phrase-decode-command.ts b/lib/commands/wallet-phrase-decode-command.ts index 298f4f4..0bcfdc2 100644 --- a/lib/commands/wallet-phrase-decode-command.ts +++ b/lib/commands/wallet-phrase-decode-command.ts @@ -3,13 +3,13 @@ import { CommandInterface } from "./command.interface"; import { decodeMnemonicPhrase } from "../utils/decode-mnemonic-phrase"; export class WalletPhraseDecodeCommand implements CommandInterface { - constructor(private phrase, private path) { + constructor(private phrase: string, private path: string, private passphrase?: string) { } async run(): Promise { - const wallet = await decodeMnemonicPhrase(this.phrase, this.path); + const wallet = await decodeMnemonicPhrase(this.phrase, this.path, this.passphrase); return { success: true, data: wallet } } -} \ No newline at end of file +} diff --git a/lib/index.ts b/lib/index.ts index 0af8dae..f5cfce6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,7 +2,7 @@ import { APIInterface, BaseRequestOptions } from "./interfaces/api.interface"; const bitcoin = require('bitcoinjs-lib'); import * as ecc from 'tiny-secp256k1'; bitcoin.initEccLib(ecc); -import * as cbor from 'borc'; + export { ElectrumApiMock } from "./api/electrum-api-mock"; import { ConfigurationInterface } from "./interfaces/configuration.interface"; import { ElectrumApiInterface } from "./api/electrum-api.interface"; @@ -248,9 +248,9 @@ export class Atomicals implements APIInterface { } } - static async walletPhraseDecode(phrase: string, path: string): Promise { + static async walletPhraseDecode(phrase: string, path: string, passphrase?: string): Promise { try { - const command: CommandInterface = new WalletPhraseDecodeCommand(phrase, path); + const command: CommandInterface = new WalletPhraseDecodeCommand(phrase, path, passphrase); return command.run(); } catch (error: any) { return { @@ -261,9 +261,9 @@ export class Atomicals implements APIInterface { } } - static async walletInit(phrase: string | undefined, path: string, n?: number): Promise { + static async walletInit(phrase: string | undefined, path: string, passphrase?: string, n?: number): Promise { try { - const command: CommandInterface = new WalletInitCommand(phrase, path, n); + const command: CommandInterface = new WalletInitCommand(phrase, path, passphrase, n); return command.run(); } catch (error: any) { return { diff --git a/lib/utils/address-helpers.ts b/lib/utils/address-helpers.ts index 89af81b..594df3c 100644 --- a/lib/utils/address-helpers.ts +++ b/lib/utils/address-helpers.ts @@ -12,6 +12,8 @@ import { toXOnly } from "./create-key-pair"; import { Network } from "bitcoinjs-lib"; dotenv.config(); +export const defaultDerivedPath = `m/86'/0'/0'/0/0`; + function convertAddressToScripthash(address, network) { const output = bitcoin.address.toOutputScript(address, network); return { @@ -103,8 +105,8 @@ export function performAddressAliasReplacement(walletInfo: IValidatedWalletInfo, /** * Whether the atomical for the mint is owned by the provided wallet or not * @param ownerRecord The proposed wallet that owns the atomical - * @param atomical - * @returns + * @param atomical + * @returns */ export function IsAtomicalOwnedByWalletRecord(address: string, atomical: AtomicalStatus): IInputUtxoPartial | null { if (!(atomical.location_info_obj as any)) { diff --git a/lib/utils/address-keypair-path.ts b/lib/utils/address-keypair-path.ts index ac52d76..eebf999 100644 --- a/lib/utils/address-keypair-path.ts +++ b/lib/utils/address-keypair-path.ts @@ -16,8 +16,12 @@ export interface ExtendTaprootAddressScriptKeyPairInfo { path: string; } -export const getExtendTaprootAddressKeypairPath = async (phrase: string, path: string): Promise => { - const seed = await bip39.mnemonicToSeed(phrase); +export const getExtendTaprootAddressKeypairPath = async ( + phrase: string, + path: string, + passphrase?: string, +): Promise => { + const seed = await bip39.mnemonicToSeed(phrase, passphrase); const rootKey = bip32.fromSeed(seed); const childNode = rootKey.derivePath(path); const childNodeXOnlyPubkey = childNode.publicKey.slice(1, 33); diff --git a/lib/utils/create-key-pair.ts b/lib/utils/create-key-pair.ts index f6e6b01..f75b881 100644 --- a/lib/utils/create-key-pair.ts +++ b/lib/utils/create-key-pair.ts @@ -1,10 +1,11 @@ - const bitcoin = require('bitcoinjs-lib'); -import ECPairFactory from 'ecpair'; import * as ecc from 'tiny-secp256k1'; -import { createMnemonicPhrase } from './create-mnemonic-phrase'; bitcoin.initEccLib(ecc); +import ECPairFactory from 'ecpair'; +import { defaultDerivedPath } from './address-helpers'; +import { createMnemonicPhrase } from './create-mnemonic-phrase'; + const ECPair = ECPairFactory(ecc); import BIP32Factory from 'bip32'; import { NETWORK } from '../commands/command-helpers'; @@ -24,12 +25,16 @@ export interface KeyPair { privateKey?: string } -export const createKeyPair = async (phrase: string = '', path = `m/44'/0'/0'/0/0`) : Promise => { +export const createKeyPair = async ( + phrase: string = '', + path = defaultDerivedPath, + passphrase: string = '' +) : Promise => { if (!phrase || phrase === '') { - const phraseResult = await createMnemonicPhrase(); + const phraseResult = createMnemonicPhrase(); phrase = phraseResult.phrase; } - const seed = await bip39.mnemonicToSeed(phrase); + const seed = await bip39.mnemonicToSeed(phrase, passphrase); const rootKey = bip32.fromSeed(seed); const childNodePrimary = rootKey.derivePath(path); // const p2pkh = bitcoin.payments.p2pkh({ pubkey: childNodePrimary.publicKey }); @@ -74,13 +79,16 @@ export interface WalletRequestDefinition { path?: string | undefined } -export const createPrimaryAndFundingImportedKeyPairs = async (phrase?: string | undefined, path?: string | undefined, n?: number) => { - let phraseResult: any = phrase; - if (!phraseResult) { - phraseResult = await createMnemonicPhrase(); - phraseResult = phraseResult.phrase; +export const createPrimaryAndFundingImportedKeyPairs = async ( + phrase?: string | undefined, + path?: string | undefined, + passphrase?: string | undefined, + n?: number +) => { + if (!phrase) { + phrase = createMnemonicPhrase().phrase; } - let pathUsed = `m/44'/0'/0'`; + let pathUsed = defaultDerivedPath.substring(0, 11); if (path) { pathUsed = path; } @@ -88,23 +96,28 @@ export const createPrimaryAndFundingImportedKeyPairs = async (phrase?: string | if (n) { for (let i = 2; i < n + 2; i++) { - imported[i+''] = await createKeyPair(phraseResult, `${pathUsed}/0/` + i) + imported[i+''] = await createKeyPair(phrase, `${pathUsed}/0/` + i, passphrase) } } return { wallet: { - phrase: phraseResult, - primary: await createKeyPair(phraseResult, `${pathUsed}/0/0`), - funding: await createKeyPair(phraseResult, `${pathUsed}/1/0`) + phrase, + passphrase, + primary: await createKeyPair(phrase, `${pathUsed}/0/0`, passphrase), + funding: await createKeyPair(phrase, `${pathUsed}/1/0`, passphrase) }, imported } } -export const createNKeyPairs = async (phrase, n = 1) => { +export const createNKeyPairs = async ( + phrase: string | undefined, + passphrase: string | undefined, + n = 1 +) => { const keypairs: any = []; for (let i = 0; i < n; i++) { - keypairs.push(await createKeyPair(phrase, `m/44'/0'/0'/0/${i}`)); + keypairs.push(await createKeyPair(phrase, `${defaultDerivedPath.substring(0, 13)}/${i}`, passphrase)); } return { phrase, diff --git a/lib/utils/decode-mnemonic-phrase.ts b/lib/utils/decode-mnemonic-phrase.ts index 80c8d78..1c96d97 100644 --- a/lib/utils/decode-mnemonic-phrase.ts +++ b/lib/utils/decode-mnemonic-phrase.ts @@ -13,11 +13,11 @@ const toXOnly = (publicKey) => { } const bip39 = require('bip39'); -export const decodeMnemonicPhrase = async (phrase: string, path: string) => { +export const decodeMnemonicPhrase = async (phrase: string, path: string, passphrase?: string) => { if (!bip39.validateMnemonic(phrase)) { throw new Error("Invalid mnemonic phrase provided!"); } - const seed = await bip39.mnemonicToSeed(phrase); + const seed = await bip39.mnemonicToSeed(phrase, passphrase); const rootKey = bip32.fromSeed(seed); const childNode = rootKey.derivePath(path); // const { address } = bitcoin.payments.p2pkh({ pubkey: childNode.publicKey }); @@ -46,4 +46,4 @@ export const decodeMnemonicPhrase = async (phrase: string, path: string) => { privateKey: childNode.privateKey?.toString('hex'), } -} \ No newline at end of file +} diff --git a/lib/utils/validate-wallet-storage.ts b/lib/utils/validate-wallet-storage.ts index 5afe789..3c873e5 100644 --- a/lib/utils/validate-wallet-storage.ts +++ b/lib/utils/validate-wallet-storage.ts @@ -52,17 +52,6 @@ export const validateWalletStorage = async (): Promise => throw new Error(`Wallet needs a funding address`); } - // Validate paths - /*if (wallet.primary.path !== `m/44'/0'/0'/0/0`) { - console.log(`Primary path must be m/44'/0'/0'/0/0`); - throw new Error(`Primary path must be m/44'/0'/0'/0/0`); - } - - if (wallet.funding.path !== `m/44'/0'/0'/1/0`) { - console.log(`Funding path must be m/44'/0'/0'/1/0`); - throw new Error(`Funding path must be m/44'/0'/0'/1/0`); - }*/ - // Validate WIF if (!wallet.primary.WIF) { console.log(`Primary WIF not set`); @@ -83,9 +72,9 @@ export const validateWalletStorage = async (): Promise => throw new Error(`Funding address not set`); } - const seed = await bip39.mnemonicToSeed(wallet.phrase); + const seed = await bip39.mnemonicToSeed(wallet.phrase, wallet.passphrase); const rootKey = bip32.fromSeed(seed); - const derivePathPrimary = wallet.primary.path; //`m/44'/0'/0'/0/0`; + const derivePathPrimary = wallet.primary.path; const childNodePrimary = rootKey.derivePath(derivePathPrimary); @@ -97,7 +86,7 @@ export const validateWalletStorage = async (): Promise => if (!p2trPrimary.address || !p2trPrimary.output) { throw "error creating p2tr primary" } - const derivePathFunding = wallet.funding.path; //`m/44'/0'/0'/1/0`; + const derivePathFunding = wallet.funding.path; const childNodeFunding = rootKey.derivePath(derivePathFunding); const childNodeXOnlyPubkeyFunding = toXOnly(childNodeFunding.publicKey); const p2trFunding = bitcoin.payments.p2tr({ @@ -107,7 +96,6 @@ export const validateWalletStorage = async (): Promise => if (!p2trFunding.address || !p2trFunding.output) { throw "error creating p2tr funding" } - // const derivePathFunding = `m/44'/0'/0'/1/0`; //const childNodeFunding = rootKey.derivePath(derivePathFunding); // const { address } = bitcoin.payments.p2pkh({ pubkey: childNode.publicKey }); // const wif = childNodePrimary.toWIF();