@dolaned/wallet-sdk-ts
Version:
Wallet SDK for the Nexa blockchain
439 lines (396 loc) • 14.7 kB
text/typescript
import {
Address,
AddressType,
GroupToken,
Networkish,
Networks, Output, Script,
ScriptFactory,
Transaction,
TransactionBuilder
} from "libnexa-ts";
import {PermissionLabel, TokenAction, TxOptions} from "../../../models/transaction.entities";
import {isValidNexaAddress} from "../../../utils/WalletUtils";
import {parseInt} from "lodash-es";
import {MAX_INT64} from "../../../utils/CommonUtils";
import {rostrumProvider} from "../../../network/RostrumProvider";
/**
* Abstract base class for creating and managing transactions in the NEXA blockchain.
* Provides common functionality for transaction building including token operations,
* address validation, and output creation.
*/
export abstract class TransactionCreator {
/** The underlying transaction builder instance */
private _transactionBuilder!: TransactionBuilder;
/** Set of token actions to be performed in this transaction */
private _tokens!: Set<TokenAction>;
/** Array of async functions to execute when building the transaction */
private _builder: (() => Promise<any>)[] = [];
/** Total value of NEXA being sent in this transaction */
private _totalValue: bigint = BigInt(0);
/** Network this transaction will be broadcast on */
private _network: Networkish = Networks.mainnet
/** Transaction options for customizing behavior */
private _txOptions: TxOptions = {}
/**
* Creates a new TransactionCreator instance
* @param tx Optional existing TransactionBuilder, hex string, or buffer
*/
protected constructor(tx?: TransactionBuilder | string | Buffer) {
if (tx instanceof TransactionBuilder) {
this.transactionBuilder = tx;
}
this.tokens = new Set<TokenAction>()
this.transactionBuilder = new TransactionBuilder();
}
/** Parse transaction from hex string - must be implemented by subclasses */
public abstract parseTxHex(tx:string): this
/** Parse transaction from buffer - must be implemented by subclasses */
public abstract parseTxBuffer(tx: Buffer): this
/**
* Sets the network for this transaction
* @param network Network name or Networkish object
* @returns This instance for chaining
*/
public onNetwork(network: string | Networkish) {
this.network = Networks.get(network)!
return this
}
/** Gets transaction options */
get txOptions(): TxOptions {
return this._txOptions;
}
/** Sets transaction options */
set txOptions(value: TxOptions) {
this._txOptions = value;
}
/** Gets the network for this transaction */
get network(): Networkish {
return this._network;
}
/** Sets the network for this transaction */
set network(value: Networkish) {
this._network = value;
}
/** Gets the builder function array */
get builder(): (() => Promise<any>)[] {
return this._builder;
}
/** Sets the builder function array */
set builder(value: (() => Promise<any>)[]) {
this._builder = value;
}
/** Gets the underlying transaction builder */
get transactionBuilder(): TransactionBuilder {
return this._transactionBuilder;
}
/** Sets the underlying transaction builder */
set transactionBuilder(value: TransactionBuilder) {
this._transactionBuilder = value;
}
/** Gets the set of token actions */
get tokens(): Set<TokenAction> {
return this._tokens;
}
/** Sets the set of token actions */
set tokens(value: Set<TokenAction>) {
this._tokens = value;
}
/** Gets the total NEXA value being sent */
get totalValue(): bigint {
return this._totalValue;
}
/** Sets the total NEXA value being sent */
set totalValue(value: bigint) {
this._totalValue = value;
}
/**
* Validates and creates a token action
* @param toAddr Destination address
* @param amount Amount to send
* @param token Token ID
* @param action Action type (mint, melt, send, etc.)
* @throws Error if validation fails
*/
protected tokenAction(toAddr: string, amount: string, token: string, action: string){
// Validate destination address
if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
// Validate amount ranges
if ((token && BigInt(amount) < 1n) || (!token && parseInt(amount) < Transaction.DUST_AMOUNT)) {
throw new Error("The amount is too low.");
}
if ((token && BigInt(amount) > MAX_INT64) || (!token && parseInt(amount) > Transaction.MAX_MONEY)) {
throw new Error("The amount is too high.");
}
// Validate token ID
if (!isValidNexaAddress(token, this.network, AddressType.GroupIdAddress)) {
throw new Error('Invalid Token ID');
}
// Ensure tokens are sent to script template addresses
if (Address.getOutputType(toAddr) === 0) {
throw new Error('Token must be sent to script template address');
}
// Add output to transaction
this.transactionBuilder.to(toAddr, Transaction.DUST_AMOUNT, token, BigInt(amount))
// Record the token action
this.tokens.add({
token: token,
amount: BigInt(amount),
action: action
})
}
/**
* Configures transaction to consolidate UTXOs to a single address
* @param toAddr Address to consolidate funds to
* @returns This instance for chaining
*/
public consolidate(toAddr: string): this {
this.builder.push(async () => {
if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
this._txOptions.isConsolidate = true
this._txOptions.toChange = toAddr
})
return this
}
/**
* Configures transaction to deduct fee from the send amount
* @returns This instance for chaining
*/
public feeFromAmount(): this{
this.builder.push(async () => {
this._txOptions.feeFromAmount = true
})
return this
}
/**
* Adds a token send operation to the transaction
* @param toAddr Destination address
* @param amount Amount of tokens to send
* @param token Token ID
* @returns This instance for chaining
*/
public sendToToken(toAddr: string, amount: string, token: string): this {
this.builder.push(async () => {
this.tokenAction(toAddr, amount, token, 'send')
})
return this;
}
/**
* Adds a NEXA send operation to the transaction
* @param toAddr Destination address
* @param amount Amount of NEXA to send
* @returns This instance for chaining
*/
public sendTo(toAddr: string, amount: string){
this.builder.push(async () => {
if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
this.transactionBuilder.to(toAddr, amount);
this.totalValue = BigInt(this.totalValue + amount)
})
return this;
}
/**
* Adds a token authority renewal operation
* @param token Token ID to renew authority for
* @param perms Permissions to renew
* @param toAddr
* @returns This instance for chaining
*/
public renewAuthority(token: string, perms: PermissionLabel[], toAddr?: string): this {
this.builder.push(async() => {
if(toAddr != null) {
if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
}
this.tokens.add({
token: token,
action: 'renew',
amount: BigInt(Transaction.DUST_AMOUNT),
parentToken: undefined,
extraData: {
perms: perms,
address: toAddr
}
})
})
return this
}
/**
* Adds a token authority deletion operation
* @param token Token ID to delete authority for
* @param outpoint Outpoint of the authority to delete
* @returns This instance for chaining
*/
public deleteAuthority(token:string, outpoint: string): this {
this.builder.push(async () => {
this.tokens.add({
token: token,
action: 'delete',
amount: BigInt(Transaction.DUST_AMOUNT),
parentToken: undefined,
extraData: {
outpoint: outpoint
}
})
})
return this
}
/**
* Creates a legacy token (not implemented)
* @returns This instance for chaining
*/
public legacyToken(name: string, ticker: string, decimals: number, docUrl: string, docHash: string): this {
this.builder.push(async () => {
const opReturn = ScriptFactory.buildTokenDescriptionLegacy(
ticker,
name,
docUrl,
docHash,
decimals
)
this.transactionBuilder.addData(opReturn, true)
this.tokens.add({
action: 'group',
amount: BigInt(Transaction.DUST_AMOUNT),
extraData: {
opReturnData: opReturn.toHex()
}
})
})
return this
}
/**
* Creates a legacy group (not implemented)
* @returns This instance for chaining
*/
public legacyGroup(name: string, ticker: string, docUrl: string, docHash: string): this {
this.builder.push(async () => {
const opReturn = ScriptFactory.buildTokenDescriptionLegacy(
ticker,
name,
docUrl,
docHash,
)
this.transactionBuilder.addData(opReturn, true)
this.tokens.add({
action: 'group',
amount: BigInt(Transaction.DUST_AMOUNT),
extraData: {
opReturnData: opReturn.toHex()
}
})
})
return this
}
/**
* Creates a token with metadata
* @param name Token name
* @param ticker Token ticker symbol
* @param decimals Number of decimal places
* @param docUrl URL to token documentation
* @param docHash Hash of token documentation
* @returns This instance for chaining
*/
public token(name: string, ticker: string, decimals: number, docUrl: string, docHash: string): this {
this.builder.push(async () => {
const opReturn = ScriptFactory.buildTokenDescription(
ticker,
name,
docUrl,
docHash,
decimals
)
this.transactionBuilder.addData(opReturn, true)
this.tokens.add({
action: 'group',
amount: BigInt(Transaction.DUST_AMOUNT),
extraData: {
opReturnData: opReturn.toHex()
}
})
})
return this
}
/**
* Creates an NFT collection with metadata
* @param name Collection name
* @param ticker Collection ticker symbol
* @param docUrl URL to collection documentation
* @param docHash Hash of collection documentation
* @returns This instance for chaining
*/
public collection(name: string, ticker: string, docUrl: string, docHash: string): this {
this.builder.push(async () => {
const opReturn = ScriptFactory.buildNFTCollectionDescription(
ticker,
name,
docUrl,
docHash
)
this.transactionBuilder.addData(opReturn, true)
this.tokens.add({
action: 'group',
amount: BigInt(Transaction.DUST_AMOUNT),
extraData: {
opReturnData: opReturn.toHex()
}
})
})
return this
}
/**
* Creates an NFT within a collection
* @param parent Parent collection token ID
* @param zipUrl URL to NFT content ZIP file
* @param zipHash Hash of NFT content ZIP file
* @returns This instance for chaining
*/
public nft(parent: string, zipUrl: string, zipHash: string) {
this.builder.push(async () => {
// add op_return for the nft
let opReturn = ScriptFactory.buildNFTDescription(zipUrl, zipHash);
this.transactionBuilder.addData(opReturn, true);
// generate subgroup ID
const subGroup = GroupToken.generateSubgroupId(parent, opReturn.toBuffer()).toString('hex')
this.tokens.add({
token: subGroup,
parentToken: parent,
amount: BigInt(Transaction.DUST_AMOUNT),
action: 'subgroup'
})
})
return this
}
/**
* Adds an OP_RETURN output to the transaction
* @param data Data to include in the OP_RETURN
* @param isFullScript Whether the data is already a complete script
* @returns This instance for chaining
*/
public addOpReturn(data: Buffer | string | Script, isFullScript = false) {
this.builder.push(async () => {
let script = isFullScript ? new Script(data) : ScriptFactory.buildDataOut(data);
let output = new Output(0, script);
this.transactionBuilder.transaction.addOutput(output);
})
return this;
}
/** Populates the transaction with inputs and outputs - must be implemented by subclasses */
abstract populate(): this
/**
* Builds the transaction by executing all queued operations
* @returns Promise resolving to the serialized transaction hex
*/
public async build(): Promise<string>{
for (const task of this.builder) {
await task();
}
return this.transactionBuilder.transaction.serialize(({disableAll: true}));
}
}