@otoplo/wallet-common 0.1.0
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +2278 -0
- package/dist/index.js +2005 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +5 -0
- package/src/persistence/datastore/db.ts +47 -0
- package/src/persistence/datastore/index.ts +2 -0
- package/src/persistence/datastore/kv.ts +129 -0
- package/src/persistence/index.ts +2 -0
- package/src/persistence/wallet-db.ts +251 -0
- package/src/services/asset.ts +220 -0
- package/src/services/cache.ts +54 -0
- package/src/services/discovery.ts +110 -0
- package/src/services/index.ts +8 -0
- package/src/services/key.ts +55 -0
- package/src/services/rostrum.ts +214 -0
- package/src/services/session.ts +225 -0
- package/src/services/transaction.ts +388 -0
- package/src/services/tx-transformer.ts +244 -0
- package/src/services/wallet.ts +650 -0
- package/src/state/hooks.ts +45 -0
- package/src/state/index.ts +3 -0
- package/src/state/slices/auth.ts +28 -0
- package/src/state/slices/dapp.ts +32 -0
- package/src/state/slices/index.ts +6 -0
- package/src/state/slices/loader.ts +31 -0
- package/src/state/slices/notifications.ts +44 -0
- package/src/state/slices/status.ts +70 -0
- package/src/state/slices/wallet.ts +112 -0
- package/src/state/store.ts +24 -0
- package/src/types/dapp.types.ts +21 -0
- package/src/types/db.types.ts +142 -0
- package/src/types/index.ts +5 -0
- package/src/types/notification.types.ts +13 -0
- package/src/types/rostrum.types.ts +161 -0
- package/src/types/wallet.types.ts +62 -0
- package/src/utils/asset.ts +103 -0
- package/src/utils/common.ts +159 -0
- package/src/utils/enums.ts +22 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/keypath.ts +15 -0
- package/src/utils/price.ts +40 -0
- package/src/utils/seed.ts +57 -0
- package/src/utils/vault.ts +39 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import type { KVStore, WalletDB } from "../persistence";
|
|
2
|
+
import type { Account, AccountDTO, AddressDTO, AddressKey, Balance, ITXHistory, RostrumParams, VaultDTO, VaultInfo } from "../types";
|
|
3
|
+
import { AccountType, keyPathToString, KeySpace, MAIN_WALLET_ID, sumBalance, sumTokensBalance } from "../utils";
|
|
4
|
+
import type { AssetService } from "./asset";
|
|
5
|
+
import { WalletDiscoveryService } from "./discovery";
|
|
6
|
+
import type { KeyManager } from "./key";
|
|
7
|
+
import type { RostrumService } from "./rostrum";
|
|
8
|
+
import type { SessionManager } from "./session";
|
|
9
|
+
import type { TransactionService } from "./transaction";
|
|
10
|
+
|
|
11
|
+
type WalletAddress = AccountDTO | AddressDTO | VaultDTO;
|
|
12
|
+
|
|
13
|
+
interface AddressHistory {
|
|
14
|
+
address: WalletAddress;
|
|
15
|
+
txs: ITXHistory[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UpdateInfo {
|
|
19
|
+
address: WalletAddress;
|
|
20
|
+
result: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type WalletEvent =
|
|
24
|
+
| { type: 'new_tip'; tip: number; }
|
|
25
|
+
| { type: 'new_account'; account: Account; }
|
|
26
|
+
| { type: 'new_vault'; vault: VaultInfo; }
|
|
27
|
+
| { type: 'discover_wallet'; loading: boolean; }
|
|
28
|
+
| { type: 'load_wallet'; loading: boolean; }
|
|
29
|
+
| { type: 'sync_wallet'; loading: boolean; }
|
|
30
|
+
| { type: 'account_balance_updated'; accountId: number; balance: Balance, tokensBalance: Record<string, Balance>; }
|
|
31
|
+
| { type: 'vault_balance_updated'; address: string, balance: Balance; }
|
|
32
|
+
| { type: 'main_address_updated'; address: string; };
|
|
33
|
+
|
|
34
|
+
export type WalletUpdateCallback = (event: WalletEvent) => void;
|
|
35
|
+
|
|
36
|
+
export class WalletManager {
|
|
37
|
+
|
|
38
|
+
private static readonly GAP_LIMIT = 20;
|
|
39
|
+
private static readonly DEBOUNCE_MS = 1000;
|
|
40
|
+
|
|
41
|
+
private addressResolvers: Map<string, () => void>;
|
|
42
|
+
|
|
43
|
+
public accounts: Map<number, AccountDTO>;
|
|
44
|
+
public accountsAddressToId: Map<string, number>;
|
|
45
|
+
|
|
46
|
+
public receiveAddresses: AddressDTO[];
|
|
47
|
+
public changeAddresses: AddressDTO[];
|
|
48
|
+
|
|
49
|
+
public vaults: Map<string, VaultDTO>;
|
|
50
|
+
|
|
51
|
+
private pendingUpdates: Map<string, UpdateInfo>;
|
|
52
|
+
private updateTimer?: number;
|
|
53
|
+
|
|
54
|
+
private updateCallback?: WalletUpdateCallback;
|
|
55
|
+
|
|
56
|
+
private readonly discoveryService: WalletDiscoveryService;
|
|
57
|
+
|
|
58
|
+
public constructor(
|
|
59
|
+
private readonly keyManager: KeyManager,
|
|
60
|
+
private readonly kvStore: KVStore,
|
|
61
|
+
private readonly walletDb: WalletDB,
|
|
62
|
+
private readonly rostrumService: RostrumService,
|
|
63
|
+
private readonly assetService: AssetService,
|
|
64
|
+
private readonly transactionService: TransactionService,
|
|
65
|
+
private readonly sessionManager: SessionManager,
|
|
66
|
+
) {
|
|
67
|
+
this.discoveryService = new WalletDiscoveryService(this.rostrumService, this.keyManager);
|
|
68
|
+
this.accounts = new Map();
|
|
69
|
+
this.accountsAddressToId = new Map();
|
|
70
|
+
this.receiveAddresses = [];
|
|
71
|
+
this.changeAddresses = [];
|
|
72
|
+
this.vaults = new Map();
|
|
73
|
+
this.pendingUpdates = new Map();
|
|
74
|
+
this.addressResolvers = new Map();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public onUpdate(callback: WalletUpdateCallback): void {
|
|
78
|
+
this.updateCallback = callback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private notify(event: WalletEvent): void {
|
|
82
|
+
this.updateCallback?.(event);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private getAllAddresses(): WalletAddress[] {
|
|
86
|
+
return [...this.receiveAddresses, ...this.changeAddresses, ...this.accounts.values(), ...this.vaults.values()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public getMainAddresses(): AddressDTO[] {
|
|
90
|
+
return [...this.receiveAddresses, ...this.changeAddresses];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public getReceiveAddress(): string {
|
|
94
|
+
return this.receiveAddresses.find(addr => !addr.used)!.address;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public getChangeAddress(accountId = MAIN_WALLET_ID): string {
|
|
98
|
+
if (accountId != MAIN_WALLET_ID) {
|
|
99
|
+
return this.accounts.get(accountId)!.address;
|
|
100
|
+
}
|
|
101
|
+
return this.changeAddresses.find(addr => !addr.used)!.address;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public getUsedAddressKeys(accountId = MAIN_WALLET_ID): AddressKey[] {
|
|
105
|
+
if (accountId != MAIN_WALLET_ID) {
|
|
106
|
+
const account = this.accounts.get(accountId)!;
|
|
107
|
+
return [{
|
|
108
|
+
address: account.address,
|
|
109
|
+
keyPath: keyPathToString(AccountType.DAPP, 0, account.id),
|
|
110
|
+
balance: account.balance,
|
|
111
|
+
tokensBalance: account.tokensBalance
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this.getMainAddresses().filter(addr => addr.used).map(addr => ({
|
|
116
|
+
address: addr.address,
|
|
117
|
+
keyPath: keyPathToString(AccountType.MAIN, addr.space, addr.idx),
|
|
118
|
+
balance: addr.balance,
|
|
119
|
+
tokensBalance: addr.tokensBalance
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async reloadRostrum(params: RostrumParams): Promise<void> {
|
|
124
|
+
await this.kvStore.saveRostrumParams(params);
|
|
125
|
+
await this.rostrumService.disconnect(true);
|
|
126
|
+
await this.initRostrum();
|
|
127
|
+
await this.subscribeAddresses(this.getAllAddresses())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public async initRostrum(): Promise<void> {
|
|
131
|
+
await this.rostrumService.connect();
|
|
132
|
+
await this.rostrumService.subscribeHeaders(tip => this.notify({ type: 'new_tip', tip }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public async initialize(): Promise<void> {
|
|
136
|
+
await this.initRostrum();
|
|
137
|
+
|
|
138
|
+
const isExist = await this.walletDb.countAddresses();
|
|
139
|
+
if (isExist) {
|
|
140
|
+
await this.loadWallet();
|
|
141
|
+
} else {
|
|
142
|
+
await this.discoverWallet();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async discoverWallet(): Promise<void> {
|
|
147
|
+
this.notify({ type: 'discover_wallet', loading: true });
|
|
148
|
+
|
|
149
|
+
const rIndexPromise = this.discoveryService.discoverWalletIndex(AccountType.MAIN, KeySpace.RECEIVE);
|
|
150
|
+
const cIndexPromise = this.discoveryService.discoverWalletIndex(AccountType.MAIN, KeySpace.CHANGE);
|
|
151
|
+
const dappIndexPromis = this.discoveryService.discoverWalletIndex(AccountType.DAPP, KeySpace.RECEIVE);
|
|
152
|
+
|
|
153
|
+
const [rIndex, cIndex, dappIndex] = await Promise.all([rIndexPromise, cIndexPromise, dappIndexPromis]);
|
|
154
|
+
|
|
155
|
+
this.receiveAddresses = this.deriveAddresses(KeySpace.RECEIVE, 0, rIndex + WalletManager.GAP_LIMIT);
|
|
156
|
+
this.changeAddresses = this.deriveAddresses(KeySpace.CHANGE, 0, cIndex + WalletManager.GAP_LIMIT);
|
|
157
|
+
this.accounts = this.deriveAccounts(0, dappIndex + 1);
|
|
158
|
+
this.accounts.forEach(entry => this.accountsAddressToId.set(entry.address, entry.id));
|
|
159
|
+
|
|
160
|
+
await this.saveAddresses(this.receiveAddresses);
|
|
161
|
+
await this.saveAddresses(this.changeAddresses);
|
|
162
|
+
await this.saveAccounts(this.accounts);
|
|
163
|
+
|
|
164
|
+
this.initState();
|
|
165
|
+
|
|
166
|
+
await this.initialSync();
|
|
167
|
+
|
|
168
|
+
this.notify({ type: 'discover_wallet', loading: false });
|
|
169
|
+
this.notify({ type: 'load_wallet', loading: false });
|
|
170
|
+
|
|
171
|
+
await this.rescanVaults();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async loadWallet(): Promise<void> {
|
|
175
|
+
this.receiveAddresses = await this.walletDb.getReceiveAddresses();
|
|
176
|
+
this.changeAddresses = await this.walletDb.getChangeAddresses();
|
|
177
|
+
|
|
178
|
+
const accs = await this.walletDb.getAccounts();
|
|
179
|
+
this.accounts = new Map(accs.map(a => [a.id, a]));
|
|
180
|
+
this.accountsAddressToId = new Map(accs.map(a => [a.address, a.id]));
|
|
181
|
+
|
|
182
|
+
const vaults = await this.walletDb.getVaults();
|
|
183
|
+
this.vaults = new Map(vaults.map(v => [v.address, v]));
|
|
184
|
+
|
|
185
|
+
this.initState();
|
|
186
|
+
|
|
187
|
+
this.notify({ type: 'load_wallet', loading: false });
|
|
188
|
+
|
|
189
|
+
await this.reconnectSync();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async initialSync(): Promise<void> {
|
|
193
|
+
await this.subscribeAddresses(this.getAllAddresses(), true);
|
|
194
|
+
|
|
195
|
+
const tokensLoadPromises: Promise<void>[] = [];
|
|
196
|
+
tokensLoadPromises.push(this.assetService.fetchAndSaveTokens(MAIN_WALLET_ID, sumTokensBalance(this.getMainAddresses().map(a => a.tokensBalance))));
|
|
197
|
+
for (const account of this.accounts.values()) {
|
|
198
|
+
tokensLoadPromises.push(this.assetService.fetchAndSaveTokens(account.id, account.tokensBalance));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await Promise.all(tokensLoadPromises);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public async reconnectSync(): Promise<void> {
|
|
205
|
+
await Promise.all([
|
|
206
|
+
this.sessionManager.reload(this.accounts),
|
|
207
|
+
this.subscribeAddresses(this.getAllAddresses())
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private initState(): void {
|
|
212
|
+
const walletAddresses = this.getMainAddresses();
|
|
213
|
+
|
|
214
|
+
this.notify({
|
|
215
|
+
type: 'new_account',
|
|
216
|
+
account: {
|
|
217
|
+
id: MAIN_WALLET_ID,
|
|
218
|
+
name: 'Main Wallet',
|
|
219
|
+
address: this.getReceiveAddress(),
|
|
220
|
+
balance: sumBalance(walletAddresses.map(a => a.balance)),
|
|
221
|
+
tokensBalance: sumTokensBalance(walletAddresses.map(a => a.tokensBalance)),
|
|
222
|
+
tokens: [],
|
|
223
|
+
sessions: {}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
for (const account of this.accounts.values()) {
|
|
228
|
+
this.notify({
|
|
229
|
+
type: 'new_account',
|
|
230
|
+
account: {
|
|
231
|
+
id: account.id,
|
|
232
|
+
name: account.name,
|
|
233
|
+
address: account.address,
|
|
234
|
+
balance: account.balance,
|
|
235
|
+
tokensBalance: account.tokensBalance,
|
|
236
|
+
tokens: [],
|
|
237
|
+
sessions: {}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const vault of this.vaults.values()) {
|
|
243
|
+
this.notify({
|
|
244
|
+
type: 'new_vault',
|
|
245
|
+
vault: {
|
|
246
|
+
address: vault.address,
|
|
247
|
+
balance: vault.balance,
|
|
248
|
+
block: vault.block,
|
|
249
|
+
index: vault.idx
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private deriveAddresses(space: KeySpace, startIndex: number, count: number): AddressDTO[] {
|
|
256
|
+
const newAddresses: AddressDTO[] = [];
|
|
257
|
+
for (let i = startIndex; i < startIndex + count; i++) {
|
|
258
|
+
const path = keyPathToString(AccountType.MAIN, space, i);
|
|
259
|
+
const address = this.keyManager.getKey(path).privateKey.toAddress().toString();
|
|
260
|
+
|
|
261
|
+
const addressDTO: AddressDTO = {
|
|
262
|
+
address: address,
|
|
263
|
+
space: space,
|
|
264
|
+
idx: i,
|
|
265
|
+
used: false,
|
|
266
|
+
height: 0,
|
|
267
|
+
statusHash: '',
|
|
268
|
+
balance: { confirmed: "0", unconfirmed: "0" },
|
|
269
|
+
tokensBalance: {},
|
|
270
|
+
type: AccountType.MAIN
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
newAddresses.push(addressDTO);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return newAddresses;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private deriveAccounts(startIndex: number, count: number): Map<number, AccountDTO> {
|
|
280
|
+
const newAccounts = new Map<number, AccountDTO>();
|
|
281
|
+
for (let i = startIndex; i < startIndex + count; i++) {
|
|
282
|
+
const path = keyPathToString(AccountType.DAPP, 0, i);
|
|
283
|
+
const address = this.keyManager.getKey(path).privateKey.toAddress().toString();
|
|
284
|
+
|
|
285
|
+
const account: AccountDTO = {
|
|
286
|
+
id: i,
|
|
287
|
+
name: `Account ${i + 1}`,
|
|
288
|
+
address: address,
|
|
289
|
+
height: 0,
|
|
290
|
+
hidden: 0,
|
|
291
|
+
statusHash: '',
|
|
292
|
+
balance: { confirmed: "0", unconfirmed: "0" },
|
|
293
|
+
tokensBalance: {},
|
|
294
|
+
type: AccountType.DAPP
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
newAccounts.set(account.id, account);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return newAccounts;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async saveAddresses(addresses: AddressDTO[]): Promise<void> {
|
|
304
|
+
for (const addr of addresses) {
|
|
305
|
+
await this.walletDb.saveAddress(addr);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async saveAccounts(accounts: Map<number, AccountDTO>): Promise<void> {
|
|
310
|
+
for (const acc of accounts.values()) {
|
|
311
|
+
await this.walletDb.saveAccount(acc);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async subscribeAddresses(addresses: WalletAddress[], isInit = false): Promise<void> {
|
|
316
|
+
const subscriptionPromises = addresses.map(async addr => {
|
|
317
|
+
const result = await this.rostrumService.subscribeAddress(addr.address, this.onSubscribeEvent);
|
|
318
|
+
return { addr, result };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const initPromises: Promise<void>[] = [];
|
|
322
|
+
const subscriptionResults = await Promise.all(subscriptionPromises);
|
|
323
|
+
|
|
324
|
+
for (const { addr, result } of subscriptionResults) {
|
|
325
|
+
if (result && typeof result === 'string' && addr.statusHash != result) {
|
|
326
|
+
if (isInit) {
|
|
327
|
+
const p = new Promise<void>((resolve) => {
|
|
328
|
+
this.addressResolvers.set(addr.address, resolve);
|
|
329
|
+
});
|
|
330
|
+
initPromises.push(p);
|
|
331
|
+
}
|
|
332
|
+
this.registerUpdate({address: addr, result});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await Promise.all(initPromises);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private onSubscribeEvent = (data: unknown): void => {
|
|
340
|
+
if (!Array.isArray(data) || data.length < 2) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const [address, hash] = data as [string, string];
|
|
345
|
+
const allAddresses = this.getAllAddresses();
|
|
346
|
+
const addrDTO = allAddresses.find(a => a.address === address);
|
|
347
|
+
|
|
348
|
+
if (addrDTO && addrDTO.statusHash !== hash) {
|
|
349
|
+
this.registerUpdate({address: addrDTO, result: hash});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private registerUpdate(updateInfo: UpdateInfo): void {
|
|
354
|
+
this.pendingUpdates.set(updateInfo.address.address, updateInfo);
|
|
355
|
+
|
|
356
|
+
clearTimeout(this.updateTimer);
|
|
357
|
+
this.updateTimer = setTimeout(() => {
|
|
358
|
+
this.processPendingUpdates();
|
|
359
|
+
}, WalletManager.DEBOUNCE_MS);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async processPendingUpdates(): Promise<void> {
|
|
363
|
+
if (this.pendingUpdates.size === 0) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
this.notify({ type: 'sync_wallet', loading: true });
|
|
369
|
+
console.log(`Processing ${this.pendingUpdates.size} pending updates...`);
|
|
370
|
+
|
|
371
|
+
const updates = Array.from(this.pendingUpdates);
|
|
372
|
+
this.pendingUpdates.clear();
|
|
373
|
+
|
|
374
|
+
const accountsHistoryPromises: Promise<AddressHistory>[] = [];
|
|
375
|
+
const vaultsHistoryPromises: Promise<AddressHistory>[] = [];
|
|
376
|
+
const walletHistoryPromises: Promise<AddressHistory>[] = [];
|
|
377
|
+
for (const [, update] of updates) {
|
|
378
|
+
if (this.isAccountAddress(update.address)) {
|
|
379
|
+
const p = this.fetchAndUpdateAccount(update.address, update.result);
|
|
380
|
+
accountsHistoryPromises.push(p);
|
|
381
|
+
} else if (this.isVaultAddress(update.address)) {
|
|
382
|
+
const p = this.fetchAndUpdateVault(update.address, update.result);
|
|
383
|
+
vaultsHistoryPromises.push(p);
|
|
384
|
+
} else {
|
|
385
|
+
const p = this.fetchAndUpdateAddress(update.address, update.result);
|
|
386
|
+
walletHistoryPromises.push(p);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const accountsTxs = await Promise.all(accountsHistoryPromises);
|
|
391
|
+
await Promise.all(vaultsHistoryPromises);
|
|
392
|
+
const walletTxs = await Promise.all(walletHistoryPromises);
|
|
393
|
+
|
|
394
|
+
if (this.addressResolvers.size > 0) {
|
|
395
|
+
for (const [address,] of updates) {
|
|
396
|
+
const resolver = this.addressResolvers.get(address);
|
|
397
|
+
if (resolver) {
|
|
398
|
+
resolver();
|
|
399
|
+
this.addressResolvers.delete(address);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await Promise.all([this.postProcessWalletUpdate(walletTxs), this.postProcessAccountUpdate(accountsTxs)]);
|
|
405
|
+
} catch (e) {
|
|
406
|
+
console.error('Error processing pending updates:', e);
|
|
407
|
+
} finally {
|
|
408
|
+
this.notify({ type: 'sync_wallet', loading: false });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async postProcessWalletUpdate(history: AddressHistory[]): Promise<void> {
|
|
413
|
+
if (history.length === 0) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const txHistoryMap = new Map<string, ITXHistory>();
|
|
418
|
+
for (const { txs } of history) {
|
|
419
|
+
for (const tx of txs) {
|
|
420
|
+
txHistoryMap.set(tx.tx_hash, tx);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await this.checkGapLimit(KeySpace.RECEIVE);
|
|
425
|
+
await this.checkGapLimit(KeySpace.CHANGE);
|
|
426
|
+
|
|
427
|
+
const walletAddresses = this.getMainAddresses();
|
|
428
|
+
const walletBalance = sumBalance(walletAddresses.map(a => a.balance));
|
|
429
|
+
const walletTokenBalances = sumTokensBalance(walletAddresses.map(a => a.tokensBalance));
|
|
430
|
+
|
|
431
|
+
this.notify({ type: 'account_balance_updated', accountId: MAIN_WALLET_ID, balance: walletBalance, tokensBalance: walletTokenBalances });
|
|
432
|
+
this.notify({ type: 'main_address_updated', address: this.getReceiveAddress() });
|
|
433
|
+
|
|
434
|
+
const nftPromise = this.assetService.syncNfts(MAIN_WALLET_ID, walletTokenBalances);
|
|
435
|
+
for (const tx of txHistoryMap.values()) {
|
|
436
|
+
await this.transactionService.classifyAndSaveTransaction(MAIN_WALLET_ID, tx.tx_hash, walletAddresses.map(a => a.address));
|
|
437
|
+
}
|
|
438
|
+
await nftPromise;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private async postProcessAccountUpdate(history: AddressHistory[]): Promise<void> {
|
|
442
|
+
if (history.length === 0) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const { address, txs } of history) {
|
|
447
|
+
const account = address as AccountDTO;
|
|
448
|
+
|
|
449
|
+
this.notify({ type: 'account_balance_updated', accountId: account.id, balance: account.balance, tokensBalance: account.tokensBalance });
|
|
450
|
+
|
|
451
|
+
const nftPromise = this.assetService.syncNfts(account.id, account.tokensBalance);
|
|
452
|
+
for (const tx of txs) {
|
|
453
|
+
await this.transactionService.classifyAndSaveTransaction(account.id, tx.tx_hash, [account.address]);
|
|
454
|
+
}
|
|
455
|
+
await nftPromise;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private async fetchAndUpdateAddress(addrDTO: AddressDTO, hash: string): Promise<AddressHistory> {
|
|
460
|
+
addrDTO.statusHash = hash;
|
|
461
|
+
addrDTO.balance = await this.rostrumService.getBalance(addrDTO.address);
|
|
462
|
+
addrDTO.tokensBalance = await this.rostrumService.getTokensBalance(addrDTO.address);
|
|
463
|
+
|
|
464
|
+
const history = await this.transactionService.fetchTransactionsHistory(addrDTO.address, addrDTO.height);
|
|
465
|
+
addrDTO.height = history.lastHeight;
|
|
466
|
+
if (!addrDTO.used) {
|
|
467
|
+
addrDTO.used = history.txs.length > 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await this.walletDb.saveAddress(addrDTO);
|
|
471
|
+
|
|
472
|
+
return { address: addrDTO, txs: history.txs };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async fetchAndUpdateAccount(account: AccountDTO, hash: string): Promise<AddressHistory> {
|
|
476
|
+
account.statusHash = hash;
|
|
477
|
+
account.balance = await this.rostrumService.getBalance(account.address);
|
|
478
|
+
account.tokensBalance = await this.rostrumService.getTokensBalance(account.address);
|
|
479
|
+
|
|
480
|
+
const history = await this.transactionService.fetchTransactionsHistory(account.address, account.height);
|
|
481
|
+
account.height = history.lastHeight;
|
|
482
|
+
|
|
483
|
+
await this.walletDb.saveAccount(account);
|
|
484
|
+
|
|
485
|
+
return { address: account, txs: history.txs };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private async fetchAndUpdateVault(vault: VaultDTO, hash: string): Promise<AddressHistory> {
|
|
489
|
+
vault.statusHash = hash;
|
|
490
|
+
vault.balance = await this.rostrumService.getBalance(vault.address);
|
|
491
|
+
|
|
492
|
+
const history = await this.transactionService.fetchTransactionsHistory(vault.address, vault.height);
|
|
493
|
+
vault.height = history.lastHeight;
|
|
494
|
+
|
|
495
|
+
await this.walletDb.saveVault(vault);
|
|
496
|
+
this.notify({ type: 'vault_balance_updated', address: vault.address, balance: vault.balance });
|
|
497
|
+
|
|
498
|
+
return { address: vault, txs: history.txs };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private async checkGapLimit(keySpace: KeySpace): Promise<void> {
|
|
502
|
+
const addresses = keySpace === KeySpace.RECEIVE
|
|
503
|
+
? this.receiveAddresses
|
|
504
|
+
: this.changeAddresses;
|
|
505
|
+
|
|
506
|
+
const spaceName = keySpace === KeySpace.RECEIVE ? 'Receive' : 'Change';
|
|
507
|
+
const unused = addresses.filter(a => !a.used).length;
|
|
508
|
+
|
|
509
|
+
if (unused < WalletManager.GAP_LIMIT) {
|
|
510
|
+
const needed = WalletManager.GAP_LIMIT - unused;
|
|
511
|
+
const lastIndex = addresses[addresses.length - 1].idx;
|
|
512
|
+
|
|
513
|
+
console.log(`Deriving ${needed} more ${spaceName} addresses...`);
|
|
514
|
+
|
|
515
|
+
const newAddresses = this.deriveAddresses(keySpace, lastIndex + 1, needed);
|
|
516
|
+
|
|
517
|
+
if (keySpace === KeySpace.RECEIVE) {
|
|
518
|
+
this.receiveAddresses.push(...newAddresses);
|
|
519
|
+
} else {
|
|
520
|
+
this.changeAddresses.push(...newAddresses);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await this.saveAddresses(newAddresses);
|
|
524
|
+
await this.subscribeAddresses(newAddresses);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public async addNewAccount(id: number, name: string): Promise<void> {
|
|
529
|
+
const path = keyPathToString(AccountType.DAPP, 0, id);
|
|
530
|
+
const address = this.keyManager.getKey(path).privateKey.toAddress().toString();
|
|
531
|
+
|
|
532
|
+
const account: AccountDTO = {
|
|
533
|
+
id: id,
|
|
534
|
+
name: name,
|
|
535
|
+
address: address,
|
|
536
|
+
height: 0,
|
|
537
|
+
hidden: 0,
|
|
538
|
+
statusHash: '',
|
|
539
|
+
balance: { confirmed: "0", unconfirmed: "0" },
|
|
540
|
+
tokensBalance: {},
|
|
541
|
+
type: AccountType.DAPP
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
this.accounts.set(id, account);
|
|
545
|
+
this.accountsAddressToId.set(address, id);
|
|
546
|
+
await this.walletDb.saveAccount(account);
|
|
547
|
+
await this.subscribeAddresses([account]);
|
|
548
|
+
|
|
549
|
+
this.notify({
|
|
550
|
+
type: 'new_account',
|
|
551
|
+
account: {
|
|
552
|
+
id: account.id,
|
|
553
|
+
name: account.name,
|
|
554
|
+
address: account.address,
|
|
555
|
+
balance: account.balance,
|
|
556
|
+
tokensBalance: account.tokensBalance,
|
|
557
|
+
tokens: [],
|
|
558
|
+
sessions: {}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
public async updateAccountName(id: number, name: string): Promise<void> {
|
|
564
|
+
if (!name) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const account = this.accounts.get(id)!;
|
|
569
|
+
account.name = name;
|
|
570
|
+
|
|
571
|
+
await this.walletDb.updateAccountName(id, name);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
public rescanAccount(id: number): void {
|
|
575
|
+
if (id == MAIN_WALLET_ID) {
|
|
576
|
+
const addrs = this.getMainAddresses();
|
|
577
|
+
for (const addr of addrs) {
|
|
578
|
+
addr.height = 0;
|
|
579
|
+
this.registerUpdate({address: addr, result: addr.statusHash});
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
const account = this.accounts.get(id)!
|
|
583
|
+
account.height = 0;
|
|
584
|
+
this.registerUpdate({address: account, result: account.statusHash});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
public getVaultNextIndex(): number {
|
|
589
|
+
if (this.vaults.size == 0) {
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
return Math.max(...Array.from(this.vaults.values(), v => v.idx)) + 1;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public async addNewVault(address: string, block: number, index: number): Promise<void> {
|
|
596
|
+
const vault: VaultDTO = {
|
|
597
|
+
address: address,
|
|
598
|
+
block: block,
|
|
599
|
+
idx: index,
|
|
600
|
+
height: 0,
|
|
601
|
+
statusHash: '',
|
|
602
|
+
balance: { confirmed: "0", unconfirmed: "0" },
|
|
603
|
+
tokensBalance: {},
|
|
604
|
+
type: AccountType.VAULT
|
|
605
|
+
};
|
|
606
|
+
await this.addVault(vault);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private async addVault(vault: VaultDTO): Promise<void> {
|
|
610
|
+
this.vaults.set(vault.address, vault);
|
|
611
|
+
await this.walletDb.saveVault(vault);
|
|
612
|
+
await this.subscribeAddresses([vault]);
|
|
613
|
+
|
|
614
|
+
this.notify({
|
|
615
|
+
type: 'new_vault',
|
|
616
|
+
vault: {
|
|
617
|
+
address: vault.address,
|
|
618
|
+
balance: vault.balance,
|
|
619
|
+
block: vault.block,
|
|
620
|
+
index: vault.idx
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
public async rescanVaults(): Promise<boolean> {
|
|
626
|
+
let found = false;
|
|
627
|
+
const vaults = await this.discoveryService.discoverVaults(this.getMainAddresses().map(a => a.address));
|
|
628
|
+
const promises: Promise<void>[] = [];
|
|
629
|
+
for (const [address, vault] of vaults) {
|
|
630
|
+
if (!this.vaults.has(address)) {
|
|
631
|
+
found = true;
|
|
632
|
+
promises.push(this.addVault(vault));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
await Promise.all(promises);
|
|
636
|
+
return found;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private isVaultAddress(address: AccountDTO | AddressDTO | VaultDTO): address is VaultDTO {
|
|
640
|
+
return address.type == AccountType.VAULT;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private isAccountAddress(address: AccountDTO | AddressDTO | VaultDTO): address is AccountDTO {
|
|
644
|
+
return address.type == AccountType.DAPP;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private isMainAddress(address: AccountDTO | AddressDTO | VaultDTO): address is AddressDTO {
|
|
648
|
+
return address.type == AccountType.MAIN;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useSelector } from "react-redux";
|
|
2
|
+
import type { SharedState } from "./store";
|
|
3
|
+
import { createSelector } from "@reduxjs/toolkit";
|
|
4
|
+
import type { AuthState } from "./slices";
|
|
5
|
+
import type { Account, AppNotification, Balance, SessionInfo, VaultInfo } from "../types";
|
|
6
|
+
|
|
7
|
+
const useSharedSelector = useSelector.withTypes<SharedState>();
|
|
8
|
+
|
|
9
|
+
export const useAuth = (): AuthState => useSharedSelector(state => state.auth);
|
|
10
|
+
|
|
11
|
+
export const useBlockHeight = (): number => useSharedSelector(state => state.status.height);
|
|
12
|
+
|
|
13
|
+
export const useAccount = (id: number): Account => useSharedSelector(state => state.wallet.accounts[id]);
|
|
14
|
+
export const useSelectedAccount = (): number => useSharedSelector(state => state.wallet.selectedAccount);
|
|
15
|
+
export const useLastAccountId = (): number => useSharedSelector(state => Math.max(...Object.keys(state.wallet.accounts).map(Number)));
|
|
16
|
+
|
|
17
|
+
export const useMainReceiveAddress = (account: number): string => useSharedSelector(state => state.wallet.accounts[account].address);
|
|
18
|
+
|
|
19
|
+
export const useWalletBalance = (account: number): Balance => useSharedSelector(state => state.wallet.accounts[account].balance);
|
|
20
|
+
export const useTokenBalance = (account: number, tokenIdHex: string): Balance =>
|
|
21
|
+
useSharedSelector(state => state.wallet.accounts[account].tokensBalance[tokenIdHex]);
|
|
22
|
+
|
|
23
|
+
export const useDAppSession = (account: number, session: string): SessionInfo =>
|
|
24
|
+
useSharedSelector(state => state.wallet.accounts[account].sessions[session]);
|
|
25
|
+
|
|
26
|
+
export const useVaults = (): Record<string, VaultInfo> => useSharedSelector(state => state.wallet.vaults);
|
|
27
|
+
|
|
28
|
+
const selectWalletNotificationsArray = createSelector(
|
|
29
|
+
[(state: SharedState) => state.notifications.wallet],
|
|
30
|
+
(wallet) => Object.values(wallet).sort((a, b) => b.createdAt - a.createdAt)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const selectWeb3NotificationsArray = createSelector(
|
|
34
|
+
[(state: SharedState) => state.notifications.web3],
|
|
35
|
+
(web3) => Object.values(web3).sort((a, b) => b.createdAt - a.createdAt)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const selectAllNotificationsArray = createSelector(
|
|
39
|
+
[selectWalletNotificationsArray, selectWeb3NotificationsArray],
|
|
40
|
+
(wallet, web3) => [...wallet, ...web3].sort((a, b) => b.createdAt - a.createdAt)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const useWalletNotifications = (): AppNotification[] => useSharedSelector(selectWalletNotificationsArray);
|
|
44
|
+
export const useWeb3Notifications = (): AppNotification[] => useSharedSelector(selectWeb3NotificationsArray);
|
|
45
|
+
export const useAllNotifications = (): AppNotification[] => useSharedSelector(selectAllNotificationsArray);
|