@dolaned/wallet-sdk-ts
Version:
Wallet SDK for the Nexa blockchain
276 lines (231 loc) • 9.99 kB
text/typescript
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");
}