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,75 @@
1
+ import {PermissionLabel} from "../models/transaction.entities";
2
+ import {GroupToken} from "libnexa-ts";
3
+
4
+ export function isAuthFit(authFlags: bigint | number, permission: PermissionLabel): boolean {
5
+ if (authFlags > 0) {
6
+ return false;
7
+ }
8
+
9
+ let flags = BigInt.asUintN(64, BigInt(authFlags));
10
+ switch (permission) {
11
+ case 'authorise':
12
+ return GroupToken.allowsRenew(flags);
13
+ case 'mint':
14
+ return GroupToken.allowsMint(flags);
15
+ case 'melt':
16
+ return GroupToken.allowsMelt(flags);
17
+ case 'rescript':
18
+ return GroupToken.allowsRescript(flags);
19
+ case 'subgroup':
20
+ return GroupToken.allowsSubgroup(flags);
21
+ default:
22
+ return false;
23
+ }
24
+ }
25
+
26
+ export function dupAuthority(authFlags: bigint | number, withSubgroup = true): bigint {
27
+ if (authFlags > 0) {
28
+ return 0n;
29
+ }
30
+
31
+ let flags = BigInt.asUintN(64, BigInt(authFlags));
32
+ let newFlags = GroupToken.authFlags.AUTHORITY;
33
+
34
+ if (GroupToken.allowsRenew(flags)) {
35
+ newFlags |= GroupToken.authFlags.BATON;
36
+ }
37
+ if (GroupToken.allowsMint(flags)) {
38
+ newFlags |= GroupToken.authFlags.MINT;
39
+ }
40
+ if (GroupToken.allowsMelt(flags)) {
41
+ newFlags |= GroupToken.authFlags.MELT;
42
+ }
43
+ if (GroupToken.allowsRescript(flags)) {
44
+ newFlags |= GroupToken.authFlags.RESCRIPT;
45
+ }
46
+ if (GroupToken.allowsSubgroup(flags) && withSubgroup) {
47
+ newFlags |= GroupToken.authFlags.SUBGROUP;
48
+ }
49
+
50
+ return newFlags;
51
+ }
52
+
53
+ export function buildAuthority(perms: PermissionLabel[]): bigint {
54
+ let newFlags = GroupToken.authFlags.AUTHORITY;
55
+ for (let perm of perms) {
56
+ switch (perm) {
57
+ case 'authorise':
58
+ newFlags |= GroupToken.authFlags.BATON;
59
+ break;
60
+ case 'mint':
61
+ newFlags |= GroupToken.authFlags.MINT;
62
+ break;
63
+ case 'melt':
64
+ newFlags |= GroupToken.authFlags.MELT;
65
+ break;
66
+ case 'rescript':
67
+ newFlags |= GroupToken.authFlags.RESCRIPT;
68
+ break;
69
+ case 'subgroup':
70
+ newFlags |= GroupToken.authFlags.SUBGROUP;
71
+ break;
72
+ }
73
+ }
74
+ return newFlags;
75
+ }
@@ -0,0 +1,86 @@
1
+ import { isString } from 'lodash-es';
2
+
3
+ /** Type definition for constructor functions */
4
+ type Class<T> = new (...args: any[]) => T; // eslint-disable-line @typescript-eslint/no-explicit-any
5
+
6
+ /**
7
+ * Utility class for validating arguments and state conditions
8
+ *
9
+ * Provides methods to validate function arguments and application state
10
+ * with consistent error handling and messaging.
11
+ */
12
+ export default class ValidationUtils {
13
+
14
+ /**
15
+ * Validate that a state condition is true
16
+ *
17
+ * @param condition - The condition to validate
18
+ * @param message - Error message to throw if condition is false
19
+ * @throws {Error} If condition is false
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * ValidationUtils.validateState(wallet.isInitialized, 'Wallet must be initialized');
24
+ * ```
25
+ */
26
+ public static validateState(condition: boolean, message: string): void {
27
+ if (!condition) {
28
+ throw new Error(`Invalid State: ${message}`);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Validate that an argument condition is true
34
+ *
35
+ * @param condition - The condition to validate
36
+ * @param argumentName - Name of the argument being validated
37
+ * @param message - Optional additional error message
38
+ * @throws {Error} If condition is false
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * ValidationUtils.validateArgument(
43
+ * typeof amount === 'number',
44
+ * 'amount',
45
+ * 'must be a number'
46
+ * );
47
+ * ```
48
+ */
49
+ public static validateArgument(condition: boolean, argumentName: string, message = ""): void {
50
+ if (!condition) {
51
+ throw new Error(`Invalid Argument: ${argumentName}. ${message}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate that an argument is of the expected type
57
+ *
58
+ * @param argument - The argument to validate
59
+ * @param type - Expected type (string name or constructor function)
60
+ * @param argumentName - Name of the argument being validated
61
+ * @throws {TypeError} If argument is not of expected type
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * ValidationUtils.validateArgumentType(buffer, 'Buffer', 'data');
66
+ * ValidationUtils.validateArgumentType(wallet, Wallet, 'wallet');
67
+ * ValidationUtils.validateArgumentType(amount, 'number', 'amount');
68
+ * ```
69
+ */
70
+ public static validateArgumentType<T>(argument: unknown, type: string | Class<T>, argumentName?: string): void {
71
+ argumentName = argumentName || '(unknown name)';
72
+ if (isString(type)) {
73
+ if (type === 'Buffer') {
74
+ if (!Buffer.isBuffer(argument)) {
75
+ throw new TypeError(`Invalid Argument for ${argumentName}, expected ${type} but got ${typeof argument}`);
76
+ }
77
+ } else if (typeof argument !== type) {
78
+ throw new TypeError(`Invalid Argument for ${argumentName}, expected ${type} but got ${typeof argument}`);
79
+ }
80
+ } else {
81
+ if (!(argument instanceof type)) {
82
+ throw new TypeError(`Invalid Argument for ${argumentName}, expected ${type} but got ${typeof argument}`);
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,522 @@
1
+ import {rostrumProvider} from "../network/RostrumProvider";
2
+ import {AccountIndexes, AccountKeys, AddressKey, Balance} from "../models/wallet.entities";
3
+ import * as Bip39 from 'bip39';
4
+ import bigDecimal from "js-big-decimal";
5
+ import {ITXHistory, ITXInput, ITXOutput} from "../models/rostrum.entities";
6
+ import {Address, AddressType, HDPrivateKey, Networkish} from "libnexa-ts";
7
+
8
+ import {isNil} from "lodash-es";
9
+ import {BaseAccount} from "../wallet/accounts/interfaces/BaseAccountInterface";
10
+ import DAppAccount from "../wallet/accounts/models/DappAccount";
11
+ import DefaultAccount from "../wallet/accounts/models/DefaultAccount";
12
+ import VaultAccount from "../wallet/accounts/models/VaultAccount";
13
+ import {TransactionEntity, TxEntityState} from "../models/transaction.entities";
14
+ import {currentTimestamp, isNullOrEmpty} from "./CommonUtils";
15
+
16
+ export enum TxTokenType {
17
+ NO_GROUP,
18
+ CREATE,
19
+ MINT,
20
+ MELT,
21
+ RENEW,
22
+ TRANSFER
23
+ }
24
+
25
+ export enum AccountType {
26
+ NEXA_ACCOUNT,
27
+ VAULT_ACCOUNT,
28
+ DAPP_ACCOUNT,
29
+ }
30
+
31
+ export function isValidNexaAddress(address: string, network: Networkish, type = AddressType.PayToScriptTemplate) {
32
+ return Address.isValid(address, network, type);
33
+ }
34
+
35
+ export function generateMasterKey(mnemonic: string, passphrase?: string | undefined) {
36
+ const seed = Bip39.mnemonicToSeedSync(mnemonic, passphrase);
37
+ const masterKey = HDPrivateKey.fromSeed(seed);
38
+ return masterKey.deriveChild(44, true).deriveChild(29223, true);
39
+ }
40
+
41
+ export function generateAccountKey(masterKey: HDPrivateKey, account: number) {
42
+ return masterKey.deriveChild(account, true);
43
+ }
44
+
45
+ export function generateKeysAndAddresses(accountKey: HDPrivateKey, fromRIndex: number, rIndex: number, fromCIndex: number, cIndex: number): AccountKeys {
46
+ if (fromRIndex < 0) {
47
+ throw new Error(`Can not generate keys with fromRIndex ${fromRIndex}. must be >= 0.`);
48
+ }
49
+ if (fromCIndex < 0) {
50
+ throw new Error(`Can not generate keys with fromCIndex ${fromCIndex}. must be >= 0.`);
51
+ }
52
+ let receive = accountKey.deriveChild(0, false);
53
+ let change = accountKey.deriveChild(1, false);
54
+ let rKeys: AddressKey[] = [], cKeys: AddressKey[] = [];
55
+ for (let index = fromRIndex; index < rIndex; index++) {
56
+ let k = receive.deriveChild(index, false);
57
+ let addr = k.privateKey.toAddress().toString();
58
+ rKeys.push({key: k, address: addr, balance: "0", tokensBalance: {}});
59
+ }
60
+ for (let index = fromCIndex; index < cIndex; index++) {
61
+ let k = change.deriveChild(index, false);
62
+ let addr = k.privateKey.toAddress().toString();
63
+ cKeys.push({key: k, address: addr, balance: "0", tokensBalance: {}});
64
+ }
65
+ return {receiveKeys: rKeys, changeKeys: cKeys};
66
+ }
67
+
68
+ export function generateKeyAndAddress(accountKey: HDPrivateKey, rIndex: number): AddressKey {
69
+ let receive = accountKey.deriveChild(0, false);
70
+ let k = receive.deriveChild(rIndex, false);
71
+ let addr = k.privateKey.toAddress().toString();
72
+ return ({key: k, address: addr, balance: "0", tokensBalance: {}});
73
+ }
74
+
75
+ async function discoverUsedAccountIndexes(deriveKey: HDPrivateKey) {
76
+ let lastUsed = -1, index = 0, toScan = 20;
77
+
78
+ while (toScan > 0) {
79
+ toScan--;
80
+ let rAddr = deriveKey.deriveChild(index, false).privateKey.toAddress().toString();
81
+ let isUsed = await isAddressUsed(rAddr);
82
+ if (isUsed) {
83
+ lastUsed = index;
84
+ toScan = 20;
85
+ }
86
+ index++;
87
+ }
88
+ // return the last used index, returns -1 if no indexes are used
89
+ return lastUsed;
90
+ }
91
+
92
+ export async function discoverNexaAccount(accountKey: HDPrivateKey) {
93
+
94
+ let receiveKey = accountKey.deriveChild(0, false);
95
+ let changeKey = accountKey.deriveChild(1, false);
96
+
97
+ let rIndexPromise = discoverUsedAccountIndexes(receiveKey);
98
+ let cIndexPromise = discoverUsedAccountIndexes(changeKey);
99
+
100
+ let [rIndex, cIndex] = await Promise.all([rIndexPromise, cIndexPromise]);
101
+
102
+ // get the index that is the last used nexa addr
103
+ let indexes: AccountIndexes = { rIndex: rIndex, cIndex: cIndex };
104
+
105
+ return indexes;
106
+ }
107
+
108
+ export async function discoverNexaAccounts(masterKey: HDPrivateKey) {
109
+ let accounts: DefaultAccount[] = [];
110
+
111
+ let index = 0;
112
+ while (true) {
113
+ const nexaAccountKey = generateAccountKey(masterKey, index);
114
+ const indexes = await discoverNexaAccount(nexaAccountKey);
115
+ if (indexes.rIndex < 0 && indexes.cIndex < 0)
116
+ {
117
+ break;
118
+ }
119
+ if (indexes.rIndex < 0) {
120
+ indexes.rIndex = 0;
121
+ }
122
+ if (indexes.cIndex < 0) {
123
+ indexes.cIndex = 0;
124
+ }
125
+ // make account after break check, otherwise we might push an empty account
126
+ const nexaAccount = new DefaultAccount(index, indexes, generateKeysAndAddresses(nexaAccountKey, indexes.rIndex + 1, indexes.rIndex + 20, indexes.cIndex + 1, indexes.cIndex + 20))
127
+ await nexaAccount.loadBalances()
128
+ accounts.push(nexaAccount);
129
+ if (index == 0) {
130
+ index = 100;
131
+ }
132
+ else {
133
+ index++;
134
+ }
135
+ }
136
+ if (accounts.length == 0) {
137
+ // default account was unused but we need to populate at least one account,
138
+ // make the default account here
139
+ let defaultNexaAccountKey = generateAccountKey(masterKey, 0);
140
+ // get the last used indexes, -1 means unused
141
+ let defaultIndexes: AccountIndexes = { rIndex: 0, cIndex: 0 };
142
+ const defaultAccount = new DefaultAccount(0, defaultIndexes, generateKeysAndAddresses(defaultNexaAccountKey, defaultIndexes.rIndex, defaultIndexes.rIndex + 20, defaultIndexes.cIndex, defaultIndexes.cIndex + 20))
143
+ await defaultAccount.loadBalances()
144
+ accounts.push(defaultAccount)
145
+ }
146
+ return accounts;
147
+ }
148
+
149
+ async function findUsedVaultAccounts(masterKey: HDPrivateKey) {
150
+ // all vaults are in bip44 account 1
151
+ let vaultAccountKey = generateAccountKey(masterKey, 1);
152
+ // all vaults are in the external chain
153
+ let vaultChain = vaultAccountKey.deriveChild(0, false);
154
+ // get the index that is the last used vault
155
+ return await discoverUsedAccountIndexes(vaultChain);
156
+ }
157
+
158
+ export async function discoverVaults(masterKey: HDPrivateKey) {
159
+ let accounts: VaultAccount[] = [];
160
+ // all vaults are in bip44 account 1
161
+ let vaultAccountKey = generateAccountKey(masterKey, 1);
162
+ // find the next unused vault
163
+ let lastUsedVaultIndex: number = await findUsedVaultAccounts(masterKey);
164
+ // if all vaults unused, generate at least the first vault account
165
+ if (lastUsedVaultIndex < 0) lastUsedVaultIndex = 0;
166
+ // for each vault found, make the DefaultAccount for that vault
167
+ for (let index = 0; index <= lastUsedVaultIndex; index++)
168
+ {
169
+ const vaultAccount = new VaultAccount(1, index, generateKeyAndAddress(vaultAccountKey, index))
170
+ await vaultAccount.loadBalances();
171
+ accounts.push(vaultAccount);
172
+ }
173
+ return accounts;
174
+ }
175
+
176
+ async function findUsedDappAccounts(masterKey: HDPrivateKey) {
177
+ // all dApp accounts are in bip44 account 2
178
+ let dappAccountKey = generateAccountKey(masterKey, 2);
179
+ // all dApp accounts use the external chain
180
+ let dappChain = dappAccountKey.deriveChild(0, false);
181
+ // get the index that is the next unused dApp account
182
+ return await discoverUsedAccountIndexes(dappChain);
183
+ }
184
+
185
+ export async function discoverDappAccounts(masterKey: HDPrivateKey) {
186
+ let accounts: DAppAccount[] = [];
187
+ // all dApp accounts are in bip44 account 2
188
+ let dappAccountKey = generateAccountKey(masterKey, 2);
189
+ // find the next unused dapp account
190
+ let lastUsedDappIndex: number = await findUsedDappAccounts(masterKey);
191
+ // if all dapp accounts unused, generate at least the first dapp account
192
+ if (lastUsedDappIndex < 0) lastUsedDappIndex = 0;
193
+ // for each dApp account found, make the DefaultAccount for that dApp account
194
+ // for each vault found, make the DefaultAccount for that vault
195
+ for (let index = 0; index <= lastUsedDappIndex; index++)
196
+ {
197
+ const dappAccount = new DAppAccount(2, index, generateKeyAndAddress(dappAccountKey, index))
198
+ await dappAccount.loadBalances();
199
+ accounts.push(dappAccount);
200
+ }
201
+ return accounts;
202
+ }
203
+
204
+ export async function discoverWallet(masterKey: HDPrivateKey) {
205
+ let accounts: BaseAccount[] = [];
206
+ // accounts 0, 100+
207
+ const nexaAccounts = await discoverNexaAccounts(masterKey);
208
+ // vaults in bip44 account 1
209
+ const vaultAccounts = await discoverVaults(masterKey);
210
+ // dApp accounts in bip44 account 2
211
+ const dappAccounts = await discoverDappAccounts(masterKey);
212
+ // 3 - 99 are reserved and will go here when added
213
+ accounts = accounts.concat(nexaAccounts);
214
+ accounts = accounts.concat(vaultAccounts);
215
+ accounts = accounts.concat(dappAccounts);
216
+ return accounts;
217
+ }
218
+
219
+ async function isAddressUsed(address: string) {
220
+ try {
221
+ let firstUse = await rostrumProvider.getFirstUse(address);
222
+ return firstUse.tx_hash && firstUse.tx_hash !== "";
223
+ } catch (e) {
224
+ if (e instanceof Error && e.message.includes("not found")) {
225
+ return false;
226
+ }
227
+ throw e;
228
+ }
229
+ }
230
+
231
+ async function getKeyTokenBalance(key: AddressKey) {
232
+ let tokensBalance = await rostrumProvider.getTokensBalance(key.address);
233
+ let balance: Record<string, Balance> = {};
234
+
235
+ for (const cToken in tokensBalance.confirmed) {
236
+ if (tokensBalance.confirmed[cToken] != 0) {
237
+ balance[cToken] = { confirmed: BigInt(tokensBalance.confirmed[cToken]).toString(), unconfirmed: "0" }
238
+ }
239
+ }
240
+
241
+ for (const uToken in tokensBalance.unconfirmed) {
242
+ if (tokensBalance.unconfirmed[uToken] != 0) {
243
+ if (balance[uToken]) {
244
+ balance[uToken].unconfirmed = BigInt(tokensBalance.unconfirmed[uToken]).toString();
245
+ } else {
246
+ balance[uToken] = { confirmed: "0", unconfirmed: BigInt(tokensBalance.unconfirmed[uToken]).toString() }
247
+ }
248
+ }
249
+ }
250
+
251
+ return balance;
252
+ }
253
+
254
+ async function getAndUpdateAddressKeyBalance(key: AddressKey) {
255
+ let balance = await rostrumProvider.getBalance(key.address);
256
+ key.balance = (BigInt(balance.confirmed) + BigInt(balance.unconfirmed)).toString();
257
+ key.tokensBalance = await getKeyTokenBalance(key);
258
+
259
+ return balance;
260
+ }
261
+
262
+ export async function fetchTotalBalance(keys: AddressKey[]) {
263
+ let promises: Promise<Balance>[] = [];
264
+ keys.forEach(key => {
265
+ let b = getAndUpdateAddressKeyBalance(key);
266
+ promises.push(b);
267
+ });
268
+
269
+ return await Promise.all(promises);
270
+ }
271
+
272
+ export function sumBalance(balances: Balance[]): Balance {
273
+ let confirmed = new bigDecimal(0), unconfirmed = new bigDecimal(0);
274
+ balances.forEach(b => {
275
+ confirmed = confirmed.add(new bigDecimal(b.confirmed));
276
+ unconfirmed = unconfirmed.add(new bigDecimal(b.unconfirmed));
277
+ });
278
+ return {confirmed: confirmed.getValue(), unconfirmed: unconfirmed.getValue()};
279
+ }
280
+
281
+ export function sumTokensBalance(balances: Record<string, Balance>[]) {
282
+ let tokensBalance: Record<string, Balance> = {};
283
+ balances.forEach(b => {
284
+ for (const key in b) {
285
+ if (tokensBalance[key]) {
286
+ tokensBalance[key].confirmed = (BigInt(tokensBalance[key].confirmed) + BigInt(b[key].confirmed)).toString();
287
+ tokensBalance[key].unconfirmed = (BigInt(tokensBalance[key].unconfirmed) + BigInt(b[key].unconfirmed)).toString();
288
+ } else {
289
+ tokensBalance[key] = { confirmed: b[key].confirmed, unconfirmed: b[key].unconfirmed };
290
+ }
291
+ }
292
+ });
293
+
294
+ return tokensBalance;
295
+ }
296
+
297
+ export async function fetchTransactionsHistory(addresses: string[], fromHeight: number) {
298
+ let index = 0, i = 0, data = new Map<string, ITXHistory>(), maxHeight = fromHeight;
299
+
300
+ for (let address of addresses) {
301
+ i++;
302
+ let txHistory = await rostrumProvider.getTransactionsHistory(address);
303
+ if (txHistory && txHistory.length > 0) {
304
+ index = i;
305
+ for (let tx of txHistory) {
306
+ if (tx.height === 0 || tx.height > fromHeight) {
307
+ maxHeight = Math.max(maxHeight, tx.height);
308
+ data.set(tx.tx_hash, tx);
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ return {index: index, txs: data, lastHeight: maxHeight};
315
+ }
316
+
317
+ async function rescanAddressesHistory(addresses: string[]) {
318
+ let index = 0, i = 0, minHeight = Number.MAX_SAFE_INTEGER;
319
+
320
+ for (let address of addresses) {
321
+ i++;
322
+ let txHistory = await rostrumProvider.getTransactionsHistory(address);
323
+ if (!isNil(txHistory)) {
324
+ index = i;
325
+ let heights = txHistory.filter(tx => tx.height > 0).map(h => h.height);
326
+ if (!isNil(heights)) {
327
+ minHeight = Math.min(minHeight, ...heights);
328
+ }
329
+ }
330
+ }
331
+
332
+ return {index: (index > 0 ? index + 1 : 0), height: (minHeight == Number.MAX_SAFE_INTEGER ? 0 : minHeight)};
333
+ }
334
+
335
+ export async function getNextAccountIndex(accountType: AccountType, masterKey: HDPrivateKey) {
336
+ if (accountType == AccountType.NEXA_ACCOUNT) {
337
+ let defaultNexaAccountKey = generateAccountKey(masterKey, 0);
338
+ const defaultIndexes = await discoverNexaAccount(defaultNexaAccountKey);
339
+ if (defaultIndexes.rIndex < 0 && defaultIndexes.cIndex < 0){
340
+ return 0;
341
+ }
342
+ else {
343
+ // account 0 was not empty
344
+ for (let index = 100; ; index++) {
345
+ const nexaAccountKey = generateAccountKey(masterKey, index);
346
+ const indexes = await discoverNexaAccount(nexaAccountKey);
347
+ if (indexes.rIndex < 0 && indexes.cIndex < 0)
348
+ {
349
+ return index;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ else if (accountType == AccountType.VAULT_ACCOUNT) {
355
+ // find the next unused vault
356
+ const lastUsedVault: number = await findUsedVaultAccounts(masterKey);
357
+ return lastUsedVault + 1;
358
+ }
359
+ else if (accountType == AccountType.DAPP_ACCOUNT) {
360
+ // find the next unused dapp account
361
+ const lastUsedDappAccount: number = await findUsedDappAccounts(masterKey);
362
+ return lastUsedDappAccount + 1;
363
+ }
364
+ else {
365
+ throw new Error("Can not get next account index. Invalid accountType.");
366
+ }
367
+ }
368
+
369
+ export async function classifyTransaction(txHistory: ITXHistory, myAddresses: string[]) {
370
+ let t = await rostrumProvider.getTransaction(txHistory.tx_hash);
371
+
372
+ let outputs = t.vout.filter(utxo => !isNil(utxo.scriptPubKey.addresses));
373
+
374
+ let isOutgoing = t.vin.length > 0 && myAddresses.includes(t.vin[0].addresses[0]);
375
+ let isIncoming = !isOutgoing || outputs.every(utxo => myAddresses.includes(utxo.scriptPubKey.addresses[0]));
376
+ let isConfirmed = t.height > 0;
377
+
378
+ let txEntry = {} as TransactionEntity;
379
+ txEntry.txId = t.txid;
380
+ txEntry.txIdem = t.txidem;
381
+ txEntry.height = isConfirmed ? t.height : 0;
382
+ txEntry.time = isConfirmed ? t.time : currentTimestamp();
383
+ txEntry.fee = t.fee_satoshi;
384
+
385
+ if (isOutgoing && isIncoming) {
386
+ txEntry.state = 'both';
387
+ txEntry.value = "0";
388
+ txEntry.payTo = "Payment to yourself";
389
+ } else if (isIncoming) {
390
+ txEntry.state = 'incoming';
391
+ let utxos = outputs.filter(utxo => myAddresses.includes(utxo.scriptPubKey.addresses[0]));
392
+ let amount = new bigDecimal(0);
393
+ utxos.forEach(utxo => {
394
+ amount = amount.add(new bigDecimal(utxo.value_satoshi));
395
+ });
396
+ txEntry.value = amount.getValue();
397
+ txEntry.payTo = utxos[0].scriptPubKey.addresses[0];
398
+ } else if(isOutgoing) {
399
+ txEntry.state = 'outgoing';
400
+ let utxos = outputs.filter(utxo => !myAddresses.includes(utxo.scriptPubKey.addresses[0]));
401
+ let amount = new bigDecimal(0);
402
+ utxos.forEach(utxo => {
403
+ amount = amount.add(new bigDecimal(utxo.value_satoshi));
404
+ });
405
+ txEntry.value = amount.getValue();
406
+ txEntry.payTo = utxos[0].scriptPubKey.addresses[0];
407
+ }
408
+
409
+ let [txType, txGroup, tokenAmount, extraGroup] = classifyTokenTransaction(t.vin, outputs, txEntry.state, myAddresses);
410
+ txEntry.txGroupType = txType;
411
+ txEntry.token = txGroup;
412
+ txEntry.tokenAmount = tokenAmount;
413
+ txEntry.extraGroup = extraGroup;
414
+
415
+ return txEntry;
416
+ }
417
+
418
+ function classifyTokenTransaction(vin: ITXInput[], vout: ITXOutput[], txState: TxEntityState, myAddresses: string[]): [TxTokenType, string, string, string] {
419
+ let groupInputs = vin.filter(input => !isNullOrEmpty(input.group));
420
+ let groupOutputs = vout.filter(output => !isNullOrEmpty(output.scriptPubKey.group));
421
+
422
+ if (isNullOrEmpty(groupInputs) && isNullOrEmpty(groupOutputs)) {
423
+ return [TxTokenType.NO_GROUP, "none", "0", "none"];
424
+ }
425
+
426
+ let myGroupInputs = groupInputs.filter(input => myAddresses.includes(input.addresses[0]));
427
+ let myGroupOutputs = groupOutputs.filter(output => myAddresses.includes(output.scriptPubKey.addresses[0]));
428
+
429
+ if (isNullOrEmpty(myGroupInputs) && isNullOrEmpty(myGroupOutputs)) {
430
+ return [TxTokenType.NO_GROUP, "none", "0", "none"];
431
+ }
432
+
433
+ if (isNullOrEmpty(groupInputs)) {
434
+ let group = myGroupOutputs.find(output => BigInt(output.scriptPubKey.groupQuantity) < 0n)?.scriptPubKey.group ?? "none";
435
+ return [TxTokenType.CREATE, group, "0", "none"];
436
+ }
437
+
438
+ if (isNullOrEmpty(groupOutputs)) {
439
+ if (txState === 'incoming') {
440
+ return [TxTokenType.NO_GROUP, "none", "0", "none"];
441
+ }
442
+
443
+ let inputs = myGroupInputs.filter(input => BigInt(input.groupQuantity) > 0n);
444
+ if (!isNullOrEmpty(inputs)) {
445
+ let amount = new bigDecimal(0);
446
+ inputs.forEach(utxo => {
447
+ amount = amount.add(new bigDecimal(utxo.groupQuantity));
448
+ });
449
+ let group = inputs[0].group;
450
+ let extraGroup = myGroupInputs.find(input => BigInt(input.groupQuantity) < 0n && inputs[0].group != input.group)?.group ?? "none";
451
+ return [TxTokenType.MELT, group, amount.getValue(), extraGroup];
452
+ }
453
+
454
+ let group = myGroupInputs.find(input => BigInt(input.groupQuantity) < 0n)?.group ?? "none";
455
+ let extraGroup = myGroupInputs.find(input => BigInt(input.groupQuantity) < 0n && group != input.group)?.group ?? "none";
456
+ return [TxTokenType.MELT, group, "0", extraGroup];
457
+ }
458
+
459
+ let tokenInputs = groupInputs.filter(input => BigInt(input.groupQuantity) > 0n);
460
+ let tokenOutputs = groupOutputs.filter(output => BigInt(output.scriptPubKey.groupQuantity) > 0n);
461
+
462
+ if (isNullOrEmpty(tokenInputs) && isNullOrEmpty(tokenOutputs)) {
463
+ let group = groupInputs.find(input => BigInt(input.groupQuantity) < 0n)?.group ?? "none";
464
+ let extraGroup = groupOutputs.find(output => BigInt(output.scriptPubKey.groupQuantity) < 0n && group != output.scriptPubKey.group)?.scriptPubKey.group ?? "none";
465
+ return [TxTokenType.RENEW, extraGroup !== 'none' ? extraGroup : group, "0", extraGroup !== 'none' ? group : extraGroup];
466
+ }
467
+
468
+ if (isNullOrEmpty(tokenInputs)) {
469
+ let group = tokenOutputs[0].scriptPubKey.group;
470
+ let amount = new bigDecimal(0);
471
+ tokenOutputs.forEach(utxo => {
472
+ amount = amount.add(new bigDecimal(utxo.scriptPubKey.groupQuantity));
473
+ });
474
+ let extraGroup = groupInputs.find(input => BigInt(input.groupQuantity) < 0n && group != input.group)?.group ?? "none";
475
+ return [TxTokenType.MINT, group, amount.getValue(), extraGroup];
476
+ }
477
+
478
+ if (isNullOrEmpty(tokenOutputs)) {
479
+ let group = tokenInputs[0].group;
480
+ let amount = new bigDecimal(0);
481
+ tokenInputs.forEach(utxo => {
482
+ amount = amount.add(new bigDecimal(utxo.groupQuantity));
483
+ });
484
+ let extraGroup = groupInputs.find(input => BigInt(input.groupQuantity) < 0n && group != input.group)?.group ?? "none";
485
+ return [TxTokenType.MELT, group, amount.getValue(), extraGroup];
486
+ }
487
+
488
+ let outQuantitySum = tokenOutputs.map(output => BigInt(output.scriptPubKey.groupQuantity)).reduce((a, b) => a + b, 0n);
489
+ let inQuantitySum = tokenInputs.map(input => BigInt(input.groupQuantity)).reduce((a, b) => a + b, 0n);
490
+
491
+ if (outQuantitySum > inQuantitySum) {
492
+ let group = tokenOutputs[0].scriptPubKey.group;
493
+ let extraGroup = groupInputs.find(input => BigInt(input.groupQuantity) < 0n && group != input.group)?.group ?? "none";
494
+ return [TxTokenType.MINT, group, (outQuantitySum - inQuantitySum).toString(), extraGroup];
495
+ }
496
+
497
+ if (inQuantitySum > outQuantitySum) {
498
+ let group = tokenInputs[0].group;
499
+ let extraGroup = groupInputs.find(input => BigInt(input.groupQuantity) < 0n && group != input.group)?.group ?? "none";
500
+ return [TxTokenType.MELT, group, (inQuantitySum - outQuantitySum).toString(), extraGroup];
501
+ }
502
+
503
+ let group = tokenOutputs[0].scriptPubKey.group;
504
+ let amount = "";
505
+ if (txState === 'incoming') {
506
+ amount = tokenOutputs
507
+ .filter(output => myAddresses.includes(output.scriptPubKey.addresses[0]))
508
+ .map(output => BigInt(output.scriptPubKey.groupQuantity))
509
+ .reduce((a, b) => a + b, 0n)
510
+ .toString();
511
+ } else if (txState === 'outgoing') {
512
+ amount = tokenOutputs
513
+ .filter(output => !myAddresses.includes(output.scriptPubKey.addresses[0]))
514
+ .map(output => BigInt(output.scriptPubKey.groupQuantity))
515
+ .reduce((a, b) => a + b, 0n)
516
+ .toString();
517
+ } else {
518
+ amount = "0";
519
+ }
520
+
521
+ return [TxTokenType.TRANSFER, group, amount, "none"];
522
+ }