UNPKG

@bitgo-beta/unspents

Version:

Defines the chain codes used for different unspent types and methods to calculate bitcoin transaction sizes

441 lines • 17 kB
"use strict"; 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