nexa-wallet-sdk 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.parcel-cache/3e09f086f3c4d605-AssetGraph +0 -0
  2. package/.parcel-cache/5eac57ec674cdae8-AssetGraph +0 -0
  3. package/.parcel-cache/data.mdb +0 -0
  4. package/.parcel-cache/e43547b6c9167b58-RequestGraph +0 -0
  5. package/.parcel-cache/ecfe15d74834bbfd-BundleGraph +0 -0
  6. package/.parcel-cache/lock.mdb +0 -0
  7. package/.parcel-cache/snapshot-e43547b6c9167b58.txt +2 -0
  8. package/README.md +445 -0
  9. package/dist/browser/index.js +2456 -0
  10. package/dist/browser/index.js.map +1 -0
  11. package/dist/index.d.ts +918 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +2915 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/index.mjs +2456 -0
  16. package/dist/index.mjs.map +1 -0
  17. package/package.json +90 -0
  18. package/spec.md +257 -0
  19. package/src/index.ts +93 -0
  20. package/src/models/rostrum.entities.ts +159 -0
  21. package/src/models/transaction.entities.ts +46 -0
  22. package/src/models/wallet.entities.ts +42 -0
  23. package/src/network/RostrumProvider.ts +137 -0
  24. package/src/types.ts +0 -0
  25. package/src/utils/CommonUtils.ts +123 -0
  26. package/src/utils/TXUtils.ts +445 -0
  27. package/src/utils/TokenUtils.ts +75 -0
  28. package/src/utils/ValidationUtils.ts +86 -0
  29. package/src/utils/WalletUtils.ts +522 -0
  30. package/src/utils/WatchOnlyTXUtils.ts +275 -0
  31. package/src/wallet/Wallet.ts +397 -0
  32. package/src/wallet/WatchOnlyWallet.ts +169 -0
  33. package/src/wallet/accounts/AccountStore.ts +173 -0
  34. package/src/wallet/accounts/interfaces/BaseAccountInterface.ts +56 -0
  35. package/src/wallet/accounts/models/DappAccount.ts +80 -0
  36. package/src/wallet/accounts/models/DefaultAccount.ts +96 -0
  37. package/src/wallet/accounts/models/VaultAccount.ts +81 -0
  38. package/src/wallet/transactions/WalletTransactionCreator.ts +145 -0
  39. package/src/wallet/transactions/WatchOnlyTransactionCreator.ts +189 -0
  40. package/src/wallet/transactions/interfaces/TransactionCreator.ts +438 -0
  41. package/tests/core/tx/transactioncreator.test.ts +455 -0
  42. package/tests/core/tx/wallettransactioncreator.test.ts +362 -0
  43. package/tests/core/tx/watchonlytransactioncreator.test.ts +258 -0
  44. package/tests/core/wallet/accountstore.test.ts +341 -0
  45. package/tests/core/wallet/wallet.test.ts +69 -0
  46. package/tests/core/watchonlywallet/watchonlywallet.test.ts +251 -0
  47. package/tests/index.test.ts +12 -0
  48. package/tsconfig.json +113 -0
@@ -0,0 +1,137 @@
1
+ import {ElectrumClient, SubscribeCallback} from "@vgrunner/electrum-cash";
2
+ import {
3
+ BlockTip,
4
+ IFirstUse,
5
+ IListUnspentRecord,
6
+ ITXHistory,
7
+ ITokensBalance,
8
+ ITokenListUnspent,
9
+ ITransaction,
10
+ IUtxo,
11
+ RostrumParams,
12
+ ITokenGenesis,
13
+ RostrumScheme, RostrumTransportScheme
14
+ } from "../models/rostrum.entities";
15
+ import { Balance } from "../models/wallet.entities";
16
+
17
+ type RPCParameter = string | number | boolean | null;
18
+
19
+ export class RostrumProvider {
20
+
21
+ private client?: ElectrumClient;
22
+
23
+ public constructor() {}
24
+
25
+ public async getVersion() {
26
+ return await this.execute<string[]>('server.version');
27
+ }
28
+
29
+ public async getBlockTip() {
30
+ return await this.execute<BlockTip>('blockchain.headers.tip');
31
+ }
32
+
33
+ public async getBalance(address: string) {
34
+ return await this.execute<Balance>('blockchain.address.get_balance', address, 'exclude_tokens');
35
+ }
36
+
37
+ public async getTransactionsHistory(address: string) {
38
+ return await this.execute<ITXHistory[]>('blockchain.address.get_history', address);
39
+ }
40
+
41
+ public async getFirstUse(address: string) {
42
+ return await this.execute<IFirstUse>('blockchain.address.get_first_use', address);
43
+ }
44
+
45
+ public async getTransaction(id: string, verbose: boolean = true) {
46
+ return await this.execute<ITransaction>('blockchain.transaction.get', id, verbose);
47
+ }
48
+
49
+ public async getUtxo(outpoint: string) {
50
+ return await this.execute<IUtxo>('blockchain.utxo.get', outpoint);
51
+ }
52
+
53
+ public async getNexaUtxos(address: string) {
54
+ return await this.execute<IListUnspentRecord[]>('blockchain.address.listunspent', address, 'exclude_tokens');
55
+ }
56
+
57
+ public async getTokenUtxos(address: string, token: string) {
58
+ let listunspent = await this.execute<ITokenListUnspent>('token.address.listunspent', address, null, token);
59
+ return listunspent.unspent;
60
+ }
61
+
62
+ public async getTokensBalance(address: string, token?: string) {
63
+ if (token) {
64
+ return await this.execute<ITokensBalance>('token.address.get_balance', address, null, token);
65
+ }
66
+ return await this.execute<ITokensBalance>('token.address.get_balance', address);
67
+ }
68
+
69
+ public async getTokenGenesis(token: string) {
70
+ return await this.execute<ITokenGenesis>('token.genesis.info', token);
71
+ }
72
+
73
+ public async subscribeToAddresses(addresses:string[], callback: SubscribeCallback) {
74
+ for (const addr of addresses) {
75
+ await this.client?.subscribe(callback, 'blockchain.address.subscribe', addr)
76
+ }
77
+ }
78
+
79
+ public async broadcast(txHex: string) {
80
+ return await this.execute<string>('blockchain.transaction.broadcast', txHex);
81
+ }
82
+
83
+ public async getLatency() {
84
+ try {
85
+ let start = Date.now();
86
+ let res = await this.getBlockTip();
87
+ if (res) {
88
+ return Date.now() - start;
89
+ }
90
+ return 0;
91
+ } catch {
92
+ return 0;
93
+ }
94
+ }
95
+
96
+ public async connect(params?: RostrumParams) {
97
+ try {
98
+ if (!params) {
99
+ params = {
100
+ host: 'aus.electrum.nexa.onethirtyseven.dev',
101
+ port: 30004,
102
+ scheme: (RostrumScheme.WSS as RostrumTransportScheme)
103
+ }
104
+ // params = await StorageProvider.getRostrumParams();
105
+ }
106
+
107
+ this.client = new ElectrumClient("com.otoplo.wallet", "1.4.3", params.host, params.port, params.scheme, 30*1000, 10*1000, true);
108
+ await this.client.connect();
109
+ } catch (e) {
110
+ if (e instanceof Error) {
111
+ console.info(e.message);
112
+ } else {
113
+ console.error(e);
114
+ }
115
+ throw e;
116
+ }
117
+ }
118
+
119
+ public async disconnect(force?: boolean) {
120
+ try {
121
+ return await this.client!.disconnect(force);
122
+ } catch (e) {
123
+ console.log(e)
124
+ return false;
125
+ }
126
+ }
127
+
128
+ private async execute<T>(method: string, ...parameters: RPCParameter[]) {
129
+ var res = await this.client!.request(method, ...parameters);
130
+ if (res instanceof Error) {
131
+ throw res;
132
+ }
133
+ return res as T;
134
+ }
135
+ }
136
+
137
+ export const rostrumProvider = new RostrumProvider();
package/src/types.ts ADDED
File without changes
@@ -0,0 +1,123 @@
1
+ import bigDecimal from 'js-big-decimal';
2
+ import { Address, CommonUtils } from 'libnexa-ts';
3
+
4
+ /** Maximum value for a 64-bit signed integer */
5
+ export const MAX_INT64: bigint = 9223372036854775807n;
6
+
7
+ /**
8
+ * Get the current Unix timestamp in seconds
9
+ *
10
+ * @returns Current timestamp as number of seconds since Unix epoch
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const now = currentTimestamp();
15
+ * console.log('Current time:', now);
16
+ * ```
17
+ */
18
+ export function currentTimestamp() {
19
+ return Math.floor(Date.now() / 1000);
20
+ }
21
+
22
+ /**
23
+ * Check if a value is null, undefined, or empty
24
+ *
25
+ * @param arg - The value to check
26
+ * @returns true if the value is null, undefined, empty string, or empty array
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * isNullOrEmpty(''); // true
31
+ * isNullOrEmpty([]); // true
32
+ * isNullOrEmpty(null); // true
33
+ * isNullOrEmpty('hello'); // false
34
+ * ```
35
+ */
36
+ export function isNullOrEmpty(arg?: string | any[] | null): arg is undefined | [] | null | '' {
37
+ return !arg || arg.length === 0;
38
+ }
39
+
40
+ /**
41
+ * Parse an amount with decimal places and format it for display
42
+ *
43
+ * Converts a raw amount to a human-readable format by dividing by 10^decimals
44
+ * and removing trailing zeros.
45
+ *
46
+ * @param amount - The raw amount to parse
47
+ * @param decimals - Number of decimal places
48
+ * @returns Formatted amount string
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * parseAmountWithDecimals(1000000, 6); // '1'
53
+ * parseAmountWithDecimals(1500000, 6); // '1.5'
54
+ * parseAmountWithDecimals(1000000n, 6); // '1'
55
+ * ```
56
+ */
57
+ export function parseAmountWithDecimals(amount: string | number | bigint, decimals: number) {
58
+ let val = new bigDecimal(amount).divide(new bigDecimal(Math.pow(10, decimals)), decimals).getPrettyValue();
59
+ if (val.match(/\./)) {
60
+ val = val.replace(/\.?0+$/, '');
61
+ }
62
+ return val;
63
+ }
64
+
65
+ /**
66
+ * Convert a decimal amount to raw integer format
67
+ *
68
+ * Multiplies the amount by 10^decimals to get the raw integer representation
69
+ * used in transactions.
70
+ *
71
+ * @param amount - The decimal amount to convert
72
+ * @param decimals - Number of decimal places
73
+ * @returns Raw amount as string
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * getRawAmount('1.5', 6); // '1500000'
78
+ * getRawAmount(1.5, 6); // '1500000'
79
+ * ```
80
+ */
81
+ export function getRawAmount(amount: string | number | bigint, decimals: number) {
82
+ return new bigDecimal(amount).multiply(new bigDecimal(Math.pow(10, decimals))).getValue();
83
+ }
84
+
85
+ /**
86
+ * Get the buffer representation of an address
87
+ *
88
+ * @param address - The address string (hex or Nexa address format)
89
+ * @returns Buffer containing the address data
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const buffer = getAddressBuffer('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc');
94
+ * const hexBuffer = getAddressBuffer('a1b2c3d4e5f6');
95
+ * ```
96
+ */
97
+ export function getAddressBuffer(address: string) {
98
+ if (CommonUtils.isHexa(address)) {
99
+ return Buffer.from(address, 'hex') ;
100
+ }
101
+ return Address.fromString(address).data;
102
+ }
103
+
104
+ /**
105
+ * Convert a token ID to hex format
106
+ *
107
+ * @param token - The token ID (hex string or Nexa address format)
108
+ * @returns Token ID in hex format
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const hexId = tokenIdToHex('nexatest:tq8r37lcjlqazz7vuvug84q2ev50573hesrnxkv9y6hvhhl5k5qqqnmyf79mx');
113
+ * const hexId2 = tokenIdToHex('a1b2c3d4e5f6'); // already hex, returns as-is
114
+ * ```
115
+ */
116
+ export function tokenIdToHex(token: string) {
117
+ if (CommonUtils.isHexa(token)) {
118
+ return token;
119
+ }
120
+ return getAddressBuffer(token).toString('hex');
121
+ }
122
+
123
+
@@ -0,0 +1,445 @@
1
+ import {rostrumProvider} from '../network/RostrumProvider';
2
+ import {AccountKeys} from '../models/wallet.entities';
3
+ import {isNullOrEmpty, MAX_INT64, tokenIdToHex} from './CommonUtils';
4
+ import {
5
+ Address,
6
+ AddressType,
7
+ GroupToken,
8
+ Networkish,
9
+ PrivateKey,
10
+ Transaction,
11
+ TransactionBuilder,
12
+ UnitUtils,
13
+ UTXO
14
+ } from "libnexa-ts";
15
+ import {PermissionLabel, TxOptions} from "../models/transaction.entities";
16
+ import {dupAuthority, isAuthFit} from "./TokenUtils";
17
+
18
+ /** Maximum number of inputs/outputs allowed in a single transaction */
19
+ const MAX_INPUTS_OUTPUTS = 250;
20
+
21
+ /**
22
+ * Populate a transaction with Nexa UTXO inputs and set change output if needed
23
+ *
24
+ * This function automatically selects and adds UTXOs from the provided account keys
25
+ * to satisfy the transaction's Nexa amount requirements. It handles change calculation
26
+ * and can consolidate UTXOs when requested.
27
+ *
28
+ * @param txBuilder - The transaction builder to populate
29
+ * @param keys - Account keys containing receive and change addresses with balances
30
+ * @param totalTxValue - Total amount of Nexa required for the transaction (in satoshis)
31
+ * @param options - Transaction options including consolidation settings and change address
32
+ * @returns Promise resolving to array of private keys needed for signing
33
+ * @throws {Error} If insufficient balance or too many inputs required
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const txBuilder = new TransactionBuilder();
38
+ * const keys = await account.getKeys();
39
+ * const privateKeys = await populateNexaInputsAndChange(
40
+ * txBuilder,
41
+ * keys,
42
+ * 1000000n, // 1 NEXA
43
+ * { isConsolidate: false, feeFromAmount: false }
44
+ * );
45
+ * ```
46
+ */
47
+ export async function populateNexaInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, totalTxValue: bigint, options: TxOptions): Promise<PrivateKey[]> {
48
+ let rKeys = keys.receiveKeys.filter(k => BigInt(k.balance) > 0n);
49
+ let cKeys = keys.changeKeys.filter(k => BigInt(k.balance) > 0n);
50
+ let allKeys = rKeys.concat(cKeys);
51
+ if (isNullOrEmpty(allKeys)) {
52
+ throw new Error("Not enough Nexa balance.");
53
+ }
54
+
55
+ let usedKeys = new Map<string, PrivateKey>();
56
+ let origAmount = options.isConsolidate ? 0 : Number(totalTxValue);
57
+
58
+ for (let key of allKeys) {
59
+ let utxos = await rostrumProvider.getNexaUtxos(key.address);
60
+ for (let utxo of utxos) {
61
+ let input: UTXO = {
62
+ outpoint: utxo.outpoint_hash,
63
+ address: key.address,
64
+ satoshis: utxo.value,
65
+ templateData: options.templateData
66
+ }
67
+ txBuilder.from(input);
68
+
69
+ if (!usedKeys.has(key.address)) {
70
+ usedKeys.set(key.address, key.key.privateKey);
71
+ }
72
+
73
+ if (options.isConsolidate) {
74
+ txBuilder.change(options.toChange ?? keys.receiveKeys[keys.receiveKeys.length - 1].address);
75
+ if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
76
+ return Array.from(usedKeys.values());
77
+ }
78
+ } else {
79
+ let tx = txBuilder.transaction;
80
+ if (tx.inputs.length > MAX_INPUTS_OUTPUTS) {
81
+ throw new Error("Too many inputs. Consider consolidate transactions or reduce the send amount.");
82
+ }
83
+
84
+ let unspent = tx.getUnspentValue();
85
+ if (unspent < 0n) {
86
+ continue;
87
+ }
88
+
89
+ if (unspent == 0n && options.feeFromAmount) {
90
+ let txFee = tx.estimateRequiredFee();
91
+ tx.updateOutputAmount(0, origAmount - txFee);
92
+ return Array.from(usedKeys.values());
93
+ }
94
+
95
+ txBuilder.change(options.toChange ?? keys.changeKeys[keys.changeKeys.length - 1].address);
96
+ if (options.feeFromAmount) {
97
+ let hasChange = tx.getChangeOutput();
98
+ let txFee = tx.estimateRequiredFee();
99
+ tx.updateOutputAmount(0, origAmount - txFee);
100
+
101
+ // edge case where change added after update
102
+ if (!hasChange && tx.getChangeOutput()) {
103
+ txFee = tx.estimateRequiredFee();
104
+ tx.updateOutputAmount(0, origAmount - txFee);
105
+ }
106
+ }
107
+
108
+ // check again after change output manipulation
109
+ if (tx.getUnspentValue() < tx.estimateRequiredFee()) {
110
+ // try to add more utxos to satisfy the minimum fee
111
+ continue;
112
+ }
113
+ return Array.from(usedKeys.values());
114
+ }
115
+ }
116
+ }
117
+
118
+ if (options.isConsolidate) {
119
+ if (usedKeys.size > 0) {
120
+ return Array.from(usedKeys.values());
121
+ }
122
+ throw new Error("Not enough Nexa balance.");
123
+ }
124
+
125
+ let err = {
126
+ errorMsg: "Not enough Nexa balance.",
127
+ amount: UnitUtils.formatNEXA(txBuilder.transaction.outputs[0].value),
128
+ fee: UnitUtils.formatNEXA(txBuilder.transaction.estimateRequiredFee())
129
+ }
130
+
131
+ throw new Error(JSON.stringify(err));
132
+ }
133
+
134
+ /**
135
+ * Populate a transaction with token UTXO inputs and set token change output if needed
136
+ *
137
+ * This function selects and adds token UTXOs from the provided account keys
138
+ * to satisfy the transaction's token amount requirements. It automatically
139
+ * handles token change calculation.
140
+ *
141
+ * @param txBuilder - The transaction builder to populate
142
+ * @param keys - Account keys containing addresses with token balances
143
+ * @param token - The token ID to spend
144
+ * @param outTokenAmount - Amount of tokens required for the transaction
145
+ * @returns Promise resolving to array of private keys needed for signing
146
+ * @throws {Error} If insufficient token balance or too many inputs required
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * const txBuilder = new TransactionBuilder();
151
+ * const keys = await account.getKeys();
152
+ * const privateKeys = await populateTokenInputsAndChange(
153
+ * txBuilder,
154
+ * keys,
155
+ * 'token_id_hex',
156
+ * 1000n // Amount of tokens
157
+ * );
158
+ * ```
159
+ */
160
+ export async function populateTokenInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, outTokenAmount: bigint): Promise<PrivateKey[]> {
161
+ let tokenHex = tokenIdToHex(token);
162
+ let rKeys = keys.receiveKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex));
163
+ let cKeys = keys.changeKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex));
164
+ let allKeys = rKeys.concat(cKeys);
165
+
166
+ if (isNullOrEmpty(allKeys)) {
167
+ throw new Error("Not enough token balance.");
168
+ }
169
+
170
+ let usedKeys = new Map<string, PrivateKey>();
171
+ let inTokenAmount = 0n;
172
+
173
+ for (let key of allKeys) {
174
+ let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
175
+ for (let utxo of utxos) {
176
+ if (utxo.token_amount < 0) {
177
+ continue;
178
+ }
179
+ txBuilder.from({
180
+ outpoint: utxo.outpoint_hash,
181
+ address: key.address,
182
+ satoshis: utxo.value,
183
+ groupId: utxo.group,
184
+ groupAmount: BigInt(utxo.token_amount),
185
+ });
186
+
187
+ inTokenAmount = inTokenAmount + BigInt(utxo.token_amount);
188
+ if (!usedKeys.has(key.address)) {
189
+ usedKeys.set(key.address, key.key.privateKey);
190
+ }
191
+
192
+ if (inTokenAmount > MAX_INT64) {
193
+ throw new Error("Token inputs exceeded max amount. Consider sending in small chunks");
194
+ }
195
+ if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
196
+ throw new Error("Too many inputs. Consider consolidating transactions or reduce the send amount.");
197
+ }
198
+
199
+ if (inTokenAmount == outTokenAmount) {
200
+ return Array.from(usedKeys.values());
201
+ }
202
+ if (inTokenAmount > outTokenAmount) {
203
+ // change
204
+ txBuilder.to(keys.changeKeys[keys.changeKeys.length - 1].address, Transaction.DUST_AMOUNT, token, inTokenAmount - outTokenAmount);
205
+ return Array.from(usedKeys.values());
206
+ }
207
+ }
208
+ }
209
+
210
+ throw new Error("Not enough token balance");
211
+ }
212
+
213
+ /**
214
+ * Build a transaction to create a new token group
215
+ *
216
+ * This function creates a group token by using the first UTXO's outpoint
217
+ * to generate a unique group ID. The group token represents the authority
218
+ * to create fungible tokens within this group.
219
+ *
220
+ * @param txBuilder - The transaction builder to populate
221
+ * @param keys - Account keys to use for funding the transaction
222
+ * @param opReturnData - Optional data to include in the group creation
223
+ * @param network - Network to create the group on
224
+ * @returns Promise resolving to array of private keys needed for signing
225
+ * @throws {Error} If insufficient balance for group creation
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const txBuilder = new TransactionBuilder();
230
+ * const keys = await account.getKeys();
231
+ * const privateKeys = await buildCreateGroupTransaction(
232
+ * txBuilder,
233
+ * keys,
234
+ * 'my_token_data',
235
+ * Networks.mainnet
236
+ * );
237
+ * ```
238
+ */
239
+ export async function buildCreateGroupTransaction(
240
+ txBuilder: TransactionBuilder,
241
+ keys: AccountKeys,
242
+ opReturnData: string,
243
+ network: Networkish
244
+ ): Promise<PrivateKey[]> {
245
+ // TODO validate opreturn data
246
+ const allKeys = keys.receiveKeys.concat(keys.changeKeys)
247
+ let outpoint = '';
248
+ let usedKeys: PrivateKey[] = [];
249
+ let signKey: PrivateKey | undefined = undefined;
250
+ for (let key of allKeys) {
251
+ let utxos = await rostrumProvider.getNexaUtxos(key.address);
252
+ for (let utxo of utxos) {
253
+ txBuilder.from({
254
+ outpoint: utxo.outpoint_hash,
255
+ address: key.address,
256
+ satoshis: utxo.value
257
+ });
258
+
259
+ if (isNullOrEmpty(outpoint)) {
260
+ outpoint = utxo.outpoint_hash;
261
+ let id = GroupToken.findGroupId(Buffer.from(outpoint, 'hex'), Buffer.from(opReturnData, 'hex'), GroupToken.authFlags.ACTIVE_FLAG_BITS);
262
+ const groupId = new Address(id.hashBuffer, network, AddressType.GroupIdAddress).toString()
263
+ txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, groupId, GroupToken.authFlags.ACTIVE_FLAG_BITS | id.nonce)
264
+ signKey = key.key.privateKey
265
+ usedKeys.push(signKey);
266
+ return usedKeys
267
+ }
268
+ }
269
+ }
270
+
271
+ throw new Error("Not enough Nexa balance.");
272
+ }
273
+
274
+ /**
275
+ * Prepare a transaction to delete/spend a specific UTXO
276
+ *
277
+ * This function adds a specific UTXO as input to the transaction.
278
+ * It's commonly used for spending token authority UTXOs or consolidating specific outputs.
279
+ *
280
+ * @param txBuilder - The transaction builder to populate
281
+ * @param keys - Account keys to find the private key for the UTXO
282
+ * @param outpoint - The outpoint (txid:vout) of the UTXO to spend
283
+ * @returns Promise resolving to array containing the private key for the UTXO
284
+ * @throws {Error} If the UTXO is not found or the associated key is not in the wallet
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * const txBuilder = new TransactionBuilder();
289
+ * const keys = await account.getKeys();
290
+ * const privateKeys = await prepareDeleteTransaction(
291
+ * txBuilder,
292
+ * keys,
293
+ * 'txid:0'
294
+ * );
295
+ * ```
296
+ */
297
+ export async function prepareDeleteTransaction(txBuilder: TransactionBuilder, keys: AccountKeys, outpoint: string): Promise<PrivateKey[]> {
298
+ let utxo = await rostrumProvider.getUtxo(outpoint);
299
+ let address = utxo.addresses[0];
300
+
301
+ txBuilder.from({
302
+ outpoint: outpoint,
303
+ address: address,
304
+ satoshis: utxo.amount
305
+ });
306
+
307
+ let allKeys = keys.receiveKeys.concat(keys.changeKeys);
308
+ let addrKey = allKeys.find(k => k.address === address);
309
+
310
+ if (!addrKey) {
311
+ throw new Error('UTXO associated key not found in the wallet');
312
+ }
313
+ return [addrKey.key.privateKey];
314
+ }
315
+
316
+ /**
317
+ * Populate a transaction with token authority inputs for specific permissions
318
+ *
319
+ * This function finds and adds token authority UTXOs that have the required
320
+ * permissions for token operations like minting, melting, or creating subgroups.
321
+ * It automatically handles authority renewal if the authority allows it.
322
+ *
323
+ * @param txBuilder - The transaction builder to populate
324
+ * @param keys - Account keys containing addresses with token authorities
325
+ * @param token - The token ID to find authorities for
326
+ * @param perm - The permission type required ('mint', 'melt', 'subgroup', etc.)
327
+ * @param subgroup - Optional subgroup token ID for subgroup operations
328
+ * @param subgroupAddr - Optional address to receive subgroup authority
329
+ * @returns Promise resolving to array of private keys needed for signing
330
+ * @throws {Error} If the required authority is not found
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const txBuilder = new TransactionBuilder();
335
+ * const keys = await account.getKeys();
336
+ * const privateKeys = await populateTokenAuth(
337
+ * txBuilder,
338
+ * keys,
339
+ * 'token_id_hex',
340
+ * 'mint'
341
+ * );
342
+ * ```
343
+ */
344
+ export async function populateTokenAuth(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perm: PermissionLabel, subgroup = '', subgroupAddr = ''): Promise<PrivateKey[]> {
345
+ let allKeys = keys.receiveKeys.concat(keys.changeKeys);
346
+ for (let key of allKeys) {
347
+ let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
348
+ for (let utxo of utxos) {
349
+ if (!isAuthFit(utxo.token_amount, perm)) {
350
+ continue;
351
+ }
352
+
353
+ txBuilder.from({
354
+ outpoint: utxo.outpoint_hash,
355
+ address: key.address,
356
+ satoshis: utxo.value
357
+ });
358
+
359
+ if (perm === 'subgroup') {
360
+ txBuilder.to(subgroupAddr, Transaction.DUST_AMOUNT, subgroup, dupAuthority(utxo.token_amount, false));
361
+ }
362
+
363
+ // if renew flag included, we don't want to burn it
364
+ if (GroupToken.allowsRenew(BigInt.asUintN(64, BigInt(utxo.token_amount)))) {
365
+ txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));
366
+ }
367
+
368
+ return [key.key.privateKey];
369
+ }
370
+ }
371
+
372
+ throw new Error("The requested authority not found");
373
+ }
374
+
375
+ /**
376
+ * Populate a transaction with multiple token authorities and create duplicates
377
+ *
378
+ * This function finds token authority UTXOs for multiple permissions and
379
+ * creates duplicate outputs for each authority. This is useful for complex
380
+ * token operations that require multiple permissions while preserving the authorities.
381
+ *
382
+ * @param txBuilder - The transaction builder to populate
383
+ * @param keys - Account keys containing addresses with token authorities
384
+ * @param token - The token ID to find authorities for
385
+ * @param perms - Array of permission types required
386
+ * @param toAddr
387
+ * @returns Promise resolving to array of private keys needed for signing
388
+ * @throws {Error} If any required authority is not found
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * const txBuilder = new TransactionBuilder();
393
+ * const keys = await account.getKeys();
394
+ * const privateKeys = await populateAndDuplicateTokenAuths(
395
+ * txBuilder,
396
+ * keys,
397
+ * 'token_id_hex',
398
+ * ['mint', 'melt']
399
+ * );
400
+ * ```
401
+ */
402
+ export async function populateAndDuplicateTokenAuths(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perms: PermissionLabel[], toAddr?: string): Promise<PrivateKey[]> {
403
+ let allKeys = keys.receiveKeys.concat(keys.changeKeys);
404
+ let usedKeys: PrivateKey[] = [];
405
+
406
+ let reqiredPerms = new Set(perms);
407
+ reqiredPerms.add('authorise');
408
+
409
+ for (let key of allKeys) {
410
+ let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
411
+ for (let utxo of utxos) {
412
+ if (utxo.token_amount > 0) {
413
+ continue;
414
+ }
415
+
416
+ let found = false;
417
+ for (let perm of reqiredPerms) {
418
+ if (isAuthFit(utxo.token_amount, perm)) {
419
+ reqiredPerms.delete(perm);
420
+ found = true;
421
+ }
422
+ }
423
+
424
+ if (!found) {
425
+ continue;
426
+ }
427
+
428
+ txBuilder.from({
429
+ outpoint: utxo.outpoint_hash,
430
+ address: key.address,
431
+ satoshis: utxo.value
432
+ });
433
+ usedKeys.push(key.key.privateKey);
434
+
435
+ // duplicate
436
+ txBuilder.to(toAddr != null ? toAddr : keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));
437
+
438
+ if (reqiredPerms.size === 0) {
439
+ return usedKeys;
440
+ }
441
+ }
442
+ }
443
+
444
+ throw new Error("The required authorities not found");
445
+ }