UNPKG

@dolaned/wallet-sdk-ts

Version:

Wallet SDK for the Nexa blockchain

439 lines (396 loc) 14.7 kB
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})); } }