PHP WebShell
Текущая директория: /opt/BitGoJS/modules/unspents/src
Просмотр файла: dimensions.ts
import * as utxolib from '@bitgo/utxo-lib';
import { bitgo } from '@bitgo/utxo-lib';
const { isChainCode, scriptTypeForChain } = bitgo;
type ChainCode = bitgo.ChainCode;
import { compactSize } from './scriptSizes';
import { PositiveInteger } from './types';
import { VirtualSizes } from './virtualSizes';
export { VirtualSizes };
/**
* Apply `f` to all properties of `d`
*/
function mapDimensions(
d: Partial<Dimensions>,
f: <T extends keyof Dimensions>(key: T, v: Dimensions[T] | undefined) => unknown
): Dimensions {
return new Dimensions(
Object.fromEntries(Object.entries(d).map(([key, value]) => [key, f(key as keyof Dimensions, value)]))
);
}
/**
* Aggregate count and size of transaction outputs
*/
export class OutputDimensions {
/**
* Number of outputs
*/
count: number;
/**
* Aggregate vSize
*/
size: number;
constructor({ count = 0, size = 0 }: OutputDimensions = { 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);
}
}
interface FromInputParams {
// In cases where the input type is ambiguous, we must provide a hint about spend script type.
assumeUnsigned?: Dimensions;
}
export interface FromUnspentParams {
p2tr: {
scriptPathLevel?: number;
};
p2trMusig2: {
scriptPathLevel?: number;
};
}
const defaultUnspentParams: FromUnspentParams = {
p2tr: {
// Default to recovery script paths, to make it easier for recovery case callers (WRW etc).
// WP can explicitly pass scriptPathLevel: 1 to use happy path.
scriptPathLevel: 2,
},
p2trMusig2: {
// Default to script path spend, to make it easier for recovery case callers (WRW etc).
// WP can explicitly pass scriptPathLevel: undefined to use key path.
scriptPathLevel: 1,
},
};
/**
* Dimensions of a BitGo wallet transactions.
*/
export class Dimensions {
/** Input counts for BitGo wallet multi-signature inputs */
public readonly nP2shInputs: number = 0;
public readonly nP2shP2wshInputs: number = 0;
public readonly nP2wshInputs: number = 0;
public readonly nP2trKeypathInputs: number = 0;
public readonly nP2trScriptPathLevel1Inputs: number = 0;
public readonly nP2trScriptPathLevel2Inputs: number = 0;
/* Input count for single-signature inputs (Replay Protection inputs) */
public readonly nP2shP2pkInputs: number = 0;
public readonly outputs: OutputDimensions = new OutputDimensions();
constructor(d: Partial<Dimensions> = {}) {
Object.entries(d).forEach(([key, value]) => this.setProperty(key, value));
Object.freeze(this);
}
private setProperty(k: string, v: unknown): void {
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 as OutputDimensions);
}
break;
default:
throw new Error(`unknown property ${k}`);
}
(this as any)[k] = v;
}
static readonly ZERO = Object.freeze(new Dimensions());
/**
* @deprecated use ZERO
* @return Dimensions for an empty transaction
*/
static zero(): Readonly<Dimensions> {
return this.ZERO;
}
/**
* @param size
* @return Dimensions for a single output with given size
*/
static singleOutput(size: number): Dimensions {
return Dimensions.sum({ outputs: { count: 1, size } });
}
static readonly SingleOutput = Object.freeze({
p2sh: Dimensions.singleOutput(VirtualSizes.txP2shOutputSize),
p2shP2wsh: Dimensions.singleOutput(VirtualSizes.txP2shP2wshOutputSize),
p2wsh: Dimensions.singleOutput(VirtualSizes.txP2wshOutputSize),
p2tr: Dimensions.singleOutput(VirtualSizes.txP2trOutputSize),
p2pkh: Dimensions.singleOutput(VirtualSizes.txP2pkhOutputSize),
p2wpkh: Dimensions.singleOutput(VirtualSizes.txP2wpkhOutputSize),
});
/**
* @return Number of total inputs (p2sh + p2shP2wsh + p2wsh + p2tr)
*/
get nInputs(): number {
return (
this.nP2shInputs +
this.nP2shP2wshInputs +
this.nP2wshInputs +
this.nP2trKeypathInputs +
this.nP2trScriptPathLevel1Inputs +
this.nP2trScriptPathLevel2Inputs +
this.nP2shP2pkInputs
);
}
set nInputs(_: number) {
throw new Error('read-only property nInputs');
}
/**
* @return Number of total outputs
*/
get nOutputs(): number {
return this.outputs.count;
}
set nOutputs(_: number) {
throw new Error(`read-only property nOutputs`);
}
/**
* @param args - Dimensions (can be partially defined)
* @return {Dimensions} sum of arguments
*/
static sum(...args: Partial<Dimensions>[]): Dimensions {
return args.reduce((a: Dimensions, b: Partial<Dimensions>) => a.plus(b), new Dimensions());
}
/**
* @param chain
* @return {Number}
*/
static getOutputScriptLengthForChain(chain: ChainCode): number {
switch (scriptTypeForChain(chain)) {
case 'p2wsh':
case 'p2tr':
case 'p2trMusig2':
return 34;
default:
return 23;
}
}
/**
* @param scriptLength
* @return {Number} vSize of an output with script length
*/
static getVSizeForOutputWithScriptLength(scriptLength: number): number {
if (!PositiveInteger.is(scriptLength)) {
throw new TypeError(`expected positive integer for scriptLength, got ${scriptLength}`);
}
return scriptLength + compactSize(scriptLength) + VirtualSizes.txOutputAmountSize;
}
static readonly 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 }),
});
/**
* @return
*/
static fromScriptType(
scriptType: utxolib.bitgo.outputScripts.ScriptType | utxolib.bitgo.ParsedScriptType2Of3 | 'p2pkh',
params: {
scriptPathLevel?: number;
} = {}
): Dimensions {
switch (scriptType) {
case 'p2sh':
case 'p2shP2wsh':
case 'p2wsh':
case 'p2shP2pk':
return Dimensions.SingleInput[scriptType];
case 'p2tr':
case 'taprootScriptPathSpend':
switch (params.scriptPathLevel) {
case 1:
return Dimensions.SingleInput.p2trScriptPathLevel1;
case 2:
return Dimensions.SingleInput.p2trScriptPathLevel2;
default:
throw new Error(`unexpected script path level`);
}
case 'p2trMusig2':
switch (params.scriptPathLevel) {
case undefined:
return Dimensions.SingleInput.p2trKeypath;
case 1:
return Dimensions.SingleInput.p2trScriptPathLevel1;
default:
throw new Error(`unexpected script path level`);
}
case 'taprootKeyPathSpend':
return Dimensions.SingleInput.p2trKeypath;
default:
throw new Error(`unexpected scriptType ${scriptType}`);
}
}
static readonly ASSUME_P2SH = Dimensions.SingleInput.p2sh;
static readonly ASSUME_P2SH_P2WSH = Dimensions.SingleInput.p2shP2wsh;
static readonly ASSUME_P2WSH = Dimensions.SingleInput.p2wsh;
static readonly ASSUME_P2TR_KEYPATH = Dimensions.SingleInput.p2trKeypath;
static readonly ASSUME_P2TR_SCRIPTPATH_LEVEL1 = Dimensions.SingleInput.p2trScriptPathLevel1;
static readonly ASSUME_P2TR_SCRIPTPATH_LEVEL2 = Dimensions.SingleInput.p2trScriptPathLevel2;
static readonly ASSUME_P2SH_P2PK_INPUT = Dimensions.SingleInput.p2shP2pk;
private static getAssumedDimension(params: FromInputParams = {}, index: number) {
const { assumeUnsigned } = params;
if (!assumeUnsigned) {
throw new Error(`illegal input ${index}: empty script and assumeUnsigned not set`);
}
return assumeUnsigned;
}
/**
* @param input - the transaction input to count
* @param params
* [param.assumeUnsigned] - default type for unsigned input
*/
static fromInput(input: utxolib.TxInput, params: FromInputParams = {}): Dimensions {
if (input.script?.length || input.witness?.length) {
const parsed = utxolib.bitgo.parseSignatureScript(input);
return Dimensions.fromScriptType(parsed.scriptType, parsed as { scriptPathLevel?: number });
}
return Dimensions.getAssumedDimension(params, input.index);
}
/**
* Create Dimensions from psbt input
* @param input - psbt input
*/
static fromPsbtInput(input: bitgo.PsbtInputType): Dimensions {
const parsed = bitgo.parsePsbtInput(input);
return Dimensions.fromScriptType(parsed.scriptType, parsed as { scriptPathLevel?: number });
}
/**
* @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: utxolib.TxInput[], params?: FromInputParams): Dimensions {
if (!Array.isArray(inputs)) {
throw new TypeError(`inputs must be array`);
}
return Dimensions.sum(...inputs.map((i) => Dimensions.fromInput(i, params)));
}
/**
* Create Dimensions from multiple psbt inputs
* @param inputs psbt input array
* @return {Dimensions} sum of the dimensions for each input (@see Dimensions.fromPsbtInput())
*/
static fromPsbtInputs(inputs: bitgo.PsbtInputType[]): Dimensions {
if (!Array.isArray(inputs)) {
throw new TypeError(`inputs must be array`);
}
return Dimensions.sum(...inputs.map((input, _) => Dimensions.fromPsbtInput(input)));
}
/**
* @param scriptLength {number} - size of the output script in bytes
* @return {Dimensions} - Dimensions of the output
*/
static fromOutputScriptLength(scriptLength: number): Dimensions {
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 }: { script: Buffer }): Dimensions {
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: { script: Buffer }[]): Dimensions {
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: ChainCode): Dimensions {
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, p2trMusig2)
* @return {Dimensions} of the unspent
* @throws if the chain code is invalid or unsupported
*/
static fromUnspent({ chain }: { chain: number }, params: FromUnspentParams = defaultUnspentParams): Dimensions {
if (!isChainCode(chain)) {
throw new TypeError('invalid chain code');
}
const scriptType = scriptTypeForChain(chain);
return Dimensions.fromScriptType(
scriptType,
scriptType === 'p2tr' ? params.p2tr : scriptType === 'p2trMusig2' ? params.p2trMusig2 : {}
);
}
/**
* @param unspents
* @param params - Hint for unspents with variable input sizes (p2tr, p2trMusig2)
* @return {Dimensions} sum of the dimensions for each unspent (@see Dimensions.fromUnspent())
*/
static fromUnspents(unspents: { chain: ChainCode }[], params: FromUnspentParams = defaultUnspentParams): Dimensions {
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, params)));
}
/**
* @param transaction - bitcoin-like transaction
* @param [param.assumeUnsigned] - default type for unsigned inputs
* @return {Dimensions}
*/
static fromTransaction(
{
ins,
outs,
}: {
ins: utxolib.TxInput[];
outs: utxolib.TxOutput[];
},
params?: FromInputParams
): Dimensions {
return Dimensions.fromInputs(ins, params).plus(Dimensions.fromOutputs(outs));
}
/**
* Create Dimensions from psbt inputs and outputs
* @param psbt
* @return {Dimensions}
*/
static fromPsbt(psbt: bitgo.UtxoPsbt): Dimensions {
return Dimensions.fromPsbtInputs(psbt.data.inputs).plus(Dimensions.fromOutputs(psbt.getUnsignedTx().outs));
}
/**
* @param dimensions (can be partially defined)
* @return new dimensions with argument added
*/
plus(dimensions: Partial<Dimensions>): 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 as Dimensions;
if (outputs.count !== nOutputs) {
throw new Error('deprecated partial addition: inconsistent values for "nOutputs" and "outputs.count"');
}
}
return mapDimensions(this, (key, v) => {
const w = dimensions[key] ?? Dimensions.ZERO[key];
if (key === 'outputs') {
const vOutputs = v as OutputDimensions;
const wOutputs = w as OutputDimensions;
return new OutputDimensions({
count: vOutputs.count + wOutputs.count,
size: vOutputs.size + wOutputs.size,
});
}
return (v as number) + (w as number);
});
}
/**
* Multiply dimensions by a given factor
* @param factor - Positive integer
* @return {Dimensions}
*/
times(factor: number): Dimensions {
if (!PositiveInteger.is(factor)) {
throw new TypeError(`expected factor to be positive integer`);
}
return mapDimensions(this, (key, value) => {
if (key === 'outputs') {
const vOutputs = value as OutputDimensions;
return {
count: vOutputs.count * factor,
size: vOutputs.size * factor,
};
}
return (value as number) * factor;
});
}
/**
* @return Number of total inputs (p2sh, p2shP2wsh and p2wsh)
* @deprecated use `dimension.nInputs` instead
*/
getNInputs(): number {
return this.nInputs;
}
/**
* @returns {boolean} true iff dimensions have one or more (p2sh)p2wsh inputs
*/
isSegwit(): boolean {
return (
this.nP2wshInputs +
this.nP2shP2wshInputs +
this.nP2trKeypathInputs +
this.nP2trScriptPathLevel1Inputs +
this.nP2trScriptPathLevel2Inputs >
0
);
}
/**
* @return {Number} overhead vsize, based on result isSegwit().
*/
getOverheadVSize(): number {
return this.isSegwit() ? VirtualSizes.txSegOverheadVSize : VirtualSizes.txOverheadSize;
}
/**
* @returns {number} vsize of inputs, without transaction overhead
*/
getInputsVSize(): number {
const {
txP2shInputSize,
txP2shP2wshInputSize,
txP2wshInputSize,
txP2trKeypathInputSize,
txP2trScriptPathLevel1InputSize,
txP2trScriptPathLevel2InputSize,
txP2shP2pkInputSize,
} = 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(): number {
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(): number {
return this.getOverheadVSize() + this.getInputsVSize() + this.getOutputsVSize();
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!