UNPKG

liquidjs-lib

Version:

Client-side Liquid JavaScript library

464 lines (463 loc) 14.8 kB
'use strict'; var __createBinding = (this && this.__createBinding) || (Object.create ? function (o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if ( !desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable) ) { desc = { enumerable: true, get: function () { return m[k]; }, }; } Object.defineProperty(o, k2, desc); } : 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.Pset = exports.magicPrefixWithSeparator = exports.magicPrefix = void 0; const bufferutils_1 = require('../bufferutils'); const crypto_1 = require('../crypto'); const transaction_1 = require('../transaction'); const fields_1 = require('./fields'); const globals_1 = require('./globals'); const input_1 = require('./input'); const output_1 = require('./output'); const bscript = __importStar(require('../script')); const address_1 = require('../address'); const payments_1 = require('../payments'); const value_1 = require('../value'); const asset_1 = require('../asset'); const utils_1 = require('./utils'); exports.magicPrefix = Buffer.from([0x70, 0x73, 0x65, 0x74]); exports.magicPrefixWithSeparator = Buffer.concat([ exports.magicPrefix, Buffer.of(0xff), ]); class Pset { constructor(globals, inputs, outputs) { this.inputs = inputs || []; this.outputs = outputs || []; this.globals = globals || new globals_1.PsetGlobal(); } static fromBase64(data) { const buf = Buffer.from(data, 'base64'); return this.fromBuffer(buf); } static fromBuffer(buf) { const r = new bufferutils_1.BufferReader(buf); const magic = r.readSlice(exports.magicPrefixWithSeparator.length); if (!magic.equals(exports.magicPrefixWithSeparator)) { throw new Error('invalid magic prefix'); } const globals = globals_1.PsetGlobal.fromBuffer(r); const inputs = []; for (let i = 0; i < globals.inputCount; i++) { const input = input_1.PsetInput.fromBuffer(r); inputs.push(input); } const outputs = []; for (let i = 0; i < globals.outputCount; i++) { const output = output_1.PsetOutput.fromBuffer(r); outputs.push(output); } const pset = new Pset(globals, inputs, outputs); pset.sanityCheck(); return pset; } static ECCKeysGenerator(ecc) { return (opts) => { const privateKey = (0, utils_1.randomBytes)(opts); const publicKey = ecc.pointFromScalar(privateKey); if (!publicKey) throw new Error('Failed to generate public key'); return { privateKey, publicKey: Buffer.from(publicKey), }; }; } static ECDSASigValidator(ecc) { return (pubkey, msghash, signature) => { return ecc.verify(msghash, pubkey, signature); }; } static SchnorrSigValidator(ecc) { return (pubkey, msghash, signature) => ecc.verifySchnorr(msghash, pubkey, signature.slice(0, 64)); } sanityCheck() { this.globals.sanityCheck(); this.inputs.forEach((input) => input.sanityCheck()); this.outputs.forEach((output) => output.sanityCheck()); if ( this.isFullyBlinded() && this.globals.scalars && this.globals.scalars.length > 0 ) { throw new Error('global scalars must be empty for fully blinded pset'); } return this; } copy() { return new Pset(this.globals, this.inputs, this.outputs); } inputsModifiable() { if (!this.globals.txModifiable) { return true; } return this.globals.txModifiable.get(0) === 1; } outputsModifiable() { if (!this.globals.txModifiable) { return true; } return this.globals.txModifiable.get(1) === 1; } hasSighashSingle() { if (!this.globals.txModifiable) { return false; } return this.globals.txModifiable.get(2) === 1; } needsBlinding() { return this.outputs.some( (out) => out.needsBlinding() && !out.isFullyBlinded(), ); } isFullyBlinded() { if (!this.needsBlinding()) { return false; } return !this.outputs.some( (out) => out.needsBlinding() && !out.isFullyBlinded(), ); } isComplete() { return this.inputs.every((input) => input.isFinalized()); } locktime() { let heightLocktime = 0; let timeLocktime = 0; this.inputs.forEach((input) => { if (input.requiredTimeLocktime > 0) { if (input.requiredTimeLocktime > timeLocktime) { timeLocktime = input.requiredTimeLocktime; } } if (input.requiredHeightLocktime > 0) { if (input.requiredHeightLocktime > heightLocktime) { heightLocktime = input.requiredHeightLocktime; } } }); if (heightLocktime > 0) { return heightLocktime; } if (timeLocktime > 0) { return timeLocktime; } return this.globals.fallbackLocktime || 0; } unsignedTx() { const tx = new transaction_1.Transaction(); tx.version = this.globals.txVersion; tx.locktime = this.locktime(); this.inputs.forEach((input) => { let issuance; if (input.hasIssuance() || input.hasReissuance()) { let assetAmount = input.issuanceValueCommitment; if (!assetAmount || assetAmount.length === 0) { assetAmount = value_1.ElementsValue.fromNumber( input.issuanceValue, ).bytes; } let tokenAmount = input.issuanceInflationKeysCommitment; if (!tokenAmount || tokenAmount.length === 0) { tokenAmount = !input.issuanceInflationKeys ? Buffer.of(0x00) : value_1.ElementsValue.fromNumber(input.issuanceInflationKeys) .bytes; } const assetEntropy = input.issuanceAssetEntropy; const assetBlindingNonce = input.issuanceBlindingNonce; issuance = { assetEntropy, assetAmount, tokenAmount, assetBlindingNonce, }; } tx.addInput( input.previousTxid, input.previousTxIndex, input.sequence, undefined, issuance, ); }); this.outputs.forEach((output) => { const value = output.valueCommitment || value_1.ElementsValue.fromNumber(output.value).bytes; const asset = output.assetCommitment || asset_1.AssetHash.fromBytes(output.asset).bytes; const script = output.script || Buffer.from([]); const nonce = output.ecdhPubkey || Buffer.of(0x00); tx.addOutput( script, value, asset, nonce, output.valueRangeproof, output.assetSurjectionProof, ); }); return tx; } validateAllSignatures(validator) { return this.inputs.every((_, i) => this.validateInputSignatures(i, validator), ); } addInput(newInput) { newInput.sanityCheck(); if (this.isDuplicatedInput(newInput)) { throw new Error('given input already exists in pset'); } if (!this.inputsModifiable()) { throw new Error('pset is locked for updates on inputs'); } if ( newInput.requiredHeightLocktime > 0 || newInput.requiredTimeLocktime > 0 ) { const oldLocktime = this.locktime(); let timeLocktime = newInput.requiredTimeLocktime; let heightLocktime = newInput.requiredHeightLocktime; let hasSigs = false; this.inputs.forEach((input) => { if (input.requiredTimeLocktime > 0 && !input.requiredHeightLocktime) { heightLocktime = 0; if (timeLocktime === 0) { throw new Error('invalid input locktime'); } } if (!input.requiredTimeLocktime && input.requiredHeightLocktime > 0) { timeLocktime = 0; if (heightLocktime === 0) { throw new Error('invalid input locktime'); } } if (input.requiredTimeLocktime > 0 && timeLocktime > 0) { timeLocktime = Math.max(timeLocktime, input.requiredTimeLocktime); } if (input.requiredHeightLocktime > 0 && heightLocktime > 0) { heightLocktime = Math.max( heightLocktime, input.requiredHeightLocktime, ); } if ((input.partialSigs ?? []).length > 0) { hasSigs = true; } }); let newLocktime = this.globals.fallbackLocktime; if (timeLocktime > 0) { newLocktime = timeLocktime; } if (heightLocktime > 0) { newLocktime = heightLocktime; } if (hasSigs && oldLocktime !== newLocktime) { throw new Error('invalid input locktime'); } } this.inputs.push(newInput); this.globals.inputCount++; return this; } addOutput(newOutput) { newOutput.sanityCheck(); if (!this.outputsModifiable()) { throw new Error('pset is locked for updates on outputs'); } this.outputs.push(newOutput); this.globals.outputCount++; return this; } validateInputSignatures(index, validator) { if (index < 0 || index >= this.globals.inputCount) { throw new Error('input index out of range'); } const input = this.inputs[index]; if (!input.partialSigs || input.partialSigs.length === 0) { return false; } return input.partialSigs.every((ps) => this.validatePartialSignature(index, validator, ps), ); } validatePartialSignature(index, validator, ps) { if (index < 0 || index >= this.globals.inputCount) { throw new Error('input index out of range'); } const input = this.inputs[index]; if (!input.partialSigs || input.partialSigs.length === 0) { return false; } const prevout = input.getUtxo(); if (!prevout) { throw new Error('missing input (non-)witness utxo'); } const sighashType = ps.signature[ps.signature.length - 1]; const preimage = this.getInputPreimage(index, sighashType); const script = input.witnessScript || input.redeemScript || prevout.script; checkScriptForPubkey(ps.pubkey, script, 'verify'); const { signature } = bscript.signature.decode(ps.signature); return validator(ps.pubkey, preimage, signature); } getInputPreimage(index, sighashType, genesisBlockHash, leafHash) { if (index < 0 || index >= this.globals.inputCount) { throw new Error('input index out of range'); } const input = this.inputs[index]; const prevout = input.getUtxo(); if (!prevout) { throw new Error('missing input (non-)witness utxo'); } const unsignedTx = this.unsignedTx(); if (input.isTaproot()) { if (!genesisBlockHash || genesisBlockHash.length !== 32) { throw new Error('Missing or invalid genesis block hash'); } const prevoutScripts = []; const prevoutAssetsValues = []; this.inputs.forEach((v, i) => { const u = v.getUtxo(); if (!u) throw new Error(`Missing input ${i} (non-)witness utxo`); prevoutScripts.push(u.script); prevoutAssetsValues.push({ asset: u.asset, value: u.value, }); }); return unsignedTx.hashForWitnessV1( index, prevoutScripts, prevoutAssetsValues, sighashType, genesisBlockHash, leafHash, ); } const script = input.redeemScript || prevout.script; const scriptType = (0, address_1.getScriptType)(script); switch (scriptType) { case address_1.ScriptType.P2Pkh: case address_1.ScriptType.P2Sh: return unsignedTx.hashForSignature(index, script, sighashType); case address_1.ScriptType.P2Wpkh: const legacyScript = (0, payments_1.p2pkh)({ hash: prevout.script.slice(2), }).output; return unsignedTx.hashForWitnessV0( index, legacyScript, prevout.value, sighashType, ); case address_1.ScriptType.P2Wsh: if (!input.witnessScript || input.witnessScript.length === 0) { throw new Error('missing witness script for p2wsh input'); } return unsignedTx.hashForWitnessV0( index, input.witnessScript, prevout.value, sighashType, ); default: throw new Error('unknown input (non-)witness utxo script type'); } } toBase64() { const buffer = this.toBuffer(); return buffer.toString('base64'); } toBuffer() { let size = exports.magicPrefixWithSeparator.length; const globalsBuffer = this.globals.toBuffer(); size += globalsBuffer.length + 1; const inputBuffers = this.inputs.map((input) => input.toBuffer()); inputBuffers.forEach((buf) => (size += buf.length + 1)); const outputBuffers = this.outputs.map((output) => output.toBuffer()); outputBuffers.forEach((buf) => (size += buf.length + 1)); const w = bufferutils_1.BufferWriter.withCapacity(size); w.writeSlice(exports.magicPrefixWithSeparator); w.writeSlice(globalsBuffer); w.writeUInt8(fields_1.separator); inputBuffers.forEach((buf) => { w.writeSlice(buf); w.writeUInt8(fields_1.separator); }); outputBuffers.forEach((buf) => { w.writeSlice(buf); w.writeUInt8(fields_1.separator); }); return w.buffer; } isDuplicatedInput(input) { return this.inputs.some( (inp) => inp.previousTxid.equals(input.previousTxid) && inp.previousTxIndex === input.previousTxIndex, ); } } exports.Pset = Pset; function checkScriptForPubkey(pubkey, script, action) { if (!pubkeyInScript(pubkey, script)) { throw new Error( `Cannot ${action} for this input with the key ${pubkey.toString('hex')}`, ); } } function pubkeyInScript(pubkey, script) { const pubkeyHash = (0, crypto_1.hash160)(pubkey); const decompiled = bscript.decompile(script); if (decompiled === null) throw new Error('Unknown script error'); return decompiled.some((element) => { if (typeof element === 'number') return false; return element.equals(pubkey) || element.equals(pubkeyHash); }); }