UNPKG

@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
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]; } }