@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/index.d.ts +2278 -0
  4. package/dist/index.js +2005 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +56 -0
  7. package/src/index.ts +5 -0
  8. package/src/persistence/datastore/db.ts +47 -0
  9. package/src/persistence/datastore/index.ts +2 -0
  10. package/src/persistence/datastore/kv.ts +129 -0
  11. package/src/persistence/index.ts +2 -0
  12. package/src/persistence/wallet-db.ts +251 -0
  13. package/src/services/asset.ts +220 -0
  14. package/src/services/cache.ts +54 -0
  15. package/src/services/discovery.ts +110 -0
  16. package/src/services/index.ts +8 -0
  17. package/src/services/key.ts +55 -0
  18. package/src/services/rostrum.ts +214 -0
  19. package/src/services/session.ts +225 -0
  20. package/src/services/transaction.ts +388 -0
  21. package/src/services/tx-transformer.ts +244 -0
  22. package/src/services/wallet.ts +650 -0
  23. package/src/state/hooks.ts +45 -0
  24. package/src/state/index.ts +3 -0
  25. package/src/state/slices/auth.ts +28 -0
  26. package/src/state/slices/dapp.ts +32 -0
  27. package/src/state/slices/index.ts +6 -0
  28. package/src/state/slices/loader.ts +31 -0
  29. package/src/state/slices/notifications.ts +44 -0
  30. package/src/state/slices/status.ts +70 -0
  31. package/src/state/slices/wallet.ts +112 -0
  32. package/src/state/store.ts +24 -0
  33. package/src/types/dapp.types.ts +21 -0
  34. package/src/types/db.types.ts +142 -0
  35. package/src/types/index.ts +5 -0
  36. package/src/types/notification.types.ts +13 -0
  37. package/src/types/rostrum.types.ts +161 -0
  38. package/src/types/wallet.types.ts +62 -0
  39. package/src/utils/asset.ts +103 -0
  40. package/src/utils/common.ts +159 -0
  41. package/src/utils/enums.ts +22 -0
  42. package/src/utils/index.ts +7 -0
  43. package/src/utils/keypath.ts +15 -0
  44. package/src/utils/price.ts +40 -0
  45. package/src/utils/seed.ts +57 -0
  46. package/src/utils/vault.ts +39 -0
@@ -0,0 +1,129 @@
1
+ import type { RostrumParams } from "../../types/rostrum.types";
2
+ import { isNullOrEmpty, isTestnet } from "../../utils/common";
3
+
4
+ export abstract class KVStore {
5
+
6
+ public abstract getValue(key: string): Promise<string | null>;
7
+ public abstract setValue(key: string, value: string): Promise<void>;
8
+ public abstract removeValue(key: string): Promise<void>;
9
+ public abstract clearData(): Promise<void>;
10
+
11
+ protected getPrefix(): string {
12
+ return isTestnet() ? 'testnet-' : '';
13
+ }
14
+
15
+ public async getEncryptedSeed(): Promise<string | null> {
16
+ return this.getValue("seed");
17
+ }
18
+
19
+ public async saveEncryptedSeed(seed: string): Promise<void> {
20
+ return this.setValue('seed', seed);
21
+ }
22
+
23
+ public async getVersionCode(): Promise<string | null> {
24
+ return this.getValue("version-code");
25
+ }
26
+
27
+ public async setVersionCode(code: string): Promise<void> {
28
+ return this.setValue('version-code', code);
29
+ }
30
+
31
+ public async getReleaseNumber(): Promise<string | null> {
32
+ return this.getValue("release-number");
33
+ }
34
+
35
+ public async setReleaseNumber(release: string): Promise<void> {
36
+ return this.setValue('release-number', release);
37
+ }
38
+
39
+ public async getRostrumParams(): Promise<RostrumParams | undefined> {
40
+ const params = await this.getValue(this.getPrefix() + "rostrum-params");
41
+ if (params) {
42
+ return JSON.parse(params);
43
+ }
44
+ }
45
+
46
+ public async saveRostrumParams(params: RostrumParams): Promise<void> {
47
+ return this.setValue(this.getPrefix() + "rostrum-params", JSON.stringify(params));
48
+ }
49
+
50
+ public async removeRostrumParams(): Promise<void> {
51
+ return this.removeValue(this.getPrefix() + "rostrum-params");
52
+ }
53
+
54
+ public async setHideZeroTokenConfig(hide: boolean): Promise<void> {
55
+ return this.setValue("zero-tokens", JSON.stringify(hide));
56
+ }
57
+
58
+ public async getHideZeroTokenConfig(): Promise<boolean> {
59
+ const hide = await this.getValue("zero-tokens");
60
+ return hide === "true";
61
+ }
62
+
63
+ public async setHideZeroVaultsConfig(hide: boolean): Promise<void> {
64
+ return this.setValue("zero-vaults", JSON.stringify(hide));
65
+ }
66
+
67
+ public async getHideZeroVaultsConfig(): Promise<boolean> {
68
+ const hide = await this.getValue("zero-vaults");
69
+ return hide === "true";
70
+ }
71
+
72
+ public async getSelectedCurrency(): Promise<string> {
73
+ const value = await this.getValue('selectedCurrency');
74
+ return value || 'usd';
75
+ }
76
+
77
+ public async setSelectedCurrency(currency: string): Promise<void> {
78
+ return this.setValue('selectedCurrency', currency);
79
+ }
80
+
81
+ public async getUseBiometric(): Promise<boolean> {
82
+ const value = await this.getValue('biometrics');
83
+ return value === "true";
84
+ }
85
+
86
+ public async setUseBiometric(useBiometric: boolean): Promise<void> {
87
+ return this.setValue('biometrics', JSON.stringify(useBiometric));
88
+ }
89
+
90
+ public async getRequireAuth(): Promise<boolean> {
91
+ const value = await this.getValue('auth');
92
+ return isNullOrEmpty(value) || value === "true";
93
+ }
94
+
95
+ public async setRequireAuth(requireAuth: boolean): Promise<void> {
96
+ await this.setValue('auth', JSON.stringify(requireAuth));
97
+ }
98
+
99
+ public async getReleaseNotesTime(): Promise<number | undefined> {
100
+ const value = await this.getValue('show-notes');
101
+ return value ? Number(value) : undefined;
102
+ }
103
+
104
+ public async setReleaseNotesTime(time: string): Promise<void> {
105
+ if (isNullOrEmpty(time)) {
106
+ await this.removeValue('show-notes');
107
+ } else {
108
+ await this.setValue('show-notes', time);
109
+ }
110
+ }
111
+
112
+ public async getIsTestnet(): Promise<boolean> {
113
+ const value = await this.getValue('testnet');
114
+ return value === "1";
115
+ }
116
+
117
+ public async setIsTestnet(isTestnet: boolean): Promise<void> {
118
+ await this.setValue('testnet', isTestnet ? "1" : "0");
119
+ }
120
+
121
+ public async getAutoLockSeconds(): Promise<number> {
122
+ const value = await this.getValue('auto-lock');
123
+ return value ? parseInt(value) : 60;
124
+ }
125
+
126
+ public async setAutoLockSeconds(autoLock: number): Promise<void> {
127
+ await this.setValue('auto-lock', `${autoLock}`);
128
+ }
129
+ }
@@ -0,0 +1,2 @@
1
+ export * from './datastore';
2
+ export * from './wallet-db';
@@ -0,0 +1,251 @@
1
+ import type { DBStore } from "./datastore/db";
2
+ import { AccountType, AssetType } from "../utils/enums";
3
+ import type { AccountDTO, AccountEntity, AddressDTO, AddressEntity, AssetEntity, AssetTransactionEntity, NftEntity, SessionEntity, TokenEntity, TransactionEntity, VaultDTO, VaultEntity } from "../types/db.types";
4
+ import { currentTimestamp } from "../utils/common";
5
+
6
+ export type DBEvent =
7
+ | { type: 'tx_refresh'; }
8
+ | { type: 'nft_refresh'; }
9
+ | { type: 'token_added'; accountId: number; token: AssetEntity; }
10
+ | { type: 'token_removed'; accountId: number; tokenId: string; }
11
+ | { type: 'nft_deleted'; id: string; }
12
+
13
+ export type DBUpdateCallback = (event: DBEvent) => void;
14
+
15
+ export class WalletDB {
16
+
17
+ private readonly store: DBStore;
18
+ private updateCallback?: DBUpdateCallback;
19
+
20
+ constructor(store: DBStore) {
21
+ this.store = store;
22
+ }
23
+
24
+ public onUpdate(callback: DBUpdateCallback): void {
25
+ this.updateCallback = callback;
26
+ }
27
+
28
+ private notify(event: DBEvent): void {
29
+ this.updateCallback?.(event);
30
+ }
31
+
32
+ public async initSchema(): Promise<boolean> {
33
+ return this.store.initSchema();
34
+ }
35
+
36
+ public async clearData(): Promise<void> {
37
+ return this.store.clearData();
38
+ }
39
+
40
+ public async addLocalTransaction(tx: TransactionEntity): Promise<void> {
41
+ try {
42
+ await this.store.upsertTransaction(tx);
43
+ this.notify({ type: 'tx_refresh' });
44
+ } catch (error) {
45
+ console.error(error);
46
+ }
47
+ }
48
+
49
+ public async addAssetTransaction(assetTx: AssetTransactionEntity): Promise<void> {
50
+ return this.store.upsertAssetTransaction(assetTx);
51
+ }
52
+
53
+ public async getLocalTransactions(account: number, tokenId?: string): Promise<TransactionEntity[] | undefined> {
54
+ return this.store.getTransactions(account, tokenId);
55
+ }
56
+
57
+ public async getPageLocalTransactions(account: number, pageNum: number, pageSize: number, tokenId?: string): Promise<TransactionEntity[] | undefined> {
58
+ return this.store.getPageTransactions(account, pageNum, pageSize, tokenId);
59
+ }
60
+
61
+ public async countLocalTransactions(account: number, tokenId?: string): Promise<number> {
62
+ return this.store.countTransactions(account, tokenId);
63
+ }
64
+
65
+ public async clearLocalTransactions(account: number): Promise<void> {
66
+ await this.store.clearTransactions(account);
67
+ this.notify({ type: 'tx_refresh' });
68
+ }
69
+
70
+ public async saveToken(account: number, token: TokenEntity): Promise<void> {
71
+ const asset: AssetEntity = {
72
+ accountId: account,
73
+ tokenIdHex: token.tokenIdHex,
74
+ type: AssetType.TOKEN,
75
+ addedTime: currentTimestamp()
76
+ }
77
+ await this.store.upsertAsset(asset);
78
+ await this.store.upsertToken(token);
79
+ this.notify({ type: 'token_added', accountId: account, token: asset });
80
+ }
81
+
82
+ public async getLocalTokens(account: number): Promise<AssetEntity[] | undefined> {
83
+ return this.store.getTokens(account);
84
+ }
85
+
86
+ public async getToken(id: string): Promise<TokenEntity | undefined> {
87
+ return this.store.getToken(id);
88
+ }
89
+
90
+ public async deleteToken(account: number, tokenId: string): Promise<void> {
91
+ await this.removeAsset(account, tokenId);
92
+ const count = await this.store.countAssetsById(tokenId);
93
+ if (count == 0) {
94
+ await this.store.deleteToken(tokenId);
95
+ }
96
+ this.notify({ type: 'token_removed', accountId: account, tokenId: tokenId });
97
+ }
98
+
99
+ public async saveNft(asset: AssetEntity, nft: NftEntity): Promise<void> {
100
+ await this.store.upsertNft(nft);
101
+ await this.saveAsset(asset, true);
102
+ }
103
+
104
+ public async getLocalNfts(account: number, pageNum: number, pageSize: number): Promise<AssetEntity[] | undefined> {
105
+ return this.store.getNfts(account, pageNum, pageSize);
106
+ }
107
+
108
+ public async getLocalNft(id: string): Promise<NftEntity | undefined> {
109
+ return this.store.getNft(id);
110
+ }
111
+
112
+ public async deleteNft(account: number, tokenId: string, notify = false): Promise<void> {
113
+ await this.removeAsset(account, tokenId);
114
+ const count = await this.store.countAssetsById(tokenId);
115
+ if (count == 0) {
116
+ await this.store.deleteNft(tokenId);
117
+ this.notify({ type: 'nft_deleted', id: tokenId });
118
+ }
119
+ if (notify) {
120
+ this.notify({ type: 'nft_refresh' });
121
+ }
122
+ }
123
+
124
+ public async countLocalNfts(account: number): Promise<number> {
125
+ return this.store.countNfts(account);
126
+ }
127
+
128
+ public async isNftExist(id: string): Promise<boolean> {
129
+ return this.store.isNftExist(id);
130
+ }
131
+
132
+ public async saveAsset(asset: AssetEntity, notifyNft = false): Promise<void> {
133
+ await this.store.upsertAsset(asset);
134
+ if (notifyNft) {
135
+ this.notify({ type: 'nft_refresh' });
136
+ }
137
+ }
138
+
139
+ public async removeAsset(account: number, id: string): Promise<void> {
140
+ return this.store.deleteAsset(account, id);
141
+ }
142
+
143
+ public async isAssetExistForAccount(account: number, id: string): Promise<boolean> {
144
+ return this.store.isAssetExistForAccount(account, id);
145
+ }
146
+
147
+ public async saveAccount(account: AccountDTO): Promise<void> {
148
+ const accountEntity: AccountEntity = {
149
+ id: account.id,
150
+ address: account.address,
151
+ name: account.name,
152
+ height: account.height,
153
+ hidden: account.hidden,
154
+ statusHash: account.statusHash,
155
+ balance: JSON.stringify(account.balance),
156
+ tokensBalance: JSON.stringify(account.tokensBalance)
157
+ }
158
+ return this.store.upsertAccount(accountEntity);
159
+ }
160
+
161
+ public async getAccounts(): Promise<AccountDTO[]> {
162
+ const accs = await this.store.getAccounts();
163
+ return accs.map(a => ({
164
+ ...a,
165
+ type: AccountType.DAPP,
166
+ balance: JSON.parse(a.balance),
167
+ tokensBalance: JSON.parse(a.tokensBalance)
168
+ }));
169
+ }
170
+
171
+ public async countAccounts(): Promise<number> {
172
+ return this.store.countAccounts();
173
+ }
174
+
175
+ public async updateAccountName(account: number, name: string): Promise<void> {
176
+ return this.store.updateAccountName(account, name);
177
+ }
178
+
179
+ public async saveSession(session: SessionEntity): Promise<void> {
180
+ return this.store.upsertSession(session);
181
+ }
182
+
183
+ public async getAccountSessions(accountId: number): Promise<SessionEntity[]> {
184
+ return this.store.getSessionsByAccount(accountId);
185
+ }
186
+
187
+ public async removeSession(sessionId: string): Promise<void> {
188
+ return this.store.deleteSession(sessionId);
189
+ }
190
+
191
+ public async saveAddress(address: AddressDTO): Promise<void> {
192
+ const addressEntity: AddressEntity = {
193
+ address: address.address,
194
+ idx: address.idx,
195
+ space: address.space,
196
+ height: address.height,
197
+ statusHash: address.statusHash,
198
+ used: address.used,
199
+ balance: JSON.stringify(address.balance),
200
+ tokensBalance: JSON.stringify(address.tokensBalance)
201
+ }
202
+ return this.store.upsertAddress(addressEntity);
203
+ }
204
+
205
+ public async getReceiveAddresses(): Promise<AddressDTO[]> {
206
+ const addrs = await this.store.getReceiveAddresses();
207
+ return addrs.map(a => ({
208
+ ...a,
209
+ type: AccountType.MAIN,
210
+ balance: JSON.parse(a.balance),
211
+ tokensBalance: JSON.parse(a.tokensBalance)
212
+ }));
213
+ }
214
+
215
+ public async getChangeAddresses(): Promise<AddressDTO[]> {
216
+ const addrs = await this.store.getChangeAddresses();
217
+ return addrs.map(a => ({
218
+ ...a,
219
+ type: AccountType.MAIN,
220
+ balance: JSON.parse(a.balance),
221
+ tokensBalance: JSON.parse(a.tokensBalance)
222
+ }));
223
+ }
224
+
225
+ public async countAddresses(): Promise<number> {
226
+ return this.store.countAddresses();
227
+ }
228
+
229
+ public async getVaults(): Promise<VaultDTO[]> {
230
+ const vaults = await this.store.getVaults();
231
+ return vaults.map(v => ({
232
+ ...v,
233
+ type: AccountType.VAULT,
234
+ balance: JSON.parse(v.balance),
235
+ tokensBalance: JSON.parse(v.tokensBalance)
236
+ }));
237
+ }
238
+
239
+ public async saveVault(vault: VaultDTO): Promise<void> {
240
+ const vaultEntity: VaultEntity = {
241
+ address: vault.address,
242
+ idx: vault.idx,
243
+ block: vault.block,
244
+ height: vault.height,
245
+ statusHash: vault.statusHash,
246
+ balance: JSON.stringify(vault.balance),
247
+ tokensBalance: JSON.stringify(vault.tokensBalance)
248
+ };
249
+ return this.store.upsertVault(vaultEntity);
250
+ }
251
+ }
@@ -0,0 +1,220 @@
1
+ import { Address, AddressType, BufferUtils, GroupIdType, GroupToken, Networks } from "libnexa-ts";
2
+ import type { AssetEntity, NftEntity, TokenEntity } from "../types/db.types";
3
+ import { fetchAssetBlob, fetchAssetDoc, getNiftyToken, isNiftySubgroup, transformTokenIconUrl } from "../utils/asset";
4
+ import { currentTimestamp, getAddressBuffer, tokenHexToAddr, tokenIdToHex } from "../utils/common";
5
+ import { AssetType } from "../utils/enums";
6
+ import type { WalletDB } from "../persistence/wallet-db";
7
+ import type { RostrumService } from "./rostrum";
8
+ import type { WalletCache } from "./cache";
9
+ import JSZip from "jszip";
10
+ import type { AssetInfo, Balance } from "../types/wallet.types";
11
+
12
+ export class AssetService {
13
+
14
+ private readonly walletDb: WalletDB;
15
+ private readonly rostrumService: RostrumService;
16
+ private readonly walletCache: WalletCache;
17
+
18
+ constructor(walletDb: WalletDB, rostrumService: RostrumService, walletCache: WalletCache) {
19
+ this.walletDb = walletDb;
20
+ this.rostrumService = rostrumService;
21
+ this.walletCache = walletCache;
22
+ }
23
+
24
+ public async getTokenInfo(token: string, checkCache = true): Promise<TokenEntity | undefined> {
25
+ try {
26
+ if (checkCache) {
27
+ const cachedToken = await this.walletCache.getTokenById(token);
28
+ if (cachedToken) {
29
+ return cachedToken;
30
+ }
31
+ }
32
+
33
+ const genesis = await this.rostrumService.getTokenGenesis(token);
34
+ const groupId = BufferUtils.hexToBuffer(genesis.token_id_hex);
35
+ let parent = "";
36
+ let iconUrl = "";
37
+
38
+ if (genesis.op_return_id == GroupIdType.NRC2 || genesis.op_return_id == GroupIdType.NRC3 || isNiftySubgroup(token)) {
39
+ return undefined;
40
+ }
41
+
42
+ if (GroupToken.isSubgroup(groupId)) {
43
+ parent = new Address(GroupToken.getParentGroupId(groupId), Networks.defaultNetwork, AddressType.GroupIdAddress).toString();
44
+ }
45
+
46
+ if (genesis.document_url) {
47
+ try {
48
+ if (genesis.op_return_id == GroupIdType.NRC1) {
49
+ const token_zip = await fetchAssetBlob(genesis.document_url);
50
+ const zip = await JSZip.loadAsync(token_zip);
51
+ const info = zip.file('info.json');
52
+ if (info) {
53
+ const infoData = await info.async('string');
54
+ const infoJson = JSON.parse(infoData);
55
+ iconUrl = transformTokenIconUrl(infoJson[0]?.icon, genesis.document_url);
56
+ }
57
+ } else {
58
+ const infoJson = await fetchAssetDoc(genesis.document_url);
59
+ iconUrl = transformTokenIconUrl(infoJson[0]?.icon, genesis.document_url);
60
+ }
61
+ } catch (e) {
62
+ console.error("Failed to load metadata", e)
63
+ }
64
+ }
65
+
66
+ const tokenEntity: TokenEntity = {
67
+ token: genesis.group,
68
+ tokenIdHex: genesis.token_id_hex,
69
+ decimals: genesis.decimal_places ?? 0,
70
+ parentGroup: parent,
71
+ name: genesis.name ?? "",
72
+ ticker: genesis.ticker ?? "",
73
+ iconUrl: iconUrl
74
+ };
75
+
76
+ return tokenEntity;
77
+ } catch (e) {
78
+ console.error(e)
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ public async isNftToken(token: string): Promise<boolean> {
84
+ try {
85
+ if (isNiftySubgroup(token)) {
86
+ return true;
87
+ }
88
+
89
+ const cachedToken = await this.walletCache.getNftById(token);
90
+ if (cachedToken) {
91
+ return true;
92
+ }
93
+
94
+ const genesis = await this.rostrumService.getTokenGenesis(token);
95
+ if (genesis.op_return_id == GroupIdType.NRC3) {
96
+ return true;
97
+ }
98
+ } catch (e) {
99
+ console.error(e)
100
+ }
101
+
102
+ return false;
103
+ }
104
+
105
+ public async getAssetInfo(token: string): Promise<AssetInfo | undefined> {
106
+ let asset: AssetInfo | undefined = await this.getTokenInfo(token);
107
+ if (!asset) {
108
+ const isNft = await this.isNftToken(token);
109
+ if (isNft) {
110
+ asset = {
111
+ token: tokenHexToAddr(token),
112
+ tokenIdHex: tokenIdToHex(token),
113
+ decimals: 0,
114
+ };
115
+ }
116
+ }
117
+ return asset;
118
+ }
119
+
120
+ public async handleNftReceive(accountId: number, tokenIdHex: string, time: number): Promise<void> {
121
+ const exist = await this.walletDb.isNftExist(tokenIdHex);
122
+ if (exist) {
123
+ const asset: AssetEntity = {
124
+ accountId: accountId,
125
+ tokenIdHex: tokenIdHex,
126
+ type: AssetType.NFT,
127
+ addedTime: time
128
+ }
129
+ await this.walletDb.saveAsset(asset, true);
130
+ return;
131
+ }
132
+
133
+ const genesis = await this.rostrumService.getTokenGenesis(tokenIdHex);
134
+ if (genesis.op_return_id == GroupIdType.NRC3) {
135
+ const groupId = getAddressBuffer(tokenIdHex);
136
+ if (GroupToken.isSubgroup(groupId)) {
137
+ const parentGroupId = new Address(groupId.subarray(0, 32), Networks.defaultNetwork, AddressType.GroupIdAddress).toString();
138
+ const parentGenesis = await this.rostrumService.getTokenGenesis(parentGroupId);
139
+ if (parentGenesis?.op_return_id == GroupIdType.NRC2) {
140
+ // parent is an NRC-2 collection, this is an NRC-3 NFT. The name of the collection and NRC-3 NFT belongs
141
+ // to is the name of the NRC-2 collection.
142
+ await this.saveNft(accountId, tokenIdHex, genesis.document_url ?? '', parentGroupId, time, parentGenesis?.name ?? "");
143
+ }
144
+ }
145
+ } else if (isNiftySubgroup(tokenIdHex)) {
146
+ // NiftyArt does not have collections, pass "" for the collection
147
+ await this.saveNft(accountId, tokenIdHex, 'nifty', getNiftyToken().token, time, "");
148
+ }
149
+ }
150
+
151
+ private async saveNft(account: number, hexId: string, source: string, parent: string, time: number, collection: string): Promise<void> {
152
+ if (!source) {
153
+ return;
154
+ }
155
+
156
+ try {
157
+ const asset: AssetEntity = {
158
+ accountId: account,
159
+ tokenIdHex: hexId,
160
+ type: AssetType.NFT,
161
+ addedTime: time
162
+ }
163
+ const nftEntity: NftEntity = {
164
+ parentGroup: parent,
165
+ token: tokenHexToAddr(hexId),
166
+ tokenIdHex: hexId,
167
+ source: source,
168
+ collection: collection
169
+ }
170
+
171
+ await this.walletDb.saveNft(asset, nftEntity);
172
+ } catch (e) {
173
+ console.error('failed to save NFT', e);
174
+ }
175
+ }
176
+
177
+ public async fetchAndSaveTokens(accountId: number, tokenBalances: Record<string, Balance>): Promise<void> {
178
+ for (const tokenId in tokenBalances) {
179
+ const token = await this.getTokenInfo(tokenId);
180
+ if (token) {
181
+ await this.walletDb.saveToken(accountId, token);
182
+ }
183
+ }
184
+ }
185
+
186
+ public async syncNfts(accountId: number, tokensBalance: Record<string, Balance>): Promise<void> {
187
+ const currentAssets = Object.keys(tokensBalance);
188
+ const internalAssets = await this.walletDb.getLocalNfts(accountId, 1, 1000);
189
+
190
+ const handleNftDelete = async (tokenId: string): Promise<void> => {
191
+ await this.walletDb.deleteNft(accountId, tokenId, true);
192
+ this.walletCache.removeNft(tokenId);
193
+ }
194
+
195
+ const staleAssets = internalAssets?.filter(a => !currentAssets.includes(a.tokenIdHex)) ?? [];
196
+ for (const asset of staleAssets) {
197
+ await handleNftDelete(asset.tokenIdHex);
198
+ }
199
+
200
+ for (const [token, balance] of Object.entries(tokensBalance)) {
201
+ const tokenId = tokenIdToHex(token);
202
+ const hasBalance = balance && BigInt(balance.confirmed) + BigInt(balance.unconfirmed) > 0n;
203
+ const isAssetExist = internalAssets?.some(a => a.tokenIdHex == tokenId);
204
+ if ((!hasBalance && !isAssetExist) || (hasBalance && isAssetExist)) {
205
+ continue;
206
+ }
207
+
208
+ const isNft = await this.isNftToken(tokenId);
209
+ if (!isNft) {
210
+ continue;
211
+ }
212
+
213
+ if (!hasBalance && isAssetExist) {
214
+ await handleNftDelete(tokenId);
215
+ } else {
216
+ await this.handleNftReceive(accountId, tokenId, currentTimestamp());
217
+ }
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,54 @@
1
+ import type { NftEntity, TokenEntity } from "../types/db.types";
2
+ import { tokenIdToHex } from "../utils/common";
3
+ import type { WalletDB } from "../persistence/wallet-db";
4
+
5
+ export class WalletCache {
6
+
7
+ private readonly walletDb: WalletDB;
8
+
9
+ private readonly tokens = new Map<string, TokenEntity>();
10
+ private readonly nfts = new Map<string, NftEntity>();
11
+
12
+ public constructor(walletDb: WalletDB) {
13
+ this.walletDb = walletDb;
14
+ }
15
+
16
+ public clear(): void {
17
+ this.tokens.clear();
18
+ this.nfts.clear();
19
+ }
20
+
21
+ public async getTokenById(id: string): Promise<TokenEntity | undefined> {
22
+ id = tokenIdToHex(id);
23
+ if (this.tokens.has(id)) {
24
+ return this.tokens.get(id)!;
25
+ }
26
+
27
+ const token = await this.walletDb.getToken(id);
28
+ if (token) {
29
+ this.tokens.set(id, token);
30
+ }
31
+ return token;
32
+ }
33
+
34
+ public async getNftById(id: string): Promise<NftEntity | undefined> {
35
+ id = tokenIdToHex(id);
36
+ if (this.nfts.has(id)) {
37
+ return this.nfts.get(id)!;
38
+ }
39
+
40
+ const nft = await this.walletDb.getLocalNft(id);
41
+ if (nft) {
42
+ this.nfts.set(id, nft);
43
+ }
44
+ return nft;
45
+ }
46
+
47
+ public removeToken(id: string): void {
48
+ this.tokens.delete(tokenIdToHex(id));
49
+ }
50
+
51
+ public removeNft(id: string): void {
52
+ this.nfts.delete(tokenIdToHex(id));
53
+ }
54
+ }