libnexa-ts
Version:
A pure and powerful Nexa SDK library.
307 lines (279 loc) • 10.9 kB
text/typescript
import Transaction from "./transaction";
import type { ITransaction, ScriptTemplateObject, UTXO } from "./interfaces";
import UnspentOutput from "./unspentoutput";
import PublicKeyHashInput from "./input/publickeyhash";
import PublicKeyTemplateInput from "./input/publickeytemplate";
import Input from "./input/input";
import Output from "./output";
import Script from "../script/script";
import ScriptTemplateInput from "./input/scripttemplate";
import type Address from "../address/address";
import ScriptFactory from "../script/script.factory";
import ValidationUtils from "../../utils/validation.utils";
import PrivateKey from "../../keys/privatekey";
import SighashType from "./sighashtype";
import type TxSignature from "./txsignature";
import TxSigner from "./txsigner";
import BufferUtils from "../../utils/buffer.utils";
export default class TransactionBuilder {
public transaction: Transaction;
constructor(tx?: ITransaction | string | Uint8Array) {
if (tx instanceof Transaction) {
this.transaction = tx;
} else {
this.transaction = new Transaction(tx);
}
}
public build(): Transaction {
return this.transaction;
}
/**
* Add an input to this transaction. This is a high level interface
* to add an input, for more control, use {@link Transaction.addInput}.
*
* Can receive, as output information, the output of nexad's `listunspent` command,
* with a slightly fancier format recognized by this sdk:
*
* ```json
* {
* outpoint: "fcf7d303d67f19568cf4ab72d36d583baac461e0f62f289b3dff68da96c2117c"
* scriptPubKey: "005114891c4b19cbcaefc31770a938ebd6b1fafabb1be6",
* satoshis: 181998351
* }
* // or alternative:
* {
* outpoint: "fcf7d303d67f19568cf4ab72d36d583baac461e0f62f289b3dff68da96c2117c"
* address: "nexa:nqtsq5g53ywykxwtethux9ms4yuwh443ltatkxlx3s5pnvwh",
* amount: 1819983.51
* groupId: <token address if relevant>
* groupAmount: <token amount if relevant>
* }
* ```
* Where `address` can be either a string or a nexcore Address object. The
* same is true for `script`, which can be a string or a nexcore Script.
*
* @see {@link UTXO}
*
* Beware that this resets all the signatures for inputs.
*
* @example
* ```javascript
* let builder = new TransactionBuilder();
*
* // From a pay to public key template output from nexad's listunspent
* builder.from({'outpoint': '0000...', amount: 123.23, scriptPubKey: 'OP_0 OP_1 ...'});
*
* // From a pay to public key template output (with optional group data)
* builder.from({'outpoint': '0000...', satoshis: 12323, address: 'nexa:nqtsq5g...', groupId? 'nexa:tnq...', groupAmount: 56446n });
*
* // From a script template output
* builder.from({'outpoint': '0000...', satoshis: 1000, scriptPubKey: '...', templateData: { templateScript: '...', constraintScript: '...' }};
*
* let transaction = builder.build();
* ```
*
* @param utxo details on the utxo
* @returns this, for chaining
*/
public from(utxo: UTXO | UTXO[]): this {
if (Array.isArray(utxo)) {
utxo.forEach(u => this.from(u));
return this;
}
let exist = this.transaction.inputs.some(input => BufferUtils.bufferToHex(input.outpoint) === utxo.outpoint);
if (exist) {
return this;
}
return this._fromUtxo(new UnspentOutput(utxo), utxo.templateData);
}
private _fromUtxo(utxo: UnspentOutput, templateData?: ScriptTemplateObject): this {
let clazz;
if (utxo.scriptPubKey.isPublicKeyHashOut()) {
clazz = PublicKeyHashInput;
} else if (utxo.scriptPubKey.isPublicKeyTemplateOut()) {
clazz = PublicKeyTemplateInput;
} else if (utxo.scriptPubKey.isScriptTemplateOut() && typeof templateData === 'object' && templateData != null) {
clazz = ScriptTemplateInput;
} else {
clazz = Input;
}
let input = new clazz({
output: new Output(utxo.satoshis, utxo.scriptPubKey),
outpoint: utxo.outpoint,
scriptSig: Script.empty(),
amount: utxo.satoshis,
templateData: templateData
});
this.transaction.addInput(input);
return this;
}
/**
* Add an output to the transaction.
*
* Beware that this resets all the signatures for inputs.
*
* @param address the destination address
* @param amount in satoshis, the nexa amount
* @param groupId optional. the token address if sending tokens
* @param groupAmount optional. the token amount if sending tokens
*
* @remarks if sending token, the nexa amount is usually {@link Transaction.DUST_AMOUNT}
*
* @returns this, for chaining
*/
public to(address: string | Address, amount: number | string | bigint, groupId?: string | Address, groupAmount?: bigint): this {
let script = ScriptFactory.buildOutFromAddress(address, groupId, groupAmount);
let output = new Output(amount, script);
this.transaction.addOutput(output);
return this;
}
/**
* Add an OP_RETURN output to the transaction.
*
* Beware that this resets all the signatures for inputs.
*
* @param data the data to be stored in the OP_RETURN output.
* In case of a string, the UTF-8 representation will be stored
* @param isFullScript if the provided data is already an op_return script. default false.
* @returns this, for chaining
*/
public addData(data: Uint8Array | string | Script, isFullScript = false): this {
let script = isFullScript ? new Script(data) : ScriptFactory.buildDataOut(data);
let output = new Output(0, script);
this.transaction.addOutput(output);
return this;
}
/**
* Set the change address for this transaction
*
* Beware that this resets all the signatures for inputs.
*
* @param address An address for change to be sent to.
* @returns this, for chaining
*/
public change(address: Address | string): this {
this.transaction.setChangeOutput(address);
return this;
}
/**
* Manually set the fee for this transaction.
*
* Beware that this resets all the signatures for inputs.
*
* @param amount satoshis to be used as fee
* @returns this, for chaining
*/
public fee(amount: number): this {
this.transaction.setFee(amount);
return this;
}
/**
* Manually set the fee per Byte rate for this transaction.
*
* Beware that this resets all the signatures for inputs.
*
* @remarks fee per Byte will be ignored if fee property was set manually
*
* @param amount satoshis per Byte to be used as fee rate
* @returns this, for chaining
*/
public feePerByte(amount: number): this {
this.transaction.setFeePerByte(amount);
return this;
}
/**
* Sets nLockTime so that transaction is not valid until the desired date
*
* (a timestamp in seconds since UNIX epoch is also accepted)
*
* @param datetime Date object or unix timestamp number
* @returns this, for chaining
*/
public lockUntilDate(datetime: number | Date): this {
ValidationUtils.validateArgument(datetime != null, 'datetime');
if (typeof datetime === 'number' && datetime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
throw new Error("Lock Time can't be earlier than UNIX date 500 000 000");
}
if (datetime instanceof Date) {
datetime = datetime.getTime() / 1000;
}
this.transaction.setLockTime(datetime);
return this;
};
/**
* Sets nLockTime so that transaction is not valid until the desired block height.
*
* @param height the block height
* @returns this, for chaining
*/
public lockUntilBlockHeight(height: number): this {
if (height >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
throw new Error("Block Height can be at most 2^32 - 1");
}
if (height < 0) {
throw new Error("Block Height cannot be negative");
}
this.transaction.setLockTime(height);
return this;
}
/**
* Sign the transaction using one or more private keys.
*
* It tries to sign each input, verifying that the signature will be valid
* (matches a public key). Usually this is the last step that should be used with the builder.
*
* @remarks this method sign all inputs and outputs (sighash type ALL).
*
* if you need to sign a specific input or partial transaction
* (create new or complete existing one), use {@link signInput} method.
*
* @param privateKey private key(s) that be used to sign
* @returns this, for chaining
*/
public sign(privateKey: PrivateKey | PrivateKey[]): this {
ValidationUtils.validateState(this.transaction.hasAllUtxoInfo(), 'Not all utxo information is available to sign the transaction.');
if (Array.isArray(privateKey)) {
privateKey.forEach(key => this.sign(key));
return this;
}
this._getSignatures(privateKey, SighashType.ALL).forEach(sig => this._applySignature(sig));
return this;
}
/**
* Sign specific input using private key and sighash type.
*
* Use sigtype to determine which parts of the transaction to sign.
*
* @param input The input to sign. can be input index (number) or input outpoint hash (string)
* @param privateKey private key that be used to sign
* @param sigtype the sighash type to define which parts to include in the sighash
* @returns this, for chaining
*/
public signInput(input: number | string, privateKey: PrivateKey, sigtype: SighashType): this {
if (typeof input === 'string') {
input = this.transaction.inputs.findIndex(inp => BufferUtils.bufferToHex(inp.outpoint) === input);
}
ValidationUtils.validateArgument(input >= 0 && input < this.transaction.inputs.length, 'input', 'out of range.');
ValidationUtils.validateState(this.transaction.inputs[input].canSign(privateKey), 'The provided key cannot sign this input');
let txSig = TxSigner.buildTxSignature(this.transaction, input, sigtype, privateKey);
this._applySignature(txSig);
return this;
}
private _getSignatures(privKey: PrivateKey, sigtype: SighashType): TxSignature[] {
ValidationUtils.validateArgument(privKey instanceof PrivateKey, 'privKey', 'not a private key');
let signatures: TxSignature[] = [];
for (let i = 0; i < this.transaction.inputs.length; i++) {
if (!this.transaction.inputs[i].canSign(privKey)) {
continue;
}
let txSig = TxSigner.buildTxSignature(this.transaction, i, sigtype, privKey);
signatures.push(txSig);
}
return signatures;
}
private _applySignature(signature: TxSignature): void {
let isValid = TxSigner.verify(this.transaction, signature.inputIndex, signature.signature, signature.sigType, signature.subscript, signature.publicKey);
ValidationUtils.validateState(isValid, 'Signature is invalid');
this.transaction.inputs[signature.inputIndex].addSignature(signature);
}
}