@dolaned/wallet-sdk-ts
Version:
Wallet SDK for the Nexa blockchain
190 lines (170 loc) • 8.59 kB
text/typescript
import {
AddressType,
TransactionBuilder,
} from "libnexa-ts";
import {WatchOnlyAddress} from "../../models/wallet.entities";
import {TransactionCreator} from "./interfaces/TransactionCreator";
import {
watchOnlyBuildCreateGroupTransaction, watchOnlyPopulateAndDuplicateTokenAuths,
watchOnlyPopulateNexaInputsAndChange,
watchOnlyPopulateTokenAuth, watchOnlyPopulateTokenInputsAndChange, watchOnlyPrepareDeleteTransaction
} from "../../utils/WatchOnlyTXUtils";
import {isString} from "lodash-es";
import {rostrumProvider} from "../../network/RostrumProvider";
import {PermissionLabel} from "../../models/transaction.entities";
import {isValidNexaAddress} from "../../utils/WalletUtils";
/**
* WatchOnlyTransactionCreator extends TransactionCreator to handle transaction creation
* for watch-only wallets. It manages addresses without private keys and delegates
* UTXO selection and input population to specialized utility functions.
*/
export default class WatchOnlyTransactionCreator extends TransactionCreator {
/** Addresses that need to be signed with (populated during transaction building) */
private _addressesToSignWith: string[] = [];
/** Available addresses for input selection and change */
private _availableAddresses: WatchOnlyAddress[] = []
/**
* Creates a new WatchOnlyTransactionCreator
* @param tx Optional existing transaction builder or transaction data
*/
constructor(tx?: TransactionBuilder | string | Buffer) {
super(tx)
}
/**
* Sets the source addresses for transaction inputs
* @param address Single address string, array of addresses, or WatchOnlyAddress objects
* @returns This instance for chaining
*/
public from(address: string | string[] | WatchOnlyAddress[] | WatchOnlyAddress): this {
if (isString(address)) {
if (!isValidNexaAddress(address, this.network) && !isValidNexaAddress(address, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
// Single address string
this._availableAddresses.push({address: address});
} else if(Array.isArray(address)) {
// Array of addresses or WatchOnlyAddress objects
address.forEach((addr) => {
if (isString(addr)) {
if (!isValidNexaAddress(addr, this.network) && !isValidNexaAddress(addr, this.network, AddressType.PayToPublicKeyHash)) {
throw new Error('Invalid Address.');
}
// String address
this._availableAddresses.push({address: addr});
} else if (addr && typeof addr === 'object' && 'address' in addr) {
// WatchOnlyAddress object
this._availableAddresses.push(<WatchOnlyAddress>addr);
}
})
} else if(address.address != null) {
// Single WatchOnlyAddress object
this._availableAddresses.push(<WatchOnlyAddress>address)
}
return this
}
/**
* Adds a token minting operation to the transaction
* @param token Token ID to mint
* @param amount Amount to mint
* @param toAddr Destination address for minted tokens
* @returns This instance for chaining
*/
public mint(token: string, amount: string, toAddr:string): this {
this.builder.push(async() => {
this.tokenAction(toAddr, amount, token, 'mint')
})
return this
}
/**
* Adds a token melting operation to the transaction
* @param token Token ID to melt
* @param amount Amount to melt
* @param toAddr Destination address for melted tokens
* @returns This instance for chaining
*/
public melt(token: string, amount: string, toAddr:string): this {
this.builder.push(async() => {
this.tokenAction(toAddr, amount, token, 'melt')
})
return this
}
/**
* Populates the transaction with inputs and outputs based on the configured actions.
* Handles different token operations (mint, melt, group creation, etc.) and
* populates NEXA inputs for transaction fees.
* @returns This instance for chaining
*/
public populate(): this {
this.builder.push(async () => {
let tokenAddresses: string[] = []
let nexaAddresses: string[] = []
// Process token operations if any are configured
if(this.tokens.size > 0) {
for (const tokenAction of this.tokens) {
if(tokenAction.action == 'mint' || tokenAction.action == 'melt') {
// Handle token minting/melting - requires authority
tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenAuth(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.action))
} else if (tokenAction.action == 'group') {
// Handle group token creation
tokenAddresses = tokenAddresses.concat(await watchOnlyBuildCreateGroupTransaction(this.transactionBuilder, this._availableAddresses, tokenAction.token!, this.network))
} else if (tokenAction.action == 'subgroup') {
// Handle subgroup token creation
tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenAuth(this.transactionBuilder, this._availableAddresses, tokenAction.parentToken!, tokenAction.action, tokenAction.token));
} else if(tokenAction.action == 'renew') {
// Handle authority renewal
tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateAndDuplicateTokenAuths(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.extraData!.perms!, tokenAction.extraData?.address!))
} else if(tokenAction.action == 'delete') {
// Handle authority deletion
tokenAddresses = tokenAddresses.concat(await watchOnlyPrepareDeleteTransaction(this.transactionBuilder, this._availableAddresses, tokenAction.extraData!.outpoint!))
} else {
// Handle regular token transfers
tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenInputsAndChange(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.amount))
}
// Accumulate addresses that need signing
this._addressesToSignWith!.concat(tokenAddresses)
}
}
// Populate NEXA inputs for transaction fees and change
nexaAddresses = nexaAddresses.concat(await watchOnlyPopulateNexaInputsAndChange(this.transactionBuilder, this._availableAddresses, this.totalValue, this.txOptions))
// Combine all addresses that need signing
this._addressesToSignWith! = tokenAddresses.concat(nexaAddresses)
})
return this
}
/**
* Parse transaction from buffer (not implemented for watch-only)
* @param tx Transaction buffer
* @returns This instance for chaining
* @throws Error indicating method not implemented
*/
public parseTxBuffer(tx: Buffer): this {
this.builder.push(async () => {
this.transactionBuilder = new TransactionBuilder(tx);
})
return this
}
/**
* Parse transaction from hex string (not implemented for watch-only)
* @param tx Transaction hex string
* @returns This instance for chaining
* @throws Error indicating method not implemented
*/
public parseTxHex(tx: string) {
this.builder.push(async () => {
const txBuilder = new TransactionBuilder(tx)
const newTxBuilder = new TransactionBuilder()
const oldInputs = txBuilder.transaction.inputs
for (const input of oldInputs) {
const utxo = await rostrumProvider.getUtxo(input.outpoint.toString('hex'))
newTxBuilder.from({
outpoint: utxo.tx_hash,
amount: utxo.amount,
scriptPubKey: utxo.scriptpubkey
})
}
newTxBuilder.transaction.outputs = txBuilder.transaction.outputs
this.transactionBuilder = newTxBuilder;
})
return this
}
}