UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

438 lines 14.6 kB
import { TransactionType } from '../enums/TransactionType.js'; import { concat, crypto as bitCrypto, fromHex, PaymentType, Psbt, toHex, toXOnly, } from '@btc-vision/bitcoin'; import { TransactionBuilder } from './TransactionBuilder.js'; import { DeploymentGenerator, versionBuffer, } from '../../generators/builders/DeploymentGenerator.js'; import { EcKeyPair } from '../../keypair/EcKeyPair.js'; import { BitcoinUtils } from '../../utils/BitcoinUtils.js'; import { Compressor } from '../../bytecode/Compressor.js'; import { SharedInteractionTransaction } from './SharedInteractionTransaction.js'; import {} from '@btc-vision/ecpair'; import { isUniversalSigner } from '../../signer/TweakedSigner.js'; import { Address } from '../../keypair/Address.js'; import { UnisatSigner } from '../browser/extensions/UnisatSigner.js'; import { TimeLockGenerator } from '../mineable/TimelockGenerator.js'; import { FeaturePriority, Features } from '../../generators/Features.js'; export class DeploymentTransaction extends TransactionBuilder { static MAXIMUM_CONTRACT_SIZE = 1536 * 1024; type = TransactionType.DEPLOYMENT; challenge; epochChallenge; /** * The contract address * @protected */ _contractAddress; /** * The tap leaf script * @private */ tapLeafScript = null; deploymentVersion = 0x00; /** * The target script redeem * @private */ targetScriptRedeem = null; /** * The left over funds script redeem * @private */ leftOverFundsScriptRedeem = null; /** * The compiled target script * @private */ compiledTargetScript; /** * The script tree * @private */ scriptTree; /** * The deployment bitcoin generator * @private */ deploymentGenerator; /** * The contract seed * @private */ contractSeed; /** * The contract bytecode * @private */ bytecode; /** * Constructor calldata * @private */ calldata; /** * The contract signer * @private */ contractSigner; /** * The contract public key * @private */ _contractPubKey; /** * The contract salt random bytes * @private */ randomBytes; _computedAddress; constructor(parameters) { super(parameters); if (!this.hashedPublicKey) { throw new Error('MLDSA signer must be defined to deploy a contract.'); } this.bytecode = Compressor.compress(new Uint8Array([...versionBuffer, ...parameters.bytecode])); this.verifyBytecode(); if (parameters.calldata) { this.calldata = parameters.calldata; this.verifyCalldata(); } if (!parameters.challenge) throw new Error('Challenge solution is required'); this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes(); this.challenge = parameters.challenge; this.LOCK_LEAF_SCRIPT = this.defineLockScript(); this.epochChallenge = TimeLockGenerator.generateTimeLockAddress(this.challenge.publicKey.originalPublicKeyBuffer(), this.network); this.contractSeed = this.getContractSeed(); this.contractSigner = EcKeyPair.fromSeedKeyPair(this.contractSeed, this.network); this.deploymentGenerator = new DeploymentGenerator(this.signer.publicKey, this.contractSignerXOnlyPubKey(), this.network); if (parameters.compiledTargetScript) { if (parameters.compiledTargetScript instanceof Uint8Array) { this.compiledTargetScript = parameters.compiledTargetScript; } else if (typeof parameters.compiledTargetScript === 'string') { this.compiledTargetScript = fromHex(parameters.compiledTargetScript); } else { throw new Error('Invalid compiled target script format.'); } } else { this.compiledTargetScript = this.deploymentGenerator.compile(this.bytecode, this.randomBytes, this.challenge, this.priorityFee, this.calldata, this.generateFeatures(parameters)); } this.scriptTree = this.getScriptTree(); this.internalInit(); this._contractPubKey = '0x' + toHex(this.contractSeed); this._contractAddress = new Address(this.contractSeed); } /** * Get the contract public key */ get contractPubKey() { return this._contractPubKey; } /** * @description Get the contract address (PKSH) */ get contractAddress() { return this._contractAddress; } /** * @description Get the P2TR address */ get p2trAddress() { return this.to || this.getScriptAddress(); } exportCompiledTargetScript() { return this.compiledTargetScript; } /** * Get the random bytes used for the interaction * @returns {Uint8Array} The random bytes */ getRndBytes() { return this.randomBytes; } /** * Get the contract bytecode * @returns {Uint8Array} The contract bytecode */ getChallenge() { return this.challenge; } getContractAddress() { if (this._computedAddress) { return this._computedAddress; } this._computedAddress = EcKeyPair.p2op(this.contractSeed, this.network, this.deploymentVersion); return this._computedAddress; } /** * Get the contract signer public key * @protected */ contractSignerXOnlyPubKey() { return toXOnly(this.contractSigner.publicKey); } /** * Build the transaction * @protected */ async buildTransaction() { if (!this.to) { this.to = this.getScriptAddress(); } const selectedRedeem = this.contractSigner ? this.targetScriptRedeem : this.leftOverFundsScriptRedeem; if (!selectedRedeem) { throw new Error('Left over funds script redeem is required'); } if (!selectedRedeem.redeemVersion) { throw new Error('Left over funds script redeem version is required'); } if (!selectedRedeem.output) { throw new Error('Left over funds script redeem output is required'); } this.tapLeafScript = { leafVersion: selectedRedeem.redeemVersion, script: selectedRedeem.output, controlBlock: this.getWitness(), }; this.addInputsFromUTXO(); const amountSpent = this.getTransactionOPNetFee(); this.addFeeToOutput(amountSpent, this.getContractAddress(), this.epochChallenge, true); await this.addRefundOutput(amountSpent + this.addOptionalOutputsAndGetAmount()); } async signInputsWalletBased(transaction) { const signer = this.signer; // first, we sign the first input with the script signer. await this.signInput(transaction, transaction.data.inputs[0], 0, this.contractSigner); // then, we sign all the remaining inputs with the wallet signer. await signer.multiSignPsbt([transaction]); // Then, we finalize every input. for (let i = 0; i < transaction.data.inputs.length; i++) { if (i === 0) { transaction.finalizeInput(i, this.customFinalizer.bind(this)); } else { try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch (e) { transaction.finalizeInput(i); } } } } /** * Sign the inputs * @param {Psbt} transaction The transaction to sign * @protected */ async signInputs(transaction) { if (!this.contractSigner) { await super.signInputs(transaction); return; } if ('multiSignPsbt' in this.signer) { await this.signInputsWalletBased(transaction); return; } // Input 0: sequential (contractSigner + main signer, custom finalizer) transaction.signInput(0, this.contractSigner); transaction.signInput(0, this.getSignerKey()); transaction.finalizeInput(0, this.customFinalizer.bind(this)); // Inputs 1+: parallel key-path if available, then sequential for remaining const signedIndices = new Set([0]); if (this.canUseParallelSigning && isUniversalSigner(this.signer)) { try { const result = await this.signKeyPathInputsParallel(transaction, new Set([0])); if (result.success) { for (const idx of result.signatures.keys()) signedIndices.add(idx); } } catch (e) { this.error(`Parallel signing failed: ${e.message}`); } } for (let i = 1; i < transaction.data.inputs.length; i++) { if (!signedIndices.has(i)) { transaction.signInput(i, this.getSignerKey()); } } // Finalize inputs 1+ for (let i = 1; i < transaction.data.inputs.length; i++) { try { transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this)); } catch { transaction.finalizeInput(i); } } } /** * Get the tap output * @protected */ generateScriptAddress() { if (this.useP2MR) { return { name: PaymentType.P2MR, network: this.network, scriptTree: this.scriptTree, }; } return { name: PaymentType.P2TR, internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, }; } /** * Generate the tap data * @protected */ generateTapData() { const selectedRedeem = this.contractSigner ? this.targetScriptRedeem : this.leftOverFundsScriptRedeem; if (!selectedRedeem) { throw new Error('Left over funds script redeem is required'); } if (!this.scriptTree) { throw new Error('Script tree is required'); } if (this.useP2MR) { return { name: PaymentType.P2MR, network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, }; } return { name: PaymentType.P2TR, internalPubkey: this.internalPubKeyToXOnly(), network: this.network, scriptTree: this.scriptTree, redeem: selectedRedeem, }; } generateFeatures(parameters) { const features = []; const submission = parameters.challenge.getSubmission(); if (submission) { features.push({ priority: FeaturePriority.MLDSA_LINK_PUBKEY, opcode: Features.EPOCH_SUBMISSION, data: submission, }); } if (parameters.revealMLDSAPublicKey && !parameters.linkMLDSAPublicKeyToAddress) { throw new Error('To reveal the MLDSA public key, you must set linkMLDSAPublicKeyToAddress to true.'); } if (parameters.linkMLDSAPublicKeyToAddress) { this.generateMLDSALinkRequest(parameters, features); } return features; } verifyCalldata() { if (this.calldata && this.calldata.length > SharedInteractionTransaction.MAXIMUM_CALLDATA_SIZE) { throw new Error('Calldata size overflow.'); } } verifyBytecode() { if (!this.bytecode) throw new Error('Bytecode is required'); if (this.bytecode.length > DeploymentTransaction.MAXIMUM_CONTRACT_SIZE) { throw new Error('Contract size overflow.'); } } /** * Generate the contract seed for the deployment * @private */ getContractSeed() { if (!this.bytecode) { throw new Error('Bytecode is required'); } // Concatenate deployer pubkey, salt, and sha256(bytecode) const deployerPubKey = this.internalPubKeyToXOnly(); const salt = bitCrypto.hash256(this.randomBytes); const sha256OfBytecode = bitCrypto.hash256(this.bytecode); const buf = concat([deployerPubKey, salt, sha256OfBytecode]); return bitCrypto.hash256(buf); } /** * Finalize the transaction * @param _inputIndex * @param input */ customFinalizer = (_inputIndex, input) => { if (!this.tapLeafScript) { throw new Error('Tap leaf script is required'); } if (!input.tapScriptSig) { throw new Error('Tap script signature is required'); } const scriptSolution = [ this.randomBytes, input.tapScriptSig[0].signature, input.tapScriptSig[1].signature, ]; const witness = scriptSolution .concat(this.tapLeafScript.script) .concat(this.tapLeafScript.controlBlock); return { finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness), }; }; /** * Generate the redeem scripts * @private */ generateRedeemScripts() { this.targetScriptRedeem = { name: PaymentType.P2TR, //pubkeys: this.getPubKeys(), output: this.compiledTargetScript, redeemVersion: 192, }; this.leftOverFundsScriptRedeem = { name: PaymentType.P2TR, //pubkeys: this.getPubKeys(), output: this.getLeafScript(), redeemVersion: 192, }; } /** * Get the second leaf script * @private */ getLeafScript() { return this.LOCK_LEAF_SCRIPT; } /** * Get the script tree * @private */ getScriptTree() { if (!this.bytecode) { throw new Error('Contract bytecode is required'); } this.generateRedeemScripts(); return [ { output: this.compiledTargetScript, version: 192, }, { output: this.getLeafScript(), version: 192, }, ]; } } //# sourceMappingURL=DeploymentTransaction.js.map