@bitgo-beta/unspents
Version:
Defines the chain codes used for different unspent types and methods to calculate bitcoin transaction sizes
441 lines • 17 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Dimensions = exports.OutputDimensions = exports.VirtualSizes = void 0;
const utxolib = __importStar(require("@bitgo-beta/utxo-lib"));
const utxo_lib_1 = require("@bitgo-beta/utxo-lib");
const { isChainCode, scriptTypeForChain } = utxo_lib_1.bitgo;
const scriptSizes_1 = require("./scriptSizes");
const types_1 = require("./types");
const virtualSizes_1 = require("./virtualSizes");
Object.defineProperty(exports, "VirtualSizes", { enumerable: true, get: function () { return virtualSizes_1.VirtualSizes; } });
/**
* Apply `f` to all properties of `d`
*/
function mapDimensions(d, f) {
return new Dimensions(Object.fromEntries(Object.entries(d).map(([key, value]) => [key, f(key, value)])));
}
/**
* Aggregate count and size of transaction outputs
*/
class OutputDimensions {
constructor({ count = 0, size = 0 } = { count: 0, size: 0 }) {
if (count === 0 || size === 0) {
if (count !== 0 || size !== 0) {
throw new Error(`count and size must both be zero if one is zero`);
}
}
this.count = count;
this.size = size;
Object.freeze(this);
}
}
exports.OutputDimensions = OutputDimensions;
const defaultUnspentParams = {
p2tr: {
scriptPathLevel: 1,
},
};
/**
* Dimensions of a BitGo wallet transactions.
*/
class Dimensions {
constructor(d = {}) {
/** Input counts for BitGo wallet multi-signature inputs */
this.nP2shInputs = 0;
this.nP2shP2wshInputs = 0;
this.nP2wshInputs = 0;
this.nP2trKeypathInputs = 0;
this.nP2trScriptPathLevel1Inputs = 0;
this.nP2trScriptPathLevel2Inputs = 0;
/* Input count for single-signature inputs (Replay Protection inputs) */
this.nP2shP2pkInputs = 0;
this.outputs = new OutputDimensions();
Object.entries(d).forEach(([key, value]) => this.setProperty(key, value));
Object.freeze(this);
}
setProperty(k, v) {
switch (k) {
case 'nP2shInputs':
case 'nP2shP2wshInputs':
case 'nP2wshInputs':
case 'nP2trKeypathInputs':
case 'nP2trScriptPathLevel1Inputs':
case 'nP2trScriptPathLevel2Inputs':
case 'nP2shP2pkInputs':
if (typeof v !== 'number') {
throw new Error(`property ${k} must be number`);
}
if (!Number.isSafeInteger(v) || v < 0) {
throw new Error(`property ${k} must be zero or positive integer`);
}
break;
case 'outputs':
if (!(v instanceof OutputDimensions)) {
v = new OutputDimensions(v);
}
break;
default:
throw new Error(`unknown property ${k}`);
}
this[k] = v;
}
/**
* @deprecated use ZERO
* @return Dimensions for an empty transaction
*/
static zero() {
return this.ZERO;
}
/**
* @param size
* @return Dimensions for a single output with given size
*/
static singleOutput(size) {
return Dimensions.sum({ outputs: { count: 1, size } });
}
/**
* @return Number of total inputs (p2sh + p2shP2wsh + p2wsh + p2tr)
*/
get nInputs() {
return (this.nP2shInputs +
this.nP2shP2wshInputs +
this.nP2wshInputs +
this.nP2trKeypathInputs +
this.nP2trScriptPathLevel1Inputs +
this.nP2trScriptPathLevel2Inputs +
this.nP2shP2pkInputs);
}
set nInputs(_) {
throw new Error('read-only property nInputs');
}
/**
* @return Number of total outputs
*/
get nOutputs() {
return this.outputs.count;
}
set nOutputs(_) {
throw new Error(`read-only property nOutputs`);
}
/**
* @param args - Dimensions (can be partially defined)
* @return {Dimensions} sum of arguments
*/
static sum(...args) {
return args.reduce((a, b) => a.plus(b), new Dimensions());
}
/**
* @param chain
* @return {Number}
*/
static getOutputScriptLengthForChain(chain) {
switch (scriptTypeForChain(chain)) {
case 'p2wsh':
case 'p2tr':
return 34;
default:
return 23;
}
}
/**
* @param scriptLength
* @return {Number} vSize of an output with script length
*/
static getVSizeForOutputWithScriptLength(scriptLength) {
if (!types_1.PositiveInteger.is(scriptLength)) {
throw new TypeError(`expected positive integer for scriptLength, got ${scriptLength}`);
}
return scriptLength + scriptSizes_1.compactSize(scriptLength) + virtualSizes_1.VirtualSizes.txOutputAmountSize;
}
/**
* @return
*/
static fromScriptType(scriptType, params = {}) {
switch (scriptType) {
case 'p2sh':
case 'p2shP2wsh':
case 'p2wsh':
case 'p2shP2pk':
return Dimensions.SingleInput[scriptType];
case 'p2tr':
switch (params.scriptPathLevel) {
case undefined:
return Dimensions.SingleInput.p2trKeypath;
case 1:
return Dimensions.SingleInput.p2trScriptPathLevel1;
case 2:
return Dimensions.SingleInput.p2trScriptPathLevel2;
default:
throw new Error(`unexpected script path level`);
}
default:
throw new Error(`unexpected scriptType ${scriptType}`);
}
}
/**
* @param input - the transaction input to count
* @param params
* [param.assumeUnsigned] - default type for unsigned input
*/
static fromInput(input, params = {}) {
var _a, _b;
if (((_a = input.script) === null || _a === void 0 ? void 0 : _a.length) || ((_b = input.witness) === null || _b === void 0 ? void 0 : _b.length)) {
const parsed = utxolib.bitgo.parseSignatureScript(input);
if (parsed.scriptType) {
return Dimensions.fromScriptType(parsed.scriptType, parsed);
}
}
const { assumeUnsigned } = params;
if (!assumeUnsigned) {
throw new Error(`illegal input ${input.index}: empty script and assumeUnsigned not set`);
}
return assumeUnsigned;
}
/**
* @param inputs - Array of inputs
* @param params - @see Dimensions.fromInput()
* @return {Dimensions} sum of the dimensions for each input (@see Dimensions.fromInput())
*/
static fromInputs(inputs, params) {
if (!Array.isArray(inputs)) {
throw new TypeError(`inputs must be array`);
}
return Dimensions.sum(...inputs.map((i) => Dimensions.fromInput(i, params)));
}
/**
* @param scriptLength {number} - size of the output script in bytes
* @return {Dimensions} - Dimensions of the output
*/
static fromOutputScriptLength(scriptLength) {
return Dimensions.sum({
outputs: {
count: 1,
size: Dimensions.getVSizeForOutputWithScriptLength(scriptLength),
},
});
}
/**
* @param output - a tx output
* @return Dimensions - the dimensions of the given output
*/
static fromOutput({ script }) {
if (!script) {
throw new Error('expected output script to be defined');
}
if (!Buffer.isBuffer(script)) {
throw new TypeError('expected script to be buffer, got ' + typeof script);
}
return Dimensions.fromOutputScriptLength(script.length);
}
/**
* @param outputs - Array of outputs
* @return {Dimensions} sum of the dimensions for each output (@see Dimensions.fromOutput())
*/
static fromOutputs(outputs) {
if (!Array.isArray(outputs)) {
throw new TypeError(`outputs must be array`);
}
return Dimensions.sum(...outputs.map(Dimensions.fromOutput));
}
/**
* Returns the dimensions of an output that will be created on a specific chain.
* Currently, this simply adds a default output.
*
* @param chain - Chain code as defined by utxolib.bitgo
* @return {Dimensions} - Dimensions for a single output on the given chain.
*/
static fromOutputOnChain(chain) {
return Dimensions.fromOutputScriptLength(Dimensions.getOutputScriptLengthForChain(chain));
}
/**
* Return dimensions of an unspent according to `chain` parameter
* @param chain - Chain code as defined by utxo.chain
* @param params - Hint for unspents with variable input sizes (p2tr).
* @return {Dimensions} of the unspent
* @throws if the chain code is invalid or unsupported
*/
static fromUnspent({ chain }, params = defaultUnspentParams) {
if (!isChainCode(chain)) {
throw new TypeError('invalid chain code');
}
const scriptType = scriptTypeForChain(chain);
return Dimensions.fromScriptType(scriptType, scriptType === 'p2tr' ? params.p2tr : {});
}
/**
* @param unspents
* @return {Dimensions} sum of the dimensions for each unspent (@see Dimensions.fromUnspent())
*/
static fromUnspents(unspents) {
if (!Array.isArray(unspents)) {
throw new TypeError(`unspents must be array`);
}
// Convert the individual unspents into dimensions and sum them up
return Dimensions.sum(...unspents.map((u) => Dimensions.fromUnspent(u)));
}
/**
* @param transaction - bitcoin-like transaction
* @param [param.assumeUnsigned] - default type for unsigned inputs
* @return {Dimensions}
*/
static fromTransaction({ ins, outs, }, params) {
return Dimensions.fromInputs(ins, params).plus(Dimensions.fromOutputs(outs));
}
/**
* @param dimensions (can be partially defined)
* @return new dimensions with argument added
*/
plus(dimensions) {
if (typeof dimensions !== 'object') {
throw new TypeError(`expected argument to be object`);
}
if (!(dimensions instanceof Dimensions)) {
dimensions = new Dimensions(dimensions);
}
// Catch instances where we try to initialize Dimensions from partial data using deprecated parameters
// using only "nOutputs".
if ('nOutputs' in dimensions) {
if (!('outputs' in dimensions)) {
throw new Error('deprecated partial addition: argument has key "nOutputs" but no "outputs"');
}
const { outputs, nOutputs } = dimensions;
if (outputs.count !== nOutputs) {
throw new Error('deprecated partial addition: inconsistent values for "nOutputs" and "outputs.count"');
}
}
return mapDimensions(this, (key, v) => {
var _a;
const w = (_a = dimensions[key]) !== null && _a !== void 0 ? _a : Dimensions.ZERO[key];
if (key === 'outputs') {
const vOutputs = v;
const wOutputs = w;
return new OutputDimensions({
count: vOutputs.count + wOutputs.count,
size: vOutputs.size + wOutputs.size,
});
}
return v + w;
});
}
/**
* Multiply dimensions by a given factor
* @param factor - Positive integer
* @return {Dimensions}
*/
times(factor) {
if (!types_1.PositiveInteger.is(factor)) {
throw new TypeError(`expected factor to be positive integer`);
}
return mapDimensions(this, (key, value) => {
if (key === 'outputs') {
const vOutputs = value;
return {
count: vOutputs.count * factor,
size: vOutputs.size * factor,
};
}
return value * factor;
});
}
/**
* @return Number of total inputs (p2sh, p2shP2wsh and p2wsh)
* @deprecated use `dimension.nInputs` instead
*/
getNInputs() {
return this.nInputs;
}
/**
* @returns {boolean} true iff dimensions have one or more (p2sh)p2wsh inputs
*/
isSegwit() {
return (this.nP2wshInputs +
this.nP2shP2wshInputs +
this.nP2trKeypathInputs +
this.nP2trScriptPathLevel1Inputs +
this.nP2trScriptPathLevel2Inputs >
0);
}
/**
* @return {Number} overhead vsize, based on result isSegwit().
*/
getOverheadVSize() {
return this.isSegwit() ? virtualSizes_1.VirtualSizes.txSegOverheadVSize : virtualSizes_1.VirtualSizes.txOverheadSize;
}
/**
* @returns {number} vsize of inputs, without transaction overhead
*/
getInputsVSize() {
const { txP2shInputSize, txP2shP2wshInputSize, txP2wshInputSize, txP2trKeypathInputSize, txP2trScriptPathLevel1InputSize, txP2trScriptPathLevel2InputSize, txP2shP2pkInputSize, } = virtualSizes_1.VirtualSizes;
const { nP2shInputs, nP2shP2wshInputs, nP2wshInputs, nP2trKeypathInputs, nP2trScriptPathLevel1Inputs, nP2trScriptPathLevel2Inputs, nP2shP2pkInputs, } = this;
const size = nP2shInputs * txP2shInputSize +
nP2shP2wshInputs * txP2shP2wshInputSize +
nP2wshInputs * txP2wshInputSize +
nP2trKeypathInputs * txP2trKeypathInputSize +
nP2shP2pkInputs * txP2shP2pkInputSize +
nP2trScriptPathLevel1Inputs * txP2trScriptPathLevel1InputSize +
nP2trScriptPathLevel2Inputs * txP2trScriptPathLevel2InputSize;
if (Number.isNaN(size)) {
throw new Error(`invalid size`);
}
return size;
}
/**
* @returns {number} return vsize of outputs, without overhead
*/
getOutputsVSize() {
return this.outputs.size;
}
/**
* Estimates the virtual size (1/4 weight) of a signed transaction as sum of
* overhead vsize, input vsize and output vsize.
* @returns {Number} The estimated vsize of the transaction dimensions.
*/
getVSize() {
return this.getOverheadVSize() + this.getInputsVSize() + this.getOutputsVSize();
}
}
exports.Dimensions = Dimensions;
Dimensions.ZERO = Object.freeze(new Dimensions());
Dimensions.SingleOutput = Object.freeze({
p2sh: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2shOutputSize),
p2shP2wsh: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2shP2wshOutputSize),
p2wsh: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2wshOutputSize),
p2tr: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2trOutputSize),
p2pkh: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2pkhOutputSize),
p2wpkh: Dimensions.singleOutput(virtualSizes_1.VirtualSizes.txP2wpkhOutputSize),
});
Dimensions.SingleInput = Object.freeze({
p2sh: Dimensions.sum({ nP2shInputs: 1 }),
p2shP2wsh: Dimensions.sum({ nP2shP2wshInputs: 1 }),
p2wsh: Dimensions.sum({ nP2wshInputs: 1 }),
p2trKeypath: Dimensions.sum({ nP2trKeypathInputs: 1 }),
p2trScriptPathLevel1: Dimensions.sum({ nP2trScriptPathLevel1Inputs: 1 }),
p2trScriptPathLevel2: Dimensions.sum({ nP2trScriptPathLevel2Inputs: 1 }),
p2shP2pk: Dimensions.sum({ nP2shP2pkInputs: 1 }),
});
Dimensions.ASSUME_P2SH = Dimensions.SingleInput.p2sh;
Dimensions.ASSUME_P2SH_P2WSH = Dimensions.SingleInput.p2shP2wsh;
Dimensions.ASSUME_P2WSH = Dimensions.SingleInput.p2wsh;
Dimensions.ASSUME_P2TR_KEYPATH = Dimensions.SingleInput.p2trKeypath;
Dimensions.ASSUME_P2TR_SCRIPTPATH_LEVEL1 = Dimensions.SingleInput.p2trScriptPathLevel1;
Dimensions.ASSUME_P2TR_SCRIPTPATH_LEVEL2 = Dimensions.SingleInput.p2trScriptPathLevel2;
Dimensions.ASSUME_P2SH_P2PK_INPUT = Dimensions.SingleInput.p2shP2pk;
//# sourceMappingURL=dimensions.js.map