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.
- package/.parcel-cache/3e09f086f3c4d605-AssetGraph +0 -0
- package/.parcel-cache/5eac57ec674cdae8-AssetGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/e43547b6c9167b58-RequestGraph +0 -0
- package/.parcel-cache/ecfe15d74834bbfd-BundleGraph +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-e43547b6c9167b58.txt +2 -0
- package/README.md +445 -0
- package/dist/browser/index.js +2456 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/index.d.ts +918 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2915 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2456 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/spec.md +257 -0
- package/src/index.ts +93 -0
- package/src/models/rostrum.entities.ts +159 -0
- package/src/models/transaction.entities.ts +46 -0
- package/src/models/wallet.entities.ts +42 -0
- package/src/network/RostrumProvider.ts +137 -0
- package/src/types.ts +0 -0
- package/src/utils/CommonUtils.ts +123 -0
- package/src/utils/TXUtils.ts +445 -0
- package/src/utils/TokenUtils.ts +75 -0
- package/src/utils/ValidationUtils.ts +86 -0
- package/src/utils/WalletUtils.ts +522 -0
- package/src/utils/WatchOnlyTXUtils.ts +275 -0
- package/src/wallet/Wallet.ts +397 -0
- package/src/wallet/WatchOnlyWallet.ts +169 -0
- package/src/wallet/accounts/AccountStore.ts +173 -0
- package/src/wallet/accounts/interfaces/BaseAccountInterface.ts +56 -0
- package/src/wallet/accounts/models/DappAccount.ts +80 -0
- package/src/wallet/accounts/models/DefaultAccount.ts +96 -0
- package/src/wallet/accounts/models/VaultAccount.ts +81 -0
- package/src/wallet/transactions/WalletTransactionCreator.ts +145 -0
- package/src/wallet/transactions/WatchOnlyTransactionCreator.ts +189 -0
- package/src/wallet/transactions/interfaces/TransactionCreator.ts +438 -0
- package/tests/core/tx/transactioncreator.test.ts +455 -0
- package/tests/core/tx/wallettransactioncreator.test.ts +362 -0
- package/tests/core/tx/watchonlytransactioncreator.test.ts +258 -0
- package/tests/core/wallet/accountstore.test.ts +341 -0
- package/tests/core/wallet/wallet.test.ts +69 -0
- package/tests/core/watchonlywallet/watchonlywallet.test.ts +251 -0
- package/tests/index.test.ts +12 -0
- 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
|
+
}
|