@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
368 lines (367 loc) • 15.3 kB
JavaScript
import { currentConsensus } from '../consensus/ConsensusConfig.js';
import { CustomScriptTransaction, } from './builders/CustomScriptTransaction.js';
import { DeploymentTransaction } from './builders/DeploymentTransaction.js';
import { FundingTransaction } from './builders/FundingTransaction.js';
import { InteractionTransaction } from './builders/InteractionTransaction.js';
import { TransactionBuilder } from './builders/TransactionBuilder.js';
import { ChallengeSolutionTransaction } from './builders/ChallengeSolutionTransaction.js';
export class TransactionFactory {
async createCustomScriptTransaction(interactionParameters) {
if (!interactionParameters.to) {
throw new Error('Field "to" not provided.');
}
if (!interactionParameters.from) {
throw new Error('Field "from" not provided.');
}
if (!interactionParameters.utxos[0]) {
throw new Error('Missing at least one UTXO.');
}
if (!('signer' in interactionParameters)) {
throw new Error('Field "signer" not provided, OP_WALLET not detected.');
}
const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
const preTransaction = new CustomScriptTransaction({
...interactionParameters,
utxos: [interactionParameters.utxos[0]],
optionalInputs: inputs,
});
await preTransaction.generateTransactionMinimalSignatures();
const parameters = await preTransaction.getFundingTransactionParameters();
parameters.utxos = interactionParameters.utxos;
parameters.amount =
(await preTransaction.estimateTransactionFees()) +
this.getPriorityFee(interactionParameters) +
preTransaction.getOptionalOutputValue();
const feeEstimationFundingTransaction = await this.createFundTransaction({
...parameters,
optionalOutputs: [],
optionalInputs: [],
});
if (!feeEstimationFundingTransaction) {
throw new Error('Could not sign funding transaction.');
}
parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
const signedTransaction = await this.createFundTransaction({
...parameters,
optionalOutputs: [],
optionalInputs: [],
});
if (!signedTransaction) {
throw new Error('Could not sign funding transaction.');
}
interactionParameters.utxos = this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0);
const newParams = {
...interactionParameters,
utxos: [
...this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0),
],
randomBytes: preTransaction.getRndBytes(),
nonWitnessUtxo: signedTransaction.tx.toBuffer(),
estimatedFees: preTransaction.estimatedFees,
optionalInputs: inputs,
};
const finalTransaction = new CustomScriptTransaction(newParams);
const outTx = await finalTransaction.signTransaction();
return [
signedTransaction.tx.toHex(),
outTx.toHex(),
this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.from, 1),
];
}
async signInteraction(interactionParameters) {
if (!interactionParameters.to) {
throw new Error('Field "to" not provided.');
}
if (!interactionParameters.from) {
throw new Error('Field "from" not provided.');
}
if (!interactionParameters.utxos[0]) {
throw new Error('Missing at least one UTXO.');
}
const opWalletInteraction = await this.detectInteractionOPWallet(interactionParameters);
if (opWalletInteraction) {
return opWalletInteraction;
}
if (!('signer' in interactionParameters)) {
throw new Error('Field "signer" not provided, OP_WALLET not detected.');
}
const inputs = this.parseOptionalInputs(interactionParameters.optionalInputs);
const preTransaction = new InteractionTransaction({
...interactionParameters,
utxos: [interactionParameters.utxos[0]],
optionalInputs: inputs,
});
await preTransaction.generateTransactionMinimalSignatures();
const parameters = await preTransaction.getFundingTransactionParameters();
parameters.utxos = interactionParameters.utxos;
parameters.amount =
(await preTransaction.estimateTransactionFees()) +
this.getPriorityFee(interactionParameters) +
preTransaction.getOptionalOutputValue();
const feeEstimationFundingTransaction = await this.createFundTransaction({
...parameters,
optionalOutputs: [],
optionalInputs: [],
});
if (!feeEstimationFundingTransaction) {
throw new Error('Could not sign funding transaction.');
}
parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
const signedTransaction = await this.createFundTransaction({
...parameters,
optionalOutputs: [],
optionalInputs: [],
});
if (!signedTransaction) {
throw new Error('Could not sign funding transaction.');
}
interactionParameters.utxos = this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0);
const newParams = {
...interactionParameters,
utxos: [
...this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.to, 0),
],
randomBytes: preTransaction.getRndBytes(),
preimage: preTransaction.getPreimage(),
nonWitnessUtxo: signedTransaction.tx.toBuffer(),
estimatedFees: preTransaction.estimatedFees,
optionalInputs: inputs,
};
const finalTransaction = new InteractionTransaction(newParams);
const outTx = await finalTransaction.signTransaction();
return {
fundingTransaction: signedTransaction.tx.toHex(),
interactionTransaction: outTx.toHex(),
estimatedFees: preTransaction.estimatedFees,
nextUTXOs: this.getUTXOAsTransaction(signedTransaction.tx, interactionParameters.from, 1),
preimage: preTransaction.getPreimage().toString('hex'),
};
}
async signDeployment(deploymentParameters) {
const opWalletDeployment = await this.detectDeploymentOPWallet(deploymentParameters);
if (opWalletDeployment) {
return opWalletDeployment;
}
if (!('signer' in deploymentParameters)) {
throw new Error('Field "signer" not provided, OP_WALLET not detected.');
}
const inputs = this.parseOptionalInputs(deploymentParameters.optionalInputs);
const preTransaction = new DeploymentTransaction({
...deploymentParameters,
utxos: [deploymentParameters.utxos[0]],
optionalInputs: inputs,
});
await preTransaction.generateTransactionMinimalSignatures();
const parameters = await preTransaction.getFundingTransactionParameters();
parameters.utxos = deploymentParameters.utxos;
parameters.amount =
(await preTransaction.estimateTransactionFees()) +
this.getPriorityFee(deploymentParameters) +
preTransaction.getOptionalOutputValue();
const feeEstimationFundingTransaction = await this.createFundTransaction({
...parameters,
optionalOutputs: [],
optionalInputs: [],
});
if (!feeEstimationFundingTransaction) {
throw new Error('Could not sign funding transaction.');
}
parameters.estimatedFees = feeEstimationFundingTransaction.estimatedFees;
const fundingTransaction = new FundingTransaction({
...parameters,
optionalInputs: [],
optionalOutputs: [],
});
const signedTransaction = await fundingTransaction.signTransaction();
if (!signedTransaction) {
throw new Error('Could not sign funding transaction.');
}
const out = signedTransaction.outs[0];
const newUtxo = {
transactionId: signedTransaction.getId(),
outputIndex: 0,
scriptPubKey: {
hex: out.script.toString('hex'),
address: preTransaction.getScriptAddress(),
},
value: BigInt(out.value),
};
const newParams = {
...deploymentParameters,
utxos: [newUtxo],
randomBytes: preTransaction.getRndBytes(),
preimage: preTransaction.getPreimage(),
nonWitnessUtxo: signedTransaction.toBuffer(),
estimatedFees: preTransaction.estimatedFees,
optionalInputs: inputs,
};
const finalTransaction = new DeploymentTransaction(newParams);
const outTx = await finalTransaction.signTransaction();
const out2 = signedTransaction.outs[1];
const refundUTXO = {
transactionId: signedTransaction.getId(),
outputIndex: 1,
scriptPubKey: {
hex: out2.script.toString('hex'),
address: deploymentParameters.from,
},
value: BigInt(out2.value),
};
return {
transaction: [signedTransaction.toHex(), outTx.toHex()],
contractAddress: finalTransaction.getContractAddress(),
contractPubKey: finalTransaction.contractPubKey,
utxos: [refundUTXO],
preimage: preTransaction.getPreimage().toString('hex'),
};
}
async createBTCTransfer(parameters) {
if (!parameters.from) {
throw new Error('Field "from" not provided.');
}
const resp = await this.createFundTransaction(parameters);
return {
estimatedFees: resp.estimatedFees,
original: resp.original,
tx: resp.tx.toHex(),
nextUTXOs: this.getAllNewUTXOs(resp.original, resp.tx, parameters.from),
};
}
async createChallengeSolution(parameters) {
if (!parameters.from) {
throw new Error('Field "from" not provided.');
}
const resp = await this._createChallengeSolution(parameters);
return {
estimatedFees: resp.estimatedFees,
original: resp.original,
tx: resp.tx.toHex(),
nextUTXOs: this.getAllNewUTXOs(resp.original, resp.tx, parameters.from),
};
}
getAllNewUTXOs(original, tx, to) {
const outputs = original.getOutputs();
const utxos = [];
for (let i = 0; i < tx.outs.length; i++) {
const output = outputs[i];
if ('address' in output) {
if (output.address !== to)
continue;
}
else {
continue;
}
utxos.push(...this.getUTXOAsTransaction(tx, to, i));
}
return utxos;
}
parseOptionalInputs(optionalInputs) {
return (optionalInputs || []).map((input) => {
let nonWitness = input.nonWitnessUtxo;
if (nonWitness &&
!(nonWitness instanceof Uint8Array) &&
typeof nonWitness === 'object') {
nonWitness = Buffer.from(Uint8Array.from(Object.values(input.nonWitnessUtxo)));
}
return {
...input,
nonWitnessUtxo: nonWitness,
};
});
}
async detectInteractionOPWallet(interactionParameters) {
if (typeof window === 'undefined') {
return null;
}
const _window = window;
if (!_window || !_window.opnet || !_window.opnet.web3) {
return null;
}
const opnet = _window.opnet.web3;
const interaction = await opnet.signInteraction({
...interactionParameters,
signer: undefined,
});
if (!interaction) {
throw new Error('Could not sign interaction transaction.');
}
return interaction;
}
async detectDeploymentOPWallet(deploymentParameters) {
if (typeof window === 'undefined') {
return null;
}
const _window = window;
if (!_window || !_window.opnet || !_window.opnet.web3) {
return null;
}
const opnet = _window.opnet.web3;
const deployment = await opnet.deployContract({
...deploymentParameters,
signer: undefined,
});
if (!deployment) {
throw new Error('Could not sign interaction transaction.');
}
return deployment;
}
async _createChallengeSolution(parameters) {
if (!parameters.to)
throw new Error('Field "to" not provided.');
const challengeTransaction = new ChallengeSolutionTransaction(parameters);
const signedTransaction = await challengeTransaction.signTransaction();
if (!signedTransaction) {
throw new Error('Could not sign funding transaction.');
}
return {
tx: signedTransaction,
original: challengeTransaction,
estimatedFees: challengeTransaction.estimatedFees,
nextUTXOs: this.getUTXOAsTransaction(signedTransaction, parameters.to, 0),
};
}
async createFundTransaction(parameters) {
if (!parameters.to)
throw new Error('Field "to" not provided.');
const fundingTransaction = new FundingTransaction(parameters);
const signedTransaction = await fundingTransaction.signTransaction();
if (!signedTransaction) {
throw new Error('Could not sign funding transaction.');
}
return {
tx: signedTransaction,
original: fundingTransaction,
estimatedFees: fundingTransaction.estimatedFees,
nextUTXOs: this.getUTXOAsTransaction(signedTransaction, parameters.to, 0),
};
}
writePSBTHeader(type, psbt) {
const buf = Buffer.from(psbt, 'base64');
const header = Buffer.alloc(2);
header.writeUInt8(type, 0);
header.writeUInt8(currentConsensus, 1);
return Buffer.concat([header, buf]).toString('hex');
}
getPriorityFee(params) {
const totalFee = params.priorityFee + params.gasSatFee;
if (totalFee < TransactionBuilder.MINIMUM_DUST) {
return TransactionBuilder.MINIMUM_DUST;
}
return totalFee;
}
getUTXOAsTransaction(tx, to, index) {
if (!tx.outs[index])
return [];
const out = tx.outs[index];
const newUtxo = {
transactionId: tx.getId(),
outputIndex: index,
scriptPubKey: {
hex: out.script.toString('hex'),
address: to,
},
value: BigInt(out.value),
};
return [newUtxo];
}
}