UNPKG

@dolaned/wallet-sdk-ts

Version:

Wallet SDK for the Nexa blockchain

446 lines (401 loc) 16.7 kB
import {rostrumProvider} from '../network/RostrumProvider'; import {AccountKeys} from '../models/wallet.entities'; import {isNullOrEmpty, MAX_INT64, tokenIdToHex} from './CommonUtils'; import { Address, AddressType, GroupToken, Networkish, PrivateKey, Transaction, TransactionBuilder, UnitUtils, UTXO } from "libnexa-ts"; import {PermissionLabel, TxOptions} from "../models/transaction.entities"; import {dupAuthority, isAuthFit} from "./TokenUtils"; /** Maximum number of inputs/outputs allowed in a single transaction */ const MAX_INPUTS_OUTPUTS = 250; /** * Populate a transaction with Nexa UTXO inputs and set change output if needed * * This function automatically selects and adds UTXOs from the provided account keys * to satisfy the transaction's Nexa amount requirements. It handles change calculation * and can consolidate UTXOs when requested. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys containing receive and change addresses with balances * @param totalTxValue - Total amount of Nexa required for the transaction (in satoshis) * @param options - Transaction options including consolidation settings and change address * @returns Promise resolving to array of private keys needed for signing * @throws {Error} If insufficient balance or too many inputs required * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await populateNexaInputsAndChange( * txBuilder, * keys, * 1000000n, // 1 NEXA * { isConsolidate: false, feeFromAmount: false } * ); * ``` */ export async function populateNexaInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, totalTxValue: bigint, options: TxOptions): Promise<PrivateKey[]> { let rKeys = keys.receiveKeys.filter(k => BigInt(k.balance) > 0n); let cKeys = keys.changeKeys.filter(k => BigInt(k.balance) > 0n); let allKeys = rKeys.concat(cKeys); if (isNullOrEmpty(allKeys)) { throw new Error("Not enough Nexa balance."); } let usedKeys = new Map<string, PrivateKey>(); let origAmount = options.isConsolidate ? 0 : Number(totalTxValue); for (let key of allKeys) { let utxos = await rostrumProvider.getNexaUtxos(key.address); for (let utxo of utxos) { let input: UTXO = { outpoint: utxo.outpoint_hash, address: key.address, satoshis: utxo.value, templateData: options.templateData } txBuilder.from(input); if (!usedKeys.has(key.address)) { usedKeys.set(key.address, key.key.privateKey); } if (options.isConsolidate) { txBuilder.change(options.toChange ?? keys.receiveKeys[keys.receiveKeys.length - 1].address); if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) { return Array.from(usedKeys.values()); } } else { let tx = txBuilder.transaction; if (tx.inputs.length > MAX_INPUTS_OUTPUTS) { throw new Error("Too many inputs. Consider consolidate transactions or reduce the send amount."); } let unspent = tx.getUnspentValue(); if (unspent < 0n) { continue; } if (unspent == 0n && options.feeFromAmount) { let txFee = tx.estimateRequiredFee(); tx.updateOutputAmount(0, origAmount - txFee); return Array.from(usedKeys.values()); } txBuilder.change(options.toChange ?? keys.changeKeys[keys.changeKeys.length - 1].address); if (options.feeFromAmount) { let hasChange = tx.getChangeOutput(); let txFee = tx.estimateRequiredFee(); tx.updateOutputAmount(0, origAmount - txFee); // edge case where change added after update if (!hasChange && tx.getChangeOutput()) { txFee = tx.estimateRequiredFee(); tx.updateOutputAmount(0, origAmount - txFee); } } // check again after change output manipulation if (tx.getUnspentValue() < tx.estimateRequiredFee()) { // try to add more utxos to satisfy the minimum fee continue; } return Array.from(usedKeys.values()); } } } if (options.isConsolidate) { if (usedKeys.size > 0) { return Array.from(usedKeys.values()); } throw new Error("Not enough Nexa balance."); } let err = { errorMsg: "Not enough Nexa balance.", amount: UnitUtils.formatNEXA(txBuilder.transaction.outputs[0].value), fee: UnitUtils.formatNEXA(txBuilder.transaction.estimateRequiredFee()) } throw new Error(JSON.stringify(err)); } /** * Populate a transaction with token UTXO inputs and set token change output if needed * * This function selects and adds token UTXOs from the provided account keys * to satisfy the transaction's token amount requirements. It automatically * handles token change calculation. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys containing addresses with token balances * @param token - The token ID to spend * @param outTokenAmount - Amount of tokens required for the transaction * @returns Promise resolving to array of private keys needed for signing * @throws {Error} If insufficient token balance or too many inputs required * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await populateTokenInputsAndChange( * txBuilder, * keys, * 'token_id_hex', * 1000n // Amount of tokens * ); * ``` */ export async function populateTokenInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, outTokenAmount: bigint): Promise<PrivateKey[]> { let tokenHex = tokenIdToHex(token); let rKeys = keys.receiveKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex)); let cKeys = keys.changeKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex)); let allKeys = rKeys.concat(cKeys); if (isNullOrEmpty(allKeys)) { throw new Error("Not enough token balance."); } let usedKeys = new Map<string, PrivateKey>(); let inTokenAmount = 0n; for (let key of allKeys) { let utxos = await rostrumProvider.getTokenUtxos(key.address, token); for (let utxo of utxos) { if (utxo.token_amount < 0) { continue; } txBuilder.from({ outpoint: utxo.outpoint_hash, address: key.address, satoshis: utxo.value, groupId: utxo.group, groupAmount: BigInt(utxo.token_amount), }); inTokenAmount = inTokenAmount + BigInt(utxo.token_amount); if (!usedKeys.has(key.address)) { usedKeys.set(key.address, key.key.privateKey); } if (inTokenAmount > MAX_INT64) { throw new Error("Token inputs exceeded max amount. Consider sending in small chunks"); } if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) { throw new Error("Too many inputs. Consider consolidating transactions or reduce the send amount."); } if (inTokenAmount == outTokenAmount) { return Array.from(usedKeys.values()); } if (inTokenAmount > outTokenAmount) { // change txBuilder.to(keys.changeKeys[keys.changeKeys.length - 1].address, Transaction.DUST_AMOUNT, token, inTokenAmount - outTokenAmount); return Array.from(usedKeys.values()); } } } throw new Error("Not enough token balance"); } /** * Build a transaction to create a new token group * * This function creates a group token by using the first UTXO's outpoint * to generate a unique group ID. The group token represents the authority * to create fungible tokens within this group. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys to use for funding the transaction * @param opReturnData - Optional data to include in the group creation * @param network - Network to create the group on * @returns Promise resolving to array of private keys needed for signing * @throws {Error} If insufficient balance for group creation * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await buildCreateGroupTransaction( * txBuilder, * keys, * 'my_token_data', * Networks.mainnet * ); * ``` */ export async function buildCreateGroupTransaction( txBuilder: TransactionBuilder, keys: AccountKeys, opReturnData: string, network: Networkish ): Promise<PrivateKey[]> { // TODO validate opreturn data const allKeys = keys.receiveKeys.concat(keys.changeKeys) let outpoint = ''; let usedKeys: PrivateKey[] = []; let signKey: PrivateKey | undefined = undefined; for (let key of allKeys) { let utxos = await rostrumProvider.getNexaUtxos(key.address); for (let utxo of utxos) { txBuilder.from({ outpoint: utxo.outpoint_hash, address: key.address, satoshis: utxo.value }); if (isNullOrEmpty(outpoint)) { outpoint = utxo.outpoint_hash; let id = GroupToken.findGroupId(Buffer.from(outpoint, 'hex'), Buffer.from(opReturnData, 'hex'), GroupToken.authFlags.ACTIVE_FLAG_BITS); const groupId = new Address(id.hashBuffer, network, AddressType.GroupIdAddress).toString() txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, groupId, GroupToken.authFlags.ACTIVE_FLAG_BITS | id.nonce) signKey = key.key.privateKey usedKeys.push(signKey); return usedKeys } } } throw new Error("Not enough Nexa balance."); } /** * Prepare a transaction to delete/spend a specific UTXO * * This function adds a specific UTXO as input to the transaction. * It's commonly used for spending token authority UTXOs or consolidating specific outputs. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys to find the private key for the UTXO * @param outpoint - The outpoint (txid:vout) of the UTXO to spend * @returns Promise resolving to array containing the private key for the UTXO * @throws {Error} If the UTXO is not found or the associated key is not in the wallet * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await prepareDeleteTransaction( * txBuilder, * keys, * 'txid:0' * ); * ``` */ export async function prepareDeleteTransaction(txBuilder: TransactionBuilder, keys: AccountKeys, outpoint: string): Promise<PrivateKey[]> { let utxo = await rostrumProvider.getUtxo(outpoint); let address = utxo.addresses[0]; txBuilder.from({ outpoint: outpoint, address: address, satoshis: utxo.amount }); let allKeys = keys.receiveKeys.concat(keys.changeKeys); let addrKey = allKeys.find(k => k.address === address); if (!addrKey) { throw new Error('UTXO associated key not found in the wallet'); } return [addrKey.key.privateKey]; } /** * Populate a transaction with token authority inputs for specific permissions * * This function finds and adds token authority UTXOs that have the required * permissions for token operations like minting, melting, or creating subgroups. * It automatically handles authority renewal if the authority allows it. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys containing addresses with token authorities * @param token - The token ID to find authorities for * @param perm - The permission type required ('mint', 'melt', 'subgroup', etc.) * @param subgroup - Optional subgroup token ID for subgroup operations * @param subgroupAddr - Optional address to receive subgroup authority * @returns Promise resolving to array of private keys needed for signing * @throws {Error} If the required authority is not found * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await populateTokenAuth( * txBuilder, * keys, * 'token_id_hex', * 'mint' * ); * ``` */ export async function populateTokenAuth(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perm: PermissionLabel, subgroup = '', subgroupAddr = ''): Promise<PrivateKey[]> { let allKeys = keys.receiveKeys.concat(keys.changeKeys); for (let key of allKeys) { let utxos = await rostrumProvider.getTokenUtxos(key.address, token); for (let utxo of utxos) { if (!isAuthFit(utxo.token_amount, perm)) { continue; } txBuilder.from({ outpoint: utxo.outpoint_hash, address: key.address, satoshis: utxo.value }); if (perm === 'subgroup') { txBuilder.to(subgroupAddr, Transaction.DUST_AMOUNT, subgroup, dupAuthority(utxo.token_amount, false)); } // if renew flag included, we don't want to burn it if (GroupToken.allowsRenew(BigInt.asUintN(64, BigInt(utxo.token_amount)))) { txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount)); } return [key.key.privateKey]; } } throw new Error("The requested authority not found"); } /** * Populate a transaction with multiple token authorities and create duplicates * * This function finds token authority UTXOs for multiple permissions and * creates duplicate outputs for each authority. This is useful for complex * token operations that require multiple permissions while preserving the authorities. * * @param txBuilder - The transaction builder to populate * @param keys - Account keys containing addresses with token authorities * @param token - The token ID to find authorities for * @param perms - Array of permission types required * @param toAddr * @returns Promise resolving to array of private keys needed for signing * @throws {Error} If any required authority is not found * * @example * ```typescript * const txBuilder = new TransactionBuilder(); * const keys = await account.getKeys(); * const privateKeys = await populateAndDuplicateTokenAuths( * txBuilder, * keys, * 'token_id_hex', * ['mint', 'melt'] * ); * ``` */ export async function populateAndDuplicateTokenAuths(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perms: PermissionLabel[], toAddr?: string): Promise<PrivateKey[]> { let allKeys = keys.receiveKeys.concat(keys.changeKeys); let usedKeys: PrivateKey[] = []; let reqiredPerms = new Set(perms); reqiredPerms.add('authorise'); for (let key of allKeys) { let utxos = await rostrumProvider.getTokenUtxos(key.address, token); for (let utxo of utxos) { if (utxo.token_amount > 0) { continue; } let found = false; for (let perm of reqiredPerms) { if (isAuthFit(utxo.token_amount, perm)) { reqiredPerms.delete(perm); found = true; } } if (!found) { continue; } txBuilder.from({ outpoint: utxo.outpoint_hash, address: key.address, satoshis: utxo.value }); usedKeys.push(key.key.privateKey); // duplicate txBuilder.to(toAddr != null ? toAddr : keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount)); if (reqiredPerms.size === 0) { return usedKeys; } } } throw new Error("The required authorities not found"); }