@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
228 lines (227 loc) • 8.45 kB
JavaScript
import { PaymentType, toXOnly } from '@btc-vision/bitcoin';
import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js';
import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
import { Compressor } from '../../bytecode/Compressor.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
import { ChallengeGenerator } from '../mineable/ChallengeGenerator.js';
export class SharedInteractionTransaction extends TransactionBuilder {
constructor(parameters) {
super(parameters);
this.targetScriptRedeem = null;
this.leftOverFundsScriptRedeem = null;
this.customFinalizer = (_inputIndex, input) => {
if (!this.tapLeafScript) {
throw new Error('Tap leaf script is required');
}
if (!this.contractSecret) {
throw new Error('Contract secret is required');
}
const scriptSolution = this.getScriptSolution(input);
const witness = scriptSolution
.concat(this.tapLeafScript.script)
.concat(this.tapLeafScript.controlBlock);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witness),
};
};
if (!parameters.calldata) {
throw new Error('Calldata is required');
}
if (!parameters.preimage) {
throw new Error('Preimage is required');
}
this.preimage = parameters.preimage;
this.disableAutoRefund = parameters.disableAutoRefund || false;
this.rewardChallenge = ChallengeGenerator.generateMineableReward(this.preimage, this.network);
this.calldata = Compressor.compress(parameters.calldata);
this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
this.scriptSigner = this.generateKeyPairFromSeed();
this.calldataGenerator = new CalldataGenerator(Buffer.from(this.signer.publicKey), this.scriptSignerXOnlyPubKey(), this.network);
}
getContractSecret() {
return this.contractSecret;
}
getRndBytes() {
return this.randomBytes;
}
getPreimage() {
return this.preimage;
}
scriptSignerXOnlyPubKey() {
return toXOnly(Buffer.from(this.scriptSigner.publicKey));
}
generateKeyPairFromSeed() {
return EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network);
}
async buildTransaction() {
const selectedRedeem = this.scriptSigner
? 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(),
};
if (!this.regenerated) {
this.addInputsFromUTXO();
}
await this.createMineableRewardOutputs();
}
async signInputs(transaction) {
if (!this.scriptSigner) {
await super.signInputs(transaction);
return;
}
if ('multiSignPsbt' in this.signer) {
await this.signInputsWalletBased(transaction);
}
else {
await this.signInputsNonWalletBased(transaction);
}
}
generateScriptAddress() {
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
name: PaymentType.P2TR,
};
}
generateTapData() {
const selectedRedeem = this.scriptSigner
? 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');
}
return {
internalPubkey: this.internalPubKeyToXOnly(),
network: this.network,
scriptTree: this.scriptTree,
redeem: selectedRedeem,
name: PaymentType.P2TR,
};
}
getScriptSolution(input) {
if (!input.tapScriptSig) {
throw new Error('Tap script signature is required');
}
return [
this.contractSecret,
input.tapScriptSig[0].signature,
input.tapScriptSig[1].signature,
];
}
getScriptTree() {
if (!this.calldata) {
throw new Error('Calldata is required');
}
this.generateRedeemScripts();
return [
{
output: this.compiledTargetScript,
version: 192,
},
{
output: SharedInteractionTransaction.LOCK_LEAF_SCRIPT,
version: 192,
},
];
}
async signInputsWalletBased(transaction) {
const signer = this.signer;
await this.signInput(transaction, transaction.data.inputs[0], 0, this.scriptSigner);
await signer.multiSignPsbt([transaction]);
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);
}
}
}
}
async signInputsNonWalletBased(transaction) {
for (let i = 0; i < transaction.data.inputs.length; i++) {
if (i === 0) {
await this.signInput(transaction, transaction.data.inputs[i], i, this.scriptSigner);
await this.signInput(transaction, transaction.data.inputs[i], i, this.getSignerKey());
transaction.finalizeInput(0, this.customFinalizer.bind(this));
}
else {
await this.signInput(transaction, transaction.data.inputs[i], i, this.signer);
try {
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
}
catch (e) {
transaction.finalizeInput(i);
}
}
}
}
async createMineableRewardOutputs() {
if (!this.to)
throw new Error('To address is required');
const amountSpent = this.getTransactionOPNetFee();
let amountToCA;
if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
amountToCA = MINIMUM_AMOUNT_CA;
}
else {
amountToCA = amountSpent;
}
this.addOutput({
value: Number(amountToCA),
address: this.to,
});
if (amountToCA === MINIMUM_AMOUNT_CA &&
amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD) {
this.addOutput({
value: Number(amountSpent - amountToCA),
address: this.rewardChallenge.address,
});
}
const amount = this.addOptionalOutputsAndGetAmount();
if (!this.disableAutoRefund) {
await this.addRefundOutput(amountSpent + amount);
}
}
getPubKeys() {
const pubKeys = [Buffer.from(this.signer.publicKey)];
if (this.scriptSigner) {
pubKeys.push(Buffer.from(this.scriptSigner.publicKey));
}
return pubKeys;
}
generateRedeemScripts() {
this.targetScriptRedeem = {
name: PaymentType.P2TR,
output: this.compiledTargetScript,
redeemVersion: 192,
};
this.leftOverFundsScriptRedeem = {
name: PaymentType.P2TR,
output: SharedInteractionTransaction.LOCK_LEAF_SCRIPT,
redeemVersion: 192,
};
}
}
SharedInteractionTransaction.MAXIMUM_CALLDATA_SIZE = 1024 * 1024;