diff --git a/README.md b/README.md index 7387d14..8a2c644 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Here are the parameters that can be used to create a `new Descriptor`: ```javascript constructor({ expression, // The descriptor string in ASCII format. It may include a "*" - // to denote an arbitrary index. + // to denote an arbitrary index (aka ranged descriptors). index, // The descriptor's index in the case of a range descriptor // (must be an integer >= 0). checksumRequired = false // Optional flag indicating if the descriptor is @@ -109,27 +109,35 @@ constructor({ }); ``` -The `Descriptor` class offers various helpful methods, including `getAddress()`, which returns the address associated with the descriptor, `getScriptPubKey()`, which returns the scriptPubKey for the descriptor, `expand()`, which decomposes a descriptor into its elemental parts, `updatePsbt()` and `finalizePsbt()`. +The `Descriptor` class offers various helpful methods, including `getAddress()`, which returns the address associated with the descriptor, `getScriptPubKey()`, which returns the scriptPubKey for the descriptor, `expand()`, which decomposes a descriptor into its elemental parts, `updatePsbtAsInput()`, `updatePsbtAsOutput()` and `finalizePsbtInput()`. -The `updatePsbt()` method is an essential part of the library, responsible for adding an input to the PSBT corresponding to the UTXO (unspent transaction output) described by the descriptor. Additionally, when the descriptor expresses an absolute time-spending condition, such as "This UTXO can only be spent after block N," `updatePsbt()` adds timelock information to the PSBT. +The `updatePsbtAsInput()` method is an essential part of the library, responsible for adding an input to the PSBT corresponding to the UTXO (unspent transaction output) described by the descriptor. Additionally, when the descriptor expresses an absolute time-spending condition, such as "This UTXO can only be spent after block N," `updatePsbtAsInput()` adds timelock information to the PSBT. -To call `updatePsbt()`, use the following syntax: +To call `updatePsbtAsInput()`, use the following syntax: ```javascript -const inputIndex = descriptor.updatePsbt({ psbt, txHex, vout }); +const inputIndex = descriptor.updatePsbtAsInput({ psbt, txHex, vout }); ``` Here, `psbt` is an instance of a [bitconjs-lib Psbt class](https://github.com/bitcoinjs/bitcoinjs-lib), `txHex` is the hex string that serializes the previous transaction, and `vout` is an integer corresponding to the output index of the descriptor in the previous transaction. The method returns a number that corresponds to the input number that this descriptor will take in the `psbt`. -The `finalizePsbt()` method is the final step in adding the unlocking script (scriptWitness or scriptSig) that satisfies the spending condition to the transaction, effectively finalizing the Psbt. It should be called after all necessary signing operations have been completed. The syntax for calling this method is as follows: +Conversely, `updatePsbtAsOutput` allows you to add an output to a PSBT. For instance, to configure a `psbt` that sends `10,000` sats to the SegWit address `bc1qgw6xanldsz959z45y4dszehx4xkuzf7nfhya8x`: ```javascript -descriptor.finalizePsbt({ index, psbt }); +const desc = + new Descriptor({ expression: `addr(bc1qgw6xanldsz959z45y4dszehx4xkuzf7nfhya8x)` }); +desc.updatePsbtAsOutput({ psbt, value: 10000 }); ``` -Here, `index` is the `inputIndex` obtained from the `updatePsbt()` method and `psbt` is an instance of a bitcoinjs-lib `Psbt` object. +The `finalizePsbtInput()` method is the final step in adding the unlocking script (`scriptWitness` or `scriptSig`) that satisfies the spending condition to the transaction, effectively finalizing the Psbt. Note that signatures are part of the `scriptSig` / `scriptWitness`. Thus, this method should only be called after all necessary signing operations have been completed. The syntax for calling this method is as follows: -For further information on using the Descriptor class, refer to the [comprehensive guides](https://bitcoinerlab.com/guides) that offer explanations and playgrounds to help learn the module. Additionally, a [Stack Exchange answer](https://bitcoin.stackexchange.com/a/118036/89665) provides a focused explanation on the constructor, specifically the `signersPubKeys` parameter, and the usage of `updatePsbt`, `finalizePsbt`, `getAddress`, and `getScriptPubKey`. +```javascript +descriptor.finalizePsbtInput({ index, psbt }); +``` + +Here, `index` is the `inputIndex` obtained from the `updatePsbtAsInput()` method and `psbt` is an instance of a bitcoinjs-lib `Psbt` object. + +For further information on using the Descriptor class, refer to the [comprehensive guides](https://bitcoinerlab.com/guides) that offer explanations and playgrounds to help learn the module. Additionally, a [Stack Exchange answer](https://bitcoin.stackexchange.com/a/118036/89665) provides a focused explanation on the constructor, specifically the `signersPubKeys` parameter, and the usage of `updatePsbtAsInput`, `finalizePsbtInput`, `getAddress`, and `getScriptPubKey`. #### Tip: Parsing descriptors without instantiating a class @@ -327,7 +335,7 @@ Finally, `ledgerState` is an object used to store information related to Ledger For more information, refer to the following resources: - [Guides](https://bitcoinerlab.com/guides): Comprehensive explanations and playgrounds to help you learn how to use the module. -- [Stack Exchange answer](https://bitcoin.stackexchange.com/a/118036/89665): Focused explanation on the constructor, specifically the `signersPubKeys` parameter, and the usage of `updatePsbt`, `finalizePsbt`, `getAddress`, and `getScriptPubKey`. +- [Stack Exchange answer](https://bitcoin.stackexchange.com/a/118036/89665): Focused explanation on the constructor, specifically the `signersPubKeys` parameter, and the usage of `updatePsbtAsInput`, `finalizePsbtInput`, `getAddress`, and `getScriptPubKey`. - [Integration tests](https://github.com/bitcoinerlab/descriptors/tree/main/test/integration): Well-commented code examples showcasing the usage of all functions in the module. - API Documentation: Auto-generated documentation from the source code, providing detailed information about the library and its methods. To generate the API documentation locally, follow these commands: diff --git a/package-lock.json b/package-lock.json index 00f638e..79ece81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitcoinerlab/descriptors", - "version": "1.1.1", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitcoinerlab/descriptors", - "version": "1.1.1", + "version": "2.0.0", "license": "MIT", "dependencies": { "@bitcoinerlab/miniscript": "^1.2.1", diff --git a/package.json b/package.json index fe58da0..988ea6c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bitcoinerlab/descriptors", "description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.", "homepage": "https://github.com/bitcoinerlab/descriptors", - "version": "1.1.1", + "version": "2.0.0", "author": "Jose-Luis Landabaso", "license": "MIT", "repository": { diff --git a/src/checksum.ts b/src/checksum.ts index 82ba2f4..6a438d1 100644 --- a/src/checksum.ts +++ b/src/checksum.ts @@ -13,6 +13,11 @@ const PolyMod = (c: bigint, val: bigint): bigint => { }; export const CHECKSUM_CHARSET: string = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + +/** + * Implements the Bitcoin descriptor's checksum algorithm described in + * {@link https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp} + */ export const DescriptorChecksum = (span: string): string => { const INPUT_CHARSET = '0123456789()[],\'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#"\\ '; diff --git a/src/descriptors.ts b/src/descriptors.ts index 279c6cd..56ef797 100644 --- a/src/descriptors.ts +++ b/src/descriptors.ts @@ -20,8 +20,8 @@ import type { TinySecp256k1Interface, Preimage, TimeConstraints, + Expansion, ExpansionMap, - Expand, ParseKeyExpression } from './types'; @@ -53,58 +53,82 @@ function countNonPushOnlyOPs(script: Buffer): number { /* * Returns a bare descriptor without checksum and particularized for a certain * index (if desc was a range descriptor) + * @hidden */ function evaluate({ - expression, + descriptor, checksumRequired, index }: { - expression: string; + descriptor: string; checksumRequired: boolean; index?: number; }): string { - const mChecksum = expression.match(String.raw`(${RE.reChecksum})$`); + if (!descriptor) throw new Error('You must provide a descriptor.'); + + const mChecksum = descriptor.match(String.raw`(${RE.reChecksum})$`); if (mChecksum === null && checksumRequired === true) - throw new Error(`Error: descriptor ${expression} has not checksum`); - //evaluatedExpression: a bare desc without checksum and particularized for a certain + throw new Error(`Error: descriptor ${descriptor} has not checksum`); + //evaluatedDescriptor: a bare desc without checksum and particularized for a certain //index (if desc was a range descriptor) - let evaluatedExpression = expression; + let evaluatedDescriptor = descriptor; if (mChecksum !== null) { const checksum = mChecksum[0].substring(1); //remove the leading # - evaluatedExpression = expression.substring( + evaluatedDescriptor = descriptor.substring( 0, - expression.length - mChecksum[0].length + descriptor.length - mChecksum[0].length ); - if (checksum !== DescriptorChecksum(evaluatedExpression)) { - throw new Error(`Error: invalid descriptor checksum for ${expression}`); + if (checksum !== DescriptorChecksum(evaluatedDescriptor)) { + throw new Error(`Error: invalid descriptor checksum for ${descriptor}`); } } if (index !== undefined) { - const mWildcard = evaluatedExpression.match(/\*/g); + const mWildcard = evaluatedDescriptor.match(/\*/g); if (mWildcard && mWildcard.length > 0) { //From https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md //To prevent a combinatorial explosion of the search space, if more than //one of the multi() key arguments is a BIP32 wildcard path ending in /* or - //*', the multi() expression only matches multisig scripts with the ith + //*', the multi() descriptor only matches multisig scripts with the ith //child key from each wildcard path in lockstep, rather than scripts with //any combination of child keys from each wildcard path. //We extend this reasoning for musig for all cases - evaluatedExpression = evaluatedExpression.replaceAll( + evaluatedDescriptor = evaluatedDescriptor.replaceAll( '*', index.toString() ); } else throw new Error( - `Error: index passed for non-ranged descriptor: ${expression}` + `Error: index passed for non-ranged descriptor: ${descriptor}` ); } - return evaluatedExpression; + return evaluatedDescriptor; } /** - * Builds the functions needed to operate with descriptors using an external elliptic curve (ecc) library. - * @param {Object} ecc - an object containing elliptic curve operations, such as [tiny-secp256k1](https://github.com/bitcoinjs/tiny-secp256k1) or [@bitcoinerlab/secp256k1](https://github.com/bitcoinerlab/secp256k1). + * Constructs the necessary functions and classes for working with descriptors + * using an external elliptic curve (ecc) library. + * + * Notably, it returns the {@link _Internal_.Output | `Output`} class, which + * provides methods to create, sign, and finalize PSBTs based on descriptor + * expressions. + * + * While this Factory function includes the `Descriptor` class, note that + * this class was deprecated in v2.0 in favor of `Output`. For backward + * compatibility, the `Descriptor` class remains, but using `Output` is advised. + * + * The Factory also returns utility methods like `expand` (detailed below) + * and `parseKeyExpression` (see {@link ParseKeyExpression}). + * + * Additionally, for convenience, the function returns `BIP32` and `ECPair`. + * These are {@link https://github.com/bitcoinjs bitcoinjs-lib} classes designed + * for managing {@link https://github.com/bitcoinjs/bip32 | `BIP32`} keys and + * public/private key pairs: + * {@link https://github.com/bitcoinjs/ecpair | `ECPair`}, respectively. + * + * @param {Object} ecc - An object with elliptic curve operations, such as + * [tiny-secp256k1](https://github.com/bitcoinjs/tiny-secp256k1) or + * [@bitcoinerlab/secp256k1](https://github.com/bitcoinerlab/secp256k1). */ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { const BIP32: BIP32API = BIP32Factory(ecc); @@ -133,17 +157,77 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { }; /** - * Takes a descriptor (expression) and expands it to its corresponding Bitcoin script and other relevant details. + * Parses and analyzies a descriptor expression and destructures it into {@link Expansion |its elemental parts}. * * @throws {Error} Throws an error if the descriptor cannot be parsed or does not conform to the expected format. */ - const expand: Expand = ({ + function expand(params: { + /** + * The descriptor expression to be expanded. + */ + descriptor: string; + + /** + * The descriptor index, if ranged. + */ + index?: number; + + /** + * A flag indicating whether the descriptor is required to include a checksum. + * @defaultValue false + */ + checksumRequired?: boolean; + + /** + * The Bitcoin network to use. + * @defaultValue `networks.bitcoin` + */ + network?: Network; + + /** + * Flag to allow miniscript in P2SH. + * @defaultValue false + */ + allowMiniscriptInP2SH?: boolean; + }): Expansion; + + /** + * @deprecated + * @hidden + * To be removed in version 3.0 + */ + function expand(params: { + expression: string; + index?: number; + checksumRequired?: boolean; + network?: Network; + allowMiniscriptInP2SH?: boolean; + }): Expansion; + + /** + * @hidden + * To be removed in v3.0 and replaced by the version with the signature that + * does not accept descriptors + */ + function expand({ + descriptor, expression, index, checksumRequired = false, network = networks.bitcoin, allowMiniscriptInP2SH = false - }) => { + }: { + descriptor?: string; + expression?: string; + index?: number; + checksumRequired?: boolean; + network?: Network; + allowMiniscriptInP2SH?: boolean; + }): Expansion { + if (descriptor && expression) + throw new Error(`expression param has been deprecated`); + descriptor = descriptor || expression; + if (!descriptor) throw new Error(`descriptor not provided`); let expandedExpression: string | undefined; let miniscript: string | undefined; let expansionMap: ExpansionMap | undefined; @@ -152,7 +236,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { let payment: Payment | undefined; let witnessScript: Buffer | undefined; let redeemScript: Buffer | undefined; - const isRanged = expression.indexOf('*') !== -1; + const isRanged = descriptor.indexOf('*') !== -1; if (index !== undefined) if (!Number.isInteger(index) || index < 0) @@ -161,7 +245,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { //Verify and remove checksum (if exists) and //particularize range descriptor for index (if desc is range descriptor) const canonicalExpression = evaluate({ - expression, + descriptor, ...(index !== undefined ? { index } : {}), checksumRequired }); @@ -172,7 +256,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { if (isRanged) throw new Error(`Error: addr() cannot be ranged`); const matchedAddress = canonicalExpression.match(RE.reAddrAnchored)?.[1]; //[1]-> whatever is found addr(->HERE<-) if (!matchedAddress) - throw new Error(`Error: could not get an address in ${expression}`); + throw new Error(`Error: could not get an address in ${descriptor}`); let output; try { output = address.toOutputScript(matchedAddress, network); @@ -205,7 +289,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { if (!keyExpression) throw new Error(`Error: keyExpression could not me extracted`); if (canonicalExpression !== `pk(${keyExpression})`) - throw new Error(`Error: invalid expression ${expression}`); + throw new Error(`Error: invalid expression ${descriptor}`); expandedExpression = 'pk(@0)'; const pKE = parseKeyExpression({ keyExpression, network, isSegwit }); expansionMap = { '@0': pKE }; @@ -214,7 +298,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { //Note there exists no address for p2pk, but we can still use the script if (!pubkey) throw new Error( - `Error: could not extract a pubkey from ${expression}` + `Error: could not extract a pubkey from ${descriptor}` ); payment = p2pk({ pubkey, network }); } @@ -226,7 +310,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { if (!keyExpression) throw new Error(`Error: keyExpression could not me extracted`); if (canonicalExpression !== `pkh(${keyExpression})`) - throw new Error(`Error: invalid expression ${expression}`); + throw new Error(`Error: invalid expression ${descriptor}`); expandedExpression = 'pkh(@0)'; const pKE = parseKeyExpression({ keyExpression, network, isSegwit }); expansionMap = { '@0': pKE }; @@ -234,7 +318,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { const pubkey = pKE.pubkey; if (!pubkey) throw new Error( - `Error: could not extract a pubkey from ${expression}` + `Error: could not extract a pubkey from ${descriptor}` ); payment = p2pkh({ pubkey, network }); } @@ -246,7 +330,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { if (!keyExpression) throw new Error(`Error: keyExpression could not me extracted`); if (canonicalExpression !== `sh(wpkh(${keyExpression}))`) - throw new Error(`Error: invalid expression ${expression}`); + throw new Error(`Error: invalid expression ${descriptor}`); expandedExpression = 'sh(wpkh(@0))'; const pKE = parseKeyExpression({ keyExpression, network, isSegwit }); expansionMap = { '@0': pKE }; @@ -254,13 +338,13 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { const pubkey = pKE.pubkey; if (!pubkey) throw new Error( - `Error: could not extract a pubkey from ${expression}` + `Error: could not extract a pubkey from ${descriptor}` ); payment = p2sh({ redeem: p2wpkh({ pubkey, network }), network }); redeemScript = payment.redeem?.output; if (!redeemScript) throw new Error( - `Error: could not calculate redeemScript for ${expression}` + `Error: could not calculate redeemScript for ${descriptor}` ); } } @@ -271,7 +355,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { if (!keyExpression) throw new Error(`Error: keyExpression could not me extracted`); if (canonicalExpression !== `wpkh(${keyExpression})`) - throw new Error(`Error: invalid expression ${expression}`); + throw new Error(`Error: invalid expression ${descriptor}`); expandedExpression = 'wpkh(@0)'; const pKE = parseKeyExpression({ keyExpression, network, isSegwit }); expansionMap = { '@0': pKE }; @@ -279,7 +363,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { const pubkey = pKE.pubkey; if (!pubkey) throw new Error( - `Error: could not extract a pubkey from ${expression}` + `Error: could not extract a pubkey from ${descriptor}` ); payment = p2wpkh({ pubkey, network }); } @@ -289,7 +373,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { isSegwit = true; miniscript = canonicalExpression.match(RE.reShWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(wsh(->HERE<-)) if (!miniscript) - throw new Error(`Error: could not get miniscript in ${expression}`); + throw new Error(`Error: could not get miniscript in ${descriptor}`); ({ expandedMiniscript, expansionMap } = expandMiniscript({ miniscript, isSegwit, @@ -318,7 +402,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { redeemScript = payment.redeem?.output; if (!redeemScript) throw new Error( - `Error: could not calculate redeemScript for ${expression}` + `Error: could not calculate redeemScript for ${descriptor}` ); } } @@ -329,7 +413,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { isSegwit = false; miniscript = canonicalExpression.match(RE.reShMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(->HERE<-) if (!miniscript) - throw new Error(`Error: could not get miniscript in ${expression}`); + throw new Error(`Error: could not get miniscript in ${descriptor}`); if ( allowMiniscriptInP2SH === false && //These top-level expressions within sh are allowed within sh. @@ -372,7 +456,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { isSegwit = true; miniscript = canonicalExpression.match(RE.reWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found wsh(->HERE<-) if (!miniscript) - throw new Error(`Error: could not get miniscript in ${expression}`); + throw new Error(`Error: could not get miniscript in ${descriptor}`); ({ expandedMiniscript, expansionMap } = expandMiniscript({ miniscript, isSegwit, @@ -397,7 +481,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { payment = p2wsh({ redeem: { output: script, network }, network }); } } else { - throw new Error(`Error: Could not parse descriptor ${expression}`); + throw new Error(`Error: Could not parse descriptor ${descriptor}`); } return { @@ -412,7 +496,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { isRanged, canonicalExpression }; - }; + } /** * Expand a miniscript to a generalized form using variables instead of key @@ -442,7 +526,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { }); } - class Descriptor { + class Output { readonly #payment: Payment; readonly #preimages: Preimage[] = []; readonly #signersPubKeys: Buffer[]; @@ -461,7 +545,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { * @throws {Error} - when descriptor is invalid */ constructor({ - expression, + descriptor, index, checksumRequired = false, allowMiniscriptInP2SH = false, @@ -472,7 +556,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { /** * The descriptor string in ASCII format. It may include a "*" to denote an arbitrary index. */ - expression: string; + descriptor: string; /** * The descriptor's index in the case of a range descriptor (must be an integer >=0). @@ -510,11 +594,11 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { }) { this.#network = network; this.#preimages = preimages; - if (typeof expression !== 'string') + if (typeof descriptor !== 'string') throw new Error(`Error: invalid descriptor type`); const expandedResult = expand({ - expression, + descriptor, ...(index !== undefined ? { index } : {}), checksumRequired, network, @@ -524,7 +608,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { throw new Error(`Error: index was not provided for ranged descriptor`); if (!expandedResult.payment) throw new Error( - `Error: could not extract a payment from ${expression}` + `Error: could not extract a payment from ${descriptor}` ); this.#payment = expandedResult.payment; @@ -552,7 +636,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { const pubkey = keyInfo.pubkey; if (!pubkey) throw new Error( - `Error: could not extract a pubkey from ${expression}` + `Error: could not extract a pubkey from ${descriptor}` ); return pubkey; } @@ -561,7 +645,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { //We should only miss expansionMap in addr() expressions: if (!expandedResult.canonicalExpression.match(RE.reAddrAnchored)) { throw new Error( - `Error: expansionMap not available for expression ${expression} that is not an address` + `Error: expansionMap not available for expression ${descriptor} that is not an address` ); } this.#signersPubKeys = [this.getScriptPubKey()]; @@ -685,27 +769,40 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { isSegwit(): boolean | undefined { return this.#isSegwit; } + + /** @deprecated - Use updatePsbtAsInput instead + * @hidden + */ + updatePsbt(params: { + psbt: Psbt; + txHex?: string; + txId?: string; + value?: number; + vout: number; + }) { + return this.updatePsbtAsInput(params); + } + /** - * Updates a Psbt where the descriptor describes an utxo. - * The txHex (nonWitnessUtxo) and vout of the utxo must be passed. + * Sets this output as an input of the provided `psbt` and updates the + * `psbt` locktime if required by the descriptor. * - * updatePsbt adds an input to the psbt and updates the tx locktime if needed. - * It also adds a new input to the Psbt based on txHex - * It returns the number of the input that is added. - * psbt and vout are mandatory. Also pass txHex. + * `psbt` and `vout` are mandatory. Include `txHex` as well. The pair + * `vout` and `txHex` define the transaction and output number this instance + * pertains to. * - * The following is not recommended but, alternatively, ONLY for Segwit inputs, - * you can pass txId and value, instead of txHex. - * If you do so, it is your responsibility to make sure that `value` is - * correct to avoid possible fee vulnerability attacks: - * https://github.com/bitcoinjs/bitcoinjs-lib/issues/1625 - * Note that HW wallets require the full txHex also for Segwit anyways: - * https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd + * Though not advised, for Segwit inputs you can pass `txId` and `value` + * in lieu of `txHex`. If doing so, ensure `value` accuracy to avoid + * potential fee attacks - + * [See this issue](https://github.com/bitcoinjs/bitcoinjs-lib/issues/1625). * - * In doubt, simply pass txHex (and you can skip passing txId and value) and - * you shall be fine. + * Note: Hardware wallets need the [full `txHex` for Segwit](https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd). + * + * When unsure, always use `txHex`, and skip `txId` and `value` for safety. + * + * @returns The index of the added input. */ - updatePsbt({ + updatePsbtAsInput({ psbt, txHex, txId, @@ -743,6 +840,18 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { redeemScript: this.getRedeemScript() }); } + + /** + * Adds this output as an output of the provided `psbt` with the given + * value. + * + * @param psbt - The Partially Signed Bitcoin Transaction. + * @param value - The value for the output in satoshis. + */ + updatePsbtAsOutput({ psbt, value }: { psbt: Psbt; value: number }) { + psbt.addOutput({ script: this.getScriptPubKey(), value }); + } + #assertPsbtInput({ psbt, index }: { psbt: Psbt; index: number }): void { const input = psbt.data.inputs[index]; const txInput = psbt.txInputs[index]; @@ -837,18 +946,48 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { } } - return { Descriptor, parseKeyExpression, expand, ECPair, BIP32 }; + /** + * @hidden + * @deprecated Use `Output` instead + */ + class Descriptor extends Output { + constructor({ + expression, + ...rest + }: { + expression: string; + index?: number; + checksumRequired?: boolean; + allowMiniscriptInP2SH?: boolean; + network?: Network; + preimages?: Preimage[]; + signersPubKeys?: Buffer[]; + }) { + super({ descriptor: expression, ...rest }); + } + } + + return { Descriptor, Output, parseKeyExpression, expand, ECPair, BIP32 }; } -/** - * The {@link DescriptorsFactory | `DescriptorsFactory`} function internally creates and returns the {@link _Internal_.Descriptor | `Descriptor`} class. - * This class is specialized for the provided `TinySecp256k1Interface`. - * Use `DescriptorInstance` to declare instances for this class: `const: DescriptorInstance = new Descriptor();` - * - * See the {@link _Internal_.Descriptor | documentation for the internal Descriptor class} for a complete list of available methods. - */ + +/** @hidden */ type DescriptorConstructor = ReturnType< typeof DescriptorsFactory >['Descriptor']; +/** @hidden */ type DescriptorInstance = InstanceType; - export { DescriptorInstance, DescriptorConstructor }; + +type OutputConstructor = ReturnType['Output']; +/** + * The {@link DescriptorsFactory | `DescriptorsFactory`} function internally + * creates and returns the {@link _Internal_.Output | `Descriptor`} class. + * This class is specialized for the provided `TinySecp256k1Interface`. + * Use `OutputInstance` to declare instances for this class: + * `const: OutputInstance = new Output();` + * + * See the {@link _Internal_.Output | documentation for the internal `Output` + * class} for a complete list of available methods. + */ +type OutputInstance = InstanceType; +export { OutputInstance, OutputConstructor }; diff --git a/src/index.ts b/src/index.ts index 562f8f3..36d0759 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,31 +3,70 @@ export type { KeyInfo, Expansion } from './types'; import type { Psbt } from 'bitcoinjs-lib'; -import type { DescriptorInstance } from './descriptors'; +import type { DescriptorInstance, OutputInstance } from './descriptors'; export { DescriptorsFactory, DescriptorInstance, - DescriptorConstructor + DescriptorConstructor, + OutputInstance, + OutputConstructor } from './descriptors'; export { DescriptorChecksum as checksum } from './checksum'; import * as signers from './signers'; export { signers }; -export function finalizePsbt({ +/** + * To finalize the `psbt`, you can either call the method + * `output.finalizePsbtInput({ index, psbt })` on each descriptor, passing as + * arguments the `psbt` and its input `index`, or call this helper function: + * `finalizePsbt({psbt, outputs })`. In the latter case, `outputs` is an + * array of {@link _Internal_.Output | Output elements} ordered in the array by + * their respective input index in the `psbt`. + */ +function finalizePsbt(params: { + psbt: Psbt; + outputs: OutputInstance[]; + validate?: boolean | undefined; +}): void; + +/** + * @deprecated + * @hidden + * To be removed in version 3.0 + */ +function finalizePsbt(params: { + psbt: Psbt; + descriptors: DescriptorInstance[]; + validate?: boolean | undefined; +}): void; +/** + * @hidden + * To be removed in v3.0 and replaced by the version with the signature that + * does not accept descriptors + */ +function finalizePsbt({ psbt, + outputs, descriptors, validate = true }: { psbt: Psbt; - descriptors: DescriptorInstance[]; + outputs?: OutputInstance[]; + descriptors?: DescriptorInstance[]; validate?: boolean | undefined; }) { - descriptors.forEach((descriptor, inputIndex) => - descriptor.finalizePsbtInput({ index: inputIndex, psbt, validate }) + if (descriptors && outputs) + throw new Error(`descriptors param has been deprecated`); + outputs = descriptors || outputs; + if (!outputs) throw new Error(`outputs not provided`); + outputs.forEach((output, inputIndex) => + output.finalizePsbtInput({ index: inputIndex, psbt, validate }) ); } +export { finalizePsbt }; + export { keyExpressionBIP32, keyExpressionLedger } from './keyExpressions'; import * as scriptExpressions from './scriptExpressions'; export { scriptExpressions }; diff --git a/src/keyExpressions.ts b/src/keyExpressions.ts index 35b4804..7089903 100644 --- a/src/keyExpressions.ts +++ b/src/keyExpressions.ts @@ -31,7 +31,20 @@ const derivePath = (node: BIP32Interface, path: string) => { }; /** - * Parses a key expression (xpub, xprv, pubkey or wif) into KeyInfo + * Parses a key expression (xpub, xprv, pubkey or wif) into {@link KeyInfo | `KeyInfo`}. + * + * For example, given this `keyExpression`: `"[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*"`, this is its parsed result: + * + * ```javascript + * { + * keyExpression: + * "[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*", + * keyPath: '/1/2/3/4/*', + * originPath: "/49'/0'/0'", + * path: "m/49'/0'/0'/1/2/3/4/*", + * // Other relevant properties of the type `KeyInfo`: `pubkey`, `ecpair` & `bip32` interfaces, `masterFingerprint`, etc. + * } + * ``` */ export function parseKeyExpression({ keyExpression, @@ -43,9 +56,9 @@ export function parseKeyExpression({ keyExpression: string; network?: Network; /** - * Indicates if this is a SegWit key expression. When set, further checks - * ensure the public key (if present in the expression) is compressed - * (33 bytes). + * Indicates if this key expression belongs to a a SegWit output. When set, + * further checks are done to ensure the public key (if present in the + * expression) is compressed (33 bytes). */ isSegwit?: boolean; ECPair: ECPairAPI; @@ -190,6 +203,22 @@ function assertChangeIndexKeyPath({ throw new Error(`Error: Pass either change and index or a keyPath`); } +/** + * Constructs a key expression string for a Ledger device from the provided + * components. + * + * This function assists in crafting key expressions tailored for Ledger + * hardware wallets. It fetches the master fingerprint and xpub for a + * specified origin path and then combines them with the input parameters. + * + * For detailed understanding and examples of terms like `originPath`, + * `change`, and `keyPath`, refer to the documentation of + * {@link _Internal_.ParseKeyExpression | ParseKeyExpression}, which consists + * of the reverse procedure. + * + * @returns {string} - The formed key expression for the Ledger device. + */ + export async function keyExpressionLedger({ ledgerClient, ledgerState, @@ -219,6 +248,14 @@ export async function keyExpressionLedger({ else return `${keyRoot}/${change}/${index}`; } +/** + * Constructs a key expression string from its constituent components. + * + * This function essentially performs the reverse operation of + * {@link _Internal_.ParseKeyExpression | ParseKeyExpression}. For detailed + * explanations and examples of the terms used here, refer to + * {@link _Internal_.ParseKeyExpression | ParseKeyExpression}. + */ export function keyExpressionBIP32({ masterNode, originPath, diff --git a/src/ledger.ts b/src/ledger.ts index 1031523..355bbca 100644 --- a/src/ledger.ts +++ b/src/ledger.ts @@ -21,10 +21,10 @@ * 4) Since all originPaths must be the same and originPaths for the Ledger are * necessary, a Ledger device can only sign at most 1 key per policy and input. * - * All the conditions above are checked in function descriptorToLedgerFormat. + * All the conditions above are checked in function ledgerPolicyFromOutput. */ -import type { DescriptorInstance } from './descriptors'; +import type { DescriptorInstance, OutputInstance } from './descriptors'; import { Network, networks } from 'bitcoinjs-lib'; import { reOriginPath } from './re'; @@ -223,7 +223,9 @@ export async function getLedgerXpub({ } /** - * Takes a descriptor and gets its Ledger Wallet Policy, that is, its keyRoots and template. + * Given an output, it extracts its descriptor and converts it to a Ledger + * Wallet Policy, that is, its keyRoots and template. + * * keyRoots and template follow Ledger's specifications: * https://github.com/LedgerHQ/app-bitcoin-new/blob/develop/doc/wallet.md * @@ -247,19 +249,19 @@ export async function getLedgerXpub({ * This function takes into account all the considerations regarding Ledger * policy implementation details expressed in the header of this file. */ -export async function descriptorToLedgerFormat({ - descriptor, +export async function ledgerPolicyFromOutput({ + output, ledgerClient, ledgerState }: { - descriptor: DescriptorInstance; + output: OutputInstance; ledgerClient: unknown; ledgerState: LedgerState; }): Promise<{ ledgerTemplate: string; keyRoots: string[] } | null> { - const expandedExpression = descriptor.expand().expandedExpression; - const expansionMap = descriptor.expand().expansionMap; + const expandedExpression = output.expand().expandedExpression; + const expansionMap = output.expand().expansionMap; if (!expandedExpression || !expansionMap) - throw new Error(`Error: invalid descriptor`); + throw new Error(`Error: invalid output`); const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({ ledgerClient, @@ -337,12 +339,29 @@ export async function descriptorToLedgerFormat({ } /** - * It registers a policy based on a descriptor. It stores it in ledgerState. + * It registers a policy based on the descriptor retrieved from of an `output`. + * It stores the policy in `ledgerState`. * * If the policy was already registered, it does not register it. * If the policy is standard, it does not register it. * - **/ + */ +export async function registerLedgerWallet({ + output, + ledgerClient, + ledgerState, + policyName +}: { + output: OutputInstance; + ledgerClient: unknown; + ledgerState: LedgerState; + policyName: string; +}): Promise; + +/** + * @deprecated + * @hidden + */ export async function registerLedgerWallet({ descriptor, ledgerClient, @@ -353,27 +372,49 @@ export async function registerLedgerWallet({ ledgerClient: unknown; ledgerState: LedgerState; policyName: string; +}): Promise; + +/** + * To be removed in v3.0 and replaced by a version that does not accept + * descriptors + * @hidden + **/ +export async function registerLedgerWallet({ + output, + descriptor, + ledgerClient, + ledgerState, + policyName +}: { + output?: OutputInstance; + descriptor?: DescriptorInstance; + ledgerClient: unknown; + ledgerState: LedgerState; + policyName: string; }) { + if (descriptor && output) + throw new Error(`descriptor param has been deprecated`); + output = descriptor || output; + if (!output) throw new Error(`output not provided`); const { WalletPolicy, AppClient } = (await importAndValidateLedgerBitcoin( ledgerClient )) as typeof import('ledger-bitcoin'); if (!(ledgerClient instanceof AppClient)) throw new Error(`Error: pass a valid ledgerClient`); - const result = await descriptorToLedgerFormat({ - descriptor, + const result = await ledgerPolicyFromOutput({ + output, ledgerClient, ledgerState }); - if (await ledgerPolicyFromStandard({ descriptor, ledgerClient, ledgerState })) + if (await ledgerPolicyFromStandard({ output, ledgerClient, ledgerState })) return; - if (!result) - throw new Error(`Error: descriptor does not have a ledger input`); + if (!result) throw new Error(`Error: output does not have a ledger input`); const { ledgerTemplate, keyRoots } = result; if (!ledgerState.policies) ledgerState.policies = []; let walletPolicy, policyHmac; //Search in ledgerState first const policy = await ledgerPolicyFromState({ - descriptor, + output, ledgerClient, ledgerState }); @@ -401,16 +442,16 @@ export async function registerLedgerWallet({ * Retrieve a standard ledger policy or null if it does correspond. **/ export async function ledgerPolicyFromStandard({ - descriptor, + output, ledgerClient, ledgerState }: { - descriptor: DescriptorInstance; + output: OutputInstance; ledgerClient: unknown; ledgerState: LedgerState; }): Promise { - const result = await descriptorToLedgerFormat({ - descriptor, + const result = await ledgerPolicyFromOutput({ + output, ledgerClient, ledgerState }); @@ -421,7 +462,7 @@ export async function ledgerPolicyFromStandard({ isLedgerStandard({ ledgerTemplate, keyRoots, - network: descriptor.getNetwork() + network: output.getNetwork() }) ) return { ledgerTemplate, keyRoots }; @@ -450,21 +491,20 @@ export function comparePolicies(policyA: LedgerPolicy, policyB: LedgerPolicy) { * Retrieve a ledger policy from ledgerState or null if it does not exist yet. **/ export async function ledgerPolicyFromState({ - descriptor, + output, ledgerClient, ledgerState }: { - descriptor: DescriptorInstance; + output: OutputInstance; ledgerClient: unknown; ledgerState: LedgerState; }): Promise { - const result = await descriptorToLedgerFormat({ - descriptor, + const result = await ledgerPolicyFromOutput({ + output, ledgerClient, ledgerState }); - if (!result) - throw new Error(`Error: descriptor does not have a ledger input`); + if (!result) throw new Error(`Error: output does not have a ledger input`); const { ledgerTemplate, keyRoots } = result; if (!ledgerState.policies) ledgerState.policies = []; //Search in ledgerState: diff --git a/src/signers.ts b/src/signers.ts index 8dcb85a..f403c4c 100644 --- a/src/signers.ts +++ b/src/signers.ts @@ -4,14 +4,14 @@ import type { Psbt } from 'bitcoinjs-lib'; import type { ECPairInterface } from 'ecpair'; import type { BIP32Interface } from 'bip32'; -import type { DescriptorInstance } from './descriptors'; +import type { DescriptorInstance, OutputInstance } from './descriptors'; import { importAndValidateLedgerBitcoin, comparePolicies, LedgerPolicy, ledgerPolicyFromState, ledgerPolicyFromStandard, - descriptorToLedgerFormat, + ledgerPolicyFromOutput, LedgerState } from './ledger'; type DefaultDescriptorTemplate = @@ -77,6 +77,24 @@ const ledgerSignaturesForInputIndex = ( signature: partialSignature.signature })); +export async function signInputLedger({ + psbt, + index, + output, + ledgerClient, + ledgerState +}: { + psbt: Psbt; + index: number; + output: OutputInstance; + ledgerClient: unknown; + ledgerState: LedgerState; +}): Promise; + +/** + * @deprecated + * @hidden + */ export async function signInputLedger({ psbt, index, @@ -89,7 +107,32 @@ export async function signInputLedger({ descriptor: DescriptorInstance; ledgerClient: unknown; ledgerState: LedgerState; +}): Promise; + +/** + * To be removed in v3.0 and replaced by a version that does not accept + * descriptor + * @hidden + */ +export async function signInputLedger({ + psbt, + index, + output, + descriptor, + ledgerClient, + ledgerState +}: { + psbt: Psbt; + index: number; + output?: OutputInstance; + descriptor?: DescriptorInstance; + ledgerClient: unknown; + ledgerState: LedgerState; }): Promise { + if (descriptor && output) + throw new Error(`descriptor param has been deprecated`); + output = descriptor || output; + if (!output) throw new Error(`output not provided`); const { PsbtV2, DefaultWalletPolicy, WalletPolicy, AppClient } = (await importAndValidateLedgerBitcoin( ledgerClient @@ -97,18 +140,17 @@ export async function signInputLedger({ if (!(ledgerClient instanceof AppClient)) throw new Error(`Error: pass a valid ledgerClient`); - const result = await descriptorToLedgerFormat({ - descriptor, + const result = await ledgerPolicyFromOutput({ + output, ledgerClient, ledgerState }); - if (!result) - throw new Error(`Error: descriptor does not have a ledger input`); + if (!result) throw new Error(`Error: output does not have a ledger input`); const { ledgerTemplate, keyRoots } = result; let ledgerSignatures; const standardPolicy = await ledgerPolicyFromStandard({ - descriptor, + output, ledgerClient, ledgerState }); @@ -123,7 +165,7 @@ export async function signInputLedger({ ); } else { const policy = await ledgerPolicyFromState({ - descriptor, + output, ledgerClient, ledgerState }); @@ -148,9 +190,28 @@ export async function signInputLedger({ }); } -//signLedger is able to sign several inputs of the same wallet policy since it -//it clusters together wallet policy types before signing -//it throws if it cannot sign any input. +/** + * signLedger is able to sign several inputs of the same wallet policy since it + * it clusters together wallet policy types before signing. + * + * It throws if it cannot sign any input. + */ +export async function signLedger({ + psbt, + outputs, + ledgerClient, + ledgerState +}: { + psbt: Psbt; + outputs: OutputInstance[]; + ledgerClient: unknown; + ledgerState: LedgerState; +}): Promise; + +/** + * @deprecated + * @hidden + */ export async function signLedger({ psbt, descriptors, @@ -161,7 +222,30 @@ export async function signLedger({ descriptors: DescriptorInstance[]; ledgerClient: unknown; ledgerState: LedgerState; +}): Promise; + +/** + * To be removed in v3.0 and replaced by a version that does not accept + * descriptors + * @hidden + */ +export async function signLedger({ + psbt, + outputs, + descriptors, + ledgerClient, + ledgerState +}: { + psbt: Psbt; + outputs?: OutputInstance[]; + descriptors?: DescriptorInstance[]; + ledgerClient: unknown; + ledgerState: LedgerState; }): Promise { + if (descriptors && outputs) + throw new Error(`descriptors param has been deprecated`); + outputs = descriptors || outputs; + if (!outputs) throw new Error(`outputs not provided`); const { PsbtV2, DefaultWalletPolicy, WalletPolicy, AppClient } = (await importAndValidateLedgerBitcoin( ledgerClient @@ -169,18 +253,10 @@ export async function signLedger({ if (!(ledgerClient instanceof AppClient)) throw new Error(`Error: pass a valid ledgerClient`); const ledgerPolicies = []; - for (const descriptor of descriptors) { + for (const output of outputs) { const policy = - (await ledgerPolicyFromState({ - descriptor, - ledgerClient, - ledgerState - })) || - (await ledgerPolicyFromStandard({ - descriptor, - ledgerClient, - ledgerState - })); + (await ledgerPolicyFromState({ output, ledgerClient, ledgerState })) || + (await ledgerPolicyFromStandard({ output, ledgerClient, ledgerState })); if (policy) ledgerPolicies.push(policy); } if (ledgerPolicies.length === 0) diff --git a/src/types.ts b/src/types.ts index ce12541..3b18e07 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,9 @@ export type TimeConstraints = { nSequence: number | undefined; }; +/** + * See {@link _Internal_.ParseKeyExpression | ParseKeyExpression}. + */ export type KeyInfo = { keyExpression: string; pubkey?: Buffer; //Must be set unless this corresponds to a ranged-descriptor @@ -38,6 +41,35 @@ export type KeyInfo = { path?: string; //The complete path from the master. Format is: "m/val/val/...", starting with an m/, and where val are integers or integers followed by a tilde ', for the hardened case }; +/** + * An `ExpansionMap` contains destructured information of a descritptor expression. + * + * For example, this descriptor `sh(wsh(andor(pk(0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2),older(8640),pk([d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*))))` has the following + * `expandedExpression`: `sh(wsh(andor(pk(@0),older(8640),pk(@1))))` + * + * `key`'s are set using this format: `@i`, where `i` is an integer starting from `0` assigned by parsing and retrieving keys from the descriptor from left to right. + * + * For the given example, the `ExpansionMap` is: + * + * ```javascript + * { + * '@0': { + * keyExpression: + * '0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2' + * }, + * '@1': { + * keyExpression: + * "[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*", + * keyPath: '/1/2/3/4/*', + * originPath: "/49'/0'/0'", + * path: "m/49'/0'/0'/1/2/3/4/*", + * // Other relevant properties of the type `KeyInfo`: `pubkey`, `ecpair` & `bip32` interfaces, `masterFingerprint`, etc. + * } + * } + *``` + * + * + */ export type ExpansionMap = { //key will have this format: @i, where i is an integer [key: string]: KeyInfo; @@ -77,6 +109,11 @@ export interface TinySecp256k1Interface { privateNegate(d: Uint8Array): Uint8Array; } +/** + * `DescriptorsFactory` creates and returns the {@link DescriptorsFactory | `expand()`} + * function that parses a descriptor expression and destructures it + * into its elemental parts. `Expansion` is the type that `expand()` returns. + */ export type Expansion = { /** * The corresponding [bitcoinjs-lib Payment](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/payments/index.ts) for the provided expression, if applicable. @@ -85,6 +122,7 @@ export type Expansion = { /** * The expanded descriptor expression. + * See {@link ExpansionMap ExpansionMap} for a detailed explanation. */ expandedExpression?: string; @@ -95,16 +133,19 @@ export type Expansion = { /** * A map of key expressions in the descriptor to their corresponding expanded keys. + * See {@link ExpansionMap ExpansionMap} for a detailed explanation. */ expansionMap?: ExpansionMap; /** - * A boolean indicating whether the descriptor represents a SegWit script. + * A boolean indicating whether the descriptor uses SegWit. */ isSegwit?: boolean; /** * The expanded miniscript, if any. + * It corresponds to the `expandedExpression` without the top-level script + * expression. */ expandedMiniscript?: string; @@ -119,63 +160,49 @@ export type Expansion = { witnessScript?: Buffer; /** - * Whether this expression represents a ranged-descriptor. + * Whether the descriptor is a ranged-descriptor. */ isRanged: boolean; /** - * This is the preferred or authoritative representation of the descriptor expression. + * This is the preferred or authoritative representation of an output + * descriptor expression. + * It removes the checksum and, if it is a ranged-descriptor, it + * particularizes it to its index. */ canonicalExpression: string; }; /** - * The {@link DescriptorsFactory | `DescriptorsFactory`} function creates and returns an implementation of the `Expand` interface. - * This returned implementation is tailored for the provided `TinySecp256k1Interface`. - */ -export interface Expand { - (params: { - /** - * The descriptor expression to be expanded. - */ - expression: string; - - /** - * The descriptor index, if ranged. - */ - index?: number; - - /** - * A flag indicating whether the descriptor is required to include a checksum. - * @defaultValue false - */ - checksumRequired?: boolean; - - /** - * The Bitcoin network to use. - * @defaultValue `networks.bitcoin` - */ - network?: Network; - - /** - * Flag to allow miniscript in P2SH. - * @defaultValue false - */ - allowMiniscriptInP2SH?: boolean; - }): Expansion; -} - -/** - * The {@link DescriptorsFactory | `DescriptorsFactory`} function creates and returns an implementation of the `ParseKeyExpression` interface. - * This returned implementation is tailored for the provided `TinySecp256k1Interface`. + * The {@link DescriptorsFactory | `DescriptorsFactory`} function creates and + * returns the `parseKeyExpression` function, which is an implementation of this + * interface. + * + * It parses and destructures a key expression string (xpub, xprv, pubkey or + * wif) into {@link KeyInfo | `KeyInfo`}. + * + * For example, given this `keyExpression`: `[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*`, this is the parsed result: + * + * ```javascript + * { + * keyExpression: + * "[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*", + * keyPath: '/1/2/3/4/*', + * originPath: "/49'/0'/0'", + * path: "m/49'/0'/0'/1/2/3/4/*", + * // Other relevant properties of the type `KeyInfo`: `pubkey`, `ecpair` & `bip32` interfaces, `masterFingerprint`, etc. + * } + * ``` + * + * See {@link KeyInfo} for the complete list of elements retrieved by this function. */ export interface ParseKeyExpression { (params: { keyExpression: string; /** - * Indicates if this is a SegWit key expression. When set, further checks - * ensure the public key (if present in the expression) is compressed - * (33 bytes). + * Indicates if this key expression belongs to a a SegWit output. When set, + * further checks are done to ensure the public key (if present in the + * expression) is compressed (33 bytes). */ isSegwit?: boolean; network?: Network; diff --git a/test/integration/miniscript.ts b/test/integration/miniscript.ts index 0803e2e..24671be 100644 --- a/test/integration/miniscript.ts +++ b/test/integration/miniscript.ts @@ -3,7 +3,7 @@ //npm run test:integration -import { networks, Psbt, address } from 'bitcoinjs-lib'; +import { networks, Psbt } from 'bitcoinjs-lib'; import { mnemonicToSeedSync } from 'bip39'; const { encode: afterEncode } = require('bip65'); const { encode: olderEncode } = require('bip68'); @@ -15,7 +15,6 @@ const NETWORK = networks.regtest; const INITIAL_VALUE = 2e4; const FINAL_VALUE = INITIAL_VALUE - 1000; const FINAL_ADDRESS = regtestUtils.RANDOM_ADDRESS; -const FINAL_SCRIPTPUBKEY = address.toOutputScript(FINAL_ADDRESS, NETWORK); const SOFT_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const POLICY = (older: number, after: number) => @@ -126,7 +125,15 @@ const keys: { const { txHex } = await regtestUtils.fetch(txId); const psbt = new Psbt(); const index = descriptor.updatePsbt({ psbt, vout, txHex }); - psbt.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + //There are different ways to add an output: + //import { address } from 'bitcoinjs-lib'; + //const FINAL_SCRIPTPUBKEY = address.toOutputScript(FINAL_ADDRESS, NETWORK); + //psbt.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + //But can also be done like this: + new Descriptor({ + expression: `addr(${FINAL_ADDRESS})`, + network: NETWORK + }).updatePsbtAsOutput({ psbt, value: FINAL_VALUE }); if (keyExpressionType === 'BIP32') signBIP32({ masterNode, psbt }); else signECPair({ ecpair, psbt }); descriptor.finalizePsbtInput({ index, psbt });