@backpacker69/hw-app-btc
Advanced tools
Comparing version 6.24.1 to 6.24.2
/// <reference types="node" /> | ||
import type Transport from "@ledgerhq/hw-transport"; | ||
import BtcNew from "./BtcNew"; | ||
import BtcOld from "./BtcOld"; | ||
import type { CreateTransactionArg } from "./createTransaction"; | ||
@@ -45,3 +43,3 @@ import type { AddressFormat } from "./getWalletPublicKey"; | ||
* | ||
* - bech32 format with 84' paths | ||
* - bech32 format with 173' paths | ||
* | ||
@@ -86,6 +84,6 @@ * - cashaddr in case of Bitcoin Cash | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param segwit is an optional boolean indicating wether to use segwit or not | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
@@ -95,3 +93,3 @@ * @param additionals list of additionnal options | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outputs | ||
* - "bech32m" for spending native segwit outputs | ||
* - "abc" for bch | ||
@@ -102,3 +100,3 @@ * - "gold" for btg | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions | ||
* @return the signed transaction ready to be broadcast | ||
@@ -150,5 +148,4 @@ * @example | ||
private inferCorrectImpl; | ||
protected old(): BtcOld; | ||
protected new(): BtcNew; | ||
private old; | ||
} | ||
//# sourceMappingURL=Btc.d.ts.map |
@@ -37,3 +37,2 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
import { pathStringToArray } from "./bip32"; | ||
import BtcNew, { canSupportApp } from "./BtcNew"; | ||
@@ -94,3 +93,3 @@ import BtcOld from "./BtcOld"; | ||
* | ||
* - bech32 format with 84' paths | ||
* - bech32 format with 173' paths | ||
* | ||
@@ -104,3 +103,2 @@ * - cashaddr in case of Bitcoin Cash | ||
Btc.prototype.getWalletPublicKey = function (path, opts) { | ||
var _this = this; | ||
var options; | ||
@@ -119,35 +117,3 @@ if (arguments.length > 2 || typeof opts === "boolean") { | ||
return this.getCorrectImpl().then(function (impl) { | ||
/** | ||
* Definition: A "normal path" is a prefix of a standard path where all | ||
* the hardened steps of the standard path are included. For example, the | ||
* paths m/44'/1'/17' and m/44'/1'/17'/1 are normal paths, but m/44'/1' | ||
* is not. m/'199/1'/17'/0/1 is not a normal path either. | ||
* | ||
* There's a compatiblity issue between old and new app: When exporting | ||
* the key of a non-normal path with verify=false, the new app would | ||
* return an error, whereas the old app would return the key. | ||
* | ||
* See | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#get_extended_pubkey | ||
* | ||
* If format bech32m is used, we'll not use old, because it doesn't | ||
* support it. | ||
* | ||
* When to use new (given the app supports it) | ||
* * format is bech32m or | ||
* * path is normal or | ||
* * verify is true | ||
* | ||
* Otherwise use old. | ||
*/ | ||
if (impl instanceof BtcNew && | ||
options.format != "bech32m" && | ||
(!options.verify || options.verify == false) && | ||
!isPathNormal(path)) { | ||
console.warn("WARNING: Using deprecated device protocol to get the public key because\n \n * a non-standard path is requested, and\n * verify flag is false\n \n The new protocol only allows export of non-standard paths if the \n verify flag is true. Standard paths are (currently):\n\n M/44'/(1|0)'/X'\n M/49'/(1|0)'/X'\n M/84'/(1|0)'/X'\n M/86'/(1|0)'/X'\n M/48'/(1|0)'/X'/Y'\n\n followed by \"\", \"(0|1)\", or \"(0|1)/b\", where a and b are \n non-hardened. For example, the following paths are standard\n \n M/48'/1'/99'/7'\n M/86'/1'/99'/0\n M/48'/0'/99'/7'/1/17\n\n The following paths are non-standard\n\n M/48'/0'/99' // Not deepest hardened path\n M/48'/0'/99'/7'/1/17/2 // Too many non-hardened derivation steps\n M/199'/0'/1'/0/88 // Not a known purpose 199\n M/86'/1'/99'/2 // Change path item must be 0 or 1\n\n This compatibility safeguard will be removed in the future.\n Please consider calling Btc.getWalletXpub() instead."); | ||
return _this.old().getWalletPublicKey(path, options); | ||
} | ||
else { | ||
return impl.getWalletPublicKey(path, options); | ||
} | ||
return impl.getWalletPublicKey(path, options); | ||
}); | ||
@@ -177,6 +143,6 @@ }; | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param segwit is an optional boolean indicating wether to use segwit or not | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
@@ -186,3 +152,3 @@ * @param additionals list of additionnal options | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outputs | ||
* - "bech32m" for spending native segwit outputs | ||
* - "abc" for bch | ||
@@ -193,3 +159,3 @@ * - "gold" for btg | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions | ||
* @return the signed transaction ready to be broadcast | ||
@@ -289,6 +255,6 @@ * @example | ||
if (!canUseNewImplementation) { | ||
return [2 /*return*/, this.old()]; | ||
return [2 /*return*/, new BtcOld(this.transport)]; | ||
} | ||
else { | ||
return [2 /*return*/, this["new"]()]; | ||
return [2 /*return*/, new BtcNew(new AppClient(this.transport))]; | ||
} | ||
@@ -303,36 +269,5 @@ return [2 /*return*/]; | ||
}; | ||
Btc.prototype["new"] = function () { | ||
return new BtcNew(new AppClient(this.transport)); | ||
}; | ||
return Btc; | ||
}()); | ||
export default Btc; | ||
function isPathNormal(path) { | ||
//path is not deepest hardened node of a standard path or deeper, use BtcOld | ||
var h = 0x80000000; | ||
var pathElems = pathStringToArray(path); | ||
var hard = function (n) { return n >= h; }; | ||
var soft = function (n) { return !n || n < h; }; | ||
var change = function (n) { return !n || n == 0 || n == 1; }; | ||
if (pathElems.length >= 3 && | ||
pathElems.length <= 5 && | ||
[44 + h, 49 + h, 84 + h, 86 + h].some(function (v) { return v == pathElems[0]; }) && | ||
[0 + h, 1 + h].some(function (v) { return v == pathElems[1]; }) && | ||
hard(pathElems[2]) && | ||
change(pathElems[3]) && | ||
soft(pathElems[4])) { | ||
return true; | ||
} | ||
if (pathElems.length >= 4 && | ||
pathElems.length <= 6 && | ||
48 + h == pathElems[0] && | ||
[0 + h, 1 + h].some(function (v) { return v == pathElems[1]; }) && | ||
hard(pathElems[2]) && | ||
hard(pathElems[3]) && | ||
change(pathElems[4]) && | ||
soft(pathElems[5])) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
//# sourceMappingURL=Btc.js.map |
import type { CreateTransactionArg } from "./createTransaction"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
import type { AddressFormat } from "./getWalletPublicKey"; | ||
import { AppClient as Client } from "./newops/appClient"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
export declare function canSupportApp(appAndVersion: AppAndVersion): boolean; | ||
/** | ||
* This class implements the same interface as BtcOld (formerly | ||
* named Btc), but interacts with Bitcoin hardware app version 2+ | ||
* which uses a totally new APDU protocol. This new | ||
* protocol is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
* | ||
* Since the interface must remain compatible with BtcOld, the methods | ||
* of this class are quite clunky, because it needs to adapt legacy | ||
* input data into the PSBT process. In the future, a new interface should | ||
* be developed that exposes PSBT to the outer world, which would render | ||
* a much cleaner implementation. | ||
*/ | ||
export default class BtcNew { | ||
private client; | ||
constructor(client: Client); | ||
/** | ||
* This is a new method that allow users to get an xpub at a standard path. | ||
* Standard paths are described at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description | ||
* | ||
* This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
* M/44'/N'/x'/** | ||
* M/48'/N'/x'/y'/** | ||
* M/49'/N'/x'/** | ||
* M/84'/N'/x'/** | ||
* M/86'/N'/x'/** | ||
* | ||
* The method was added because of added security in the hardware app v2+. The | ||
* new hardware app will allow export of any xpub up to and including the | ||
* deepest hardened key of standard derivation paths, whereas the old app | ||
* would allow export of any key. | ||
* | ||
* This caused an issue for callers of this class, who only had | ||
* getWalletPublicKey() to call which means they have to constuct xpub | ||
* themselves: | ||
* | ||
* Suppose a user of this class wants to create an account xpub on a standard | ||
* path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
* by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
* it only allows exporting deepest level hardened path. So the options are to | ||
* allow requesting M/44'/0' from the app, or to add a new function | ||
* "getWalletXpub". | ||
* | ||
* We opted for adding a new function, which can greatly simplify client code. | ||
*/ | ||
getWalletXpub({ path, xpubVersion, }: { | ||
@@ -56,9 +13,2 @@ path: string; | ||
}): Promise<string>; | ||
/** | ||
* This method returns a public key, a bitcoin address, and and a chaincode | ||
* for a specific derivation path. | ||
* | ||
* Limitation: If the path is not a leaf node of a standard path, the address | ||
* will be the empty string "", see this.getWalletAddress() for details. | ||
*/ | ||
getWalletPublicKey(path: string, opts?: { | ||
@@ -81,3 +31,3 @@ verify?: boolean; | ||
* get it from the device to save development time. However, it shouldn't take | ||
* too much time to implement local address generation. | ||
* more than a few hours to implement local address generation. | ||
* | ||
@@ -90,36 +40,39 @@ * Moreover, if the path is not for a leaf, ie accountPath+/X/Y, there is no | ||
/** | ||
* Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
* details on how to use this method. | ||
* To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
* @param inputs is an array of [ transaction, output_index, optional redeem script, optional sequence ] where | ||
* | ||
* This method will convert the legacy arguments, CreateTransactionArg, into | ||
* a psbt which is finally signed and finalized, and the extracted fully signed | ||
* transaction is returned. | ||
* * transaction is the previously computed transaction object for this UTXO | ||
* * output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* * redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* * sequence is the sequence number to use for this input (when using RBF), or non present | ||
* @param associatedKeysets is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* @param additionals list of additionnal options | ||
* | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outptus | ||
* - "abc" for bch | ||
* - "gold" for btg | ||
* - "bipxxx" for using BIPxxx | ||
* - "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @return the signed transaction ready to be broadcast | ||
* @example | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
*/ | ||
createPaymentTransactionNew(arg: CreateTransactionArg): Promise<string>; | ||
/** | ||
* Calculates an output script along with public key and possible redeemScript | ||
* from a path and accountType. The accountPath must be a prefix of path. | ||
* | ||
* @returns an object with output script (property "script"), redeemScript (if | ||
* wrapped p2wpkh), and pubkey at provided path. The values of these three | ||
* properties depend on the accountType used. | ||
*/ | ||
private outputScriptAt; | ||
/** | ||
* Adds relevant data about an input to the psbt. This includes sequence, | ||
* previous txid, output index, spent UTXO, redeem script for wrapped p2wpkh, | ||
* public key and its derivation path. | ||
*/ | ||
private setInput; | ||
/** | ||
* This implements the "Signer" role of the BIP370 transaction signing | ||
* process. | ||
* | ||
* It ssks the hardware device to sign the a psbt using the specified wallet | ||
* policy. This method assumes BIP32 derived keys are used for all inputs, see | ||
* comment in-line. The signatures returned from the hardware device is added | ||
* to the appropriate input fields of the PSBT. | ||
*/ | ||
private signPsbt; | ||
} | ||
//# sourceMappingURL=BtcNew.d.ts.map |
@@ -0,1 +1,12 @@ | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -38,8 +49,8 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { pointCompress, pointAddScalar } from "tiny-secp256k1"; | ||
import semver from "semver"; | ||
import { pointCompress } from "tiny-secp256k1"; | ||
import { getXpubComponents, hardenedPathOf, pathArrayToString, pathStringToArray, pubkeyFromXpub, } from "./bip32"; | ||
import { BufferReader } from "./buffertools"; | ||
import { p2pkh, p2tr, p2wpkh, p2wpkhWrapped, } from "./newops/accounttype"; | ||
import { createKey, WalletPolicy, } from "./newops/policy"; | ||
import { BufferReader, BufferWriter } from "./buffertools"; | ||
import { hashPublicKey } from "./hashPublicKey"; | ||
import { createKey, WalletPolicy } from "./newops/policy"; | ||
import { extract } from "./newops/psbtExtractor"; | ||
@@ -49,2 +60,3 @@ import { finalize } from "./newops/psbtFinalizer"; | ||
import { serializeTransaction } from "./serializeTransaction"; | ||
import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, } from "./constants"; | ||
var newSupportedApps = ["Bitcoin", "Bitcoin Test"]; | ||
@@ -55,15 +67,2 @@ export function canSupportApp(appAndVersion) { | ||
} | ||
/** | ||
* This class implements the same interface as BtcOld (formerly | ||
* named Btc), but interacts with Bitcoin hardware app version 2+ | ||
* which uses a totally new APDU protocol. This new | ||
* protocol is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
* | ||
* Since the interface must remain compatible with BtcOld, the methods | ||
* of this class are quite clunky, because it needs to adapt legacy | ||
* input data into the PSBT process. In the future, a new interface should | ||
* be developed that exposes PSBT to the outer world, which would render | ||
* a much cleaner implementation. | ||
*/ | ||
var BtcNew = /** @class */ (function () { | ||
@@ -73,32 +72,2 @@ function BtcNew(client) { | ||
} | ||
/** | ||
* This is a new method that allow users to get an xpub at a standard path. | ||
* Standard paths are described at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description | ||
* | ||
* This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
* M/44'/N'/x'/** | ||
* M/48'/N'/x'/y'/** | ||
* M/49'/N'/x'/** | ||
* M/84'/N'/x'/** | ||
* M/86'/N'/x'/** | ||
* | ||
* The method was added because of added security in the hardware app v2+. The | ||
* new hardware app will allow export of any xpub up to and including the | ||
* deepest hardened key of standard derivation paths, whereas the old app | ||
* would allow export of any key. | ||
* | ||
* This caused an issue for callers of this class, who only had | ||
* getWalletPublicKey() to call which means they have to constuct xpub | ||
* themselves: | ||
* | ||
* Suppose a user of this class wants to create an account xpub on a standard | ||
* path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
* by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
* it only allows exporting deepest level hardened path. So the options are to | ||
* allow requesting M/44'/0' from the app, or to add a new function | ||
* "getWalletXpub". | ||
* | ||
* We opted for adding a new function, which can greatly simplify client code. | ||
*/ | ||
BtcNew.prototype.getWalletXpub = function (_a) { | ||
@@ -112,3 +81,3 @@ var path = _a.path, xpubVersion = _a.xpubVersion; | ||
pathElements = pathStringToArray(path); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
@@ -125,9 +94,2 @@ xpub = _b.sent(); | ||
}; | ||
/** | ||
* This method returns a public key, a bitcoin address, and and a chaincode | ||
* for a specific derivation path. | ||
* | ||
* Limitation: If the path is not a leaf node of a standard path, the address | ||
* will be the empty string "", see this.getWalletAddress() for details. | ||
*/ | ||
BtcNew.prototype.getWalletPublicKey = function (path, opts) { | ||
@@ -141,7 +103,7 @@ var _a, _b; | ||
pathElements = pathStringToArray(path); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
xpub = _c.sent(); | ||
display = (_a = opts === null || opts === void 0 ? void 0 : opts.verify) !== null && _a !== void 0 ? _a : false; | ||
return [4 /*yield*/, this.getWalletAddress(pathElements, descrTemplFrom((_b = opts === null || opts === void 0 ? void 0 : opts.format) !== null && _b !== void 0 ? _b : "legacy"), display)]; | ||
return [4 /*yield*/, this.getWalletAddress(pathElements, accountTypeFrom((_b = opts === null || opts === void 0 ? void 0 : opts.format) !== null && _b !== void 0 ? _b : "legacy"), display)]; | ||
case 2: | ||
@@ -169,3 +131,3 @@ address = _c.sent(); | ||
* get it from the device to save development time. However, it shouldn't take | ||
* too much time to implement local address generation. | ||
* more than a few hours to implement local address generation. | ||
* | ||
@@ -176,3 +138,3 @@ * Moreover, if the path is not for a leaf, ie accountPath+/X/Y, there is no | ||
*/ | ||
BtcNew.prototype.getWalletAddress = function (pathElements, descrTempl, display) { | ||
BtcNew.prototype.getWalletAddress = function (pathElements, accountType, display) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -187,3 +149,3 @@ var accountPath, accountXpub, masterFingerprint, policy, changeAndIndex; | ||
} | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, accountPath)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, accountPath)]; | ||
case 1: | ||
@@ -194,3 +156,3 @@ accountXpub = _a.sent(); | ||
masterFingerprint = _a.sent(); | ||
policy = new WalletPolicy(descrTempl, createKey(masterFingerprint, accountPath, accountXpub)); | ||
policy = new WalletPolicy(accountType, createKey(masterFingerprint, accountPath, accountXpub)); | ||
changeAndIndex = pathElements.slice(-2, pathElements.length); | ||
@@ -203,41 +165,55 @@ return [2 /*return*/, this.client.getWalletAddress(policy, Buffer.alloc(32, 0), changeAndIndex[0], changeAndIndex[1], display)]; | ||
/** | ||
* Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
* details on how to use this method. | ||
* To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
* @param inputs is an array of [ transaction, output_index, optional redeem script, optional sequence ] where | ||
* | ||
* This method will convert the legacy arguments, CreateTransactionArg, into | ||
* a psbt which is finally signed and finalized, and the extracted fully signed | ||
* transaction is returned. | ||
* * transaction is the previously computed transaction object for this UTXO | ||
* * output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* * redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* * sequence is the sequence number to use for this input (when using RBF), or non present | ||
* @param associatedKeysets is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* @param additionals list of additionnal options | ||
* | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outptus | ||
* - "abc" for bch | ||
* - "gold" for btg | ||
* - "bipxxx" for using BIPxxx | ||
* - "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @return the signed transaction ready to be broadcast | ||
* @example | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
*/ | ||
BtcNew.prototype.createPaymentTransactionNew = function (arg) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var inputCount, psbt, masterFp, accountType, notifyCount, progress, accountXpub, accountPath, i, pathElems, outputsConcat, outputsBufferReader, outputCount, changeData, changeFound, i, amount, outputScript, isChange, changePath, pubkey, key, p, firstSigned, progressCallback, serializedTx; | ||
var psbt, accountType, masterFp, accountXpub, accountPath, i, pathElems, outputsConcat, outputsBufferReader, outputCount, changeData, changeFound, i, amount, outputScript, isChange, changePath, pubkey, key, p; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
inputCount = arg.inputs.length; | ||
if (inputCount == 0) { | ||
if (arg.inputs.length == 0) { | ||
throw Error("No inputs"); | ||
} | ||
psbt = new PsbtV2(); | ||
return [4 /*yield*/, this.client.getMasterFingerprint()]; | ||
case 1: | ||
masterFp = _a.sent(); | ||
accountType = accountTypeFromArg(arg, psbt, masterFp); | ||
if (arg.lockTime != undefined) { | ||
// The signer will assume locktime 0 if unset | ||
accountType = accountTypeFromArg(arg); | ||
psbt.setGlobalTxVersion(2); | ||
if (arg.lockTime) { | ||
psbt.setGlobalFallbackLocktime(arg.lockTime); | ||
} | ||
psbt.setGlobalInputCount(inputCount); | ||
psbt.setGlobalInputCount(arg.inputs.length); | ||
psbt.setGlobalPsbtVersion(2); | ||
psbt.setGlobalTxVersion(2); | ||
notifyCount = 0; | ||
progress = function () { | ||
if (!arg.onDeviceStreaming) | ||
return; | ||
arg.onDeviceStreaming({ | ||
total: 2 * inputCount, | ||
index: notifyCount, | ||
progress: ++notifyCount / (2 * inputCount) | ||
}); | ||
}; | ||
return [4 /*yield*/, this.client.getMasterFingerprint()]; | ||
case 1: | ||
masterFp = _a.sent(); | ||
accountXpub = ""; | ||
@@ -248,4 +224,3 @@ accountPath = []; | ||
case 2: | ||
if (!(i < inputCount)) return [3 /*break*/, 7]; | ||
progress(); | ||
if (!(i < arg.inputs.length)) return [3 /*break*/, 7]; | ||
pathElems = pathStringToArray(arg.associatedKeysets[i]); | ||
@@ -256,7 +231,7 @@ if (!(accountXpub == "")) return [3 /*break*/, 4]; | ||
accountPath = pathElems.slice(0, -2); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, accountPath)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, accountPath)]; | ||
case 3: | ||
accountXpub = _a.sent(); | ||
_a.label = 4; | ||
case 4: return [4 /*yield*/, this.setInput(psbt, i, arg.inputs[i], pathElems, accountType, masterFp, arg.sigHashType)]; | ||
case 4: return [4 /*yield*/, this.setInput(psbt, i, arg.inputs[i], pathElems, accountType, masterFp)]; | ||
case 5: | ||
@@ -272,6 +247,6 @@ _a.sent(); | ||
outputCount = outputsBufferReader.readVarInt(); | ||
psbt.setGlobalOutputCount(outputCount); | ||
return [4 /*yield*/, this.outputScriptAt(accountPath, accountType, arg.changePath)]; | ||
case 8: | ||
changeData = _a.sent(); | ||
psbt.setGlobalOutputCount(outputCount); | ||
changeFound = !changeData; | ||
@@ -281,5 +256,3 @@ for (i = 0; i < outputCount; i++) { | ||
outputScript = outputsBufferReader.readVarSlice(); | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
isChange = changeData && outputScript.equals(changeData === null || changeData === void 0 ? void 0 : changeData.cond.scriptPubKey); | ||
isChange = changeData && outputScript.equals(changeData === null || changeData === void 0 ? void 0 : changeData.script); | ||
if (isChange) { | ||
@@ -289,29 +262,28 @@ changeFound = true; | ||
pubkey = changeData.pubkey; | ||
accountType.setOwnOutput(i, changeData.cond, [pubkey], [changePath]); | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
psbt.setOutputRedeemScript(i, changeData.redeemScript); | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
psbt.setOutputTapBip32Derivation(i, pubkey, [], masterFp, changePath); | ||
} | ||
} | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
} | ||
if (!changeFound) { | ||
throw new Error("Change script not found among outputs! " + | ||
(changeData === null || changeData === void 0 ? void 0 : changeData.cond.scriptPubKey.toString("hex"))); | ||
(changeData === null || changeData === void 0 ? void 0 : changeData.script.toString("hex"))); | ||
} | ||
key = createKey(masterFp, accountPath, accountXpub); | ||
p = new WalletPolicy(accountType.getDescriptorTemplate(), key); | ||
// This is cheating, because it's not actually requested on the | ||
// device yet, but it will be, soonish. | ||
if (arg.onDeviceSignatureRequested) | ||
arg.onDeviceSignatureRequested(); | ||
firstSigned = false; | ||
progressCallback = function () { | ||
if (!firstSigned) { | ||
firstSigned = true; | ||
arg.onDeviceSignatureGranted && arg.onDeviceSignatureGranted(); | ||
} | ||
progress(); | ||
}; | ||
return [4 /*yield*/, this.signPsbt(psbt, p, progressCallback)]; | ||
case 9: | ||
_a.sent(); | ||
finalize(psbt); | ||
serializedTx = extract(psbt); | ||
return [2 /*return*/, serializedTx.toString("hex")]; | ||
p = new WalletPolicy(accountType, key); | ||
return [4 /*yield*/, this.signPsbt(psbt, p)]; | ||
case 9: return [2 /*return*/, _a.sent()]; | ||
} | ||
@@ -321,13 +293,5 @@ }); | ||
}; | ||
/** | ||
* Calculates an output script along with public key and possible redeemScript | ||
* from a path and accountType. The accountPath must be a prefix of path. | ||
* | ||
* @returns an object with output script (property "script"), redeemScript (if | ||
* wrapped p2wpkh), and pubkey at provided path. The values of these three | ||
* properties depend on the accountType used. | ||
*/ | ||
BtcNew.prototype.outputScriptAt = function (accountPath, accountType, path) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var pathElems, i, xpub, pubkey, cond; | ||
var pathElems, i, xpub, pubkey, script; | ||
return __generator(this, function (_a) { | ||
@@ -346,8 +310,11 @@ switch (_a.label) { | ||
} | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElems)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElems)]; | ||
case 1: | ||
xpub = _a.sent(); | ||
pubkey = pubkeyFromXpub(xpub); | ||
cond = accountType.spendingCondition([pubkey]); | ||
return [2 /*return*/, { cond: cond, pubkey: pubkey }]; | ||
if (accountType == AccountType.p2tr) { | ||
pubkey = pubkey.slice(1); | ||
} | ||
script = outputScriptOf(pubkey, accountType); | ||
return [2 /*return*/, __assign(__assign({}, script), { pubkey: pubkey })]; | ||
} | ||
@@ -357,10 +324,5 @@ }); | ||
}; | ||
/** | ||
* Adds relevant data about an input to the psbt. This includes sequence, | ||
* previous txid, output index, spent UTXO, redeem script for wrapped p2wpkh, | ||
* public key and its derivation path. | ||
*/ | ||
BtcNew.prototype.setInput = function (psbt, i, input, pathElements, accountType, masterFP, sigHashType) { | ||
BtcNew.prototype.setInput = function (psbt, i, input, pathElements, accountType, masterFP) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var inputTx, spentOutputIndex, redeemScript, sequence, inputTxBuffer, inputTxid, xpubBase58, pubkey, spentTxOutput, spendCondition, spentOutput; | ||
var inputTx, spentOutputIndex, redeemScript, sequence, inputTxBuffer, inputTxid, xpubBase58, pubkey, spentOutput, expectedRedeemScript, xonly; | ||
return __generator(this, function (_a) { | ||
@@ -371,13 +333,10 @@ switch (_a.label) { | ||
spentOutputIndex = input[1]; | ||
redeemScript = input[2] ? Buffer.from(input[2], "hex") : undefined; | ||
redeemScript = input[2]; | ||
sequence = input[3]; | ||
if (sequence != undefined) { | ||
if (sequence) { | ||
psbt.setInputSequence(i, sequence); | ||
} | ||
if (sigHashType != undefined) { | ||
psbt.setInputSighashType(i, sigHashType); | ||
} | ||
inputTxBuffer = serializeTransaction(inputTx, true); | ||
inputTxid = crypto.hash256(inputTxBuffer); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
@@ -388,9 +347,30 @@ xpubBase58 = _a.sent(); | ||
throw Error("Missing outputs array in transaction to sign"); | ||
spentTxOutput = inputTx.outputs[spentOutputIndex]; | ||
spendCondition = { | ||
scriptPubKey: spentTxOutput.script, | ||
redeemScript: redeemScript | ||
}; | ||
spentOutput = { cond: spendCondition, amount: spentTxOutput.amount }; | ||
accountType.setInput(i, inputTxBuffer, spentOutput, [pubkey], [pathElements]); | ||
spentOutput = inputTx.outputs[spentOutputIndex]; | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
if (!redeemScript) { | ||
throw new Error("Missing redeemScript for p2wpkhWrapped input"); | ||
} | ||
expectedRedeemScript = createRedeemScript(pubkey); | ||
if (redeemScript != expectedRedeemScript.toString("hex")) { | ||
throw new Error("Unexpected redeemScript"); | ||
} | ||
psbt.setInputRedeemScript(i, expectedRedeemScript); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
xonly = pubkey.slice(1); | ||
psbt.setInputTapBip32Derivation(i, xonly, [], masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
psbt.setInputPreviousTxId(i, inputTxid); | ||
@@ -403,22 +383,13 @@ psbt.setInputOutputIndex(i, spentOutputIndex); | ||
}; | ||
/** | ||
* This implements the "Signer" role of the BIP370 transaction signing | ||
* process. | ||
* | ||
* It ssks the hardware device to sign the a psbt using the specified wallet | ||
* policy. This method assumes BIP32 derived keys are used for all inputs, see | ||
* comment in-line. The signatures returned from the hardware device is added | ||
* to the appropriate input fields of the PSBT. | ||
*/ | ||
BtcNew.prototype.signPsbt = function (psbt, walletPolicy, progressCallback) { | ||
BtcNew.prototype.signPsbt = function (psbt, walletPolicy) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var sigs; | ||
var sigs, serializedTx; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, this.client.signPsbt(psbt, walletPolicy, Buffer.alloc(32, 0), progressCallback)]; | ||
case 0: return [4 /*yield*/, this.client.signPsbt(psbt, walletPolicy, Buffer.alloc(32, 0))]; | ||
case 1: | ||
sigs = _a.sent(); | ||
sigs.forEach(function (v, k) { | ||
// Note: Looking at BIP32 derivation does not work in the generic case, | ||
// since some inputs might not have a BIP32-derived pubkey. | ||
// Note: Looking at BIP32 derivation does not work in the generic case. | ||
// some inputs might not have a BIP32-derived pubkey. | ||
var pubkeys = psbt.getInputKeyDatas(k, psbtIn.BIP32_DERIVATION); | ||
@@ -439,3 +410,5 @@ var pubkey; | ||
}); | ||
return [2 /*return*/]; | ||
finalize(psbt); | ||
serializedTx = extract(psbt); | ||
return [2 /*return*/, serializedTx.toString("hex")]; | ||
} | ||
@@ -448,22 +421,88 @@ }); | ||
export default BtcNew; | ||
function descrTemplFrom(addressFormat) { | ||
var AccountType; | ||
(function (AccountType) { | ||
AccountType["p2pkh"] = "pkh(@0)"; | ||
AccountType["p2wpkh"] = "wpkh(@0)"; | ||
AccountType["p2wpkhWrapped"] = "sh(wpkh(@0))"; | ||
AccountType["p2tr"] = "tr(@0)"; | ||
})(AccountType || (AccountType = {})); | ||
function createRedeemScript(pubkey) { | ||
var pubkeyHash = hashPublicKey(pubkey); | ||
return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]); | ||
} | ||
function outputScriptOf(pubkey, accountType) { | ||
var buf = new BufferWriter(); | ||
var pubkeyHash = hashPublicKey(pubkey); | ||
var redeemScript; | ||
if (accountType == AccountType.p2pkh) { | ||
buf.writeSlice(Buffer.of(OP_DUP, OP_HASH160, HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
buf.writeSlice(Buffer.of(OP_EQUALVERIFY, OP_CHECKSIG)); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
redeemScript = createRedeemScript(pubkey); | ||
var scriptHash = hashPublicKey(redeemScript); | ||
buf.writeSlice(Buffer.of(OP_HASH160, HASH_SIZE)); | ||
buf.writeSlice(scriptHash); | ||
buf.writeUInt8(OP_EQUAL); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
buf.writeSlice(Buffer.of(0, HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
console.log("Internal key: " + pubkey.toString("hex")); | ||
var outputKey = getTaprootOutputKey(pubkey); | ||
buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen | ||
buf.writeSlice(outputKey); | ||
} | ||
return { script: buf.buffer(), redeemScript: redeemScript }; | ||
} | ||
function accountTypeFrom(addressFormat) { | ||
if (addressFormat == "legacy") | ||
return "pkh(@0)"; | ||
return AccountType.p2pkh; | ||
if (addressFormat == "p2sh") | ||
return "sh(wpkh(@0))"; | ||
return AccountType.p2wpkhWrapped; | ||
if (addressFormat == "bech32") | ||
return "wpkh(@0)"; | ||
return AccountType.p2wpkh; | ||
if (addressFormat == "bech32m") | ||
return "tr(@0)"; | ||
return AccountType.p2tr; | ||
throw new Error("Unsupported address format " + addressFormat); | ||
} | ||
function accountTypeFromArg(arg, psbt, masterFp) { | ||
function accountTypeFromArg(arg) { | ||
if (arg.additionals.includes("bech32m")) | ||
return new p2tr(psbt, masterFp); | ||
return AccountType.p2tr; | ||
if (arg.additionals.includes("bech32")) | ||
return new p2wpkh(psbt, masterFp); | ||
return AccountType.p2wpkh; | ||
if (arg.segwit) | ||
return new p2wpkhWrapped(psbt, masterFp); | ||
return new p2pkh(psbt, masterFp); | ||
return AccountType.p2wpkhWrapped; | ||
return AccountType.p2pkh; | ||
} | ||
/* | ||
The following two functions are copied from wallet-btc and adapte. | ||
They should be moved to a library to avoid code reuse. | ||
*/ | ||
function hashTapTweak(x) { | ||
// hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340 | ||
// See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification | ||
var h = crypto.sha256(Buffer.from("TapTweak", "utf-8")); | ||
return crypto.sha256(Buffer.concat([h, h, x])); | ||
} | ||
function getTaprootOutputKey(internalPubkey) { | ||
if (internalPubkey.length != 32) { | ||
throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length); | ||
} | ||
// A BIP32 derived key can be converted to a schnorr pubkey by dropping | ||
// the first byte, which represent the oddness/evenness. In schnorr all | ||
// pubkeys are even. | ||
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion | ||
var evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]); | ||
var tweak = hashTapTweak(internalPubkey); | ||
// Q = P + int(hash_TapTweak(bytes(P)))G | ||
var outputEcdsaKey = Buffer.from(pointAddScalar(evenEcdsaPubkey, tweak)); | ||
// Convert to schnorr. | ||
var outputSchnorrKey = outputEcdsaKey.slice(1); | ||
// Create address | ||
return outputSchnorrKey; | ||
} | ||
//# sourceMappingURL=BtcNew.js.map |
/// <reference types="node" /> | ||
export declare function unsafeTo64bitLE(n: number): Buffer; | ||
export declare function unsafeFrom64bitLE(byteArray: Buffer): number; | ||
export declare class BufferWriter { | ||
@@ -10,3 +8,3 @@ private bufs; | ||
writeUInt32(i: number): void; | ||
writeUInt64(i: number): void; | ||
writeUInt64(i: bigint): void; | ||
writeVarInt(i: number): void; | ||
@@ -25,3 +23,3 @@ writeSlice(slice: Buffer): void; | ||
readUInt32(): number; | ||
readUInt64(): number; | ||
readUInt64(): bigint; | ||
readVarInt(): number; | ||
@@ -28,0 +26,0 @@ readSlice(n: number): Buffer; |
import varuint from "varuint-bitcoin"; | ||
export function unsafeTo64bitLE(n) { | ||
// we want to represent the input as a 8-bytes array | ||
if (n > Number.MAX_SAFE_INTEGER) { | ||
throw new Error("Can't convert numbers > MAX_SAFE_INT"); | ||
} | ||
var byteArray = Buffer.alloc(8, 0); | ||
for (var index = 0; index < byteArray.length; index++) { | ||
var byte = n & 0xff; | ||
byteArray[index] = byte; | ||
n = (n - byte) / 256; | ||
} | ||
return byteArray; | ||
} | ||
export function unsafeFrom64bitLE(byteArray) { | ||
var value = 0; | ||
if (byteArray.length != 8) { | ||
throw new Error("Expected Bufffer of lenght 8"); | ||
} | ||
if (byteArray[7] != 0) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
if (byteArray[6] > 0x1f) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
for (var i = byteArray.length - 1; i >= 0; i--) { | ||
value = value * 256 + byteArray[i]; | ||
} | ||
return value; | ||
} | ||
var BufferWriter = /** @class */ (function () { | ||
@@ -50,4 +21,3 @@ function BufferWriter() { | ||
BufferWriter.prototype.writeUInt64 = function (i) { | ||
var bytes = unsafeTo64bitLE(i); | ||
this.writeSlice(bytes); | ||
this.write(8, function (b) { return b.writeBigUInt64LE(i, 0); }); | ||
}; | ||
@@ -95,5 +65,5 @@ BufferWriter.prototype.writeVarInt = function (i) { | ||
BufferReader.prototype.readUInt64 = function () { | ||
var buf = this.readSlice(8); | ||
var n = unsafeFrom64bitLE(buf); | ||
return n; | ||
var result = this.buffer.readBigUInt64LE(this.offset); | ||
this.offset += 8; | ||
return result; | ||
}; | ||
@@ -100,0 +70,0 @@ BufferReader.prototype.readVarInt = function () { |
@@ -5,6 +5,2 @@ /// <reference types="node" /> | ||
import { WalletPolicy } from "./policy"; | ||
/** | ||
* This class encapsulates the APDU protocol documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
*/ | ||
export declare class AppClient { | ||
@@ -14,7 +10,7 @@ transport: Transport; | ||
private makeRequest; | ||
getExtendedPubkey(display: boolean, pathElements: number[]): Promise<string>; | ||
getPubkey(display: boolean, pathElements: number[]): Promise<string>; | ||
getWalletAddress(walletPolicy: WalletPolicy, walletHMAC: Buffer | null, change: number, addressIndex: number, display: boolean): Promise<string>; | ||
signPsbt(psbt: PsbtV2, walletPolicy: WalletPolicy, walletHMAC: Buffer | null, progressCallback: () => void): Promise<Map<number, Buffer>>; | ||
signPsbt(psbt: PsbtV2, walletPolicy: WalletPolicy, walletHMAC: Buffer | null): Promise<Map<number, Buffer>>; | ||
getMasterFingerprint(): Promise<Buffer>; | ||
} | ||
//# sourceMappingURL=appClient.d.ts.map |
@@ -68,6 +68,2 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
})(FrameworkIns || (FrameworkIns = {})); | ||
/** | ||
* This class encapsulates the APDU protocol documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
*/ | ||
var AppClient = /** @class */ (function () { | ||
@@ -105,3 +101,3 @@ function AppClient(transport) { | ||
}; | ||
AppClient.prototype.getExtendedPubkey = function (display, pathElements) { | ||
AppClient.prototype.getPubkey = function (display, pathElements) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -116,3 +112,3 @@ var response; | ||
return [4 /*yield*/, this.makeRequest(BitcoinIns.GET_PUBKEY, Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
pathElementsToBuffer(pathElements), | ||
@@ -140,3 +136,3 @@ ]))]; | ||
} | ||
clientInterpreter = new ClientCommandInterpreter(function () { }); | ||
clientInterpreter = new ClientCommandInterpreter(); | ||
clientInterpreter.addKnownList(walletPolicy.keys.map(function (k) { return Buffer.from(k, "ascii"); })); | ||
@@ -147,6 +143,6 @@ clientInterpreter.addKnownPreimage(walletPolicy.serialize()); | ||
return [4 /*yield*/, this.makeRequest(BitcoinIns.GET_WALLET_ADDRESS, Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
walletPolicy.getWalletId(), | ||
walletHMAC || Buffer.alloc(32, 0), | ||
Buffer.from([change]), | ||
Buffer.of(change), | ||
addressIndexBuffer, | ||
@@ -161,3 +157,3 @@ ]), clientInterpreter)]; | ||
}; | ||
AppClient.prototype.signPsbt = function (psbt, walletPolicy, walletHMAC, progressCallback) { | ||
AppClient.prototype.signPsbt = function (psbt, walletPolicy, walletHMAC) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -173,3 +169,3 @@ var merkelizedPsbt, clientInterpreter, _a, _b, map, _c, _d, map, inputMapsRoot, outputMapsRoot, yielded, ret, yielded_1, yielded_1_1, inputAndSig; | ||
} | ||
clientInterpreter = new ClientCommandInterpreter(progressCallback); | ||
clientInterpreter = new ClientCommandInterpreter(); | ||
// prepare ClientCommandInterpreter | ||
@@ -243,3 +239,3 @@ clientInterpreter.addKnownList(walletPolicy.keys.map(function (k) { return Buffer.from(k, "ascii"); })); | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.from([]))]; | ||
return [2 /*return*/, this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.of())]; | ||
}); | ||
@@ -246,0 +242,0 @@ }); |
@@ -16,6 +16,5 @@ /// <reference types="node" /> | ||
export declare class YieldCommand extends ClientCommand { | ||
private progressCallback; | ||
private results; | ||
code: ClientCommandCode; | ||
constructor(results: Buffer[], progressCallback: () => void); | ||
constructor(results: Buffer[]); | ||
execute(request: Buffer): Buffer; | ||
@@ -49,17 +48,2 @@ } | ||
} | ||
/** | ||
* This class will dispatch a client command coming from the hardware device to | ||
* the appropriate client command implementation. Those client commands | ||
* typically requests data from a merkle tree or merkelized maps. | ||
* | ||
* A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
* merkelized maps it should be able to serve to the hardware device. This class | ||
* doesn't know anything about the semantics of the data it holds, it just | ||
* serves merkle data. It doesn't even know in what context it is being | ||
* executed, ie SignPsbt, getWalletAddress, etc. | ||
* | ||
* If the command yelds results to the client, as signPsbt does, the yielded | ||
* data will be accessible after the command completed by calling getYielded(), | ||
* which will return the yields in the same order as they came in. | ||
*/ | ||
export declare class ClientCommandInterpreter { | ||
@@ -71,3 +55,3 @@ private roots; | ||
private commands; | ||
constructor(progressCallback: () => void); | ||
constructor(); | ||
getYielded(): Buffer[]; | ||
@@ -74,0 +58,0 @@ addKnownPreimage(preimage: Buffer): void; |
@@ -53,3 +53,2 @@ var __extends = (this && this.__extends) || (function () { | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { BufferReader } from "../buffertools"; | ||
import { createVarint } from "../varint"; | ||
@@ -72,5 +71,4 @@ import { hashLeaf, Merkle } from "./merkle"; | ||
__extends(YieldCommand, _super); | ||
function YieldCommand(results, progressCallback) { | ||
function YieldCommand(results) { | ||
var _this = _super.call(this) || this; | ||
_this.progressCallback = progressCallback; | ||
_this.code = ClientCommandCode.YIELD; | ||
@@ -82,3 +80,2 @@ _this.results = results; | ||
this.results.push(Buffer.from(request.subarray(1))); | ||
this.progressCallback(); | ||
return Buffer.from(""); | ||
@@ -99,3 +96,3 @@ }; | ||
GetPreimageCommand.prototype.execute = function (request) { | ||
var req = Buffer.from(request.subarray(1)); | ||
var req = request.subarray(1); | ||
// we expect no more data to read | ||
@@ -129,3 +126,3 @@ if (req.length != 1 + 32) { | ||
Buffer.from([payload_size]), | ||
Buffer.from(known_preimage.subarray(0, payload_size)), | ||
known_preimage.subarray(0, payload_size), | ||
]); | ||
@@ -149,18 +146,14 @@ } | ||
var _a; | ||
var req = Buffer.from(request.subarray(1)); | ||
if (req.length < 32 + 1 + 1) { | ||
throw new Error("Invalid request, expected at least 34 bytes"); | ||
var req = request.subarray(1); | ||
if (req.length != 32 + 4 + 4) { | ||
throw new Error("Invalid request, unexpected trailing data"); | ||
} | ||
var reqBuf = new BufferReader(req); | ||
var hash = reqBuf.readSlice(32); | ||
// read the hash | ||
var hash = Buffer.alloc(32); | ||
for (var i = 0; i < 32; i++) { | ||
hash[i] = req.readUInt8(i); | ||
} | ||
var hash_hex = hash.toString("hex"); | ||
var tree_size; | ||
var leaf_index; | ||
try { | ||
tree_size = reqBuf.readVarInt(); | ||
leaf_index = reqBuf.readVarInt(); | ||
} | ||
catch (e) { | ||
throw new Error("Invalid request, couldn't parse tree_size or leaf_index"); | ||
} | ||
var tree_size = req.readUInt32BE(32); | ||
var leaf_index = req.readUInt32BE(32 + 4); | ||
var mt = this.known_trees.get(hash_hex); | ||
@@ -201,3 +194,3 @@ if (!mt) { | ||
GetMerkleLeafIndexCommand.prototype.execute = function (request) { | ||
var req = Buffer.from(request.subarray(1)); | ||
var req = request.subarray(1); | ||
if (req.length != 32 + 32) { | ||
@@ -267,19 +260,4 @@ throw new Error("Invalid request, unexpected trailing data"); | ||
export { GetMoreElementsCommand }; | ||
/** | ||
* This class will dispatch a client command coming from the hardware device to | ||
* the appropriate client command implementation. Those client commands | ||
* typically requests data from a merkle tree or merkelized maps. | ||
* | ||
* A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
* merkelized maps it should be able to serve to the hardware device. This class | ||
* doesn't know anything about the semantics of the data it holds, it just | ||
* serves merkle data. It doesn't even know in what context it is being | ||
* executed, ie SignPsbt, getWalletAddress, etc. | ||
* | ||
* If the command yelds results to the client, as signPsbt does, the yielded | ||
* data will be accessible after the command completed by calling getYielded(), | ||
* which will return the yields in the same order as they came in. | ||
*/ | ||
var ClientCommandInterpreter = /** @class */ (function () { | ||
function ClientCommandInterpreter(progressCallback) { | ||
function ClientCommandInterpreter() { | ||
var e_1, _a; | ||
@@ -292,3 +270,3 @@ this.roots = new Map(); | ||
var commands = [ | ||
new YieldCommand(this.yielded, progressCallback), | ||
new YieldCommand(this.yielded), | ||
new GetPreimageCommand(this.preimages, this.queue), | ||
@@ -295,0 +273,0 @@ new GetMerkleLeafIndexCommand(this.roots), |
/// <reference types="node" /> | ||
import { MerkleMap } from "./merkleMap"; | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This class merkelizes a PSBTv2, by merkelizing the different | ||
* maps of the psbt. This is used during the transaction signing process, | ||
* where the hardware app can request specific parts of the psbt from the | ||
* client code and be sure that the response data actually belong to the psbt. | ||
* The reason for this is the limited amount of memory available to the app, | ||
* so it can't always store the full psbt in memory. | ||
* | ||
* The signing process is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt | ||
*/ | ||
export declare class MerkelizedPsbt extends PsbtV2 { | ||
@@ -16,0 +5,0 @@ globalMerkleMap: MerkleMap; |
@@ -43,13 +43,2 @@ var __extends = (this && this.__extends) || (function () { | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This class merkelizes a PSBTv2, by merkelizing the different | ||
* maps of the psbt. This is used during the transaction signing process, | ||
* where the hardware app can request specific parts of the psbt from the | ||
* client code and be sure that the response data actually belong to the psbt. | ||
* The reason for this is the limited amount of memory available to the app, | ||
* so it can't always store the full psbt in memory. | ||
* | ||
* The signing process is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt | ||
*/ | ||
var MerkelizedPsbt = /** @class */ (function (_super) { | ||
@@ -56,0 +45,0 @@ __extends(MerkelizedPsbt, _super); |
/// <reference types="node" /> | ||
/** | ||
* This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
* which is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md | ||
*/ | ||
export declare class Merkle { | ||
@@ -8,0 +3,0 @@ private leaves; |
@@ -27,7 +27,2 @@ var __read = (this && this.__read) || function (o, n) { | ||
import { crypto } from "bitcoinjs-lib"; | ||
/** | ||
* This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
* which is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md | ||
*/ | ||
var Merkle = /** @class */ (function () { | ||
@@ -83,3 +78,3 @@ function Merkle(leaves, hasher) { | ||
Merkle.prototype.hashNode = function (left, right) { | ||
return this.h(Buffer.concat([Buffer.from([1]), left, right])); | ||
return this.h(Buffer.concat([Buffer.of(1), left, right])); | ||
}; | ||
@@ -91,3 +86,3 @@ return Merkle; | ||
if (hashFunction === void 0) { hashFunction = crypto.sha256; } | ||
return hashConcat(Buffer.from([0]), buf, hashFunction); | ||
return hashConcat(Buffer.of(0), buf, hashFunction); | ||
} | ||
@@ -94,0 +89,0 @@ function hashConcat(bufA, bufB, hashFunction) { |
/// <reference types="node" /> | ||
import { Merkle } from "./merkle"; | ||
/** | ||
* This implements "Merkelized Maps", documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps | ||
* | ||
* A merkelized map consist of two merkle trees, one for the keys of | ||
* a map and one for the values of the same map, thus the two merkle | ||
* trees have the same shape. The commitment is the number elements | ||
* in the map followed by the keys' merkle root followed by the | ||
* values' merkle root. | ||
*/ | ||
export declare class MerkleMap { | ||
@@ -14,0 +4,0 @@ keys: Buffer[]; |
import { createVarint } from "../varint"; | ||
import { hashLeaf, Merkle } from "./merkle"; | ||
/** | ||
* This implements "Merkelized Maps", documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps | ||
* | ||
* A merkelized map consist of two merkle trees, one for the keys of | ||
* a map and one for the values of the same map, thus the two merkle | ||
* trees have the same shape. The commitment is the number elements | ||
* in the map followed by the keys' merkle root followed by the | ||
* values' merkle root. | ||
*/ | ||
var MerkleMap = /** @class */ (function () { | ||
@@ -14,0 +4,0 @@ /** |
/// <reference types="node" /> | ||
export declare type DefaultDescriptorTemplate = "pkh(@0)" | "sh(wpkh(@0))" | "wpkh(@0)" | "tr(@0)"; | ||
/** | ||
* The Bitcon hardware app uses a descriptors-like thing to describe | ||
* how to construct output scripts from keys. A "Wallet Policy" consists | ||
* of a "Descriptor Template" and a list of "keys". A key is basically | ||
* a serialized BIP32 extended public key with some added derivation path | ||
* information. This is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md | ||
*/ | ||
export declare class WalletPolicy { | ||
@@ -12,0 +4,0 @@ descriptorTemplate: string; |
@@ -1,13 +0,5 @@ | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { pathArrayToString } from "../bip32"; | ||
import { BufferWriter } from "../buffertools"; | ||
import { hashLeaf, Merkle } from "./merkle"; | ||
/** | ||
* The Bitcon hardware app uses a descriptors-like thing to describe | ||
* how to construct output scripts from keys. A "Wallet Policy" consists | ||
* of a "Descriptor Template" and a list of "keys". A key is basically | ||
* a serialized BIP32 extended public key with some added derivation path | ||
* information. This is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md | ||
*/ | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { Merkle, hashLeaf } from "./merkle"; | ||
var WalletPolicy = /** @class */ (function () { | ||
@@ -14,0 +6,0 @@ /** |
/// <reference types="node" /> | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However | ||
* the role is partially documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor). | ||
*/ | ||
export declare function extract(psbt: PsbtV2): Buffer; | ||
//# sourceMappingURL=psbtExtractor.d.ts.map |
import { BufferWriter } from "../buffertools"; | ||
/** | ||
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However | ||
* the role is partially documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor). | ||
*/ | ||
export function extract(psbt) { | ||
@@ -14,3 +8,3 @@ var _a, _b; | ||
if (isSegwit) { | ||
tx.writeSlice(Buffer.from([0, 1])); | ||
tx.writeSlice(Buffer.of(0, 1)); | ||
} | ||
@@ -23,3 +17,3 @@ var inputCount = psbt.getGlobalInputCount(); | ||
tx.writeUInt32(psbt.getInputOutputIndex(i)); | ||
tx.writeVarSlice((_a = psbt.getInputFinalScriptsig(i)) !== null && _a !== void 0 ? _a : Buffer.from([])); | ||
tx.writeVarSlice((_a = psbt.getInputFinalScriptsig(i)) !== null && _a !== void 0 ? _a : Buffer.of()); | ||
tx.writeUInt32(psbt.getInputSequence(i)); | ||
@@ -33,3 +27,3 @@ if (isSegwit) { | ||
for (var i = 0; i < outputCount; i++) { | ||
tx.writeUInt64(psbt.getOutputAmount(i)); | ||
tx.writeUInt64(BigInt(psbt.getOutputAmount(i))); | ||
tx.writeVarSlice(psbt.getOutputScript(i)); | ||
@@ -36,0 +30,0 @@ } |
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However | ||
* the role is documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). | ||
* | ||
* Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
* fields that aren't useful anymore, partial signatures, redeem script and | ||
* derivation paths. | ||
* | ||
* @param psbt The psbt with all signatures added as partial sigs, either | ||
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* @param psbt The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
*/ | ||
export declare function finalize(psbt: PsbtV2): void; | ||
//# sourceMappingURL=psbtFinalizer.d.ts.map |
import { BufferWriter } from "../buffertools"; | ||
import { psbtIn } from "./psbtv2"; | ||
/** | ||
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However | ||
* the role is documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). | ||
* | ||
* Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
* fields that aren't useful anymore, partial signatures, redeem script and | ||
* derivation paths. | ||
* | ||
* @param psbt The psbt with all signatures added as partial sigs, either | ||
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* @param psbt The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
*/ | ||
@@ -83,9 +73,2 @@ export function finalize(psbt) { | ||
} | ||
/** | ||
* Deletes fields that are no longer neccesary from the psbt. | ||
* | ||
* Note, the spec doesn't say anything about removing ouput fields | ||
* like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them | ||
* without actually knowing why. I think we should remove them too. | ||
*/ | ||
function clearFinalizedInput(psbt, inputIndex) { | ||
@@ -108,10 +91,2 @@ var keyTypes = [ | ||
} | ||
/** | ||
* Writes a script push operation to buf, which looks different | ||
* depending on the size of the data. See | ||
* https://en.bitcoin.it/wiki/Script#Constants | ||
* | ||
* @param buf the BufferWriter to write to | ||
* @param data the Buffer to be pushed. | ||
*/ | ||
function writePush(buf, data) { | ||
@@ -118,0 +93,0 @@ if (data.length <= 75) { |
@@ -14,3 +14,2 @@ /// <reference types="node" /> | ||
PARTIAL_SIG = 2, | ||
SIGHASH_TYPE = 3, | ||
REDEEM_SCRIPT = 4, | ||
@@ -35,20 +34,2 @@ BIP32_DERIVATION = 6, | ||
} | ||
/** | ||
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki | ||
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki | ||
* | ||
* A psbt is a data structure that can carry all relevant information about a | ||
* transaction through all stages of the signing process. From constructing an | ||
* unsigned transaction to extracting the final serialized transaction ready for | ||
* broadcast. | ||
* | ||
* This implementation is limited to what's needed in ledgerjs to carry out its | ||
* duties, which means that support for features like multisig or taproot script | ||
* path spending are not implemented. Specifically, it supports p2pkh, | ||
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
* | ||
* This class is made purposefully dumb, so it's easy to add support for | ||
* complemantary fields as needed in the future. | ||
*/ | ||
export declare class PsbtV2 { | ||
@@ -79,4 +60,2 @@ protected globalMap: Map<string, Buffer>; | ||
getInputPartialSig(inputIndex: number, pubkey: Buffer): Buffer | undefined; | ||
setInputSighashType(inputIndex: number, sigHashtype: number): void; | ||
getInputSighashType(inputIndex: number): number | undefined; | ||
setInputRedeemScript(inputIndex: number, redeemScript: Buffer): void; | ||
@@ -138,2 +117,3 @@ getInputRedeemScript(inputIndex: number): Buffer | undefined; | ||
private setInput; | ||
private getMap; | ||
private getInput; | ||
@@ -143,3 +123,3 @@ private getInputOptional; | ||
private getOutput; | ||
private getMap; | ||
private getOutputOptional; | ||
private encodeBip32Derivation; | ||
@@ -146,0 +126,0 @@ private decodeBip32Derivation; |
@@ -29,3 +29,3 @@ var __extends = (this && this.__extends) || (function () { | ||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ | ||
import { BufferReader, BufferWriter, unsafeFrom64bitLE, unsafeTo64bitLE, } from "../buffertools"; | ||
import { BufferReader, BufferWriter } from "../buffertools"; | ||
export var psbtGlobal; | ||
@@ -45,3 +45,2 @@ (function (psbtGlobal) { | ||
psbtIn[psbtIn["PARTIAL_SIG"] = 2] = "PARTIAL_SIG"; | ||
psbtIn[psbtIn["SIGHASH_TYPE"] = 3] = "SIGHASH_TYPE"; | ||
psbtIn[psbtIn["REDEEM_SCRIPT"] = 4] = "REDEEM_SCRIPT"; | ||
@@ -65,3 +64,3 @@ psbtIn[psbtIn["BIP32_DERIVATION"] = 6] = "BIP32_DERIVATION"; | ||
})(psbtOut || (psbtOut = {})); | ||
var PSBT_MAGIC_BYTES = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]); | ||
var PSBT_MAGIC_BYTES = Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff); | ||
var NoSuchEntry = /** @class */ (function (_super) { | ||
@@ -75,20 +74,2 @@ __extends(NoSuchEntry, _super); | ||
export { NoSuchEntry }; | ||
/** | ||
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki | ||
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki | ||
* | ||
* A psbt is a data structure that can carry all relevant information about a | ||
* transaction through all stages of the signing process. From constructing an | ||
* unsigned transaction to extracting the final serialized transaction ready for | ||
* broadcast. | ||
* | ||
* This implementation is limited to what's needed in ledgerjs to carry out its | ||
* duties, which means that support for features like multisig or taproot script | ||
* path spending are not implemented. Specifically, it supports p2pkh, | ||
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
* | ||
* This class is made purposefully dumb, so it's easy to add support for | ||
* complemantary fields as needed in the future. | ||
*/ | ||
var PsbtV2 = /** @class */ (function () { | ||
@@ -162,11 +143,2 @@ function PsbtV2() { | ||
}; | ||
PsbtV2.prototype.setInputSighashType = function (inputIndex, sigHashtype) { | ||
this.setInput(inputIndex, psbtIn.SIGHASH_TYPE, b(), uint32LE(sigHashtype)); | ||
}; | ||
PsbtV2.prototype.getInputSighashType = function (inputIndex) { | ||
var result = this.getInputOptional(inputIndex, psbtIn.SIGHASH_TYPE, b()); | ||
if (!result) | ||
return undefined; | ||
return result.readUInt32LE(0); | ||
}; | ||
PsbtV2.prototype.setInputRedeemScript = function (inputIndex, redeemScript) { | ||
@@ -256,4 +228,3 @@ this.setInput(inputIndex, psbtIn.REDEEM_SCRIPT, b(), redeemScript); | ||
PsbtV2.prototype.getOutputAmount = function (outputIndex) { | ||
var buf = this.getOutput(outputIndex, psbtOut.AMOUNT, b()); | ||
return unsafeFrom64bitLE(buf); | ||
return Number(this.getOutput(outputIndex, psbtOut.AMOUNT, b()).readBigUInt64LE(0)); | ||
}; | ||
@@ -301,3 +272,3 @@ PsbtV2.prototype.setOutputScript = function (outputIndex, scriptPubKey) { | ||
var buf = new BufferWriter(); | ||
buf.writeSlice(Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff])); | ||
buf.writeSlice(Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff)); | ||
serializeMap(buf, this.globalMap); | ||
@@ -356,3 +327,3 @@ this.inputMaps.forEach(function (map) { | ||
PsbtV2.prototype.setGlobal = function (keyType, value) { | ||
var key = new Key(keyType, Buffer.from([])); | ||
var key = new Key(keyType, Buffer.of()); | ||
this.globalMap.set(key.toString(), value); | ||
@@ -369,2 +340,8 @@ }; | ||
}; | ||
PsbtV2.prototype.getMap = function (index, maps) { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
}; | ||
PsbtV2.prototype.getInput = function (index, keyType, keyData) { | ||
@@ -382,7 +359,4 @@ return get(this.inputMaps[index], keyType, keyData, false); | ||
}; | ||
PsbtV2.prototype.getMap = function (index, maps) { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
PsbtV2.prototype.getOutputOptional = function (index, keyType, keyData) { | ||
return get(this.outputMaps[index], keyType, keyData, true); | ||
}; | ||
@@ -491,3 +465,3 @@ PsbtV2.prototype.encodeBip32Derivation = function (masterFingerprint, path) { | ||
function b() { | ||
return Buffer.from([]); | ||
return Buffer.of(); | ||
} | ||
@@ -504,3 +478,5 @@ function set(map, keyType, keyData, value) { | ||
function uint64LE(n) { | ||
return unsafeTo64bitLE(n); | ||
var b = Buffer.alloc(8); | ||
b.writeBigUInt64LE(BigInt(n), 0); | ||
return b; | ||
} | ||
@@ -507,0 +483,0 @@ function varint(n) { |
/// <reference types="node" /> | ||
import type Transport from "@ledgerhq/hw-transport"; | ||
import BtcNew from "./BtcNew"; | ||
import BtcOld from "./BtcOld"; | ||
import type { CreateTransactionArg } from "./createTransaction"; | ||
@@ -45,3 +43,3 @@ import type { AddressFormat } from "./getWalletPublicKey"; | ||
* | ||
* - bech32 format with 84' paths | ||
* - bech32 format with 173' paths | ||
* | ||
@@ -86,6 +84,6 @@ * - cashaddr in case of Bitcoin Cash | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param segwit is an optional boolean indicating wether to use segwit or not | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
@@ -95,3 +93,3 @@ * @param additionals list of additionnal options | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outputs | ||
* - "bech32m" for spending native segwit outputs | ||
* - "abc" for bch | ||
@@ -102,3 +100,3 @@ * - "gold" for btg | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions | ||
* @return the signed transaction ready to be broadcast | ||
@@ -150,5 +148,4 @@ * @example | ||
private inferCorrectImpl; | ||
protected old(): BtcOld; | ||
protected new(): BtcNew; | ||
private old; | ||
} | ||
//# sourceMappingURL=Btc.d.ts.map |
@@ -61,3 +61,2 @@ "use strict"; | ||
exports.__esModule = true; | ||
var bip32_1 = require("./bip32"); | ||
var BtcNew_1 = __importStar(require("./BtcNew")); | ||
@@ -118,3 +117,3 @@ var BtcOld_1 = __importDefault(require("./BtcOld")); | ||
* | ||
* - bech32 format with 84' paths | ||
* - bech32 format with 173' paths | ||
* | ||
@@ -128,3 +127,2 @@ * - cashaddr in case of Bitcoin Cash | ||
Btc.prototype.getWalletPublicKey = function (path, opts) { | ||
var _this = this; | ||
var options; | ||
@@ -143,35 +141,3 @@ if (arguments.length > 2 || typeof opts === "boolean") { | ||
return this.getCorrectImpl().then(function (impl) { | ||
/** | ||
* Definition: A "normal path" is a prefix of a standard path where all | ||
* the hardened steps of the standard path are included. For example, the | ||
* paths m/44'/1'/17' and m/44'/1'/17'/1 are normal paths, but m/44'/1' | ||
* is not. m/'199/1'/17'/0/1 is not a normal path either. | ||
* | ||
* There's a compatiblity issue between old and new app: When exporting | ||
* the key of a non-normal path with verify=false, the new app would | ||
* return an error, whereas the old app would return the key. | ||
* | ||
* See | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#get_extended_pubkey | ||
* | ||
* If format bech32m is used, we'll not use old, because it doesn't | ||
* support it. | ||
* | ||
* When to use new (given the app supports it) | ||
* * format is bech32m or | ||
* * path is normal or | ||
* * verify is true | ||
* | ||
* Otherwise use old. | ||
*/ | ||
if (impl instanceof BtcNew_1["default"] && | ||
options.format != "bech32m" && | ||
(!options.verify || options.verify == false) && | ||
!isPathNormal(path)) { | ||
console.warn("WARNING: Using deprecated device protocol to get the public key because\n \n * a non-standard path is requested, and\n * verify flag is false\n \n The new protocol only allows export of non-standard paths if the \n verify flag is true. Standard paths are (currently):\n\n M/44'/(1|0)'/X'\n M/49'/(1|0)'/X'\n M/84'/(1|0)'/X'\n M/86'/(1|0)'/X'\n M/48'/(1|0)'/X'/Y'\n\n followed by \"\", \"(0|1)\", or \"(0|1)/b\", where a and b are \n non-hardened. For example, the following paths are standard\n \n M/48'/1'/99'/7'\n M/86'/1'/99'/0\n M/48'/0'/99'/7'/1/17\n\n The following paths are non-standard\n\n M/48'/0'/99' // Not deepest hardened path\n M/48'/0'/99'/7'/1/17/2 // Too many non-hardened derivation steps\n M/199'/0'/1'/0/88 // Not a known purpose 199\n M/86'/1'/99'/2 // Change path item must be 0 or 1\n\n This compatibility safeguard will be removed in the future.\n Please consider calling Btc.getWalletXpub() instead."); | ||
return _this.old().getWalletPublicKey(path, options); | ||
} | ||
else { | ||
return impl.getWalletPublicKey(path, options); | ||
} | ||
return impl.getWalletPublicKey(path, options); | ||
}); | ||
@@ -201,6 +167,6 @@ }; | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param segwit is an optional boolean indicating wether to use segwit or not | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
@@ -210,3 +176,3 @@ * @param additionals list of additionnal options | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outputs | ||
* - "bech32m" for spending native segwit outputs | ||
* - "abc" for bch | ||
@@ -217,3 +183,3 @@ * - "gold" for btg | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions | ||
* @return the signed transaction ready to be broadcast | ||
@@ -313,6 +279,6 @@ * @example | ||
if (!canUseNewImplementation) { | ||
return [2 /*return*/, this.old()]; | ||
return [2 /*return*/, new BtcOld_1["default"](this.transport)]; | ||
} | ||
else { | ||
return [2 /*return*/, this["new"]()]; | ||
return [2 /*return*/, new BtcNew_1["default"](new appClient_1.AppClient(this.transport))]; | ||
} | ||
@@ -327,36 +293,5 @@ return [2 /*return*/]; | ||
}; | ||
Btc.prototype["new"] = function () { | ||
return new BtcNew_1["default"](new appClient_1.AppClient(this.transport)); | ||
}; | ||
return Btc; | ||
}()); | ||
exports["default"] = Btc; | ||
function isPathNormal(path) { | ||
//path is not deepest hardened node of a standard path or deeper, use BtcOld | ||
var h = 0x80000000; | ||
var pathElems = (0, bip32_1.pathStringToArray)(path); | ||
var hard = function (n) { return n >= h; }; | ||
var soft = function (n) { return !n || n < h; }; | ||
var change = function (n) { return !n || n == 0 || n == 1; }; | ||
if (pathElems.length >= 3 && | ||
pathElems.length <= 5 && | ||
[44 + h, 49 + h, 84 + h, 86 + h].some(function (v) { return v == pathElems[0]; }) && | ||
[0 + h, 1 + h].some(function (v) { return v == pathElems[1]; }) && | ||
hard(pathElems[2]) && | ||
change(pathElems[3]) && | ||
soft(pathElems[4])) { | ||
return true; | ||
} | ||
if (pathElems.length >= 4 && | ||
pathElems.length <= 6 && | ||
48 + h == pathElems[0] && | ||
[0 + h, 1 + h].some(function (v) { return v == pathElems[1]; }) && | ||
hard(pathElems[2]) && | ||
hard(pathElems[3]) && | ||
change(pathElems[4]) && | ||
soft(pathElems[5])) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
//# sourceMappingURL=Btc.js.map |
import type { CreateTransactionArg } from "./createTransaction"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
import type { AddressFormat } from "./getWalletPublicKey"; | ||
import { AppClient as Client } from "./newops/appClient"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
export declare function canSupportApp(appAndVersion: AppAndVersion): boolean; | ||
/** | ||
* This class implements the same interface as BtcOld (formerly | ||
* named Btc), but interacts with Bitcoin hardware app version 2+ | ||
* which uses a totally new APDU protocol. This new | ||
* protocol is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
* | ||
* Since the interface must remain compatible with BtcOld, the methods | ||
* of this class are quite clunky, because it needs to adapt legacy | ||
* input data into the PSBT process. In the future, a new interface should | ||
* be developed that exposes PSBT to the outer world, which would render | ||
* a much cleaner implementation. | ||
*/ | ||
export default class BtcNew { | ||
private client; | ||
constructor(client: Client); | ||
/** | ||
* This is a new method that allow users to get an xpub at a standard path. | ||
* Standard paths are described at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description | ||
* | ||
* This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
* M/44'/N'/x'/** | ||
* M/48'/N'/x'/y'/** | ||
* M/49'/N'/x'/** | ||
* M/84'/N'/x'/** | ||
* M/86'/N'/x'/** | ||
* | ||
* The method was added because of added security in the hardware app v2+. The | ||
* new hardware app will allow export of any xpub up to and including the | ||
* deepest hardened key of standard derivation paths, whereas the old app | ||
* would allow export of any key. | ||
* | ||
* This caused an issue for callers of this class, who only had | ||
* getWalletPublicKey() to call which means they have to constuct xpub | ||
* themselves: | ||
* | ||
* Suppose a user of this class wants to create an account xpub on a standard | ||
* path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
* by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
* it only allows exporting deepest level hardened path. So the options are to | ||
* allow requesting M/44'/0' from the app, or to add a new function | ||
* "getWalletXpub". | ||
* | ||
* We opted for adding a new function, which can greatly simplify client code. | ||
*/ | ||
getWalletXpub({ path, xpubVersion, }: { | ||
@@ -56,9 +13,2 @@ path: string; | ||
}): Promise<string>; | ||
/** | ||
* This method returns a public key, a bitcoin address, and and a chaincode | ||
* for a specific derivation path. | ||
* | ||
* Limitation: If the path is not a leaf node of a standard path, the address | ||
* will be the empty string "", see this.getWalletAddress() for details. | ||
*/ | ||
getWalletPublicKey(path: string, opts?: { | ||
@@ -81,3 +31,3 @@ verify?: boolean; | ||
* get it from the device to save development time. However, it shouldn't take | ||
* too much time to implement local address generation. | ||
* more than a few hours to implement local address generation. | ||
* | ||
@@ -90,36 +40,39 @@ * Moreover, if the path is not for a leaf, ie accountPath+/X/Y, there is no | ||
/** | ||
* Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
* details on how to use this method. | ||
* To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
* @param inputs is an array of [ transaction, output_index, optional redeem script, optional sequence ] where | ||
* | ||
* This method will convert the legacy arguments, CreateTransactionArg, into | ||
* a psbt which is finally signed and finalized, and the extracted fully signed | ||
* transaction is returned. | ||
* * transaction is the previously computed transaction object for this UTXO | ||
* * output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* * redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* * sequence is the sequence number to use for this input (when using RBF), or non present | ||
* @param associatedKeysets is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* @param additionals list of additionnal options | ||
* | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outptus | ||
* - "abc" for bch | ||
* - "gold" for btg | ||
* - "bipxxx" for using BIPxxx | ||
* - "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @return the signed transaction ready to be broadcast | ||
* @example | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
*/ | ||
createPaymentTransactionNew(arg: CreateTransactionArg): Promise<string>; | ||
/** | ||
* Calculates an output script along with public key and possible redeemScript | ||
* from a path and accountType. The accountPath must be a prefix of path. | ||
* | ||
* @returns an object with output script (property "script"), redeemScript (if | ||
* wrapped p2wpkh), and pubkey at provided path. The values of these three | ||
* properties depend on the accountType used. | ||
*/ | ||
private outputScriptAt; | ||
/** | ||
* Adds relevant data about an input to the psbt. This includes sequence, | ||
* previous txid, output index, spent UTXO, redeem script for wrapped p2wpkh, | ||
* public key and its derivation path. | ||
*/ | ||
private setInput; | ||
/** | ||
* This implements the "Signer" role of the BIP370 transaction signing | ||
* process. | ||
* | ||
* It ssks the hardware device to sign the a psbt using the specified wallet | ||
* policy. This method assumes BIP32 derived keys are used for all inputs, see | ||
* comment in-line. The signatures returned from the hardware device is added | ||
* to the appropriate input fields of the PSBT. | ||
*/ | ||
private signPsbt; | ||
} | ||
//# sourceMappingURL=BtcNew.d.ts.map |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -44,7 +55,7 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
var bitcoinjs_lib_1 = require("bitcoinjs-lib"); | ||
var tiny_secp256k1_1 = require("tiny-secp256k1"); | ||
var semver_1 = __importDefault(require("semver")); | ||
var tiny_secp256k1_1 = require("tiny-secp256k1"); | ||
var bip32_1 = require("./bip32"); | ||
var buffertools_1 = require("./buffertools"); | ||
var accounttype_1 = require("./newops/accounttype"); | ||
var hashPublicKey_1 = require("./hashPublicKey"); | ||
var policy_1 = require("./newops/policy"); | ||
@@ -55,2 +66,3 @@ var psbtExtractor_1 = require("./newops/psbtExtractor"); | ||
var serializeTransaction_1 = require("./serializeTransaction"); | ||
var constants_1 = require("./constants"); | ||
var newSupportedApps = ["Bitcoin", "Bitcoin Test"]; | ||
@@ -62,15 +74,2 @@ function canSupportApp(appAndVersion) { | ||
exports.canSupportApp = canSupportApp; | ||
/** | ||
* This class implements the same interface as BtcOld (formerly | ||
* named Btc), but interacts with Bitcoin hardware app version 2+ | ||
* which uses a totally new APDU protocol. This new | ||
* protocol is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
* | ||
* Since the interface must remain compatible with BtcOld, the methods | ||
* of this class are quite clunky, because it needs to adapt legacy | ||
* input data into the PSBT process. In the future, a new interface should | ||
* be developed that exposes PSBT to the outer world, which would render | ||
* a much cleaner implementation. | ||
*/ | ||
var BtcNew = /** @class */ (function () { | ||
@@ -80,32 +79,2 @@ function BtcNew(client) { | ||
} | ||
/** | ||
* This is a new method that allow users to get an xpub at a standard path. | ||
* Standard paths are described at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description | ||
* | ||
* This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
* M/44'/N'/x'/** | ||
* M/48'/N'/x'/y'/** | ||
* M/49'/N'/x'/** | ||
* M/84'/N'/x'/** | ||
* M/86'/N'/x'/** | ||
* | ||
* The method was added because of added security in the hardware app v2+. The | ||
* new hardware app will allow export of any xpub up to and including the | ||
* deepest hardened key of standard derivation paths, whereas the old app | ||
* would allow export of any key. | ||
* | ||
* This caused an issue for callers of this class, who only had | ||
* getWalletPublicKey() to call which means they have to constuct xpub | ||
* themselves: | ||
* | ||
* Suppose a user of this class wants to create an account xpub on a standard | ||
* path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
* by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
* it only allows exporting deepest level hardened path. So the options are to | ||
* allow requesting M/44'/0' from the app, or to add a new function | ||
* "getWalletXpub". | ||
* | ||
* We opted for adding a new function, which can greatly simplify client code. | ||
*/ | ||
BtcNew.prototype.getWalletXpub = function (_a) { | ||
@@ -119,3 +88,3 @@ var path = _a.path, xpubVersion = _a.xpubVersion; | ||
pathElements = (0, bip32_1.pathStringToArray)(path); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
@@ -132,9 +101,2 @@ xpub = _b.sent(); | ||
}; | ||
/** | ||
* This method returns a public key, a bitcoin address, and and a chaincode | ||
* for a specific derivation path. | ||
* | ||
* Limitation: If the path is not a leaf node of a standard path, the address | ||
* will be the empty string "", see this.getWalletAddress() for details. | ||
*/ | ||
BtcNew.prototype.getWalletPublicKey = function (path, opts) { | ||
@@ -148,7 +110,7 @@ var _a, _b; | ||
pathElements = (0, bip32_1.pathStringToArray)(path); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
xpub = _c.sent(); | ||
display = (_a = opts === null || opts === void 0 ? void 0 : opts.verify) !== null && _a !== void 0 ? _a : false; | ||
return [4 /*yield*/, this.getWalletAddress(pathElements, descrTemplFrom((_b = opts === null || opts === void 0 ? void 0 : opts.format) !== null && _b !== void 0 ? _b : "legacy"), display)]; | ||
return [4 /*yield*/, this.getWalletAddress(pathElements, accountTypeFrom((_b = opts === null || opts === void 0 ? void 0 : opts.format) !== null && _b !== void 0 ? _b : "legacy"), display)]; | ||
case 2: | ||
@@ -176,3 +138,3 @@ address = _c.sent(); | ||
* get it from the device to save development time. However, it shouldn't take | ||
* too much time to implement local address generation. | ||
* more than a few hours to implement local address generation. | ||
* | ||
@@ -183,3 +145,3 @@ * Moreover, if the path is not for a leaf, ie accountPath+/X/Y, there is no | ||
*/ | ||
BtcNew.prototype.getWalletAddress = function (pathElements, descrTempl, display) { | ||
BtcNew.prototype.getWalletAddress = function (pathElements, accountType, display) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -194,3 +156,3 @@ var accountPath, accountXpub, masterFingerprint, policy, changeAndIndex; | ||
} | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, accountPath)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, accountPath)]; | ||
case 1: | ||
@@ -201,3 +163,3 @@ accountXpub = _a.sent(); | ||
masterFingerprint = _a.sent(); | ||
policy = new policy_1.WalletPolicy(descrTempl, (0, policy_1.createKey)(masterFingerprint, accountPath, accountXpub)); | ||
policy = new policy_1.WalletPolicy(accountType, (0, policy_1.createKey)(masterFingerprint, accountPath, accountXpub)); | ||
changeAndIndex = pathElements.slice(-2, pathElements.length); | ||
@@ -210,41 +172,55 @@ return [2 /*return*/, this.client.getWalletAddress(policy, Buffer.alloc(32, 0), changeAndIndex[0], changeAndIndex[1], display)]; | ||
/** | ||
* Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
* details on how to use this method. | ||
* To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
* @param inputs is an array of [ transaction, output_index, optional redeem script, optional sequence ] where | ||
* | ||
* This method will convert the legacy arguments, CreateTransactionArg, into | ||
* a psbt which is finally signed and finalized, and the extracted fully signed | ||
* transaction is returned. | ||
* * transaction is the previously computed transaction object for this UTXO | ||
* * output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* * redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* * sequence is the sequence number to use for this input (when using RBF), or non present | ||
* @param associatedKeysets is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* @param additionals list of additionnal options | ||
* | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outptus | ||
* - "abc" for bch | ||
* - "gold" for btg | ||
* - "bipxxx" for using BIPxxx | ||
* - "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @return the signed transaction ready to be broadcast | ||
* @example | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
*/ | ||
BtcNew.prototype.createPaymentTransactionNew = function (arg) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var inputCount, psbt, masterFp, accountType, notifyCount, progress, accountXpub, accountPath, i, pathElems, outputsConcat, outputsBufferReader, outputCount, changeData, changeFound, i, amount, outputScript, isChange, changePath, pubkey, key, p, firstSigned, progressCallback, serializedTx; | ||
var psbt, accountType, masterFp, accountXpub, accountPath, i, pathElems, outputsConcat, outputsBufferReader, outputCount, changeData, changeFound, i, amount, outputScript, isChange, changePath, pubkey, key, p; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
inputCount = arg.inputs.length; | ||
if (inputCount == 0) { | ||
if (arg.inputs.length == 0) { | ||
throw Error("No inputs"); | ||
} | ||
psbt = new psbtv2_1.PsbtV2(); | ||
return [4 /*yield*/, this.client.getMasterFingerprint()]; | ||
case 1: | ||
masterFp = _a.sent(); | ||
accountType = accountTypeFromArg(arg, psbt, masterFp); | ||
if (arg.lockTime != undefined) { | ||
// The signer will assume locktime 0 if unset | ||
accountType = accountTypeFromArg(arg); | ||
psbt.setGlobalTxVersion(2); | ||
if (arg.lockTime) { | ||
psbt.setGlobalFallbackLocktime(arg.lockTime); | ||
} | ||
psbt.setGlobalInputCount(inputCount); | ||
psbt.setGlobalInputCount(arg.inputs.length); | ||
psbt.setGlobalPsbtVersion(2); | ||
psbt.setGlobalTxVersion(2); | ||
notifyCount = 0; | ||
progress = function () { | ||
if (!arg.onDeviceStreaming) | ||
return; | ||
arg.onDeviceStreaming({ | ||
total: 2 * inputCount, | ||
index: notifyCount, | ||
progress: ++notifyCount / (2 * inputCount) | ||
}); | ||
}; | ||
return [4 /*yield*/, this.client.getMasterFingerprint()]; | ||
case 1: | ||
masterFp = _a.sent(); | ||
accountXpub = ""; | ||
@@ -255,4 +231,3 @@ accountPath = []; | ||
case 2: | ||
if (!(i < inputCount)) return [3 /*break*/, 7]; | ||
progress(); | ||
if (!(i < arg.inputs.length)) return [3 /*break*/, 7]; | ||
pathElems = (0, bip32_1.pathStringToArray)(arg.associatedKeysets[i]); | ||
@@ -263,7 +238,7 @@ if (!(accountXpub == "")) return [3 /*break*/, 4]; | ||
accountPath = pathElems.slice(0, -2); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, accountPath)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, accountPath)]; | ||
case 3: | ||
accountXpub = _a.sent(); | ||
_a.label = 4; | ||
case 4: return [4 /*yield*/, this.setInput(psbt, i, arg.inputs[i], pathElems, accountType, masterFp, arg.sigHashType)]; | ||
case 4: return [4 /*yield*/, this.setInput(psbt, i, arg.inputs[i], pathElems, accountType, masterFp)]; | ||
case 5: | ||
@@ -279,6 +254,6 @@ _a.sent(); | ||
outputCount = outputsBufferReader.readVarInt(); | ||
psbt.setGlobalOutputCount(outputCount); | ||
return [4 /*yield*/, this.outputScriptAt(accountPath, accountType, arg.changePath)]; | ||
case 8: | ||
changeData = _a.sent(); | ||
psbt.setGlobalOutputCount(outputCount); | ||
changeFound = !changeData; | ||
@@ -288,5 +263,3 @@ for (i = 0; i < outputCount; i++) { | ||
outputScript = outputsBufferReader.readVarSlice(); | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
isChange = changeData && outputScript.equals(changeData === null || changeData === void 0 ? void 0 : changeData.cond.scriptPubKey); | ||
isChange = changeData && outputScript.equals(changeData === null || changeData === void 0 ? void 0 : changeData.script); | ||
if (isChange) { | ||
@@ -296,29 +269,28 @@ changeFound = true; | ||
pubkey = changeData.pubkey; | ||
accountType.setOwnOutput(i, changeData.cond, [pubkey], [changePath]); | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
psbt.setOutputRedeemScript(i, changeData.redeemScript); | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
psbt.setOutputTapBip32Derivation(i, pubkey, [], masterFp, changePath); | ||
} | ||
} | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
} | ||
if (!changeFound) { | ||
throw new Error("Change script not found among outputs! " + | ||
(changeData === null || changeData === void 0 ? void 0 : changeData.cond.scriptPubKey.toString("hex"))); | ||
(changeData === null || changeData === void 0 ? void 0 : changeData.script.toString("hex"))); | ||
} | ||
key = (0, policy_1.createKey)(masterFp, accountPath, accountXpub); | ||
p = new policy_1.WalletPolicy(accountType.getDescriptorTemplate(), key); | ||
// This is cheating, because it's not actually requested on the | ||
// device yet, but it will be, soonish. | ||
if (arg.onDeviceSignatureRequested) | ||
arg.onDeviceSignatureRequested(); | ||
firstSigned = false; | ||
progressCallback = function () { | ||
if (!firstSigned) { | ||
firstSigned = true; | ||
arg.onDeviceSignatureGranted && arg.onDeviceSignatureGranted(); | ||
} | ||
progress(); | ||
}; | ||
return [4 /*yield*/, this.signPsbt(psbt, p, progressCallback)]; | ||
case 9: | ||
_a.sent(); | ||
(0, psbtFinalizer_1.finalize)(psbt); | ||
serializedTx = (0, psbtExtractor_1.extract)(psbt); | ||
return [2 /*return*/, serializedTx.toString("hex")]; | ||
p = new policy_1.WalletPolicy(accountType, key); | ||
return [4 /*yield*/, this.signPsbt(psbt, p)]; | ||
case 9: return [2 /*return*/, _a.sent()]; | ||
} | ||
@@ -328,13 +300,5 @@ }); | ||
}; | ||
/** | ||
* Calculates an output script along with public key and possible redeemScript | ||
* from a path and accountType. The accountPath must be a prefix of path. | ||
* | ||
* @returns an object with output script (property "script"), redeemScript (if | ||
* wrapped p2wpkh), and pubkey at provided path. The values of these three | ||
* properties depend on the accountType used. | ||
*/ | ||
BtcNew.prototype.outputScriptAt = function (accountPath, accountType, path) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var pathElems, i, xpub, pubkey, cond; | ||
var pathElems, i, xpub, pubkey, script; | ||
return __generator(this, function (_a) { | ||
@@ -353,8 +317,11 @@ switch (_a.label) { | ||
} | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElems)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElems)]; | ||
case 1: | ||
xpub = _a.sent(); | ||
pubkey = (0, bip32_1.pubkeyFromXpub)(xpub); | ||
cond = accountType.spendingCondition([pubkey]); | ||
return [2 /*return*/, { cond: cond, pubkey: pubkey }]; | ||
if (accountType == AccountType.p2tr) { | ||
pubkey = pubkey.slice(1); | ||
} | ||
script = outputScriptOf(pubkey, accountType); | ||
return [2 /*return*/, __assign(__assign({}, script), { pubkey: pubkey })]; | ||
} | ||
@@ -364,10 +331,5 @@ }); | ||
}; | ||
/** | ||
* Adds relevant data about an input to the psbt. This includes sequence, | ||
* previous txid, output index, spent UTXO, redeem script for wrapped p2wpkh, | ||
* public key and its derivation path. | ||
*/ | ||
BtcNew.prototype.setInput = function (psbt, i, input, pathElements, accountType, masterFP, sigHashType) { | ||
BtcNew.prototype.setInput = function (psbt, i, input, pathElements, accountType, masterFP) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var inputTx, spentOutputIndex, redeemScript, sequence, inputTxBuffer, inputTxid, xpubBase58, pubkey, spentTxOutput, spendCondition, spentOutput; | ||
var inputTx, spentOutputIndex, redeemScript, sequence, inputTxBuffer, inputTxid, xpubBase58, pubkey, spentOutput, expectedRedeemScript, xonly; | ||
return __generator(this, function (_a) { | ||
@@ -378,13 +340,10 @@ switch (_a.label) { | ||
spentOutputIndex = input[1]; | ||
redeemScript = input[2] ? Buffer.from(input[2], "hex") : undefined; | ||
redeemScript = input[2]; | ||
sequence = input[3]; | ||
if (sequence != undefined) { | ||
if (sequence) { | ||
psbt.setInputSequence(i, sequence); | ||
} | ||
if (sigHashType != undefined) { | ||
psbt.setInputSighashType(i, sigHashType); | ||
} | ||
inputTxBuffer = (0, serializeTransaction_1.serializeTransaction)(inputTx, true); | ||
inputTxid = bitcoinjs_lib_1.crypto.hash256(inputTxBuffer); | ||
return [4 /*yield*/, this.client.getExtendedPubkey(false, pathElements)]; | ||
return [4 /*yield*/, this.client.getPubkey(false, pathElements)]; | ||
case 1: | ||
@@ -395,9 +354,30 @@ xpubBase58 = _a.sent(); | ||
throw Error("Missing outputs array in transaction to sign"); | ||
spentTxOutput = inputTx.outputs[spentOutputIndex]; | ||
spendCondition = { | ||
scriptPubKey: spentTxOutput.script, | ||
redeemScript: redeemScript | ||
}; | ||
spentOutput = { cond: spendCondition, amount: spentTxOutput.amount }; | ||
accountType.setInput(i, inputTxBuffer, spentOutput, [pubkey], [pathElements]); | ||
spentOutput = inputTx.outputs[spentOutputIndex]; | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
if (!redeemScript) { | ||
throw new Error("Missing redeemScript for p2wpkhWrapped input"); | ||
} | ||
expectedRedeemScript = createRedeemScript(pubkey); | ||
if (redeemScript != expectedRedeemScript.toString("hex")) { | ||
throw new Error("Unexpected redeemScript"); | ||
} | ||
psbt.setInputRedeemScript(i, expectedRedeemScript); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
xonly = pubkey.slice(1); | ||
psbt.setInputTapBip32Derivation(i, xonly, [], masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
psbt.setInputPreviousTxId(i, inputTxid); | ||
@@ -410,22 +390,13 @@ psbt.setInputOutputIndex(i, spentOutputIndex); | ||
}; | ||
/** | ||
* This implements the "Signer" role of the BIP370 transaction signing | ||
* process. | ||
* | ||
* It ssks the hardware device to sign the a psbt using the specified wallet | ||
* policy. This method assumes BIP32 derived keys are used for all inputs, see | ||
* comment in-line. The signatures returned from the hardware device is added | ||
* to the appropriate input fields of the PSBT. | ||
*/ | ||
BtcNew.prototype.signPsbt = function (psbt, walletPolicy, progressCallback) { | ||
BtcNew.prototype.signPsbt = function (psbt, walletPolicy) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var sigs; | ||
var sigs, serializedTx; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, this.client.signPsbt(psbt, walletPolicy, Buffer.alloc(32, 0), progressCallback)]; | ||
case 0: return [4 /*yield*/, this.client.signPsbt(psbt, walletPolicy, Buffer.alloc(32, 0))]; | ||
case 1: | ||
sigs = _a.sent(); | ||
sigs.forEach(function (v, k) { | ||
// Note: Looking at BIP32 derivation does not work in the generic case, | ||
// since some inputs might not have a BIP32-derived pubkey. | ||
// Note: Looking at BIP32 derivation does not work in the generic case. | ||
// some inputs might not have a BIP32-derived pubkey. | ||
var pubkeys = psbt.getInputKeyDatas(k, psbtv2_1.psbtIn.BIP32_DERIVATION); | ||
@@ -446,3 +417,5 @@ var pubkey; | ||
}); | ||
return [2 /*return*/]; | ||
(0, psbtFinalizer_1.finalize)(psbt); | ||
serializedTx = (0, psbtExtractor_1.extract)(psbt); | ||
return [2 /*return*/, serializedTx.toString("hex")]; | ||
} | ||
@@ -455,22 +428,88 @@ }); | ||
exports["default"] = BtcNew; | ||
function descrTemplFrom(addressFormat) { | ||
var AccountType; | ||
(function (AccountType) { | ||
AccountType["p2pkh"] = "pkh(@0)"; | ||
AccountType["p2wpkh"] = "wpkh(@0)"; | ||
AccountType["p2wpkhWrapped"] = "sh(wpkh(@0))"; | ||
AccountType["p2tr"] = "tr(@0)"; | ||
})(AccountType || (AccountType = {})); | ||
function createRedeemScript(pubkey) { | ||
var pubkeyHash = (0, hashPublicKey_1.hashPublicKey)(pubkey); | ||
return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]); | ||
} | ||
function outputScriptOf(pubkey, accountType) { | ||
var buf = new buffertools_1.BufferWriter(); | ||
var pubkeyHash = (0, hashPublicKey_1.hashPublicKey)(pubkey); | ||
var redeemScript; | ||
if (accountType == AccountType.p2pkh) { | ||
buf.writeSlice(Buffer.of(constants_1.OP_DUP, constants_1.OP_HASH160, constants_1.HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
buf.writeSlice(Buffer.of(constants_1.OP_EQUALVERIFY, constants_1.OP_CHECKSIG)); | ||
} | ||
else if (accountType == AccountType.p2wpkhWrapped) { | ||
redeemScript = createRedeemScript(pubkey); | ||
var scriptHash = (0, hashPublicKey_1.hashPublicKey)(redeemScript); | ||
buf.writeSlice(Buffer.of(constants_1.OP_HASH160, constants_1.HASH_SIZE)); | ||
buf.writeSlice(scriptHash); | ||
buf.writeUInt8(constants_1.OP_EQUAL); | ||
} | ||
else if (accountType == AccountType.p2wpkh) { | ||
buf.writeSlice(Buffer.of(0, constants_1.HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
} | ||
else if (accountType == AccountType.p2tr) { | ||
console.log("Internal key: " + pubkey.toString("hex")); | ||
var outputKey = getTaprootOutputKey(pubkey); | ||
buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen | ||
buf.writeSlice(outputKey); | ||
} | ||
return { script: buf.buffer(), redeemScript: redeemScript }; | ||
} | ||
function accountTypeFrom(addressFormat) { | ||
if (addressFormat == "legacy") | ||
return "pkh(@0)"; | ||
return AccountType.p2pkh; | ||
if (addressFormat == "p2sh") | ||
return "sh(wpkh(@0))"; | ||
return AccountType.p2wpkhWrapped; | ||
if (addressFormat == "bech32") | ||
return "wpkh(@0)"; | ||
return AccountType.p2wpkh; | ||
if (addressFormat == "bech32m") | ||
return "tr(@0)"; | ||
return AccountType.p2tr; | ||
throw new Error("Unsupported address format " + addressFormat); | ||
} | ||
function accountTypeFromArg(arg, psbt, masterFp) { | ||
function accountTypeFromArg(arg) { | ||
if (arg.additionals.includes("bech32m")) | ||
return new accounttype_1.p2tr(psbt, masterFp); | ||
return AccountType.p2tr; | ||
if (arg.additionals.includes("bech32")) | ||
return new accounttype_1.p2wpkh(psbt, masterFp); | ||
return AccountType.p2wpkh; | ||
if (arg.segwit) | ||
return new accounttype_1.p2wpkhWrapped(psbt, masterFp); | ||
return new accounttype_1.p2pkh(psbt, masterFp); | ||
return AccountType.p2wpkhWrapped; | ||
return AccountType.p2pkh; | ||
} | ||
/* | ||
The following two functions are copied from wallet-btc and adapte. | ||
They should be moved to a library to avoid code reuse. | ||
*/ | ||
function hashTapTweak(x) { | ||
// hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340 | ||
// See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification | ||
var h = bitcoinjs_lib_1.crypto.sha256(Buffer.from("TapTweak", "utf-8")); | ||
return bitcoinjs_lib_1.crypto.sha256(Buffer.concat([h, h, x])); | ||
} | ||
function getTaprootOutputKey(internalPubkey) { | ||
if (internalPubkey.length != 32) { | ||
throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length); | ||
} | ||
// A BIP32 derived key can be converted to a schnorr pubkey by dropping | ||
// the first byte, which represent the oddness/evenness. In schnorr all | ||
// pubkeys are even. | ||
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion | ||
var evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]); | ||
var tweak = hashTapTweak(internalPubkey); | ||
// Q = P + int(hash_TapTweak(bytes(P)))G | ||
var outputEcdsaKey = Buffer.from((0, tiny_secp256k1_1.pointAddScalar)(evenEcdsaPubkey, tweak)); | ||
// Convert to schnorr. | ||
var outputSchnorrKey = outputEcdsaKey.slice(1); | ||
// Create address | ||
return outputSchnorrKey; | ||
} | ||
//# sourceMappingURL=BtcNew.js.map |
/// <reference types="node" /> | ||
export declare function unsafeTo64bitLE(n: number): Buffer; | ||
export declare function unsafeFrom64bitLE(byteArray: Buffer): number; | ||
export declare class BufferWriter { | ||
@@ -10,3 +8,3 @@ private bufs; | ||
writeUInt32(i: number): void; | ||
writeUInt64(i: number): void; | ||
writeUInt64(i: bigint): void; | ||
writeVarInt(i: number): void; | ||
@@ -25,3 +23,3 @@ writeSlice(slice: Buffer): void; | ||
readUInt32(): number; | ||
readUInt64(): number; | ||
readUInt64(): bigint; | ||
readVarInt(): number; | ||
@@ -28,0 +26,0 @@ readSlice(n: number): Buffer; |
@@ -6,35 +6,4 @@ "use strict"; | ||
exports.__esModule = true; | ||
exports.BufferReader = exports.BufferWriter = exports.unsafeFrom64bitLE = exports.unsafeTo64bitLE = void 0; | ||
exports.BufferReader = exports.BufferWriter = void 0; | ||
var varuint_bitcoin_1 = __importDefault(require("varuint-bitcoin")); | ||
function unsafeTo64bitLE(n) { | ||
// we want to represent the input as a 8-bytes array | ||
if (n > Number.MAX_SAFE_INTEGER) { | ||
throw new Error("Can't convert numbers > MAX_SAFE_INT"); | ||
} | ||
var byteArray = Buffer.alloc(8, 0); | ||
for (var index = 0; index < byteArray.length; index++) { | ||
var byte = n & 0xff; | ||
byteArray[index] = byte; | ||
n = (n - byte) / 256; | ||
} | ||
return byteArray; | ||
} | ||
exports.unsafeTo64bitLE = unsafeTo64bitLE; | ||
function unsafeFrom64bitLE(byteArray) { | ||
var value = 0; | ||
if (byteArray.length != 8) { | ||
throw new Error("Expected Bufffer of lenght 8"); | ||
} | ||
if (byteArray[7] != 0) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
if (byteArray[6] > 0x1f) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
for (var i = byteArray.length - 1; i >= 0; i--) { | ||
value = value * 256 + byteArray[i]; | ||
} | ||
return value; | ||
} | ||
exports.unsafeFrom64bitLE = unsafeFrom64bitLE; | ||
var BufferWriter = /** @class */ (function () { | ||
@@ -59,4 +28,3 @@ function BufferWriter() { | ||
BufferWriter.prototype.writeUInt64 = function (i) { | ||
var bytes = unsafeTo64bitLE(i); | ||
this.writeSlice(bytes); | ||
this.write(8, function (b) { return b.writeBigUInt64LE(i, 0); }); | ||
}; | ||
@@ -104,5 +72,5 @@ BufferWriter.prototype.writeVarInt = function (i) { | ||
BufferReader.prototype.readUInt64 = function () { | ||
var buf = this.readSlice(8); | ||
var n = unsafeFrom64bitLE(buf); | ||
return n; | ||
var result = this.buffer.readBigUInt64LE(this.offset); | ||
this.offset += 8; | ||
return result; | ||
}; | ||
@@ -109,0 +77,0 @@ BufferReader.prototype.readVarInt = function () { |
@@ -5,6 +5,2 @@ /// <reference types="node" /> | ||
import { WalletPolicy } from "./policy"; | ||
/** | ||
* This class encapsulates the APDU protocol documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
*/ | ||
export declare class AppClient { | ||
@@ -14,7 +10,7 @@ transport: Transport; | ||
private makeRequest; | ||
getExtendedPubkey(display: boolean, pathElements: number[]): Promise<string>; | ||
getPubkey(display: boolean, pathElements: number[]): Promise<string>; | ||
getWalletAddress(walletPolicy: WalletPolicy, walletHMAC: Buffer | null, change: number, addressIndex: number, display: boolean): Promise<string>; | ||
signPsbt(psbt: PsbtV2, walletPolicy: WalletPolicy, walletHMAC: Buffer | null, progressCallback: () => void): Promise<Map<number, Buffer>>; | ||
signPsbt(psbt: PsbtV2, walletPolicy: WalletPolicy, walletHMAC: Buffer | null): Promise<Map<number, Buffer>>; | ||
getMasterFingerprint(): Promise<Buffer>; | ||
} | ||
//# sourceMappingURL=appClient.d.ts.map |
@@ -71,6 +71,2 @@ "use strict"; | ||
})(FrameworkIns || (FrameworkIns = {})); | ||
/** | ||
* This class encapsulates the APDU protocol documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
*/ | ||
var AppClient = /** @class */ (function () { | ||
@@ -108,3 +104,3 @@ function AppClient(transport) { | ||
}; | ||
AppClient.prototype.getExtendedPubkey = function (display, pathElements) { | ||
AppClient.prototype.getPubkey = function (display, pathElements) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -119,3 +115,3 @@ var response; | ||
return [4 /*yield*/, this.makeRequest(BitcoinIns.GET_PUBKEY, Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
(0, bip32_1.pathElementsToBuffer)(pathElements), | ||
@@ -143,3 +139,3 @@ ]))]; | ||
} | ||
clientInterpreter = new clientCommands_1.ClientCommandInterpreter(function () { }); | ||
clientInterpreter = new clientCommands_1.ClientCommandInterpreter(); | ||
clientInterpreter.addKnownList(walletPolicy.keys.map(function (k) { return Buffer.from(k, "ascii"); })); | ||
@@ -150,6 +146,6 @@ clientInterpreter.addKnownPreimage(walletPolicy.serialize()); | ||
return [4 /*yield*/, this.makeRequest(BitcoinIns.GET_WALLET_ADDRESS, Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
walletPolicy.getWalletId(), | ||
walletHMAC || Buffer.alloc(32, 0), | ||
Buffer.from([change]), | ||
Buffer.of(change), | ||
addressIndexBuffer, | ||
@@ -164,3 +160,3 @@ ]), clientInterpreter)]; | ||
}; | ||
AppClient.prototype.signPsbt = function (psbt, walletPolicy, walletHMAC, progressCallback) { | ||
AppClient.prototype.signPsbt = function (psbt, walletPolicy, walletHMAC) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -176,3 +172,3 @@ var merkelizedPsbt, clientInterpreter, _a, _b, map, _c, _d, map, inputMapsRoot, outputMapsRoot, yielded, ret, yielded_1, yielded_1_1, inputAndSig; | ||
} | ||
clientInterpreter = new clientCommands_1.ClientCommandInterpreter(progressCallback); | ||
clientInterpreter = new clientCommands_1.ClientCommandInterpreter(); | ||
// prepare ClientCommandInterpreter | ||
@@ -246,3 +242,3 @@ clientInterpreter.addKnownList(walletPolicy.keys.map(function (k) { return Buffer.from(k, "ascii"); })); | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.from([]))]; | ||
return [2 /*return*/, this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.of())]; | ||
}); | ||
@@ -249,0 +245,0 @@ }); |
@@ -16,6 +16,5 @@ /// <reference types="node" /> | ||
export declare class YieldCommand extends ClientCommand { | ||
private progressCallback; | ||
private results; | ||
code: ClientCommandCode; | ||
constructor(results: Buffer[], progressCallback: () => void); | ||
constructor(results: Buffer[]); | ||
execute(request: Buffer): Buffer; | ||
@@ -49,17 +48,2 @@ } | ||
} | ||
/** | ||
* This class will dispatch a client command coming from the hardware device to | ||
* the appropriate client command implementation. Those client commands | ||
* typically requests data from a merkle tree or merkelized maps. | ||
* | ||
* A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
* merkelized maps it should be able to serve to the hardware device. This class | ||
* doesn't know anything about the semantics of the data it holds, it just | ||
* serves merkle data. It doesn't even know in what context it is being | ||
* executed, ie SignPsbt, getWalletAddress, etc. | ||
* | ||
* If the command yelds results to the client, as signPsbt does, the yielded | ||
* data will be accessible after the command completed by calling getYielded(), | ||
* which will return the yields in the same order as they came in. | ||
*/ | ||
export declare class ClientCommandInterpreter { | ||
@@ -71,3 +55,3 @@ private roots; | ||
private commands; | ||
constructor(progressCallback: () => void); | ||
constructor(); | ||
getYielded(): Buffer[]; | ||
@@ -74,0 +58,0 @@ addKnownPreimage(preimage: Buffer): void; |
@@ -56,3 +56,2 @@ "use strict"; | ||
var bitcoinjs_lib_1 = require("bitcoinjs-lib"); | ||
var buffertools_1 = require("../buffertools"); | ||
var varint_1 = require("../varint"); | ||
@@ -75,5 +74,4 @@ var merkle_1 = require("./merkle"); | ||
__extends(YieldCommand, _super); | ||
function YieldCommand(results, progressCallback) { | ||
function YieldCommand(results) { | ||
var _this = _super.call(this) || this; | ||
_this.progressCallback = progressCallback; | ||
_this.code = ClientCommandCode.YIELD; | ||
@@ -85,3 +83,2 @@ _this.results = results; | ||
this.results.push(Buffer.from(request.subarray(1))); | ||
this.progressCallback(); | ||
return Buffer.from(""); | ||
@@ -102,3 +99,3 @@ }; | ||
GetPreimageCommand.prototype.execute = function (request) { | ||
var req = Buffer.from(request.subarray(1)); | ||
var req = request.subarray(1); | ||
// we expect no more data to read | ||
@@ -132,3 +129,3 @@ if (req.length != 1 + 32) { | ||
Buffer.from([payload_size]), | ||
Buffer.from(known_preimage.subarray(0, payload_size)), | ||
known_preimage.subarray(0, payload_size), | ||
]); | ||
@@ -152,18 +149,14 @@ } | ||
var _a; | ||
var req = Buffer.from(request.subarray(1)); | ||
if (req.length < 32 + 1 + 1) { | ||
throw new Error("Invalid request, expected at least 34 bytes"); | ||
var req = request.subarray(1); | ||
if (req.length != 32 + 4 + 4) { | ||
throw new Error("Invalid request, unexpected trailing data"); | ||
} | ||
var reqBuf = new buffertools_1.BufferReader(req); | ||
var hash = reqBuf.readSlice(32); | ||
// read the hash | ||
var hash = Buffer.alloc(32); | ||
for (var i = 0; i < 32; i++) { | ||
hash[i] = req.readUInt8(i); | ||
} | ||
var hash_hex = hash.toString("hex"); | ||
var tree_size; | ||
var leaf_index; | ||
try { | ||
tree_size = reqBuf.readVarInt(); | ||
leaf_index = reqBuf.readVarInt(); | ||
} | ||
catch (e) { | ||
throw new Error("Invalid request, couldn't parse tree_size or leaf_index"); | ||
} | ||
var tree_size = req.readUInt32BE(32); | ||
var leaf_index = req.readUInt32BE(32 + 4); | ||
var mt = this.known_trees.get(hash_hex); | ||
@@ -204,3 +197,3 @@ if (!mt) { | ||
GetMerkleLeafIndexCommand.prototype.execute = function (request) { | ||
var req = Buffer.from(request.subarray(1)); | ||
var req = request.subarray(1); | ||
if (req.length != 32 + 32) { | ||
@@ -270,19 +263,4 @@ throw new Error("Invalid request, unexpected trailing data"); | ||
exports.GetMoreElementsCommand = GetMoreElementsCommand; | ||
/** | ||
* This class will dispatch a client command coming from the hardware device to | ||
* the appropriate client command implementation. Those client commands | ||
* typically requests data from a merkle tree or merkelized maps. | ||
* | ||
* A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
* merkelized maps it should be able to serve to the hardware device. This class | ||
* doesn't know anything about the semantics of the data it holds, it just | ||
* serves merkle data. It doesn't even know in what context it is being | ||
* executed, ie SignPsbt, getWalletAddress, etc. | ||
* | ||
* If the command yelds results to the client, as signPsbt does, the yielded | ||
* data will be accessible after the command completed by calling getYielded(), | ||
* which will return the yields in the same order as they came in. | ||
*/ | ||
var ClientCommandInterpreter = /** @class */ (function () { | ||
function ClientCommandInterpreter(progressCallback) { | ||
function ClientCommandInterpreter() { | ||
var e_1, _a; | ||
@@ -295,3 +273,3 @@ this.roots = new Map(); | ||
var commands = [ | ||
new YieldCommand(this.yielded, progressCallback), | ||
new YieldCommand(this.yielded), | ||
new GetPreimageCommand(this.preimages, this.queue), | ||
@@ -298,0 +276,0 @@ new GetMerkleLeafIndexCommand(this.roots), |
/// <reference types="node" /> | ||
import { MerkleMap } from "./merkleMap"; | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This class merkelizes a PSBTv2, by merkelizing the different | ||
* maps of the psbt. This is used during the transaction signing process, | ||
* where the hardware app can request specific parts of the psbt from the | ||
* client code and be sure that the response data actually belong to the psbt. | ||
* The reason for this is the limited amount of memory available to the app, | ||
* so it can't always store the full psbt in memory. | ||
* | ||
* The signing process is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt | ||
*/ | ||
export declare class MerkelizedPsbt extends PsbtV2 { | ||
@@ -16,0 +5,0 @@ globalMerkleMap: MerkleMap; |
@@ -46,13 +46,2 @@ "use strict"; | ||
var psbtv2_1 = require("./psbtv2"); | ||
/** | ||
* This class merkelizes a PSBTv2, by merkelizing the different | ||
* maps of the psbt. This is used during the transaction signing process, | ||
* where the hardware app can request specific parts of the psbt from the | ||
* client code and be sure that the response data actually belong to the psbt. | ||
* The reason for this is the limited amount of memory available to the app, | ||
* so it can't always store the full psbt in memory. | ||
* | ||
* The signing process is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt | ||
*/ | ||
var MerkelizedPsbt = /** @class */ (function (_super) { | ||
@@ -59,0 +48,0 @@ __extends(MerkelizedPsbt, _super); |
/// <reference types="node" /> | ||
/** | ||
* This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
* which is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md | ||
*/ | ||
export declare class Merkle { | ||
@@ -8,0 +3,0 @@ private leaves; |
@@ -30,7 +30,2 @@ "use strict"; | ||
var bitcoinjs_lib_1 = require("bitcoinjs-lib"); | ||
/** | ||
* This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
* which is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md | ||
*/ | ||
var Merkle = /** @class */ (function () { | ||
@@ -86,3 +81,3 @@ function Merkle(leaves, hasher) { | ||
Merkle.prototype.hashNode = function (left, right) { | ||
return this.h(Buffer.concat([Buffer.from([1]), left, right])); | ||
return this.h(Buffer.concat([Buffer.of(1), left, right])); | ||
}; | ||
@@ -94,3 +89,3 @@ return Merkle; | ||
if (hashFunction === void 0) { hashFunction = bitcoinjs_lib_1.crypto.sha256; } | ||
return hashConcat(Buffer.from([0]), buf, hashFunction); | ||
return hashConcat(Buffer.of(0), buf, hashFunction); | ||
} | ||
@@ -97,0 +92,0 @@ exports.hashLeaf = hashLeaf; |
/// <reference types="node" /> | ||
import { Merkle } from "./merkle"; | ||
/** | ||
* This implements "Merkelized Maps", documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps | ||
* | ||
* A merkelized map consist of two merkle trees, one for the keys of | ||
* a map and one for the values of the same map, thus the two merkle | ||
* trees have the same shape. The commitment is the number elements | ||
* in the map followed by the keys' merkle root followed by the | ||
* values' merkle root. | ||
*/ | ||
export declare class MerkleMap { | ||
@@ -14,0 +4,0 @@ keys: Buffer[]; |
@@ -6,12 +6,2 @@ "use strict"; | ||
var merkle_1 = require("./merkle"); | ||
/** | ||
* This implements "Merkelized Maps", documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps | ||
* | ||
* A merkelized map consist of two merkle trees, one for the keys of | ||
* a map and one for the values of the same map, thus the two merkle | ||
* trees have the same shape. The commitment is the number elements | ||
* in the map followed by the keys' merkle root followed by the | ||
* values' merkle root. | ||
*/ | ||
var MerkleMap = /** @class */ (function () { | ||
@@ -18,0 +8,0 @@ /** |
/// <reference types="node" /> | ||
export declare type DefaultDescriptorTemplate = "pkh(@0)" | "sh(wpkh(@0))" | "wpkh(@0)" | "tr(@0)"; | ||
/** | ||
* The Bitcon hardware app uses a descriptors-like thing to describe | ||
* how to construct output scripts from keys. A "Wallet Policy" consists | ||
* of a "Descriptor Template" and a list of "keys". A key is basically | ||
* a serialized BIP32 extended public key with some added derivation path | ||
* information. This is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md | ||
*/ | ||
export declare class WalletPolicy { | ||
@@ -12,0 +4,0 @@ descriptorTemplate: string; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.createKey = exports.WalletPolicy = void 0; | ||
var bitcoinjs_lib_1 = require("bitcoinjs-lib"); | ||
var bip32_1 = require("../bip32"); | ||
var buffertools_1 = require("../buffertools"); | ||
var bitcoinjs_lib_1 = require("bitcoinjs-lib"); | ||
var merkle_1 = require("./merkle"); | ||
/** | ||
* The Bitcon hardware app uses a descriptors-like thing to describe | ||
* how to construct output scripts from keys. A "Wallet Policy" consists | ||
* of a "Descriptor Template" and a list of "keys". A key is basically | ||
* a serialized BIP32 extended public key with some added derivation path | ||
* information. This is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md | ||
*/ | ||
var WalletPolicy = /** @class */ (function () { | ||
@@ -17,0 +9,0 @@ /** |
/// <reference types="node" /> | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However | ||
* the role is partially documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor). | ||
*/ | ||
export declare function extract(psbt: PsbtV2): Buffer; | ||
//# sourceMappingURL=psbtExtractor.d.ts.map |
@@ -5,8 +5,2 @@ "use strict"; | ||
var buffertools_1 = require("../buffertools"); | ||
/** | ||
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However | ||
* the role is partially documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor). | ||
*/ | ||
function extract(psbt) { | ||
@@ -18,3 +12,3 @@ var _a, _b; | ||
if (isSegwit) { | ||
tx.writeSlice(Buffer.from([0, 1])); | ||
tx.writeSlice(Buffer.of(0, 1)); | ||
} | ||
@@ -27,3 +21,3 @@ var inputCount = psbt.getGlobalInputCount(); | ||
tx.writeUInt32(psbt.getInputOutputIndex(i)); | ||
tx.writeVarSlice((_a = psbt.getInputFinalScriptsig(i)) !== null && _a !== void 0 ? _a : Buffer.from([])); | ||
tx.writeVarSlice((_a = psbt.getInputFinalScriptsig(i)) !== null && _a !== void 0 ? _a : Buffer.of()); | ||
tx.writeUInt32(psbt.getInputSequence(i)); | ||
@@ -37,3 +31,3 @@ if (isSegwit) { | ||
for (var i = 0; i < outputCount; i++) { | ||
tx.writeUInt64(psbt.getOutputAmount(i)); | ||
tx.writeUInt64(BigInt(psbt.getOutputAmount(i))); | ||
tx.writeVarSlice(psbt.getOutputScript(i)); | ||
@@ -40,0 +34,0 @@ } |
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However | ||
* the role is documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). | ||
* | ||
* Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
* fields that aren't useful anymore, partial signatures, redeem script and | ||
* derivation paths. | ||
* | ||
* @param psbt The psbt with all signatures added as partial sigs, either | ||
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* @param psbt The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
*/ | ||
export declare function finalize(psbt: PsbtV2): void; | ||
//# sourceMappingURL=psbtFinalizer.d.ts.map |
@@ -7,14 +7,4 @@ "use strict"; | ||
/** | ||
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However | ||
* the role is documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). | ||
* | ||
* Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
* fields that aren't useful anymore, partial signatures, redeem script and | ||
* derivation paths. | ||
* | ||
* @param psbt The psbt with all signatures added as partial sigs, either | ||
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* @param psbt The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
*/ | ||
@@ -88,9 +78,2 @@ function finalize(psbt) { | ||
exports.finalize = finalize; | ||
/** | ||
* Deletes fields that are no longer neccesary from the psbt. | ||
* | ||
* Note, the spec doesn't say anything about removing ouput fields | ||
* like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them | ||
* without actually knowing why. I think we should remove them too. | ||
*/ | ||
function clearFinalizedInput(psbt, inputIndex) { | ||
@@ -113,10 +96,2 @@ var keyTypes = [ | ||
} | ||
/** | ||
* Writes a script push operation to buf, which looks different | ||
* depending on the size of the data. See | ||
* https://en.bitcoin.it/wiki/Script#Constants | ||
* | ||
* @param buf the BufferWriter to write to | ||
* @param data the Buffer to be pushed. | ||
*/ | ||
function writePush(buf, data) { | ||
@@ -123,0 +98,0 @@ if (data.length <= 75) { |
@@ -14,3 +14,2 @@ /// <reference types="node" /> | ||
PARTIAL_SIG = 2, | ||
SIGHASH_TYPE = 3, | ||
REDEEM_SCRIPT = 4, | ||
@@ -35,20 +34,2 @@ BIP32_DERIVATION = 6, | ||
} | ||
/** | ||
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki | ||
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki | ||
* | ||
* A psbt is a data structure that can carry all relevant information about a | ||
* transaction through all stages of the signing process. From constructing an | ||
* unsigned transaction to extracting the final serialized transaction ready for | ||
* broadcast. | ||
* | ||
* This implementation is limited to what's needed in ledgerjs to carry out its | ||
* duties, which means that support for features like multisig or taproot script | ||
* path spending are not implemented. Specifically, it supports p2pkh, | ||
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
* | ||
* This class is made purposefully dumb, so it's easy to add support for | ||
* complemantary fields as needed in the future. | ||
*/ | ||
export declare class PsbtV2 { | ||
@@ -79,4 +60,2 @@ protected globalMap: Map<string, Buffer>; | ||
getInputPartialSig(inputIndex: number, pubkey: Buffer): Buffer | undefined; | ||
setInputSighashType(inputIndex: number, sigHashtype: number): void; | ||
getInputSighashType(inputIndex: number): number | undefined; | ||
setInputRedeemScript(inputIndex: number, redeemScript: Buffer): void; | ||
@@ -138,2 +117,3 @@ getInputRedeemScript(inputIndex: number): Buffer | undefined; | ||
private setInput; | ||
private getMap; | ||
private getInput; | ||
@@ -143,3 +123,3 @@ private getInputOptional; | ||
private getOutput; | ||
private getMap; | ||
private getOutputOptional; | ||
private encodeBip32Derivation; | ||
@@ -146,0 +126,0 @@ private decodeBip32Derivation; |
@@ -47,3 +47,2 @@ "use strict"; | ||
psbtIn[psbtIn["PARTIAL_SIG"] = 2] = "PARTIAL_SIG"; | ||
psbtIn[psbtIn["SIGHASH_TYPE"] = 3] = "SIGHASH_TYPE"; | ||
psbtIn[psbtIn["REDEEM_SCRIPT"] = 4] = "REDEEM_SCRIPT"; | ||
@@ -67,3 +66,3 @@ psbtIn[psbtIn["BIP32_DERIVATION"] = 6] = "BIP32_DERIVATION"; | ||
})(psbtOut = exports.psbtOut || (exports.psbtOut = {})); | ||
var PSBT_MAGIC_BYTES = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]); | ||
var PSBT_MAGIC_BYTES = Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff); | ||
var NoSuchEntry = /** @class */ (function (_super) { | ||
@@ -77,20 +76,2 @@ __extends(NoSuchEntry, _super); | ||
exports.NoSuchEntry = NoSuchEntry; | ||
/** | ||
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki | ||
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki | ||
* | ||
* A psbt is a data structure that can carry all relevant information about a | ||
* transaction through all stages of the signing process. From constructing an | ||
* unsigned transaction to extracting the final serialized transaction ready for | ||
* broadcast. | ||
* | ||
* This implementation is limited to what's needed in ledgerjs to carry out its | ||
* duties, which means that support for features like multisig or taproot script | ||
* path spending are not implemented. Specifically, it supports p2pkh, | ||
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
* | ||
* This class is made purposefully dumb, so it's easy to add support for | ||
* complemantary fields as needed in the future. | ||
*/ | ||
var PsbtV2 = /** @class */ (function () { | ||
@@ -164,11 +145,2 @@ function PsbtV2() { | ||
}; | ||
PsbtV2.prototype.setInputSighashType = function (inputIndex, sigHashtype) { | ||
this.setInput(inputIndex, psbtIn.SIGHASH_TYPE, b(), uint32LE(sigHashtype)); | ||
}; | ||
PsbtV2.prototype.getInputSighashType = function (inputIndex) { | ||
var result = this.getInputOptional(inputIndex, psbtIn.SIGHASH_TYPE, b()); | ||
if (!result) | ||
return undefined; | ||
return result.readUInt32LE(0); | ||
}; | ||
PsbtV2.prototype.setInputRedeemScript = function (inputIndex, redeemScript) { | ||
@@ -258,4 +230,3 @@ this.setInput(inputIndex, psbtIn.REDEEM_SCRIPT, b(), redeemScript); | ||
PsbtV2.prototype.getOutputAmount = function (outputIndex) { | ||
var buf = this.getOutput(outputIndex, psbtOut.AMOUNT, b()); | ||
return (0, buffertools_1.unsafeFrom64bitLE)(buf); | ||
return Number(this.getOutput(outputIndex, psbtOut.AMOUNT, b()).readBigUInt64LE(0)); | ||
}; | ||
@@ -303,3 +274,3 @@ PsbtV2.prototype.setOutputScript = function (outputIndex, scriptPubKey) { | ||
var buf = new buffertools_1.BufferWriter(); | ||
buf.writeSlice(Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff])); | ||
buf.writeSlice(Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff)); | ||
serializeMap(buf, this.globalMap); | ||
@@ -358,3 +329,3 @@ this.inputMaps.forEach(function (map) { | ||
PsbtV2.prototype.setGlobal = function (keyType, value) { | ||
var key = new Key(keyType, Buffer.from([])); | ||
var key = new Key(keyType, Buffer.of()); | ||
this.globalMap.set(key.toString(), value); | ||
@@ -371,2 +342,8 @@ }; | ||
}; | ||
PsbtV2.prototype.getMap = function (index, maps) { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
}; | ||
PsbtV2.prototype.getInput = function (index, keyType, keyData) { | ||
@@ -384,7 +361,4 @@ return get(this.inputMaps[index], keyType, keyData, false); | ||
}; | ||
PsbtV2.prototype.getMap = function (index, maps) { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
PsbtV2.prototype.getOutputOptional = function (index, keyType, keyData) { | ||
return get(this.outputMaps[index], keyType, keyData, true); | ||
}; | ||
@@ -493,3 +467,3 @@ PsbtV2.prototype.encodeBip32Derivation = function (masterFingerprint, path) { | ||
function b() { | ||
return Buffer.from([]); | ||
return Buffer.of(); | ||
} | ||
@@ -506,3 +480,5 @@ function set(map, keyType, keyData, value) { | ||
function uint64LE(n) { | ||
return (0, buffertools_1.unsafeTo64bitLE)(n); | ||
var b = Buffer.alloc(8); | ||
b.writeBigUInt64LE(BigInt(n), 0); | ||
return b; | ||
} | ||
@@ -509,0 +485,0 @@ function varint(n) { |
{ | ||
"name": "@backpacker69/hw-app-btc", | ||
"version": "6.24.1", | ||
"version": "6.24.2", | ||
"description": "Ledger Hardware Wallet Bitcoin Application API", | ||
@@ -16,8 +16,8 @@ "keywords": [ | ||
"type": "git", | ||
"url": "https://github.com/backpacker69/ledgerjs" | ||
"url": "https://github.com/LedgerHQ/ledgerjs" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/backpacker69/ledgerjs/issues" | ||
"url": "https://github.com/LedgerHQ/ledgerjs/issues" | ||
}, | ||
"homepage": "https://github.com/backpacker69/ledgerjs", | ||
"homepage": "https://github.com/LedgerHQ/ledgerjs", | ||
"publishConfig": { | ||
@@ -31,3 +31,3 @@ "access": "public" | ||
"dependencies": { | ||
"@ledgerhq/hw-transport": "^6.24.1", | ||
"@ledgerhq/hw-transport": "^6.10.0", | ||
"@ledgerhq/logs": "^6.10.0", | ||
@@ -34,0 +34,0 @@ "bip32-path": "^0.4.2", |
442
README.md
@@ -39,64 +39,32 @@ <img src="https://user-images.githubusercontent.com/211411/34776833-6f1ef4da-f618-11e7-8b13-f0697901d6a8.png" height="100" /> | ||
* [Examples](#examples-6) | ||
* [impl](#impl) | ||
* [BtcNew](#btcnew) | ||
* [createPaymentTransactionNew](#createpaymenttransactionnew-1) | ||
* [Parameters](#parameters-8) | ||
* [getWalletXpub](#getwalletxpub-1) | ||
* [Parameters](#parameters-9) | ||
* [Examples](#examples-7) | ||
* [BtcOld](#btcold) | ||
* [Parameters](#parameters-9) | ||
* [Examples](#examples-8) | ||
* [getWalletPublicKey](#getwalletpublickey-1) | ||
* [Parameters](#parameters-10) | ||
* [createPaymentTransactionNew](#createpaymenttransactionnew-1) | ||
* [Examples](#examples-9) | ||
* [signMessageNew](#signmessagenew-1) | ||
* [Parameters](#parameters-11) | ||
* [BtcOld](#btcold) | ||
* [Parameters](#parameters-12) | ||
* [Examples](#examples-7) | ||
* [getWalletPublicKey](#getwalletpublickey-2) | ||
* [Parameters](#parameters-13) | ||
* [Examples](#examples-8) | ||
* [signMessageNew](#signmessagenew-1) | ||
* [Parameters](#parameters-14) | ||
* [Examples](#examples-9) | ||
* [Examples](#examples-10) | ||
* [createPaymentTransactionNew](#createpaymenttransactionnew-2) | ||
* [Parameters](#parameters-15) | ||
* [Examples](#examples-10) | ||
* [Parameters](#parameters-12) | ||
* [Examples](#examples-11) | ||
* [signP2SHTransaction](#signp2shtransaction-1) | ||
* [Parameters](#parameters-16) | ||
* [Examples](#examples-11) | ||
* [Parameters](#parameters-13) | ||
* [Examples](#examples-12) | ||
* [CreateTransactionArg](#createtransactionarg) | ||
* [Properties](#properties) | ||
* [AddressFormat](#addressformat) | ||
* [AccountType](#accounttype) | ||
* [spendingCondition](#spendingcondition) | ||
* [Parameters](#parameters-17) | ||
* [setInput](#setinput) | ||
* [Parameters](#parameters-18) | ||
* [setOwnOutput](#setownoutput) | ||
* [Parameters](#parameters-19) | ||
* [getDescriptorTemplate](#getdescriptortemplate) | ||
* [SingleKeyAccount](#singlekeyaccount) | ||
* [getTaprootOutputKey](#gettaprootoutputkey) | ||
* [Parameters](#parameters-20) | ||
* [AppClient](#appclient) | ||
* [Parameters](#parameters-21) | ||
* [ClientCommandInterpreter](#clientcommandinterpreter) | ||
* [Parameters](#parameters-22) | ||
* [MerkelizedPsbt](#merkelizedpsbt) | ||
* [Parameters](#parameters-23) | ||
* [Merkle](#merkle) | ||
* [Parameters](#parameters-24) | ||
* [MerkleMap](#merklemap) | ||
* [Parameters](#parameters-25) | ||
* [WalletPolicy](#walletpolicy) | ||
* [Parameters](#parameters-26) | ||
* [extract](#extract) | ||
* [Parameters](#parameters-27) | ||
* [constructor](#constructor) | ||
* [Parameters](#parameters-14) | ||
* [constructor](#constructor-1) | ||
* [Parameters](#parameters-15) | ||
* [finalize](#finalize) | ||
* [Parameters](#parameters-28) | ||
* [clearFinalizedInput](#clearfinalizedinput) | ||
* [Parameters](#parameters-29) | ||
* [writePush](#writepush) | ||
* [Parameters](#parameters-30) | ||
* [PsbtV2](#psbtv2) | ||
* [Parameters](#parameters-16) | ||
* [serializeTransactionOutputs](#serializetransactionoutputs-1) | ||
* [Parameters](#parameters-31) | ||
* [Examples](#examples-12) | ||
* [Parameters](#parameters-17) | ||
* [Examples](#examples-13) | ||
* [SignP2SHTransactionArg](#signp2shtransactionarg) | ||
@@ -147,3 +115,3 @@ * [Properties](#properties-1) | ||
* bech32 format with 84' paths | ||
* bech32 format with 173' paths | ||
@@ -195,9 +163,9 @@ * cashaddr in case of Bitcoin Cash | ||
* `changePath` is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* `outputScriptHex` is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* `outputScriptHex` is the hexadecimal serialized outputs of the transaction to sign | ||
* `lockTime` is the optional lockTime of the transaction to sign, or default (0) | ||
* `sigHashType` is the hash type of the transaction to sign, or default (all) | ||
* `segwit` is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* `segwit` is an optional boolean indicating wether to use segwit or not | ||
* `initialTimestamp` is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* `additionals` list of additionnal options* "bech32" for spending native segwit outputs | ||
* "bech32m" for spending segwit v1+ outputs | ||
* "bech32m" for spending native segwit outputs | ||
* "abc" for bch | ||
@@ -208,3 +176,3 @@ * "gold" for btg | ||
* `expiryHeight` is an optional Buffer for zec overwinter / sapling Txs | ||
* `useTrustedInputForSegwit` trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* `useTrustedInputForSegwit` trust inputs for segwit transactions | ||
@@ -286,115 +254,41 @@ ##### Examples | ||
### impl | ||
### createPaymentTransactionNew | ||
Definition: A "normal path" is a prefix of a standard path where all | ||
the hardened steps of the standard path are included. For example, the | ||
paths m/44'/1'/17' and m/44'/1'/17'/1 are normal paths, but m/44'/1' | ||
is not. m/'199/1'/17'/0/1 is not a normal path either. | ||
To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
There's a compatiblity issue between old and new app: When exporting | ||
the key of a non-normal path with verify=false, the new app would | ||
return an error, whereas the old app would return the key. | ||
See | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#get_extended_pubkey> | ||
If format bech32m is used, we'll not use old, because it doesn't | ||
support it. | ||
When to use new (given the app supports it) | ||
* format is bech32m or | ||
* path is normal or | ||
* verify is true | ||
Otherwise use old. | ||
### BtcNew | ||
This class implements the same interface as BtcOld (formerly | ||
named Btc), but interacts with Bitcoin hardware app version 2+ | ||
which uses a totally new APDU protocol. This new | ||
protocol is documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md> | ||
Since the interface must remain compatible with BtcOld, the methods | ||
of this class are quite clunky, because it needs to adapt legacy | ||
input data into the PSBT process. In the future, a new interface should | ||
be developed that exposes PSBT to the outer world, which would render | ||
a much cleaner implementation. | ||
#### Parameters | ||
* `` | ||
* `arg` **[CreateTransactionArg](#createtransactionarg)** | ||
* `inputs` is an array of \[ transaction, output_index, optional redeem script, optional sequence ] where* transaction is the previously computed transaction object for this UTXO | ||
* output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* sequence is the sequence number to use for this input (when using RBF), or non present | ||
* `associatedKeysets` is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* `changePath` is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* `outputScriptHex` is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* `lockTime` is the optional lockTime of the transaction to sign, or default (0) | ||
* `sigHashType` is the hash type of the transaction to sign, or default (all) | ||
* `segwit` is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* `initialTimestamp` is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* `additionals` list of additionnal options* "bech32" for spending native segwit outputs | ||
* "bech32m" for spending segwit v1+ outptus | ||
* "abc" for bch | ||
* "gold" for btg | ||
* "bipxxx" for using BIPxxx | ||
* "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* `expiryHeight` is an optional Buffer for zec overwinter / sapling Txs | ||
* `useTrustedInputForSegwit` trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
#### getWalletXpub | ||
#### Examples | ||
This is a new method that allow users to get an xpub at a standard path. | ||
Standard paths are described at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description> | ||
```javascript | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
``` | ||
This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
M/44'/N'/x'/\*\* | ||
M/48'/N'/x'/y'/\*\* | ||
M/49'/N'/x'/\*\* | ||
M/84'/N'/x'/\*\* | ||
M/86'/N'/x'/\*\* | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** the signed transaction ready to be broadcast | ||
The method was added because of added security in the hardware app v2+. The | ||
new hardware app will allow export of any xpub up to and including the | ||
deepest hardened key of standard derivation paths, whereas the old app | ||
would allow export of any key. | ||
This caused an issue for callers of this class, who only had | ||
getWalletPublicKey() to call which means they have to constuct xpub | ||
themselves: | ||
Suppose a user of this class wants to create an account xpub on a standard | ||
path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
it only allows exporting deepest level hardened path. So the options are to | ||
allow requesting M/44'/0' from the app, or to add a new function | ||
"getWalletXpub". | ||
We opted for adding a new function, which can greatly simplify client code. | ||
##### Parameters | ||
* `$0` **{path: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), xpubVersion: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)}** | ||
* `$0.path` | ||
* `$0.xpubVersion` | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** | ||
#### getWalletPublicKey | ||
This method returns a public key, a bitcoin address, and and a chaincode | ||
for a specific derivation path. | ||
Limitation: If the path is not a leaf node of a standard path, the address | ||
will be the empty string "", see this.getWalletAddress() for details. | ||
##### Parameters | ||
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** | ||
* `opts` **{verify: [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?, format: [AddressFormat](#addressformat)?}?** | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{publicKey: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), bitcoinAddress: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), chainCode: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>** | ||
#### createPaymentTransactionNew | ||
Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
details on how to use this method. | ||
This method will convert the legacy arguments, CreateTransactionArg, into | ||
a psbt which is finally signed and finalized, and the extracted fully signed | ||
transaction is returned. | ||
##### Parameters | ||
* `arg` **[CreateTransactionArg](#createtransactionarg)** | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** | ||
### BtcOld | ||
@@ -554,163 +448,12 @@ | ||
### AccountType | ||
### constructor | ||
Encapsulates differences between account types, for example p2wpkh, | ||
p2wpkhWrapped, p2tr. | ||
#### spendingCondition | ||
Generates a scriptPubKey (output script) from a list of public keys. If a | ||
p2sh redeemScript or a p2wsh witnessScript is needed it will also be set on | ||
the returned SpendingCondition. | ||
The pubkeys are expected to be 33 byte ecdsa compressed pubkeys. | ||
##### Parameters | ||
* `pubkeys` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** | ||
Returns **SpendingCondition** | ||
#### setInput | ||
Populates the psbt with account type-specific data for an input. | ||
##### Parameters | ||
* `i` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The index of the input map to populate | ||
* `inputTx` **([Buffer](https://nodejs.org/api/buffer.html) | [undefined](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined))** The full transaction containing the spent output. This may | ||
be omitted for taproot. | ||
* `spentOutput` **SpentOutput** The amount and spending condition of the spent output | ||
* `pubkeys` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** The 33 byte ecdsa compressed public keys involved in the input | ||
* `pathElems` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)>>** The paths corresponding to the pubkeys, in same order. | ||
Returns **void** | ||
#### setOwnOutput | ||
Populates the psbt with account type-specific data for an output. This is typically | ||
done for change outputs and other outputs that goes to the same account as | ||
being spent from. | ||
##### Parameters | ||
* `i` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The index of the output map to populate | ||
* `cond` **SpendingCondition** The spending condition for this output | ||
* `pubkeys` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** The 33 byte ecdsa compressed public keys involved in this output | ||
* `paths` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)>>** The paths corresponding to the pubkeys, in same order. | ||
Returns **void** | ||
#### getDescriptorTemplate | ||
Returns the descriptor template for this account type. Currently only | ||
DefaultDescriptorTemplates are allowed, but that might be changed in the | ||
future. See class WalletPolicy for more information on descriptor | ||
templates. | ||
Returns **DefaultDescriptorTemplate** | ||
### SingleKeyAccount | ||
**Extends BaseAccount** | ||
Superclass for single signature accounts. This will make sure that the pubkey | ||
arrays and path arrays in the method arguments contains exactly one element | ||
and calls an abstract method to do the actual work. | ||
### getTaprootOutputKey | ||
Calculates a taproot output key from an internal key. This output key will be | ||
used as witness program in a taproot output. The internal key is tweaked | ||
according to recommendation in BIP341: | ||
<https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_ref-22-0> | ||
#### Parameters | ||
* `internalPubkey` **[Buffer](https://nodejs.org/api/buffer.html)** A 32 byte x-only taproot internal key | ||
Returns **[Buffer](https://nodejs.org/api/buffer.html)** The output key | ||
### AppClient | ||
This class encapsulates the APDU protocol documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md> | ||
#### Parameters | ||
* `transport` **Transport** | ||
### ClientCommandInterpreter | ||
This class will dispatch a client command coming from the hardware device to | ||
the appropriate client command implementation. Those client commands | ||
typically requests data from a merkle tree or merkelized maps. | ||
A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
merkelized maps it should be able to serve to the hardware device. This class | ||
doesn't know anything about the semantics of the data it holds, it just | ||
serves merkle data. It doesn't even know in what context it is being | ||
executed, ie SignPsbt, getWalletAddress, etc. | ||
If the command yelds results to the client, as signPsbt does, the yielded | ||
data will be accessible after the command completed by calling getYielded(), | ||
which will return the yields in the same order as they came in. | ||
#### Parameters | ||
* `progressCallback` **function (): void** | ||
### MerkelizedPsbt | ||
**Extends PsbtV2** | ||
This class merkelizes a PSBTv2, by merkelizing the different | ||
maps of the psbt. This is used during the transaction signing process, | ||
where the hardware app can request specific parts of the psbt from the | ||
client code and be sure that the response data actually belong to the psbt. | ||
The reason for this is the limited amount of memory available to the app, | ||
so it can't always store the full psbt in memory. | ||
The signing process is documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt> | ||
#### Parameters | ||
* `psbt` **[PsbtV2](#psbtv2)** | ||
### Merkle | ||
This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
which is documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md> | ||
#### Parameters | ||
* `leaves` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** | ||
* `hasher` **function (buf: [Buffer](https://nodejs.org/api/buffer.html)): [Buffer](https://nodejs.org/api/buffer.html)** (optional, default `crypto.sha256`) | ||
### MerkleMap | ||
This implements "Merkelized Maps", documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps> | ||
A merkelized map consist of two merkle trees, one for the keys of | ||
a map and one for the values of the same map, thus the two merkle | ||
trees have the same shape. The commitment is the number elements | ||
in the map followed by the keys' merkle root followed by the | ||
values' merkle root. | ||
#### Parameters | ||
* `keys` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** Sorted list of (unhashed) keys | ||
* `values` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** values, in corresponding order as the keys, and of equal length | ||
### WalletPolicy | ||
### constructor | ||
The Bitcon hardware app uses a descriptors-like thing to describe | ||
how to construct output scripts from keys. A "Wallet Policy" consists | ||
of a "Descriptor Template" and a list of "keys". A key is basically | ||
a serialized BIP32 extended public key with some added derivation path | ||
information. This is documented at | ||
<https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md> | ||
For now, we only support default descriptor templates. | ||
@@ -722,77 +465,10 @@ #### Parameters | ||
### extract | ||
This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
<https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor>). However | ||
the role is partially documented in BIP174 (PSBTv0 | ||
<https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor>). | ||
#### Parameters | ||
* `psbt` **[PsbtV2](#psbtv2)** | ||
Returns **[Buffer](https://nodejs.org/api/buffer.html)** | ||
### finalize | ||
This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
<https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki>). However | ||
the role is documented in BIP174 (PSBTv0 | ||
<https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki>). | ||
Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
fields that aren't useful anymore, partial signatures, redeem script and | ||
derivation paths. | ||
#### Parameters | ||
* `psbt` **[PsbtV2](#psbtv2)** The psbt with all signatures added as partial sigs, either | ||
through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* `psbt` **PsbtV2** The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
Returns **void** | ||
### clearFinalizedInput | ||
Deletes fields that are no longer neccesary from the psbt. | ||
Note, the spec doesn't say anything about removing ouput fields | ||
like PSBT_OUT_BIP32\_DERIVATION_PATH and others, so we keep them | ||
without actually knowing why. I think we should remove them too. | ||
#### Parameters | ||
* `psbt` **[PsbtV2](#psbtv2)** | ||
* `inputIndex` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** | ||
### writePush | ||
Writes a script push operation to buf, which looks different | ||
depending on the size of the data. See | ||
<https://en.bitcoin.it/wiki/Script#Constants> | ||
#### Parameters | ||
* `buf` **BufferWriter** the BufferWriter to write to | ||
* `data` **[Buffer](https://nodejs.org/api/buffer.html)** the Buffer to be pushed. | ||
### PsbtV2 | ||
Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
documented at <https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki> | ||
and <https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki> | ||
A psbt is a data structure that can carry all relevant information about a | ||
transaction through all stages of the signing process. From constructing an | ||
unsigned transaction to extracting the final serialized transaction ready for | ||
broadcast. | ||
This implementation is limited to what's needed in ledgerjs to carry out its | ||
duties, which means that support for features like multisig or taproot script | ||
path spending are not implemented. Specifically, it supports p2pkh, | ||
p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
This class is made purposefully dumb, so it's easy to add support for | ||
complemantary fields as needed in the future. | ||
### serializeTransactionOutputs | ||
@@ -799,0 +475,0 @@ |
120
src/Btc.ts
import type Transport from "@ledgerhq/hw-transport"; | ||
import { pathStringToArray } from "./bip32"; | ||
import BtcNew, { canSupportApp } from "./BtcNew"; | ||
@@ -69,3 +68,3 @@ import BtcOld from "./BtcOld"; | ||
* | ||
* - bech32 format with 84' paths | ||
* - bech32 format with 173' paths | ||
* | ||
@@ -103,65 +102,3 @@ * - cashaddr in case of Bitcoin Cash | ||
return this.getCorrectImpl().then((impl) => { | ||
/** | ||
* Definition: A "normal path" is a prefix of a standard path where all | ||
* the hardened steps of the standard path are included. For example, the | ||
* paths m/44'/1'/17' and m/44'/1'/17'/1 are normal paths, but m/44'/1' | ||
* is not. m/'199/1'/17'/0/1 is not a normal path either. | ||
* | ||
* There's a compatiblity issue between old and new app: When exporting | ||
* the key of a non-normal path with verify=false, the new app would | ||
* return an error, whereas the old app would return the key. | ||
* | ||
* See | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#get_extended_pubkey | ||
* | ||
* If format bech32m is used, we'll not use old, because it doesn't | ||
* support it. | ||
* | ||
* When to use new (given the app supports it) | ||
* * format is bech32m or | ||
* * path is normal or | ||
* * verify is true | ||
* | ||
* Otherwise use old. | ||
*/ | ||
if ( | ||
impl instanceof BtcNew && | ||
options.format != "bech32m" && | ||
(!options.verify || options.verify == false) && | ||
!isPathNormal(path) | ||
) { | ||
console.warn(`WARNING: Using deprecated device protocol to get the public key because | ||
* a non-standard path is requested, and | ||
* verify flag is false | ||
The new protocol only allows export of non-standard paths if the | ||
verify flag is true. Standard paths are (currently): | ||
M/44'/(1|0)'/X' | ||
M/49'/(1|0)'/X' | ||
M/84'/(1|0)'/X' | ||
M/86'/(1|0)'/X' | ||
M/48'/(1|0)'/X'/Y' | ||
followed by "", "(0|1)", or "(0|1)/b", where a and b are | ||
non-hardened. For example, the following paths are standard | ||
M/48'/1'/99'/7' | ||
M/86'/1'/99'/0 | ||
M/48'/0'/99'/7'/1/17 | ||
The following paths are non-standard | ||
M/48'/0'/99' // Not deepest hardened path | ||
M/48'/0'/99'/7'/1/17/2 // Too many non-hardened derivation steps | ||
M/199'/0'/1'/0/88 // Not a known purpose 199 | ||
M/86'/1'/99'/2 // Change path item must be 0 or 1 | ||
This compatibility safeguard will be removed in the future. | ||
Please consider calling Btc.getWalletXpub() instead.`); | ||
return this.old().getWalletPublicKey(path, options); | ||
} else { | ||
return impl.getWalletPublicKey(path, options); | ||
} | ||
return impl.getWalletPublicKey(path, options); | ||
}); | ||
@@ -200,6 +137,6 @@ } | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param segwit is an optional boolean indicating wether to use segwit or not | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
@@ -209,3 +146,3 @@ * @param additionals list of additionnal options | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outputs | ||
* - "bech32m" for spending native segwit outputs | ||
* - "abc" for bch | ||
@@ -216,3 +153,3 @@ * - "gold" for btg | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions | ||
* @return the signed transaction ready to be broadcast | ||
@@ -330,50 +267,11 @@ * @example | ||
if (!canUseNewImplementation) { | ||
return this.old(); | ||
return new BtcOld(this.transport); | ||
} else { | ||
return this.new(); | ||
return new BtcNew(new AppClient(this.transport)); | ||
} | ||
} | ||
protected old(): BtcOld { | ||
private old(): BtcOld { | ||
return new BtcOld(this.transport); | ||
} | ||
protected new(): BtcNew { | ||
return new BtcNew(new AppClient(this.transport)); | ||
} | ||
} | ||
function isPathNormal(path: string): boolean { | ||
//path is not deepest hardened node of a standard path or deeper, use BtcOld | ||
const h = 0x80000000; | ||
const pathElems = pathStringToArray(path); | ||
const hard = (n: number) => n >= h; | ||
const soft = (n: number | undefined) => !n || n < h; | ||
const change = (n: number | undefined) => !n || n == 0 || n == 1; | ||
if ( | ||
pathElems.length >= 3 && | ||
pathElems.length <= 5 && | ||
[44 + h, 49 + h, 84 + h, 86 + h].some((v) => v == pathElems[0]) && | ||
[0 + h, 1 + h].some((v) => v == pathElems[1]) && | ||
hard(pathElems[2]) && | ||
change(pathElems[3]) && | ||
soft(pathElems[4]) | ||
) { | ||
return true; | ||
} | ||
if ( | ||
pathElems.length >= 4 && | ||
pathElems.length <= 6 && | ||
48 + h == pathElems[0] && | ||
[0 + h, 1 + h].some((v) => v == pathElems[1]) && | ||
hard(pathElems[2]) && | ||
hard(pathElems[3]) && | ||
change(pathElems[4]) && | ||
soft(pathElems[5]) | ||
) { | ||
return true; | ||
} | ||
return false; | ||
} |
import { crypto } from "bitcoinjs-lib"; | ||
import { pointCompress, pointAddScalar } from "tiny-secp256k1"; | ||
import semver from "semver"; | ||
import { pointCompress } from "tiny-secp256k1"; | ||
import { | ||
@@ -11,20 +11,8 @@ getXpubComponents, | ||
} from "./bip32"; | ||
import { BufferReader } from "./buffertools"; | ||
import { BufferReader, BufferWriter } from "./buffertools"; | ||
import type { CreateTransactionArg } from "./createTransaction"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
import type { AddressFormat } from "./getWalletPublicKey"; | ||
import { | ||
AccountType, | ||
p2pkh, | ||
p2tr, | ||
p2wpkh, | ||
p2wpkhWrapped, | ||
SpendingCondition, | ||
} from "./newops/accounttype"; | ||
import { hashPublicKey } from "./hashPublicKey"; | ||
import { AppClient as Client } from "./newops/appClient"; | ||
import { | ||
createKey, | ||
DefaultDescriptorTemplate, | ||
WalletPolicy, | ||
} from "./newops/policy"; | ||
import { createKey, WalletPolicy } from "./newops/policy"; | ||
import { extract } from "./newops/psbtExtractor"; | ||
@@ -35,2 +23,11 @@ import { finalize } from "./newops/psbtFinalizer"; | ||
import type { Transaction } from "./types"; | ||
import { | ||
HASH_SIZE, | ||
OP_CHECKSIG, | ||
OP_DUP, | ||
OP_EQUAL, | ||
OP_EQUALVERIFY, | ||
OP_HASH160, | ||
} from "./constants"; | ||
import { AppAndVersion } from "./getAppAndVersion"; | ||
@@ -46,48 +43,5 @@ const newSupportedApps = ["Bitcoin", "Bitcoin Test"]; | ||
/** | ||
* This class implements the same interface as BtcOld (formerly | ||
* named Btc), but interacts with Bitcoin hardware app version 2+ | ||
* which uses a totally new APDU protocol. This new | ||
* protocol is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
* | ||
* Since the interface must remain compatible with BtcOld, the methods | ||
* of this class are quite clunky, because it needs to adapt legacy | ||
* input data into the PSBT process. In the future, a new interface should | ||
* be developed that exposes PSBT to the outer world, which would render | ||
* a much cleaner implementation. | ||
*/ | ||
export default class BtcNew { | ||
constructor(private client: Client) {} | ||
/** | ||
* This is a new method that allow users to get an xpub at a standard path. | ||
* Standard paths are described at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#description | ||
* | ||
* This boils down to paths (N=0 for Bitcoin, N=1 for Testnet): | ||
* M/44'/N'/x'/** | ||
* M/48'/N'/x'/y'/** | ||
* M/49'/N'/x'/** | ||
* M/84'/N'/x'/** | ||
* M/86'/N'/x'/** | ||
* | ||
* The method was added because of added security in the hardware app v2+. The | ||
* new hardware app will allow export of any xpub up to and including the | ||
* deepest hardened key of standard derivation paths, whereas the old app | ||
* would allow export of any key. | ||
* | ||
* This caused an issue for callers of this class, who only had | ||
* getWalletPublicKey() to call which means they have to constuct xpub | ||
* themselves: | ||
* | ||
* Suppose a user of this class wants to create an account xpub on a standard | ||
* path, M/44'/0'/Z'. The user must get the parent key fingerprint (see BIP32) | ||
* by requesting the parent key M/44'/0'. The new app won't allow that, because | ||
* it only allows exporting deepest level hardened path. So the options are to | ||
* allow requesting M/44'/0' from the app, or to add a new function | ||
* "getWalletXpub". | ||
* | ||
* We opted for adding a new function, which can greatly simplify client code. | ||
*/ | ||
async getWalletXpub({ | ||
@@ -101,3 +55,3 @@ path, | ||
const pathElements: number[] = pathStringToArray(path); | ||
const xpub = await this.client.getExtendedPubkey(false, pathElements); | ||
const xpub = await this.client.getPubkey(false, pathElements); | ||
const xpubComponents = getXpubComponents(xpub); | ||
@@ -112,9 +66,2 @@ if (xpubComponents.version != xpubVersion) { | ||
/** | ||
* This method returns a public key, a bitcoin address, and and a chaincode | ||
* for a specific derivation path. | ||
* | ||
* Limitation: If the path is not a leaf node of a standard path, the address | ||
* will be the empty string "", see this.getWalletAddress() for details. | ||
*/ | ||
async getWalletPublicKey( | ||
@@ -132,3 +79,3 @@ path: string, | ||
const pathElements: number[] = pathStringToArray(path); | ||
const xpub = await this.client.getExtendedPubkey(false, pathElements); | ||
const xpub = await this.client.getPubkey(false, pathElements); | ||
@@ -139,3 +86,3 @@ const display = opts?.verify ?? false; | ||
pathElements, | ||
descrTemplFrom(opts?.format ?? "legacy"), | ||
accountTypeFrom(opts?.format ?? "legacy"), | ||
display | ||
@@ -163,3 +110,3 @@ ); | ||
* get it from the device to save development time. However, it shouldn't take | ||
* too much time to implement local address generation. | ||
* more than a few hours to implement local address generation. | ||
* | ||
@@ -172,3 +119,3 @@ * Moreover, if the path is not for a leaf, ie accountPath+/X/Y, there is no | ||
pathElements: number[], | ||
descrTempl: DefaultDescriptorTemplate, | ||
accountType: AccountType, | ||
display: boolean | ||
@@ -180,6 +127,6 @@ ): Promise<string> { | ||
} | ||
const accountXpub = await this.client.getExtendedPubkey(false, accountPath); | ||
const accountXpub = await this.client.getPubkey(false, accountPath); | ||
const masterFingerprint = await this.client.getMasterFingerprint(); | ||
const policy = new WalletPolicy( | ||
descrTempl, | ||
accountType, | ||
createKey(masterFingerprint, accountPath, accountXpub) | ||
@@ -198,8 +145,33 @@ ); | ||
/** | ||
* Build and sign a transaction. See Btc.createPaymentTransactionNew for | ||
* details on how to use this method. | ||
* To sign a transaction involving standard (P2PKH) inputs, call createTransaction with the following parameters | ||
* @param inputs is an array of [ transaction, output_index, optional redeem script, optional sequence ] where | ||
* | ||
* This method will convert the legacy arguments, CreateTransactionArg, into | ||
* a psbt which is finally signed and finalized, and the extracted fully signed | ||
* transaction is returned. | ||
* * transaction is the previously computed transaction object for this UTXO | ||
* * output_index is the output in the transaction used as input for this UTXO (counting from 0) | ||
* * redeem script is the optional redeem script to use when consuming a Segregated Witness input | ||
* * sequence is the sequence number to use for this input (when using RBF), or non present | ||
* @param associatedKeysets is an array of BIP 32 paths pointing to the path to the private key used for each UTXO | ||
* @param changePath is an optional BIP 32 path pointing to the path to the public key used to compute the change address | ||
* @param outputScriptHex is the hexadecimal serialized outputs of the transaction to sign, including leading vararg voutCount | ||
* @param lockTime is the optional lockTime of the transaction to sign, or default (0) | ||
* @param sigHashType is the hash type of the transaction to sign, or default (all) | ||
* @param segwit is an optional boolean indicating wether to use segwit or not. This includes wrapped segwit. | ||
* @param initialTimestamp is an optional timestamp of the function call to use for coins that necessitate timestamps only, (not the one that the tx will include) | ||
* @param additionals list of additionnal options | ||
* | ||
* - "bech32" for spending native segwit outputs | ||
* - "bech32m" for spending segwit v1+ outptus | ||
* - "abc" for bch | ||
* - "gold" for btg | ||
* - "bipxxx" for using BIPxxx | ||
* - "sapling" to indicate a zec transaction is supporting sapling (to be set over block 419200) | ||
* @param expiryHeight is an optional Buffer for zec overwinter / sapling Txs | ||
* @param useTrustedInputForSegwit trust inputs for segwit transactions. If app version >= 1.4.0 this should be true. | ||
* @return the signed transaction ready to be broadcast | ||
* @example | ||
btc.createTransaction({ | ||
inputs: [ [tx1, 1] ], | ||
associatedKeysets: ["0'/0/0"], | ||
outputScriptHex: "01905f0100000000001976a91472a5d75c8d2d0565b656a5232703b167d50d5a2b88ac" | ||
}).then(res => ...); | ||
*/ | ||
@@ -209,35 +181,21 @@ async createPaymentTransactionNew( | ||
): Promise<string> { | ||
const inputCount = arg.inputs.length; | ||
if (inputCount == 0) { | ||
if (arg.inputs.length == 0) { | ||
throw Error("No inputs"); | ||
} | ||
const psbt = new PsbtV2(); | ||
// The master fingerprint is needed when adding BIP32 derivation paths on | ||
// the psbt. | ||
const masterFp = await this.client.getMasterFingerprint(); | ||
const accountType = accountTypeFromArg(arg, psbt, masterFp); | ||
const accountType = accountTypeFromArg(arg); | ||
if (arg.lockTime != undefined) { | ||
// The signer will assume locktime 0 if unset | ||
psbt.setGlobalTxVersion(2); | ||
if (arg.lockTime) { | ||
psbt.setGlobalFallbackLocktime(arg.lockTime); | ||
} | ||
psbt.setGlobalInputCount(inputCount); | ||
psbt.setGlobalInputCount(arg.inputs.length); | ||
psbt.setGlobalPsbtVersion(2); | ||
psbt.setGlobalTxVersion(2); | ||
let notifyCount = 0; | ||
const progress = () => { | ||
if (!arg.onDeviceStreaming) return; | ||
arg.onDeviceStreaming({ | ||
total: 2 * inputCount, | ||
index: notifyCount, | ||
progress: ++notifyCount / (2 * inputCount), | ||
}); | ||
}; | ||
const masterFp = await this.client.getMasterFingerprint(); | ||
let accountXpub = ""; | ||
let accountPath: number[] = []; | ||
for (let i = 0; i < inputCount; i++) { | ||
progress(); | ||
for (let i = 0; i < arg.inputs.length; i++) { | ||
const pathElems: number[] = pathStringToArray(arg.associatedKeysets[i]); | ||
@@ -248,3 +206,3 @@ if (accountXpub == "") { | ||
accountPath = pathElems.slice(0, -2); | ||
accountXpub = await this.client.getExtendedPubkey(false, accountPath); | ||
accountXpub = await this.client.getPubkey(false, accountPath); | ||
} | ||
@@ -257,4 +215,3 @@ await this.setInput( | ||
accountType, | ||
masterFp, | ||
arg.sigHashType | ||
masterFp | ||
); | ||
@@ -266,3 +223,2 @@ } | ||
const outputCount = outputsBufferReader.readVarInt(); | ||
psbt.setGlobalOutputCount(outputCount); | ||
const changeData = await this.outputScriptAt( | ||
@@ -273,4 +229,3 @@ accountPath, | ||
); | ||
// If the caller supplied a changePath, we must make sure there actually is | ||
// a change output. If no change output found, we'll throw an error. | ||
psbt.setGlobalOutputCount(outputCount); | ||
let changeFound = !changeData; | ||
@@ -280,10 +235,9 @@ for (let i = 0; i < outputCount; i++) { | ||
const outputScript = outputsBufferReader.readVarSlice(); | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
// We won't know if we're paying to ourselves, because there's no | ||
// information in arg to support multiple "change paths". One exception is | ||
// if there are multiple outputs to the change address. | ||
const isChange = | ||
changeData && outputScript.equals(changeData?.cond.scriptPubKey); | ||
// We won't know if we're paying to ourselves, because | ||
// there's no information in the input arg to support this. | ||
// We only have the changePath. | ||
// One exception is if there are multiple outputs to the | ||
// change address. | ||
const isChange = changeData && outputScript.equals(changeData?.script); | ||
if (isChange) { | ||
@@ -295,4 +249,16 @@ changeFound = true; | ||
accountType.setOwnOutput(i, changeData.cond, [pubkey], [changePath]); | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} else if (accountType == AccountType.p2wpkh) { | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} else if (accountType == AccountType.p2wpkhWrapped) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
psbt.setOutputRedeemScript(i, changeData.redeemScript!); | ||
psbt.setOutputBip32Derivation(i, pubkey, masterFp, changePath); | ||
} else if (accountType == AccountType.p2tr) { | ||
psbt.setOutputTapBip32Derivation(i, pubkey, [], masterFp, changePath); | ||
} | ||
} | ||
psbt.setOutputAmount(i, amount); | ||
psbt.setOutputScript(i, outputScript); | ||
} | ||
@@ -302,3 +268,3 @@ if (!changeFound) { | ||
"Change script not found among outputs! " + | ||
changeData?.cond.scriptPubKey.toString("hex") | ||
changeData?.script.toString("hex") | ||
); | ||
@@ -308,31 +274,6 @@ } | ||
const key = createKey(masterFp, accountPath, accountXpub); | ||
const p = new WalletPolicy(accountType.getDescriptorTemplate(), key); | ||
// This is cheating, because it's not actually requested on the | ||
// device yet, but it will be, soonish. | ||
if (arg.onDeviceSignatureRequested) arg.onDeviceSignatureRequested(); | ||
let firstSigned = false; | ||
// This callback will be called once for each signature yielded. | ||
const progressCallback = () => { | ||
if (!firstSigned) { | ||
firstSigned = true; | ||
arg.onDeviceSignatureGranted && arg.onDeviceSignatureGranted(); | ||
} | ||
progress(); | ||
}; | ||
await this.signPsbt(psbt, p, progressCallback); | ||
finalize(psbt); | ||
const serializedTx = extract(psbt); | ||
return serializedTx.toString("hex"); | ||
const p = new WalletPolicy(accountType, key); | ||
return await this.signPsbt(psbt, p); | ||
} | ||
/** | ||
* Calculates an output script along with public key and possible redeemScript | ||
* from a path and accountType. The accountPath must be a prefix of path. | ||
* | ||
* @returns an object with output script (property "script"), redeemScript (if | ||
* wrapped p2wpkh), and pubkey at provided path. The values of these three | ||
* properties depend on the accountType used. | ||
*/ | ||
private async outputScriptAt( | ||
@@ -342,3 +283,5 @@ accountPath: number[], | ||
path: string | undefined | ||
): Promise<{ cond: SpendingCondition; pubkey: Buffer } | undefined> { | ||
): Promise< | ||
{ script: Buffer; redeemScript?: Buffer; pubkey: Buffer } | undefined | ||
> { | ||
if (!path) return undefined; | ||
@@ -355,13 +298,11 @@ const pathElems = pathStringToArray(path); | ||
} | ||
const xpub = await this.client.getExtendedPubkey(false, pathElems); | ||
const pubkey = pubkeyFromXpub(xpub); | ||
const cond = accountType.spendingCondition([pubkey]); | ||
return { cond, pubkey }; | ||
const xpub = await this.client.getPubkey(false, pathElems); | ||
let pubkey = pubkeyFromXpub(xpub); | ||
if (accountType == AccountType.p2tr) { | ||
pubkey = pubkey.slice(1); | ||
} | ||
const script = outputScriptOf(pubkey, accountType); | ||
return { ...script, pubkey }; | ||
} | ||
/** | ||
* Adds relevant data about an input to the psbt. This includes sequence, | ||
* previous txid, output index, spent UTXO, redeem script for wrapped p2wpkh, | ||
* public key and its derivation path. | ||
*/ | ||
private async setInput( | ||
@@ -378,20 +319,14 @@ psbt: PsbtV2, | ||
accountType: AccountType, | ||
masterFP: Buffer, | ||
sigHashType?: number | ||
masterFP: Buffer | ||
): Promise<void> { | ||
const inputTx = input[0]; | ||
const spentOutputIndex = input[1]; | ||
// redeemScript will be null for wrapped p2wpkh, we need to create it | ||
// ourselves. But if set, it should be used. | ||
const redeemScript = input[2] ? Buffer.from(input[2], "hex") : undefined; | ||
const redeemScript = input[2]; | ||
const sequence = input[3]; | ||
if (sequence != undefined) { | ||
if (sequence) { | ||
psbt.setInputSequence(i, sequence); | ||
} | ||
if (sigHashType != undefined) { | ||
psbt.setInputSighashType(i, sigHashType); | ||
} | ||
const inputTxBuffer = serializeTransaction(inputTx, true); | ||
const inputTxid = crypto.hash256(inputTxBuffer); | ||
const xpubBase58 = await this.client.getExtendedPubkey(false, pathElements); | ||
const xpubBase58 = await this.client.getPubkey(false, pathElements); | ||
@@ -401,16 +336,29 @@ const pubkey = pubkeyFromXpub(xpubBase58); | ||
throw Error("Missing outputs array in transaction to sign"); | ||
const spentTxOutput = inputTx.outputs[spentOutputIndex]; | ||
const spendCondition: SpendingCondition = { | ||
scriptPubKey: spentTxOutput.script, | ||
redeemScript: redeemScript, | ||
}; | ||
const spentOutput = { cond: spendCondition, amount: spentTxOutput.amount }; | ||
accountType.setInput( | ||
i, | ||
inputTxBuffer, | ||
spentOutput, | ||
[pubkey], | ||
[pathElements] | ||
); | ||
const spentOutput = inputTx.outputs[spentOutputIndex]; | ||
if (accountType == AccountType.p2pkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
} else if (accountType == AccountType.p2wpkh) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} else if (accountType == AccountType.p2wpkhWrapped) { | ||
psbt.setInputNonWitnessUtxo(i, inputTxBuffer); | ||
psbt.setInputBip32Derivation(i, pubkey, masterFP, pathElements); | ||
if (!redeemScript) { | ||
throw new Error("Missing redeemScript for p2wpkhWrapped input"); | ||
} | ||
const expectedRedeemScript = createRedeemScript(pubkey); | ||
if (redeemScript != expectedRedeemScript.toString("hex")) { | ||
throw new Error("Unexpected redeemScript"); | ||
} | ||
psbt.setInputRedeemScript(i, expectedRedeemScript); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} else if (accountType == AccountType.p2tr) { | ||
const xonly = pubkey.slice(1); | ||
psbt.setInputTapBip32Derivation(i, xonly, [], masterFP, pathElements); | ||
psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.script); | ||
} | ||
psbt.setInputPreviousTxId(i, inputTxid); | ||
@@ -420,25 +368,14 @@ psbt.setInputOutputIndex(i, spentOutputIndex); | ||
/** | ||
* This implements the "Signer" role of the BIP370 transaction signing | ||
* process. | ||
* | ||
* It ssks the hardware device to sign the a psbt using the specified wallet | ||
* policy. This method assumes BIP32 derived keys are used for all inputs, see | ||
* comment in-line. The signatures returned from the hardware device is added | ||
* to the appropriate input fields of the PSBT. | ||
*/ | ||
private async signPsbt( | ||
psbt: PsbtV2, | ||
walletPolicy: WalletPolicy, | ||
progressCallback: () => void | ||
): Promise<void> { | ||
walletPolicy: WalletPolicy | ||
): Promise<string> { | ||
const sigs: Map<number, Buffer> = await this.client.signPsbt( | ||
psbt, | ||
walletPolicy, | ||
Buffer.alloc(32, 0), | ||
progressCallback | ||
Buffer.alloc(32, 0) | ||
); | ||
sigs.forEach((v, k) => { | ||
// Note: Looking at BIP32 derivation does not work in the generic case, | ||
// since some inputs might not have a BIP32-derived pubkey. | ||
// Note: Looking at BIP32 derivation does not work in the generic case. | ||
// some inputs might not have a BIP32-derived pubkey. | ||
const pubkeys = psbt.getInputKeyDatas(k, psbtIn.BIP32_DERIVATION); | ||
@@ -458,24 +395,92 @@ let pubkey; | ||
}); | ||
finalize(psbt); | ||
const serializedTx = extract(psbt); | ||
return serializedTx.toString("hex"); | ||
} | ||
} | ||
function descrTemplFrom( | ||
addressFormat: AddressFormat | ||
): DefaultDescriptorTemplate { | ||
if (addressFormat == "legacy") return "pkh(@0)"; | ||
if (addressFormat == "p2sh") return "sh(wpkh(@0))"; | ||
if (addressFormat == "bech32") return "wpkh(@0)"; | ||
if (addressFormat == "bech32m") return "tr(@0)"; | ||
enum AccountType { | ||
p2pkh = "pkh(@0)", | ||
p2wpkh = "wpkh(@0)", | ||
p2wpkhWrapped = "sh(wpkh(@0))", | ||
p2tr = "tr(@0)", | ||
} | ||
function createRedeemScript(pubkey: Buffer): Buffer { | ||
const pubkeyHash = hashPublicKey(pubkey); | ||
return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]); | ||
} | ||
function outputScriptOf( | ||
pubkey: Buffer, | ||
accountType: AccountType | ||
): { script: Buffer; redeemScript?: Buffer } { | ||
const buf = new BufferWriter(); | ||
const pubkeyHash = hashPublicKey(pubkey); | ||
let redeemScript: Buffer | undefined; | ||
if (accountType == AccountType.p2pkh) { | ||
buf.writeSlice(Buffer.of(OP_DUP, OP_HASH160, HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
buf.writeSlice(Buffer.of(OP_EQUALVERIFY, OP_CHECKSIG)); | ||
} else if (accountType == AccountType.p2wpkhWrapped) { | ||
redeemScript = createRedeemScript(pubkey); | ||
const scriptHash = hashPublicKey(redeemScript); | ||
buf.writeSlice(Buffer.of(OP_HASH160, HASH_SIZE)); | ||
buf.writeSlice(scriptHash); | ||
buf.writeUInt8(OP_EQUAL); | ||
} else if (accountType == AccountType.p2wpkh) { | ||
buf.writeSlice(Buffer.of(0, HASH_SIZE)); | ||
buf.writeSlice(pubkeyHash); | ||
} else if (accountType == AccountType.p2tr) { | ||
console.log("Internal key: " + pubkey.toString("hex")); | ||
const outputKey = getTaprootOutputKey(pubkey); | ||
buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen | ||
buf.writeSlice(outputKey); | ||
} | ||
return { script: buf.buffer(), redeemScript }; | ||
} | ||
function accountTypeFrom(addressFormat: AddressFormat): AccountType { | ||
if (addressFormat == "legacy") return AccountType.p2pkh; | ||
if (addressFormat == "p2sh") return AccountType.p2wpkhWrapped; | ||
if (addressFormat == "bech32") return AccountType.p2wpkh; | ||
if (addressFormat == "bech32m") return AccountType.p2tr; | ||
throw new Error("Unsupported address format " + addressFormat); | ||
} | ||
function accountTypeFromArg( | ||
arg: CreateTransactionArg, | ||
psbt: PsbtV2, | ||
masterFp: Buffer | ||
): AccountType { | ||
if (arg.additionals.includes("bech32m")) return new p2tr(psbt, masterFp); | ||
if (arg.additionals.includes("bech32")) return new p2wpkh(psbt, masterFp); | ||
if (arg.segwit) return new p2wpkhWrapped(psbt, masterFp); | ||
return new p2pkh(psbt, masterFp); | ||
function accountTypeFromArg(arg: CreateTransactionArg): AccountType { | ||
if (arg.additionals.includes("bech32m")) return AccountType.p2tr; | ||
if (arg.additionals.includes("bech32")) return AccountType.p2wpkh; | ||
if (arg.segwit) return AccountType.p2wpkhWrapped; | ||
return AccountType.p2pkh; | ||
} | ||
/* | ||
The following two functions are copied from wallet-btc and adapte. | ||
They should be moved to a library to avoid code reuse. | ||
*/ | ||
function hashTapTweak(x: Buffer): Buffer { | ||
// hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340 | ||
// See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification | ||
const h = crypto.sha256(Buffer.from("TapTweak", "utf-8")); | ||
return crypto.sha256(Buffer.concat([h, h, x])); | ||
} | ||
function getTaprootOutputKey(internalPubkey: Buffer): Buffer { | ||
if (internalPubkey.length != 32) { | ||
throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length); | ||
} | ||
// A BIP32 derived key can be converted to a schnorr pubkey by dropping | ||
// the first byte, which represent the oddness/evenness. In schnorr all | ||
// pubkeys are even. | ||
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion | ||
const evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]); | ||
const tweak = hashTapTweak(internalPubkey); | ||
// Q = P + int(hash_TapTweak(bytes(P)))G | ||
const outputEcdsaKey = Buffer.from(pointAddScalar(evenEcdsaPubkey, tweak)); | ||
// Convert to schnorr. | ||
const outputSchnorrKey = outputEcdsaKey.slice(1); | ||
// Create address | ||
return outputSchnorrKey; | ||
} |
import varuint from "varuint-bitcoin"; | ||
export function unsafeTo64bitLE(n: number): Buffer { | ||
// we want to represent the input as a 8-bytes array | ||
if (n > Number.MAX_SAFE_INTEGER) { | ||
throw new Error("Can't convert numbers > MAX_SAFE_INT"); | ||
} | ||
const byteArray = Buffer.alloc(8, 0); | ||
for (let index = 0; index < byteArray.length; index++) { | ||
const byte = n & 0xff; | ||
byteArray[index] = byte; | ||
n = (n - byte) / 256; | ||
} | ||
return byteArray; | ||
} | ||
export function unsafeFrom64bitLE(byteArray: Buffer): number { | ||
let value = 0; | ||
if (byteArray.length != 8) { | ||
throw new Error("Expected Bufffer of lenght 8"); | ||
} | ||
if (byteArray[7] != 0) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
if (byteArray[6] > 0x1f) { | ||
throw new Error("Can't encode numbers > MAX_SAFE_INT"); | ||
} | ||
for (let i = byteArray.length - 1; i >= 0; i--) { | ||
value = value * 256 + byteArray[i]; | ||
} | ||
return value; | ||
} | ||
export class BufferWriter { | ||
@@ -55,5 +24,4 @@ private bufs: Buffer[] = []; | ||
writeUInt64(i: number): void { | ||
const bytes = unsafeTo64bitLE(i); | ||
this.writeSlice(bytes); | ||
writeUInt64(i: bigint): void { | ||
this.write(8, (b) => b.writeBigUInt64LE(i, 0)); | ||
} | ||
@@ -104,6 +72,6 @@ | ||
readUInt64(): number { | ||
const buf = this.readSlice(8); | ||
const n = unsafeFrom64bitLE(buf); | ||
return n; | ||
readUInt64(): bigint { | ||
const result = this.buffer.readBigUInt64LE(this.offset); | ||
this.offset += 8; | ||
return result; | ||
} | ||
@@ -110,0 +78,0 @@ |
@@ -26,6 +26,2 @@ import Transport from "@ledgerhq/hw-transport"; | ||
/** | ||
* This class encapsulates the APDU protocol documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md | ||
*/ | ||
export class AppClient { | ||
@@ -67,6 +63,3 @@ transport: Transport; | ||
async getExtendedPubkey( | ||
display: boolean, | ||
pathElements: number[] | ||
): Promise<string> { | ||
async getPubkey(display: boolean, pathElements: number[]): Promise<string> { | ||
if (pathElements.length > 6) { | ||
@@ -78,3 +71,3 @@ throw new Error("Path too long. At most 6 levels allowed."); | ||
Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
pathElementsToBuffer(pathElements), | ||
@@ -102,3 +95,3 @@ ]) | ||
const clientInterpreter = new ClientCommandInterpreter(() => {}); | ||
const clientInterpreter = new ClientCommandInterpreter(); | ||
clientInterpreter.addKnownList( | ||
@@ -115,6 +108,6 @@ walletPolicy.keys.map((k) => Buffer.from(k, "ascii")) | ||
Buffer.concat([ | ||
Buffer.from(display ? [1] : [0]), | ||
Buffer.of(display ? 1 : 0), | ||
walletPolicy.getWalletId(), | ||
walletHMAC || Buffer.alloc(32, 0), | ||
Buffer.from([change]), | ||
Buffer.of(change), | ||
addressIndexBuffer, | ||
@@ -131,4 +124,3 @@ ]), | ||
walletPolicy: WalletPolicy, | ||
walletHMAC: Buffer | null, | ||
progressCallback: () => void | ||
walletHMAC: Buffer | null | ||
): Promise<Map<number, Buffer>> { | ||
@@ -141,3 +133,3 @@ const merkelizedPsbt = new MerkelizedPsbt(psbt); | ||
const clientInterpreter = new ClientCommandInterpreter(progressCallback); | ||
const clientInterpreter = new ClientCommandInterpreter(); | ||
@@ -191,4 +183,4 @@ // prepare ClientCommandInterpreter | ||
async getMasterFingerprint(): Promise<Buffer> { | ||
return this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.from([])); | ||
return this.makeRequest(BitcoinIns.GET_MASTER_FINGERPRINT, Buffer.of()); | ||
} | ||
} |
import { crypto } from "bitcoinjs-lib"; | ||
import { BufferReader } from "../buffertools"; | ||
import { createVarint } from "../varint"; | ||
@@ -25,3 +24,3 @@ import { hashLeaf, Merkle } from "./merkle"; | ||
constructor(results: Buffer[], private progressCallback: () => void) { | ||
constructor(results: Buffer[]) { | ||
super(); | ||
@@ -33,3 +32,2 @@ this.results = results; | ||
this.results.push(Buffer.from(request.subarray(1))); | ||
this.progressCallback(); | ||
return Buffer.from(""); | ||
@@ -52,3 +50,3 @@ } | ||
execute(request: Buffer): Buffer { | ||
const req = Buffer.from(request.subarray(1)); | ||
const req = request.subarray(1); | ||
@@ -90,3 +88,3 @@ // we expect no more data to read | ||
Buffer.from([payload_size]), | ||
Buffer.from(known_preimage.subarray(0, payload_size)), | ||
known_preimage.subarray(0, payload_size), | ||
]); | ||
@@ -112,22 +110,17 @@ } | ||
execute(request: Buffer): Buffer { | ||
const req = Buffer.from(request.subarray(1)); | ||
const req = request.subarray(1); | ||
if (req.length < 32 + 1 + 1) { | ||
throw new Error("Invalid request, expected at least 34 bytes"); | ||
if (req.length != 32 + 4 + 4) { | ||
throw new Error("Invalid request, unexpected trailing data"); | ||
} | ||
const reqBuf = new BufferReader(req); | ||
const hash = reqBuf.readSlice(32); | ||
// read the hash | ||
const hash = Buffer.alloc(32); | ||
for (let i = 0; i < 32; i++) { | ||
hash[i] = req.readUInt8(i); | ||
} | ||
const hash_hex = hash.toString("hex"); | ||
let tree_size; | ||
let leaf_index; | ||
try { | ||
tree_size = reqBuf.readVarInt(); | ||
leaf_index = reqBuf.readVarInt(); | ||
} catch (e: any) { | ||
throw new Error( | ||
"Invalid request, couldn't parse tree_size or leaf_index" | ||
); | ||
} | ||
const tree_size = req.readUInt32BE(32); | ||
const leaf_index = req.readUInt32BE(32 + 4); | ||
@@ -182,3 +175,3 @@ const mt = this.known_trees.get(hash_hex); | ||
execute(request: Buffer): Buffer { | ||
const req = Buffer.from(request.subarray(1)); | ||
const req = request.subarray(1); | ||
@@ -263,17 +256,2 @@ if (req.length != 32 + 32) { | ||
/** | ||
* This class will dispatch a client command coming from the hardware device to | ||
* the appropriate client command implementation. Those client commands | ||
* typically requests data from a merkle tree or merkelized maps. | ||
* | ||
* A ClientCommandInterpreter is prepared by adding the merkle trees and | ||
* merkelized maps it should be able to serve to the hardware device. This class | ||
* doesn't know anything about the semantics of the data it holds, it just | ||
* serves merkle data. It doesn't even know in what context it is being | ||
* executed, ie SignPsbt, getWalletAddress, etc. | ||
* | ||
* If the command yelds results to the client, as signPsbt does, the yielded | ||
* data will be accessible after the command completed by calling getYielded(), | ||
* which will return the yields in the same order as they came in. | ||
*/ | ||
export class ClientCommandInterpreter { | ||
@@ -289,5 +267,5 @@ private roots: Map<string, Merkle> = new Map(); | ||
constructor(progressCallback: () => void) { | ||
constructor() { | ||
const commands = [ | ||
new YieldCommand(this.yielded, progressCallback), | ||
new YieldCommand(this.yielded), | ||
new GetPreimageCommand(this.preimages, this.queue), | ||
@@ -294,0 +272,0 @@ new GetMerkleLeafIndexCommand(this.roots), |
import { MerkleMap } from "./merkleMap"; | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This class merkelizes a PSBTv2, by merkelizing the different | ||
* maps of the psbt. This is used during the transaction signing process, | ||
* where the hardware app can request specific parts of the psbt from the | ||
* client code and be sure that the response data actually belong to the psbt. | ||
* The reason for this is the limited amount of memory available to the app, | ||
* so it can't always store the full psbt in memory. | ||
* | ||
* The signing process is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt | ||
*/ | ||
export class MerkelizedPsbt extends PsbtV2 { | ||
@@ -16,0 +5,0 @@ public globalMerkleMap: MerkleMap; |
import { crypto } from "bitcoinjs-lib"; | ||
/** | ||
* This class implements the merkle tree used by Ledger Bitcoin app v2+, | ||
* which is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md | ||
*/ | ||
export class Merkle { | ||
@@ -65,3 +60,3 @@ private leaves: Buffer[]; | ||
hashNode(left: Buffer, right: Buffer): Buffer { | ||
return this.h(Buffer.concat([Buffer.from([1]), left, right])); | ||
return this.h(Buffer.concat([Buffer.of(1), left, right])); | ||
} | ||
@@ -74,3 +69,3 @@ } | ||
): Buffer { | ||
return hashConcat(Buffer.from([0]), buf, hashFunction); | ||
return hashConcat(Buffer.of(0), buf, hashFunction); | ||
} | ||
@@ -77,0 +72,0 @@ |
import { createVarint } from "../varint"; | ||
import { hashLeaf, Merkle } from "./merkle"; | ||
/** | ||
* This implements "Merkelized Maps", documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps | ||
* | ||
* A merkelized map consist of two merkle trees, one for the keys of | ||
* a map and one for the values of the same map, thus the two merkle | ||
* trees have the same shape. The commitment is the number elements | ||
* in the map followed by the keys' merkle root followed by the | ||
* values' merkle root. | ||
*/ | ||
export class MerkleMap { | ||
@@ -15,0 +5,0 @@ keys: Buffer[]; |
@@ -1,5 +0,5 @@ | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { pathArrayToString } from "../bip32"; | ||
import { BufferWriter } from "../buffertools"; | ||
import { hashLeaf, Merkle } from "./merkle"; | ||
import { crypto } from "bitcoinjs-lib"; | ||
import { Merkle, hashLeaf } from "./merkle"; | ||
@@ -12,10 +12,2 @@ export type DefaultDescriptorTemplate = | ||
/** | ||
* The Bitcon hardware app uses a descriptors-like thing to describe | ||
* how to construct output scripts from keys. A "Wallet Policy" consists | ||
* of a "Descriptor Template" and a list of "keys". A key is basically | ||
* a serialized BIP32 extended public key with some added derivation path | ||
* information. This is documented at | ||
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md | ||
*/ | ||
export class WalletPolicy { | ||
@@ -22,0 +14,0 @@ descriptorTemplate: string; |
import { BufferWriter } from "../buffertools"; | ||
import { PsbtV2 } from "./psbtv2"; | ||
/** | ||
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However | ||
* the role is partially documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor). | ||
*/ | ||
export function extract(psbt: PsbtV2): Buffer { | ||
@@ -16,3 +10,3 @@ const tx = new BufferWriter(); | ||
if (isSegwit) { | ||
tx.writeSlice(Buffer.from([0, 1])); | ||
tx.writeSlice(Buffer.of(0, 1)); | ||
} | ||
@@ -25,3 +19,3 @@ const inputCount = psbt.getGlobalInputCount(); | ||
tx.writeUInt32(psbt.getInputOutputIndex(i)); | ||
tx.writeVarSlice(psbt.getInputFinalScriptsig(i) ?? Buffer.from([])); | ||
tx.writeVarSlice(psbt.getInputFinalScriptsig(i) ?? Buffer.of()); | ||
tx.writeUInt32(psbt.getInputSequence(i)); | ||
@@ -35,3 +29,3 @@ if (isSegwit) { | ||
for (let i = 0; i < outputCount; i++) { | ||
tx.writeUInt64(psbt.getOutputAmount(i)); | ||
tx.writeUInt64(BigInt(psbt.getOutputAmount(i))); | ||
tx.writeVarSlice(psbt.getOutputScript(i)); | ||
@@ -38,0 +32,0 @@ } |
@@ -5,14 +5,4 @@ import { BufferWriter } from "../buffertools"; | ||
/** | ||
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However | ||
* the role is documented in BIP174 (PSBTv0 | ||
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). | ||
* | ||
* Verify that all inputs have a signature, and set inputFinalScriptwitness | ||
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean | ||
* fields that aren't useful anymore, partial signatures, redeem script and | ||
* derivation paths. | ||
* | ||
* @param psbt The psbt with all signatures added as partial sigs, either | ||
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
* @param psbt The psbt with all signatures added as partial sigs, either through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG | ||
*/ | ||
@@ -89,9 +79,2 @@ export function finalize(psbt: PsbtV2): void { | ||
/** | ||
* Deletes fields that are no longer neccesary from the psbt. | ||
* | ||
* Note, the spec doesn't say anything about removing ouput fields | ||
* like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them | ||
* without actually knowing why. I think we should remove them too. | ||
*/ | ||
function clearFinalizedInput(psbt: PsbtV2, inputIndex: number) { | ||
@@ -115,10 +98,2 @@ const keyTypes = [ | ||
/** | ||
* Writes a script push operation to buf, which looks different | ||
* depending on the size of the data. See | ||
* https://en.bitcoin.it/wiki/Script#Constants | ||
* | ||
* @param buf the BufferWriter to write to | ||
* @param data the Buffer to be pushed. | ||
*/ | ||
function writePush(buf: BufferWriter, data: Buffer) { | ||
@@ -125,0 +100,0 @@ if (data.length <= 75) { |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ | ||
import { | ||
BufferReader, | ||
BufferWriter, | ||
unsafeFrom64bitLE, | ||
unsafeTo64bitLE, | ||
} from "../buffertools"; | ||
import { BufferReader, BufferWriter } from "../buffertools"; | ||
@@ -22,3 +17,2 @@ export enum psbtGlobal { | ||
PARTIAL_SIG = 0x02, | ||
SIGHASH_TYPE = 0x03, | ||
REDEEM_SCRIPT = 0x04, | ||
@@ -42,24 +36,6 @@ BIP32_DERIVATION = 0x06, | ||
const PSBT_MAGIC_BYTES = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]); | ||
const PSBT_MAGIC_BYTES = Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff); | ||
export class NoSuchEntry extends Error {} | ||
/** | ||
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as | ||
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki | ||
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki | ||
* | ||
* A psbt is a data structure that can carry all relevant information about a | ||
* transaction through all stages of the signing process. From constructing an | ||
* unsigned transaction to extracting the final serialized transaction ready for | ||
* broadcast. | ||
* | ||
* This implementation is limited to what's needed in ledgerjs to carry out its | ||
* duties, which means that support for features like multisig or taproot script | ||
* path spending are not implemented. Specifically, it supports p2pkh, | ||
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending. | ||
* | ||
* This class is made purposefully dumb, so it's easy to add support for | ||
* complemantary fields as needed in the future. | ||
*/ | ||
export class PsbtV2 { | ||
@@ -139,10 +115,2 @@ protected globalMap: Map<string, Buffer> = new Map(); | ||
} | ||
setInputSighashType(inputIndex: number, sigHashtype: number) { | ||
this.setInput(inputIndex, psbtIn.SIGHASH_TYPE, b(), uint32LE(sigHashtype)); | ||
} | ||
getInputSighashType(inputIndex: number): number | undefined { | ||
const result = this.getInputOptional(inputIndex, psbtIn.SIGHASH_TYPE, b()); | ||
if (!result) return undefined; | ||
return result.readUInt32LE(0); | ||
} | ||
setInputRedeemScript(inputIndex: number, redeemScript: Buffer) { | ||
@@ -274,4 +242,5 @@ this.setInput(inputIndex, psbtIn.REDEEM_SCRIPT, b(), redeemScript); | ||
getOutputAmount(outputIndex: number): number { | ||
const buf = this.getOutput(outputIndex, psbtOut.AMOUNT, b()); | ||
return unsafeFrom64bitLE(buf); | ||
return Number( | ||
this.getOutput(outputIndex, psbtOut.AMOUNT, b()).readBigUInt64LE(0) | ||
); | ||
} | ||
@@ -332,3 +301,3 @@ setOutputScript(outputIndex: number, scriptPubKey: Buffer) { | ||
const buf = new BufferWriter(); | ||
buf.writeSlice(Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff])); | ||
buf.writeSlice(Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff)); | ||
serializeMap(buf, this.globalMap); | ||
@@ -383,3 +352,3 @@ this.inputMaps.forEach((map) => { | ||
private setGlobal(keyType: KeyType, value: Buffer) { | ||
const key = new Key(keyType, Buffer.from([])); | ||
const key = new Key(keyType, Buffer.of()); | ||
this.globalMap.set(key.toString(), value); | ||
@@ -401,2 +370,11 @@ } | ||
} | ||
private getMap( | ||
index: number, | ||
maps: Map<string, Buffer>[] | ||
): Map<string, Buffer> { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
} | ||
private getInput(index: number, keyType: KeyType, keyData: Buffer): Buffer { | ||
@@ -423,10 +401,8 @@ return get(this.inputMaps[index], keyType, keyData, false)!; | ||
} | ||
private getMap( | ||
private getOutputOptional( | ||
index: number, | ||
maps: Map<string, Buffer>[] | ||
): Map<string, Buffer> { | ||
if (maps[index]) { | ||
return maps[index]; | ||
} | ||
return (maps[index] = new Map()); | ||
keyType: KeyType, | ||
keyData: Buffer | ||
): Buffer | undefined { | ||
return get(this.outputMaps[index], keyType, keyData, true); | ||
} | ||
@@ -556,3 +532,3 @@ private encodeBip32Derivation(masterFingerprint: Buffer, path: number[]) { | ||
function b(): Buffer { | ||
return Buffer.from([]); | ||
return Buffer.of(); | ||
} | ||
@@ -574,3 +550,5 @@ function set( | ||
function uint64LE(n: number): Buffer { | ||
return unsafeTo64bitLE(n); | ||
const b = Buffer.alloc(8); | ||
b.writeBigUInt64LE(BigInt(n), 0); | ||
return b; | ||
} | ||
@@ -577,0 +555,0 @@ function varint(n: number): Buffer { |
@@ -1,2 +0,1 @@ | ||
import Transport from "@ledgerhq/hw-transport"; | ||
import { | ||
@@ -7,6 +6,2 @@ openTransportReplayer, | ||
import Btc from "../src/Btc"; | ||
import BtcNew from "../src/BtcNew"; | ||
import BtcOld, { AddressFormat } from "../src/BtcOld"; | ||
import { AppAndVersion, getAppAndVersion } from "../src/getAppAndVersion"; | ||
import { TestingClient } from "./newops/integrationtools"; | ||
@@ -110,20 +105,10 @@ test("btc.getWalletXpub", async () => { | ||
/*eslint-disable */ | ||
const pubkeyParent = | ||
"045d4a72237572a91e13818fa38cedabe6174569cc9a319012f75150d5c0a0639d54eafd13a68d079b7a67764800c6a981825ef52384f08c3925109188ab21bc09"; | ||
const addrParent = Buffer.from( | ||
"1NjiCsVBuKDT62LmaUd7WZZZBK2gPAkisb", | ||
"ascii" | ||
).toString("hex"); | ||
const ccParent = | ||
"8bd937d416de7020952cc8e2c99ce9ac7e01265e31ceb8e47bf9c37f46f8abbd"; | ||
const pubkeyParent = "045d4a72237572a91e13818fa38cedabe6174569cc9a319012f75150d5c0a0639d54eafd13a68d079b7a67764800c6a981825ef52384f08c3925109188ab21bc09"; | ||
const addrParent = Buffer.from("1NjiCsVBuKDT62LmaUd7WZZZBK2gPAkisb", "ascii").toString("hex"); | ||
const ccParent = "8bd937d416de7020952cc8e2c99ce9ac7e01265e31ceb8e47bf9c37f46f8abbd"; | ||
const responseParent = `41${pubkeyParent}22${addrParent}${ccParent}`; | ||
const pubkeyAcc = | ||
"04250dfdfb84c1efd160ed0e10ebac845d0e4b04277174630ba56de96bbd3afb21fc6c04ce0d5a0cbd784fdabc99d16269c27cf3842fe8440f1f21b8af900f0eaa"; | ||
const addrAcc = Buffer.from( | ||
"16Y97ByhyboePhTYMMmFj1tq5Cy1bDq8jT", | ||
"ascii" | ||
).toString("hex"); | ||
const ccAcc = | ||
"c071c6f2d05cbc9ea9a04951b238086ce1608cf00020c3cab85b36aac5fdd591"; | ||
const pubkeyAcc = "04250dfdfb84c1efd160ed0e10ebac845d0e4b04277174630ba56de96bbd3afb21fc6c04ce0d5a0cbd784fdabc99d16269c27cf3842fe8440f1f21b8af900f0eaa"; | ||
const addrAcc = Buffer.from("16Y97ByhyboePhTYMMmFj1tq5Cy1bDq8jT", "ascii").toString("hex"); | ||
const ccAcc = "c071c6f2d05cbc9ea9a04951b238086ce1608cf00020c3cab85b36aac5fdd591"; | ||
/*eslint-enable */ | ||
@@ -138,3 +123,3 @@ const responseAcc = `41${pubkeyAcc}22${addrAcc}${ccAcc}`; | ||
=> e04000000d038000002c8000000080000011 | ||
<= ${responseAcc}9000 | ||
<= ${responseAcc}9000 | ||
`) | ||
@@ -442,98 +427,1 @@ ); | ||
}); | ||
function testBackend(s: string): any { | ||
return async () => { | ||
return { publicKey: s, bitcoinAddress: "", chainCode: "" }; | ||
}; | ||
} | ||
class TestBtc extends Btc { | ||
n: BtcNew; | ||
o: BtcOld; | ||
constructor(public tr: Transport) { | ||
super(tr); | ||
this.n = new BtcNew(new TestingClient(tr)); | ||
this.n.getWalletPublicKey = testBackend("new"); | ||
this.o = new BtcOld(tr); | ||
this.o.getWalletPublicKey = testBackend("old"); | ||
} | ||
protected new(): BtcNew { | ||
return this.n; | ||
} | ||
protected old(): BtcOld { | ||
return this.o; | ||
} | ||
} | ||
// test.each` | ||
// a | b | expected | ||
// ${1} | ${1} | ${2} | ||
// ${1} | ${2} | ${3} | ||
// ${2} | ${1} | ${3} | ||
// `('returns $expected when $a is added $c', ({ a, c, expected }) => { | ||
// expect(a + c).toBe(expected); | ||
// }); | ||
test.each` | ||
app | ver | path | format | display | exp | ||
${"Bitcoin"} | ${"1.99.99"} | ${"m/44'/0'/1'"} | ${"bech32m"} | ${false} | ${""} | ||
${"Bitcoin"} | ${"1.99.99"} | ${"m/44'/0'"} | ${"bech32m"} | ${false} | ${""} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'"} | ${"bech32m"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32m"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-beta"} | ${"m/84'/1'/0'"} | ${"bech32"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'"} | ${"bech32"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32"} | ${undefined} | ${"old"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32"} | ${true} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/0/0"} | ${"bech32"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"bech32"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"legacy"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"p2sh"} | ${false} | ${"new"} | ||
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/2/0"} | ${"bech32"} | ${false} | ${"old"} | ||
`( | ||
"dispatch $app $ver $path $format $display to $exp", | ||
async ({ app, ver, path, format, display, exp }) => { | ||
const appName = Buffer.from([app.length]) | ||
.toString("hex") | ||
.concat(Buffer.from(app, "ascii").toString("hex")); | ||
const appVersion = Buffer.from([ver.length]) | ||
.toString("hex") | ||
.concat(Buffer.from(ver, "ascii").toString("hex")); | ||
const resp = `01${appName}${appVersion}01029000`; | ||
const tr = await openTransportReplayer( | ||
RecordStore.fromString(`=> b001000000\n <= ${resp}`) | ||
); | ||
const btc = new TestBtc(tr); | ||
try { | ||
const key = await btc.getWalletPublicKey(path, { | ||
format: format, | ||
verify: display, | ||
}); | ||
if (exp === "") { | ||
expect(1).toEqual(0); // Allways fail. Don't know how to do that properly | ||
} | ||
expect(key.publicKey).toEqual(exp); | ||
} catch (e: any) { | ||
if (exp != "") { | ||
throw e; | ||
} | ||
expect(exp).toEqual(""); | ||
} | ||
} | ||
); | ||
// test("getWalletPublicKey compatibility for internal hardened keys", async () => { | ||
// await testDispatch("Bitcoin", "1.99.99", "m/44'/0'/1'", "bech32m", ""); | ||
// await testDispatch("Bitcoin", "1.99.99", "m/44'/0'", "bech32m", ""); | ||
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'/1'", "bech32m", "new"); | ||
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'", "bech32m", "new"); | ||
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'/1'", "bech32", "new"); | ||
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'", "bech32", "old"); | ||
// }); | ||
async function testDispatch( | ||
name: string, | ||
version: string, | ||
path: string, | ||
addressFormat: AddressFormat | undefined, | ||
exp: string | ||
): Promise<void> {} |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable prettier/prettier */ | ||
import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker"; | ||
@@ -12,5 +13,4 @@ import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer"; | ||
import { PsbtV2 } from "../../src/newops/psbtv2"; | ||
import { splitTransaction } from "../../src/splitTransaction"; | ||
import { StandardPurpose, addressFormatFromDescriptorTemplate, creatDummyXpub, masterFingerprint, runSignTransaction, TestingClient } from "./integrationtools"; | ||
import { CoreInput, CoreTx, p2pkh, p2tr, p2wpkh, wrappedP2wpkh, wrappedP2wpkhTwoInputs } from "./testtx"; | ||
import { AccountType, addressFormatFromDescriptorTemplate, creatDummyXpub, masterFingerprint, runSignTransaction, TestingClient } from "./integrationtools"; | ||
import { CoreTx, p2pkh, p2tr, p2wpkh, p2wpkhTwoInputs, wrappedP2wpkh, wrappedP2wpkhTwoInputs } from "./testtx"; | ||
@@ -45,3 +45,3 @@ test("getWalletPublicKey p2pkh", async () => { | ||
function testPaths(type: StandardPurpose): { ins: string[], out?: string } { | ||
function testPaths(type: AccountType): {ins: string[], out?: string} { | ||
const basePath = `m/${type}/1'/0'/`; | ||
@@ -56,3 +56,3 @@ const ins = [ | ||
]; | ||
return { ins }; | ||
return {ins}; | ||
} | ||
@@ -62,48 +62,19 @@ | ||
const changePubkey = "037ed58c914720772c59f7a1e7e76fba0ef95d7c5667119798586301519b9ad2cf"; | ||
await runSignTransactionTest(p2pkh, StandardPurpose.p2pkh, changePubkey); | ||
await runSignTransactionTest(p2pkh, AccountType.p2pkh, changePubkey); | ||
}); | ||
test("Sign p2wpkh wrapped", async () => { | ||
let changePubkey = "03efc6b990c1626d08bd176aab0e545a4f55c627c7ddee878d12bbbc46a126177a"; | ||
await runSignTransactionTest(wrappedP2wpkh, StandardPurpose.p2wpkhInP2sh, changePubkey); | ||
await runSignTransactionTest(wrappedP2wpkh, AccountType.p2wpkhInP2sh, changePubkey); | ||
changePubkey = "031175a985c56e310ce3496a819229b427a2172920fd20b5972dda62758c6def09"; | ||
await runSignTransactionTest(wrappedP2wpkhTwoInputs, StandardPurpose.p2wpkhInP2sh, changePubkey); | ||
await runSignTransactionTest(wrappedP2wpkhTwoInputs, AccountType.p2wpkhInP2sh, changePubkey); | ||
}); | ||
test("Sign p2wpkh", async () => { | ||
await runSignTransactionTest(p2wpkh, StandardPurpose.p2wpkh); | ||
await runSignTransactionTest(p2wpkh, AccountType.p2wpkh); | ||
await runSignTransactionTest(p2wpkhTwoInputs, AccountType.p2wpkh); | ||
}); | ||
test("Sign p2tr", async () => { | ||
// This tx uses locktime, so this test verifies that locktime is propagated to/from | ||
// the psbt correctly. | ||
await runSignTransactionTest(p2tr, StandardPurpose.p2tr); | ||
await runSignTransactionTest(p2tr, AccountType.p2tr); | ||
}); | ||
test("Sign p2tr with sigHashType", async () => { | ||
const testTx = JSON.parse(JSON.stringify(p2tr)); | ||
testTx.vin.forEach((input: CoreInput, index: number) => { | ||
// Test SIGHASH_SINGLE | SIGHASH_ANYONECANPAY, 0x83 | ||
const sig = input.txinwitness![0] + "83"; | ||
input.txinwitness = [sig]; | ||
}) | ||
const tx = await runSignTransactionNoVerification(testTx, StandardPurpose.p2tr); | ||
// The verification of the sighashtype is done in MockClient.signPsbt | ||
}) | ||
test("Sign p2tr sequence 0", async() => { | ||
const testTx = JSON.parse(JSON.stringify(p2tr)); | ||
testTx.vin.forEach((input: CoreInput, index: number) => { | ||
input.sequence = 0; | ||
}) | ||
const tx = await runSignTransactionNoVerification(testTx, StandardPurpose.p2tr); | ||
const txObj = splitTransaction(tx, true); | ||
txObj.inputs.forEach((input, index) => { | ||
expect(input.sequence.toString("hex")).toEqual("00000000"); | ||
}) | ||
}) | ||
async function runSignTransactionTest(testTx: CoreTx, accountType: StandardPurpose, changePubkey?: string) { | ||
const tx = await runSignTransactionNoVerification(testTx, accountType, changePubkey); | ||
expect(tx).toEqual(testTx.hex); | ||
} | ||
async function runSignTransactionNoVerification(testTx: CoreTx, accountType: StandardPurpose, changePubkey?: string): Promise<string> { | ||
async function runSignTransactionTest(testTx: CoreTx, accountType: AccountType, changePubkey?: string) { | ||
const [client, transport] = await createClient(); | ||
@@ -114,10 +85,11 @@ const accountXpub = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"; | ||
if (changePubkey) { | ||
paths.out = `m/${accountType}/1'/0'` + "/1/3"; | ||
paths.out = `m/${accountType}/1'/0'` + "/1/3"; | ||
client.mockGetPubkeyResponse(paths.out, creatDummyXpub(Buffer.from(changePubkey, "hex"))); | ||
} | ||
const tx = await runSignTransaction(testTx, paths, client, transport); | ||
expect(tx).toEqual(testTx.hex); | ||
await transport.close(); | ||
return tx; | ||
} | ||
async function testGetWalletXpub(path: string, version = 0x043587cf) { | ||
@@ -128,3 +100,3 @@ const [client] = await createClient(); | ||
const btc = new BtcNew(client); | ||
const result = await btc.getWalletXpub({ path: path, xpubVersion: version }); | ||
const result = await btc.getWalletXpub({path: path, xpubVersion: version}); | ||
expect(result).toEqual(expectedXpub); | ||
@@ -156,3 +128,3 @@ } | ||
const addressFormat = addressFormatFromDescriptorTemplate(expectedDescriptorTemplate); | ||
const result = await btcNew.getWalletPublicKey(path, { format: addressFormat }); | ||
const result = await btcNew.getWalletPublicKey(path, {format: addressFormat}); | ||
verifyGetWalletPublicKeyResult(result, keyXpub, "testaddress"); | ||
@@ -205,3 +177,3 @@ | ||
} | ||
async getExtendedPubkey(display: boolean, pathElements: number[]): Promise<string> { | ||
async getPubkey(display: boolean, pathElements: number[]): Promise<string> { | ||
const path = pathArrayToString(pathElements); | ||
@@ -234,19 +206,7 @@ const response = this.getPubkeyResponses.get(path); | ||
async signPsbt( | ||
psbt: PsbtV2, | ||
_psbt: PsbtV2, | ||
_walletPolicy: WalletPolicy, | ||
_walletHMAC: Buffer | null, | ||
_walletHMAC: Buffer | null | ||
): Promise<Map<number, Buffer>> { | ||
const sigs = this.yieldSigs.splice(0, 1)[0]; | ||
const sig0 = sigs.get(0)!; | ||
if (sig0.length == 64) { | ||
// Taproot may leave out sighash type, which defaults to 0x01 SIGHASH_ALL | ||
return sigs; | ||
} | ||
const sigHashType = sig0.readUInt8(sig0.length - 1); | ||
if (sigHashType != 0x01) { | ||
for (let i = 0; i < psbt.getGlobalInputCount(); i++) { | ||
expect(psbt.getInputSighashType(i)).toEqual(sigHashType); | ||
} | ||
} | ||
return sigs; | ||
return this.yieldSigs.splice(0, 1)[0]; | ||
} | ||
@@ -253,0 +213,0 @@ private getWalletAddressKey( |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable prettier/prettier */ | ||
import Transport from "@ledgerhq/hw-transport"; | ||
@@ -12,3 +13,3 @@ import bs58check from "bs58check"; | ||
DefaultDescriptorTemplate, | ||
WalletPolicy, | ||
WalletPolicy | ||
} from "../../src/newops/policy"; | ||
@@ -18,8 +19,8 @@ import { Transaction } from "../../src/types"; | ||
export async function runSignTransaction( | ||
testTx: CoreTx, | ||
testPaths: { ins: string[]; out?: string }, | ||
client: TestingClient, | ||
transport: Transport | ||
): Promise<string> { | ||
testTx: CoreTx, | ||
testPaths: {ins: string[], out?: string}, | ||
client: TestingClient, | ||
transport: Transport): Promise<string> { | ||
const btcNew = new BtcNew(client); | ||
@@ -30,6 +31,6 @@ // btc is needed to perform some functions like splitTransaction. | ||
const additionals: string[] = []; | ||
if (accountType == StandardPurpose.p2wpkh) { | ||
if (accountType == AccountType.p2wpkh) { | ||
additionals.push("bech32"); | ||
} | ||
if (accountType == StandardPurpose.p2tr) { | ||
if (accountType == AccountType.p2tr) { | ||
additionals.push("bech32m"); | ||
@@ -43,9 +44,3 @@ } | ||
const inputData = createInput(input, btc); | ||
const pubkey = getPubkey( | ||
index, | ||
accountType, | ||
testTx, | ||
inputData[0], | ||
inputData[1] | ||
); | ||
const pubkey = getPubkey(index, accountType, testTx, inputData[0], inputData[1]); | ||
const mockXpub = creatDummyXpub(pubkey); | ||
@@ -56,22 +51,12 @@ client.mockGetPubkeyResponse(path, mockXpub); | ||
}); | ||
const sig0 = yieldSigs.get(0)!; | ||
let sigHashType: number | undefined = sig0.readUInt8(sig0.length - 1); | ||
if (sigHashType == 0x01) { | ||
sigHashType = undefined; | ||
} | ||
client.mockSignPsbt(yieldSigs); | ||
const outputWriter = new BufferWriter(); | ||
outputWriter.writeVarInt(testTx.vout.length); | ||
testTx.vout.forEach((output) => { | ||
outputWriter.writeUInt64( | ||
Number.parseFloat((output.value * 100000000).toFixed(8)) | ||
); | ||
outputWriter.writeVarSlice(Buffer.from(output.scriptPubKey.hex, "hex")); | ||
testTx.vout.forEach(output => { | ||
outputWriter.writeUInt64(BigInt(Number.parseFloat((output.value * 100000000).toFixed(8)))); | ||
outputWriter.writeVarSlice(Buffer.from(output.scriptPubKey.hex, "hex")); | ||
}); | ||
const outputScriptHex = outputWriter.buffer().toString("hex"); | ||
let callbacks = ""; | ||
function logCallback(message: string) { | ||
callbacks += new Date().toISOString() + " " + message + "\n"; | ||
} | ||
const arg: CreateTransactionArg = { | ||
const outputScriptHex = outputWriter.buffer().toString("hex"); | ||
const arg: CreateTransactionArg = { | ||
inputs, | ||
@@ -83,19 +68,9 @@ additionals, | ||
lockTime: testTx.locktime, | ||
sigHashType, | ||
segwit: accountType != StandardPurpose.p2pkh, | ||
onDeviceSignatureGranted: () => logCallback("CALLBACK: signature granted"), | ||
onDeviceSignatureRequested: () => | ||
logCallback("CALLBACK: signature requested"), | ||
onDeviceStreaming: (arg) => logCallback("CALLBACK: " + JSON.stringify(arg)), | ||
segwit: accountType != AccountType.p2pkh, | ||
}; | ||
logCallback("Start createPaymentTransactionNew"); | ||
const tx = await btcNew.createPaymentTransactionNew(arg); | ||
logCallback("Done createPaymentTransactionNew"); | ||
// console.log(callbacks); | ||
return tx; | ||
} | ||
}; | ||
export function addressFormatFromDescriptorTemplate( | ||
descTemp: DefaultDescriptorTemplate | ||
): AddressFormat { | ||
export function addressFormatFromDescriptorTemplate(descTemp: DefaultDescriptorTemplate): AddressFormat { | ||
if (descTemp == "tr(@0)") return "bech32m"; | ||
@@ -108,27 +83,18 @@ if (descTemp == "pkh(@0)") return "legacy"; | ||
export enum StandardPurpose { | ||
export enum AccountType { | ||
p2tr = "86'", | ||
p2wpkh = "84'", | ||
p2wpkhInP2sh = "49'", | ||
p2pkh = "44'", | ||
p2pkh = "44'" | ||
} | ||
function getPubkey( | ||
inputIndex: number, | ||
accountType: StandardPurpose, | ||
testTx: CoreTx, | ||
spentTx: Transaction, | ||
spentOutputIndex: number | ||
): Buffer { | ||
function getPubkey(inputIndex: number, accountType: AccountType, testTx: CoreTx, spentTx: Transaction, spentOutputIndex: number): Buffer { | ||
const scriptSig = Buffer.from(testTx.vin[inputIndex].scriptSig.hex, "hex"); | ||
if (accountType == StandardPurpose.p2pkh) { | ||
return scriptSig.slice(scriptSig.length - 33); | ||
if (accountType == AccountType.p2pkh) { | ||
return scriptSig.slice(scriptSig.length-33); | ||
} | ||
if (accountType == StandardPurpose.p2tr) { | ||
if (accountType == AccountType.p2tr) { | ||
return spentTx.outputs![spentOutputIndex].script.slice(2, 34); // 32 bytes x-only pubkey | ||
} | ||
if ( | ||
accountType == StandardPurpose.p2wpkh || | ||
accountType == StandardPurpose.p2wpkhInP2sh | ||
) { | ||
if (accountType == AccountType.p2wpkh || accountType == AccountType.p2wpkhInP2sh) { | ||
return Buffer.from(testTx.vin[inputIndex].txinwitness![1], "hex"); | ||
@@ -139,17 +105,11 @@ } | ||
function getSignature( | ||
testTxInput: CoreInput, | ||
accountType: StandardPurpose | ||
): Buffer { | ||
function getSignature(testTxInput: CoreInput, accountType: AccountType): Buffer { | ||
const scriptSig = Buffer.from(testTxInput.scriptSig.hex, "hex"); | ||
if (accountType == StandardPurpose.p2pkh) { | ||
return scriptSig.slice(1, scriptSig.length - 34); | ||
if (accountType == AccountType.p2pkh) { | ||
return scriptSig.slice(1, scriptSig.length-34); | ||
} | ||
if (accountType == StandardPurpose.p2tr) { | ||
if (accountType == AccountType.p2tr) { | ||
return Buffer.from(testTxInput.txinwitness![0], "hex"); | ||
} | ||
if ( | ||
accountType == StandardPurpose.p2wpkh || | ||
accountType == StandardPurpose.p2wpkhInP2sh | ||
) { | ||
if (accountType == AccountType.p2wpkh || accountType == AccountType.p2wpkhInP2sh) { | ||
return Buffer.from(testTxInput.txinwitness![0], "hex"); | ||
@@ -160,3 +120,3 @@ } | ||
function getAccountType(coreInput: CoreInput, btc: Btc): StandardPurpose { | ||
function getAccountType(coreInput: CoreInput, btc: Btc): AccountType { | ||
const spentTx = spentTxs[coreInput.txid]; | ||
@@ -170,27 +130,21 @@ if (!spentTx) { | ||
if (script.length == 34 && script[0] == 0x51) { | ||
return StandardPurpose.p2tr; | ||
return AccountType.p2tr; | ||
} | ||
if (script.length == 22 && script[0] == 0x00) { | ||
return StandardPurpose.p2wpkh; | ||
return AccountType.p2wpkh; | ||
} | ||
if (script.length == 23) { | ||
return StandardPurpose.p2wpkhInP2sh; | ||
return AccountType.p2wpkhInP2sh; | ||
} | ||
return StandardPurpose.p2pkh; | ||
return AccountType.p2pkh; | ||
} | ||
export function creatDummyXpub(pubkey: Buffer): string { | ||
const xpubDecoded = bs58check.decode( | ||
"tpubDHcN44A4UHqdHJZwBxgTbu8Cy87ZrZkN8tQnmJGhcijHqe4rztuvGcD4wo36XSviLmiqL5fUbDnekYaQ7LzAnaqauBb9RsyahsTTFHdeJGd" | ||
); | ||
const pubkey33 = | ||
pubkey.length == 33 ? pubkey : Buffer.concat([Buffer.from([2]), pubkey]); | ||
xpubDecoded.fill(pubkey33, xpubDecoded.length - 33); | ||
const xpubDecoded = bs58check.decode("tpubDHcN44A4UHqdHJZwBxgTbu8Cy87ZrZkN8tQnmJGhcijHqe4rztuvGcD4wo36XSviLmiqL5fUbDnekYaQ7LzAnaqauBb9RsyahsTTFHdeJGd"); | ||
const pubkey33 = pubkey.length == 33 ? pubkey : Buffer.concat([Buffer.of(2), pubkey]); | ||
xpubDecoded.fill(pubkey33, xpubDecoded.length-33); | ||
return bs58check.encode(xpubDecoded); | ||
} | ||
function createInput( | ||
coreInput: CoreInput, | ||
btc: Btc | ||
): [Transaction, number, string | null, number] { | ||
function createInput(coreInput: CoreInput, btc: Btc): [Transaction, number, string, number] { | ||
const spentTx = spentTxs[coreInput.txid]; | ||
@@ -201,8 +155,13 @@ if (!spentTx) { | ||
const splitSpentTx = btc.splitTransaction(spentTx, true); | ||
return [splitSpentTx, coreInput.vout, null, coreInput.sequence]; | ||
const scriptSig = coreInput.scriptSig; | ||
let redeemScript; | ||
if (scriptSig?.hex && scriptSig.hex.startsWith("160014")) { | ||
redeemScript = scriptSig.hex.substring(2); | ||
} | ||
return [splitSpentTx, coreInput.vout, redeemScript, coreInput.sequence]; | ||
} | ||
export const masterFingerprint = Buffer.from([1, 2, 3, 4]); | ||
export const masterFingerprint = Buffer.of(1, 2, 3, 4); | ||
export class TestingClient extends AppClient { | ||
mockGetPubkeyResponse(_pathElements: string, _response: string): void {} | ||
mockGetPubkeyResponse(_pathElements: string, _response: string): void {}; | ||
mockGetWalletAddressResponse( | ||
@@ -213,4 +172,4 @@ _walletPolicy: WalletPolicy, | ||
_response: string | ||
): void {} | ||
mockSignPsbt(_yieldSigs: Map<number, Buffer>): void {} | ||
): void {}; | ||
mockSignPsbt(_yieldSigs: Map<number, Buffer>): void {}; | ||
} |
@@ -6,3 +6,3 @@ import { Merkle } from "../../src/newops/merkle"; | ||
function leaf(n: number) { | ||
return Buffer.from([0, n]); | ||
return Buffer.of(0, n); | ||
} | ||
@@ -9,0 +9,0 @@ function merkleOf(count: number): Merkle { |
@@ -240,2 +240,57 @@ /* eslint-disable prettier/prettier */ | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export const p2wpkhTwoInputs = { | ||
"txid": "1913b7b5ffdcb5f32b9aca1f5eec2a189e7c66650f82b560eae211265fc995b7", | ||
"hash": "c3439dcd3489373c586c7aed48c32f2b5d9c71aad24acd765a61684d98690a3f", | ||
"version": 2, | ||
"size": 388, | ||
"vsize": 226, | ||
"weight": 904, | ||
"locktime": 0, | ||
"vin": [ | ||
{ | ||
"txid": "5512d5788d4c26117f093de91223ef384c3fb22799810a92e3304bb6f0819224", | ||
"vout": 1, | ||
"scriptSig": { | ||
"asm": "0014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3", | ||
"hex": "160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3" | ||
}, | ||
"txinwitness": [ | ||
"30440220543617c5f4504dc29d34d2d06d0d7733dac4ec418b77c67feefb29f3f82ba3d80220690b784c52c3375f4ba9e64cc5c0aeb6a1b9fc6aadda0062905c06ce3bbba57501", | ||
"02fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e441" | ||
], | ||
"sequence": 4294967295 | ||
}, | ||
{ | ||
"txid": "28ad5054e029252d72da37f13fce66212d7f7763845b4a8c4aaf78e897b2bf9f", | ||
"vout": 1, | ||
"scriptSig": { | ||
"asm": "0014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3", | ||
"hex": "160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3" | ||
}, | ||
"txinwitness": [ | ||
"3044022049e7f3015a33ccdb015fe3891667564fd37111272df57e58447645c7bad8fed0022074d1e93ba946453896d0f0bc500df3a1e0d5bb5ad10cd9906736d5fbaebadd5801", | ||
"02fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e441" | ||
], | ||
"sequence": 4294967295 | ||
} | ||
], | ||
"vout": [ | ||
{ | ||
"value": 0.01800000, | ||
"n": 0, | ||
"scriptPubKey": { | ||
"asm": "OP_DUP OP_HASH160 f73384bcc3951ab6a75541ff79a9a51f82056ed8 OP_EQUALVERIFY OP_CHECKSIG", | ||
"hex": "76a914f73384bcc3951ab6a75541ff79a9a51f82056ed888ac", | ||
"address": "n442v1DrXQNim9gjjctKjyGVoe717hNdtG", | ||
"type": "pubkeyhash" | ||
} | ||
} | ||
], | ||
"hex": "02000000000102249281f0b64b30e3920a819927b23f4c38ef2312e93d097f11264c8d78d512550100000017160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3ffffffff9fbfb297e878af4a8c4a5b8463777f2d2166ce3ff137da722d2529e05450ad280100000017160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3ffffffff0140771b00000000001976a914f73384bcc3951ab6a75541ff79a9a51f82056ed888ac024730440220543617c5f4504dc29d34d2d06d0d7733dac4ec418b77c67feefb29f3f82ba3d80220690b784c52c3375f4ba9e64cc5c0aeb6a1b9fc6aadda0062905c06ce3bbba575012102fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e44102473044022049e7f3015a33ccdb015fe3891667564fd37111272df57e58447645c7bad8fed0022074d1e93ba946453896d0f0bc500df3a1e0d5bb5ad10cd9906736d5fbaebadd58012102fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e44100000000", | ||
"blockhash": "00000000025a711e6cd4bce9138dc852232a4494afbf36d8bb80499a786da2a4", | ||
"confirmations": 1, | ||
"time": 1633944124, | ||
"blocktime": 1633944124 | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export const wrappedP2wpkhTwoInputs = { | ||
@@ -242,0 +297,0 @@ "txid": "c03119b538c78f56c8ce2e6cc5fc6998d447eeef42e34c12692764a3f1a3da7c", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
0
1057850
318
16523
503