UNPKG

@dolaned/wallet-sdk-ts

Version:

Wallet SDK for the Nexa blockchain

276 lines (231 loc) 9.99 kB
import { rostrumProvider } from '../network/RostrumProvider'; import { WatchOnlyAddress } from '../models/wallet.entities'; import { MAX_INT64, isNullOrEmpty } from './CommonUtils'; import { Address, AddressType, GroupToken, Networkish, Transaction, TransactionBuilder, UnitUtils, UTXO } from "libnexa-ts"; import { PermissionLabel, TxOptions } from "../models/transaction.entities"; import {dupAuthority, isAuthFit} from "./TokenUtils"; const MAX_INPUTS_OUTPUTS = 250; export async function watchOnlyPopulateNexaInputsAndChange(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], totalTxValue: bigint, options: TxOptions): Promise<string[]> { if (isNullOrEmpty(addresses)) { throw new Error("Not enough Nexa balance."); } let usedAddresses = new Set<string>(); let origAmount = options.isConsolidate ? 0 : Number(totalTxValue); for (let item of addresses) { let utxos = await rostrumProvider.getNexaUtxos(item.address); for (let utxo of utxos) { let input: UTXO = { outpoint: utxo.outpoint_hash, address: item.address, satoshis: utxo.value, templateData: options.templateData } txBuilder.from(input); if (!usedAddresses.has(item.address)) { usedAddresses.add(item.address); } if (options.isConsolidate) { // need to handle change txBuilder.change(options.toChange ?? item.address); if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) { return Array.from(usedAddresses.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(usedAddresses.values()); } txBuilder.change(options.toChange ?? item.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(usedAddresses.values()); } } } if (options.isConsolidate) { if (usedAddresses.size > 0) { return Array.from(usedAddresses.values()); } throw new Error("Not enough Nexa balance."); } let err = { errorMsg: "Not enough Nexa balance.", amount: UnitUtils.formatNEXA(Number(totalTxValue)), fee: UnitUtils.formatNEXA(txBuilder.transaction.estimateRequiredFee()) } throw new Error(JSON.stringify(err)); } export async function watchOnlyPopulateTokenInputsAndChange(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, outTokenAmount: bigint): Promise<string[]> { if (isNullOrEmpty(addresses)) { throw new Error("Not enough token balance."); } let usedKeys = new Set<string>(); let inTokenAmount = 0n; for (let item of addresses) { let utxos = await rostrumProvider.getTokenUtxos(item.address, token); for (let utxo of utxos) { if (utxo.token_amount < 0) { continue; } txBuilder.from({ outpoint: utxo.outpoint_hash, address: item.address, satoshis: utxo.value, groupId: utxo.group, groupAmount: BigInt(utxo.token_amount), }); inTokenAmount = inTokenAmount + BigInt(utxo.token_amount); if (!usedKeys.has(item.address)) { usedKeys.add(item.address); } 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(item.address, Transaction.DUST_AMOUNT, token, inTokenAmount - outTokenAmount); return Array.from(usedKeys.values()); } } } throw new Error("Not enough token balance"); } export async function watchOnlyBuildCreateGroupTransaction( txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], opReturnData: string, network: Networkish ): Promise<string[]> { let outpoint = '', idHex = ''; let usedKeys: string[] = []; for (let item of addresses) { let utxos = await rostrumProvider.getNexaUtxos(item.address); for (let utxo of utxos) { txBuilder.from({ outpoint: utxo.outpoint_hash, address: item.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(item.address, Transaction.DUST_AMOUNT, groupId, GroupToken.authFlags.ACTIVE_FLAG_BITS | id.nonce) idHex = id.hashBuffer.toString('hex'); usedKeys.push(item.address); return usedKeys } } } throw new Error("Not enough Nexa balance."); } export async function watchOnlyPrepareDeleteTransaction(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], outpoint: string): Promise<string[]> { let utxo = await rostrumProvider.getUtxo(outpoint); let address = utxo.addresses[0]; txBuilder.from({ outpoint: outpoint, address: address, satoshis: utxo.amount }); let addrKey = addresses.find(k => k.address === address); if (!addrKey) { throw new Error('UTXO associated key not found in the wallet'); } return [addrKey.address]; } export async function watchOnlyPopulateTokenAuth(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, perm: PermissionLabel, subgroup = ''): Promise<string[]> { for (let item of addresses) { let utxos = await rostrumProvider.getTokenUtxos(item.address, token); for (let utxo of utxos) { if (!isAuthFit(utxo.token_amount, perm)) { continue; } txBuilder.from({ outpoint: utxo.outpoint_hash, address: item.address, satoshis: utxo.value }); if (perm === 'subgroup') { txBuilder.to(item.address, 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(item.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount)); } return [item.address]; } } throw new Error("The requested authority not found"); } export async function watchOnlyPopulateAndDuplicateTokenAuths(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, perms: PermissionLabel[], toAddr?: string): Promise<string[]> { let usedAddresses: string[] = []; let reqiredPerms = new Set(perms); reqiredPerms.add('authorise'); for (let item of addresses) { let utxos = await rostrumProvider.getTokenUtxos(item.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: item.address, satoshis: utxo.value }); usedAddresses.push(item.address); // duplicate txBuilder.to(toAddr != null ? toAddr : item.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount)); if (reqiredPerms.size === 0) { return usedAddresses; } } } throw new Error("The required authorities not found"); }