@subwallet/extension-base 1.3.79-1 → 1.3.80-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/background/KoniTypes.d.ts +13 -3
- package/cjs/core/substrate/system-pallet.js +3 -0
- package/cjs/koni/api/nft/index.js +0 -14
- package/cjs/koni/background/cron.js +0 -17
- package/cjs/koni/background/handlers/Extension.js +6 -5
- package/cjs/koni/background/handlers/State.js +24 -6
- package/cjs/packageInfo.js +1 -1
- package/cjs/services/balance-service/helpers/process.js +49 -21
- package/cjs/services/balance-service/index.js +2 -2
- package/cjs/services/chain-service/constants.js +6 -2
- package/cjs/services/earning-service/handlers/special.js +3 -2
- package/cjs/services/nft-service/index.js +219 -150
- package/cjs/services/nft-service/multi-chain-nft-fetcher.js +145 -0
- package/cjs/services/nft-service/nft-handlers/base-nft-handler.js +28 -0
- package/cjs/services/nft-service/nft-handlers/evm/evm-nft-handler.js +179 -0
- package/cjs/services/nft-service/nft-handlers/registry.js +37 -0
- package/cjs/services/nft-service/nft-handlers/unique/unique-nft-handler.js +187 -0
- package/cjs/services/storage-service/DatabaseService.js +1 -1
- package/core/substrate/system-pallet.js +3 -0
- package/koni/api/nft/index.js +1 -15
- package/koni/background/cron.d.ts +0 -1
- package/koni/background/cron.js +1 -18
- package/koni/background/handlers/Extension.d.ts +1 -0
- package/koni/background/handlers/Extension.js +6 -5
- package/koni/background/handlers/State.d.ts +8 -2
- package/koni/background/handlers/State.js +24 -6
- package/package.json +31 -6
- package/packageInfo.js +1 -1
- package/services/balance-service/helpers/process.d.ts +1 -1
- package/services/balance-service/helpers/process.js +48 -21
- package/services/balance-service/index.js +2 -2
- package/services/chain-service/constants.d.ts +3 -0
- package/services/chain-service/constants.js +3 -0
- package/services/earning-service/handlers/special.js +4 -3
- package/services/nft-service/index.d.ts +42 -6
- package/services/nft-service/index.js +219 -151
- package/services/nft-service/multi-chain-nft-fetcher.d.ts +13 -0
- package/services/nft-service/multi-chain-nft-fetcher.js +138 -0
- package/services/nft-service/nft-handlers/base-nft-handler.d.ts +13 -0
- package/services/nft-service/nft-handlers/base-nft-handler.js +21 -0
- package/services/nft-service/nft-handlers/evm/evm-nft-handler.d.ts +9 -0
- package/services/nft-service/nft-handlers/evm/evm-nft-handler.js +171 -0
- package/services/nft-service/nft-handlers/registry.d.ts +11 -0
- package/services/nft-service/nft-handlers/registry.js +29 -0
- package/services/nft-service/nft-handlers/unique/unique-nft-handler.d.ts +12 -0
- package/services/nft-service/nft-handlers/unique/unique-nft-handler.js +177 -0
- package/services/storage-service/DatabaseService.d.ts +1 -1
- package/services/storage-service/DatabaseService.js +1 -1
|
@@ -1,171 +1,239 @@
|
|
|
1
1
|
// Copyright 2019-2022 @subwallet/extension-base authors & contributors
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
4
|
+
import { ServiceStatus } from '@subwallet/extension-base/services/base/types';
|
|
5
|
+
import { _isChainSupportEvmNft, _isChainSupportNativeNft, _isChainSupportWasmNft } from '@subwallet/extension-base/services/chain-service/utils';
|
|
6
|
+
import { addLazy, createPromiseHandler, waitTimeout } from '@subwallet/extension-base/utils';
|
|
7
|
+
import keyring from '@subwallet/ui-keyring';
|
|
8
|
+
import { BehaviorSubject } from 'rxjs';
|
|
9
|
+
import { MultiChainNftFetcher } from "./multi-chain-nft-fetcher.js";
|
|
10
|
+
const INITIAL_NFT_STATE = {
|
|
11
|
+
nftData: {
|
|
12
|
+
total: 0,
|
|
13
|
+
nftList: []
|
|
14
|
+
},
|
|
15
|
+
nftCollections: []
|
|
16
|
+
};
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* Responsible for managing NFT detection jobs per address
|
|
14
|
-
*/
|
|
18
|
+
// HIGH PRIORITY – no lazy
|
|
19
|
+
const IMMEDIATE_EVENTS = ['account.updateCurrent', 'account.add', 'account.remove'];
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const attributes = Array.isArray(metadata.attributes) ? metadata.attributes : [];
|
|
21
|
-
let rarity;
|
|
22
|
-
const properties = {};
|
|
23
|
-
for (const attr of attributes) {
|
|
24
|
-
try {
|
|
25
|
-
var _attr$trait_type;
|
|
26
|
-
const key = (_attr$trait_type = attr.trait_type) === null || _attr$trait_type === void 0 ? void 0 : _attr$trait_type.trim();
|
|
27
|
-
if (!key) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
let value = attr.value;
|
|
31
|
-
if (typeof value === 'string') {
|
|
32
|
-
const lower = value.toLowerCase();
|
|
33
|
-
if (lower === 'true') {
|
|
34
|
-
value = true;
|
|
35
|
-
} else if (lower === 'false') {
|
|
36
|
-
value = false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
properties[key] = value;
|
|
40
|
-
if (key.toLowerCase() === 'rarity') {
|
|
41
|
-
rarity = String(value);
|
|
42
|
-
}
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
const hasProperties = Object.keys(properties).length > 0;
|
|
46
|
-
const normalizedType = (_rawInstance$token_ty = rawInstance.token_type) === null || _rawInstance$token_ty === void 0 ? void 0 : (_rawInstance$token_ty2 = _rawInstance$token_ty.replace('-', '')) === null || _rawInstance$token_ty2 === void 0 ? void 0 : _rawInstance$token_ty2.toUpperCase();
|
|
21
|
+
// LOW PRIORITY – lazy
|
|
22
|
+
const LAZY_EVENTS = ['asset.updateState', 'chain.add'];
|
|
23
|
+
export class NftService {
|
|
24
|
+
NFT_INTERVAL_TIME = 2 * 60 * 60 * 1000; // 2 hours
|
|
47
25
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
originAsset: undefined,
|
|
58
|
-
name: metadata.name || `#${rawInstance.id}`,
|
|
59
|
-
image: baseParseIPFSUrl(image),
|
|
60
|
-
externalUrl: rawInstance.external_app_url || undefined,
|
|
61
|
-
rarity,
|
|
62
|
-
description: metadata.description || undefined,
|
|
63
|
-
properties: hasProperties ? properties : null,
|
|
64
|
-
type: normalizedType === 'ERC721' ? _AssetType.ERC721 : _AssetType.ERC721,
|
|
65
|
-
// currently only support ERC721
|
|
66
|
-
rmrk_ver: undefined,
|
|
67
|
-
onChainOption: undefined,
|
|
68
|
-
assetHubType: undefined
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function mapSdkToCollection(raw, chain) {
|
|
72
|
-
var _raw$token_instances;
|
|
73
|
-
const token = raw.token || {};
|
|
74
|
-
return {
|
|
75
|
-
// must-have
|
|
76
|
-
collectionId: token.address_hash,
|
|
77
|
-
chain,
|
|
78
|
-
originAsset: undefined,
|
|
79
|
-
// optional
|
|
80
|
-
collectionName: token.name || token.symbol || 'Unknown Collection',
|
|
81
|
-
image: token.icon_url || undefined,
|
|
82
|
-
itemCount: Number(raw.amount) || ((_raw$token_instances = raw.token_instances) === null || _raw$token_instances === void 0 ? void 0 : _raw$token_instances.length) || 0,
|
|
83
|
-
externalUrl: undefined
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
export default class NftService {
|
|
87
|
-
inProgress = new Set();
|
|
26
|
+
nftStateSubject = new BehaviorSubject(INITIAL_NFT_STATE);
|
|
27
|
+
nftState$ = this.nftStateSubject.asObservable();
|
|
28
|
+
isReloading = false;
|
|
29
|
+
startPromiseHandler = createPromiseHandler();
|
|
30
|
+
stopPromiseHandler = createPromiseHandler();
|
|
31
|
+
status = ServiceStatus.NOT_INITIALIZED;
|
|
32
|
+
get isStarted() {
|
|
33
|
+
return this.status === ServiceStatus.STARTED;
|
|
34
|
+
}
|
|
88
35
|
constructor(state) {
|
|
89
36
|
this.state = state;
|
|
37
|
+
this.multiChainFetcher = new MultiChainNftFetcher(state);
|
|
38
|
+
}
|
|
39
|
+
async init() {
|
|
40
|
+
this.status = ServiceStatus.INITIALIZING;
|
|
41
|
+
await this.state.eventService.waitKeyringReady;
|
|
42
|
+
await this.state.eventService.waitChainReady;
|
|
43
|
+
await this.loadCachedData();
|
|
44
|
+
this.status = ServiceStatus.INITIALIZED;
|
|
45
|
+
this.state.eventService.onLazy(this.handleEvents.bind(this));
|
|
46
|
+
}
|
|
47
|
+
async loadCachedData() {
|
|
48
|
+
const [nftData, collections] = await Promise.all([this.state.getNft(), this.state.getNftCollection()]);
|
|
49
|
+
this.nftStateSubject.next({
|
|
50
|
+
nftData: nftData || {
|
|
51
|
+
total: 0,
|
|
52
|
+
nftList: []
|
|
53
|
+
},
|
|
54
|
+
nftCollections: collections || []
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async start() {
|
|
58
|
+
if (this.status === ServiceStatus.STOPPING) {
|
|
59
|
+
await this.waitForStopped();
|
|
60
|
+
}
|
|
61
|
+
if (this.isStarted || this.status === ServiceStatus.STARTING) {
|
|
62
|
+
return this.waitForStarted();
|
|
63
|
+
}
|
|
64
|
+
this.status = ServiceStatus.STARTING;
|
|
65
|
+
await this.refreshNftData();
|
|
66
|
+
this.status = ServiceStatus.STARTED;
|
|
67
|
+
this.startPromiseHandler.resolve();
|
|
68
|
+
this.startScanNft();
|
|
69
|
+
}
|
|
70
|
+
async stop() {
|
|
71
|
+
if (this.status === ServiceStatus.STARTING) {
|
|
72
|
+
await this.waitForStarted();
|
|
73
|
+
}
|
|
74
|
+
if (this.status === ServiceStatus.STOPPED || this.status === ServiceStatus.STOPPING) {
|
|
75
|
+
return this.waitForStopped();
|
|
76
|
+
}
|
|
77
|
+
this.status = ServiceStatus.STOPPING;
|
|
78
|
+
this.stopScanNft();
|
|
79
|
+
this.stopPromiseHandler.resolve();
|
|
80
|
+
}
|
|
81
|
+
waitForStarted() {
|
|
82
|
+
return this.startPromiseHandler.promise;
|
|
83
|
+
}
|
|
84
|
+
waitForStopped() {
|
|
85
|
+
return this.stopPromiseHandler.promise;
|
|
86
|
+
}
|
|
87
|
+
checkIfNftUpdateNeeded(events, eventTypes) {
|
|
88
|
+
if (!eventTypes.includes('chain.updateState')) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const updatedChains = this.extractUpdatedChains(events);
|
|
92
|
+
return this.hasNftSupportedChainUpdate(updatedChains);
|
|
93
|
+
}
|
|
94
|
+
extractUpdatedChains(events) {
|
|
95
|
+
return events.filter(event => event.type === 'chain.updateState').map(event => event.data[0]);
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
for (const col of collections) {
|
|
115
|
-
const mappedCollection = mapSdkToCollection(col, chain);
|
|
116
|
-
allCollections.push(mappedCollection);
|
|
117
|
-
if (Array.isArray(col.token_instances)) {
|
|
118
|
-
const items = col.token_instances.map(inst => mapSdkToNftItem(inst, chain, mappedCollection.collectionId, address)).filter(i => Boolean(i));
|
|
119
|
-
allItems.push(...items);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
await this.state.handleDetectedNftCollections(allCollections);
|
|
124
|
-
await this.state.handleDetectedNfts(address, allItems);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.warn(`[NftService] detect error for ${address}`, err);
|
|
127
|
-
} finally {
|
|
128
|
-
this.inProgress.delete(address);
|
|
129
|
-
}
|
|
97
|
+
hasNftSupportedChainUpdate(updatedChains) {
|
|
98
|
+
if (updatedChains.length === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const chainInfoMap = this.state.getServiceInfo().chainInfoMap;
|
|
102
|
+
return updatedChains.some(chainSlug => {
|
|
103
|
+
const chainInfo = chainInfoMap[chainSlug];
|
|
104
|
+
return this.isChainNftSupported(chainInfo);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
isChainNftSupported(chainInfo) {
|
|
108
|
+
return _isChainSupportNativeNft(chainInfo) || _isChainSupportEvmNft(chainInfo) || _isChainSupportWasmNft(chainInfo);
|
|
109
|
+
}
|
|
110
|
+
handleImmediateRefresh(address) {
|
|
111
|
+
this.state.resetNft(address);
|
|
112
|
+
this.refreshNftData().catch(console.error);
|
|
113
|
+
}
|
|
114
|
+
scheduleLazyRefresh(delay) {
|
|
115
|
+
addLazy('nft.refresh', () => {
|
|
116
|
+
if (!this.isReloading && this.isStarted) {
|
|
117
|
+
this.refreshNftData().catch(console.error);
|
|
130
118
|
}
|
|
119
|
+
}, delay, undefined, true);
|
|
120
|
+
}
|
|
121
|
+
handleEvents(events, eventTypes) {
|
|
122
|
+
const LAZY_REFRESH_DELAY = 1800;
|
|
123
|
+
const address = this.state.keyringService.context.currentAccount.proxyId;
|
|
124
|
+
const hasImmediateEvent = IMMEDIATE_EVENTS.some(event => eventTypes.includes(event));
|
|
125
|
+
const hasLazyEvent = LAZY_EVENTS.some(event => eventTypes.includes(event));
|
|
126
|
+
const needsNftUpdate = this.checkIfNftUpdateNeeded(events, eventTypes);
|
|
127
|
+
if (hasImmediateEvent || needsNftUpdate) {
|
|
128
|
+
this.handleImmediateRefresh(address);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (hasLazyEvent) {
|
|
132
|
+
this.scheduleLazyRefresh(LAZY_REFRESH_DELAY);
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
|
-
async
|
|
134
|
-
|
|
135
|
-
chainInfo,
|
|
136
|
-
contractAddress,
|
|
137
|
-
owners
|
|
138
|
-
} = request;
|
|
139
|
-
const chainId = _getEvmChainId(chainInfo);
|
|
140
|
-
if (!contractAddress || !owners || !chainId) {
|
|
141
|
-
console.warn('[NftService] missing params for getFullNftInstancesByCollection');
|
|
135
|
+
async fetchFullListNftOfACollection(request) {
|
|
136
|
+
if (this.isReloading) {
|
|
142
137
|
return false;
|
|
143
138
|
}
|
|
144
139
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const instances = await nftDetectionApi.getAllNftInstances(contractAddress, eachOwner, chainId.toString());
|
|
154
|
-
if (!Array.isArray(instances)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
console.log('FOR TESTER (before)', instances);
|
|
158
|
-
const nftList = instances.map(inst => mapSdkToNftItem(inst, chainInfo.slug, contractAddress, eachOwner)).filter(i => Boolean(i));
|
|
159
|
-
console.log('FOR TESTER (after)', nftList);
|
|
160
|
-
await this.state.handleDetectedNfts(eachOwner, nftList);
|
|
161
|
-
} catch (innerErr) {
|
|
162
|
-
console.warn(`[NftService] getAllNftInstances failed for ${eachOwner}`, innerErr);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
140
|
+
const result = await this.multiChainFetcher.fetchFullListNftOfACollection(request);
|
|
141
|
+
|
|
142
|
+
// Persist DB
|
|
143
|
+
this.persistNftData({
|
|
144
|
+
items: result.items,
|
|
145
|
+
collections: result.collections
|
|
146
|
+
});
|
|
165
147
|
return true;
|
|
166
|
-
} catch (
|
|
167
|
-
console.error(
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error('[NftServiceV2] fetchFullListNftOfaCollection failed', e);
|
|
168
150
|
return false;
|
|
169
151
|
}
|
|
170
152
|
}
|
|
153
|
+
async fetchNftDetail(request) {
|
|
154
|
+
if (this.isReloading) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = await this.multiChainFetcher.fetchNftDetail(request);
|
|
159
|
+
return result.items[0];
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error('[NftServiceV2] fetchNftDetail failed', e);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
startScanNft() {
|
|
166
|
+
this.stopScanNft();
|
|
167
|
+
const scanNft = () => {
|
|
168
|
+
if (!this.isStarted || this.isReloading) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.refreshNftData().catch(console.error);
|
|
172
|
+
};
|
|
173
|
+
this._intervalFetchNft = setInterval(scanNft, this.NFT_INTERVAL_TIME);
|
|
174
|
+
}
|
|
175
|
+
stopScanNft() {
|
|
176
|
+
this._intervalFetchNft && clearInterval(this._intervalFetchNft);
|
|
177
|
+
this._intervalFetchNft = undefined;
|
|
178
|
+
}
|
|
179
|
+
persistNftData(result) {
|
|
180
|
+
try {
|
|
181
|
+
for (const item of result.items) {
|
|
182
|
+
const sender = keyring.getPair(item.owner);
|
|
183
|
+
this.state.updateNftData(item.chain, item, sender.address || item.owner);
|
|
184
|
+
}
|
|
185
|
+
for (const col of result.collections) {
|
|
186
|
+
this.state.setNftCollection(col.chain, col);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error('[NftServiceV2] Persist failed:', error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async refreshNftData() {
|
|
193
|
+
if (this.isReloading) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.isReloading = true;
|
|
197
|
+
try {
|
|
198
|
+
const addresses = this.state.keyringService.context.getDecodedAddresses();
|
|
199
|
+
const activeChains = Object.keys(this.state.getActiveChainInfoMap());
|
|
200
|
+
if (addresses.length === 0 || activeChains.length === 0) {
|
|
201
|
+
this.nftStateSubject.next(INITIAL_NFT_STATE);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const result = await this.multiChainFetcher.fetch(addresses, activeChains);
|
|
205
|
+
this.persistNftData(result);
|
|
206
|
+
this.nftStateSubject.next({
|
|
207
|
+
nftData: {
|
|
208
|
+
total: result.items.length,
|
|
209
|
+
nftList: result.items
|
|
210
|
+
},
|
|
211
|
+
nftCollections: result.collections
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('[NftService] Refresh failed:', error);
|
|
215
|
+
this.nftStateSubject.next({
|
|
216
|
+
...this.nftStateSubject.getValue()
|
|
217
|
+
});
|
|
218
|
+
} finally {
|
|
219
|
+
this.isReloading = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Subscribe NFT state */
|
|
224
|
+
subscribeNftItem() {
|
|
225
|
+
return this.nftState$;
|
|
226
|
+
}
|
|
227
|
+
subscribeNftCollection() {
|
|
228
|
+
const getChains = () => this.state.activeChainSlugs;
|
|
229
|
+
return this.state.dbService.stores.nftCollection.subscribeNftCollection(getChains);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// TODO: Move NFT reset logic to this function after migration is complete
|
|
233
|
+
async forceReload() {
|
|
234
|
+
this.isReloading = true;
|
|
235
|
+
await waitTimeout(1800);
|
|
236
|
+
this.isReloading = false;
|
|
237
|
+
await this.refreshNftData().catch(console.error);
|
|
238
|
+
}
|
|
171
239
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NftDetailRequest, NftFullListRequest } from '@subwallet/extension-base/background/KoniTypes';
|
|
2
|
+
import KoniState from '@subwallet/extension-base/koni/background/handlers/State';
|
|
3
|
+
import { NftHandlerResult } from './nft-handlers/base-nft-handler';
|
|
4
|
+
export declare class MultiChainNftFetcher {
|
|
5
|
+
private readonly state;
|
|
6
|
+
constructor(state: KoniState);
|
|
7
|
+
private handlerCache;
|
|
8
|
+
private getOrCreate;
|
|
9
|
+
private getHandlersForChain;
|
|
10
|
+
fetch(addresses: string[], chainSlugs: string[]): Promise<NftHandlerResult>;
|
|
11
|
+
fetchFullListNftOfACollection(request: NftFullListRequest): Promise<NftHandlerResult>;
|
|
12
|
+
fetchNftDetail(request: NftDetailRequest): Promise<NftHandlerResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright 2019-2022 @subwallet/extension-koni authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { NFT_HANDLER_REGISTRY } from '@subwallet/extension-base/services/nft-service/nft-handlers/registry';
|
|
5
|
+
export class MultiChainNftFetcher {
|
|
6
|
+
constructor(state) {
|
|
7
|
+
this.state = state;
|
|
8
|
+
}
|
|
9
|
+
handlerCache = new Map();
|
|
10
|
+
getOrCreate(chain, desc) {
|
|
11
|
+
const key = `${chain}:${desc.id}`;
|
|
12
|
+
let handler = this.handlerCache.get(key);
|
|
13
|
+
if (!handler) {
|
|
14
|
+
handler = desc.create(chain, this.state);
|
|
15
|
+
this.handlerCache.set(key, handler);
|
|
16
|
+
}
|
|
17
|
+
return handler;
|
|
18
|
+
}
|
|
19
|
+
getHandlersForChain(chainSlug) {
|
|
20
|
+
const chainInfo = this.state.chainService.getChainInfoByKey(chainSlug);
|
|
21
|
+
if (!chainInfo) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return NFT_HANDLER_REGISTRY.filter(d => d.supports(chainInfo)).map(d => this.getOrCreate(chainSlug, d));
|
|
25
|
+
}
|
|
26
|
+
async fetch(addresses, chainSlugs) {
|
|
27
|
+
const allItems = [];
|
|
28
|
+
const allCollections = [];
|
|
29
|
+
const tasks = [];
|
|
30
|
+
for (const chain of chainSlugs) {
|
|
31
|
+
const handlers = this.getHandlersForChain(chain);
|
|
32
|
+
if (handlers.length === 0) {
|
|
33
|
+
console.warn(`[NftFetcher] No handler for chain: ${chain}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const handler of handlers) {
|
|
37
|
+
const handlerAddresses = handler.filterAddresses(addresses);
|
|
38
|
+
const task = handler.fetchPreview(handlerAddresses).then(result => {
|
|
39
|
+
allItems.push(...result.items);
|
|
40
|
+
allCollections.push(...result.collections);
|
|
41
|
+
}).catch(err => {
|
|
42
|
+
console.error(`[NftFetcher] Handler failed on ${chain}: handler.id`, err);
|
|
43
|
+
});
|
|
44
|
+
tasks.push(task);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await Promise.all(tasks);
|
|
48
|
+
|
|
49
|
+
// DEDUPLICATE
|
|
50
|
+
// Todo: Move logic DEDUPLICATE to each handler
|
|
51
|
+
const seenItemIds = new Set();
|
|
52
|
+
const seenCollectionKeys = new Set();
|
|
53
|
+
const uniqueItems = [];
|
|
54
|
+
const uniqueCollections = [];
|
|
55
|
+
for (const item of allItems) {
|
|
56
|
+
if (!seenItemIds.has(item.id)) {
|
|
57
|
+
seenItemIds.add(item.id);
|
|
58
|
+
uniqueItems.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const col of allCollections) {
|
|
62
|
+
const key = `${col.chain}:${col.collectionId}`;
|
|
63
|
+
if (!seenCollectionKeys.has(key)) {
|
|
64
|
+
seenCollectionKeys.add(key);
|
|
65
|
+
uniqueCollections.push(col);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
items: uniqueItems,
|
|
70
|
+
collections: uniqueCollections
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async fetchFullListNftOfACollection(request) {
|
|
74
|
+
const {
|
|
75
|
+
chainInfo,
|
|
76
|
+
collectionId,
|
|
77
|
+
owners,
|
|
78
|
+
tokenIds
|
|
79
|
+
} = request;
|
|
80
|
+
const items = [];
|
|
81
|
+
const collections = [];
|
|
82
|
+
const handlers = this.getHandlersForChain(chainInfo.slug);
|
|
83
|
+
for (const handler of handlers) {
|
|
84
|
+
// Todo: Improve the full-list fetch feature
|
|
85
|
+
// if (!handler.supportsFetchFullNftList) {
|
|
86
|
+
// continue;
|
|
87
|
+
// }
|
|
88
|
+
|
|
89
|
+
const handlerOwners = handler.filterAddresses(owners);
|
|
90
|
+
try {
|
|
91
|
+
const result = await handler.fetchFullListNftOfACollection({
|
|
92
|
+
collectionId: collectionId,
|
|
93
|
+
owners: handlerOwners,
|
|
94
|
+
tokenIds: tokenIds,
|
|
95
|
+
chainInfo
|
|
96
|
+
});
|
|
97
|
+
items.push(...result.items);
|
|
98
|
+
collections.push(...result.collections);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error('[NftFetcher] fetchCollection failed', e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
items,
|
|
105
|
+
collections
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async fetchNftDetail(request) {
|
|
109
|
+
const {
|
|
110
|
+
chainSlug,
|
|
111
|
+
collectionId,
|
|
112
|
+
tokenId
|
|
113
|
+
} = request;
|
|
114
|
+
const items = [];
|
|
115
|
+
const collections = [];
|
|
116
|
+
const handlers = this.getHandlersForChain(chainSlug);
|
|
117
|
+
for (const handler of handlers) {
|
|
118
|
+
// Todo: Improve the detail nft fetch feature
|
|
119
|
+
// if (!handler.supportsFetchFullNftList) {
|
|
120
|
+
// continue;
|
|
121
|
+
// }
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return await handler.fetchNftDetail({
|
|
125
|
+
collectionId: collectionId,
|
|
126
|
+
tokenId: tokenId,
|
|
127
|
+
chainSlug
|
|
128
|
+
});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error('[NftFetcher] fetchCollection failed', e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
items,
|
|
135
|
+
collections
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NftCollection, NftDetailRequest, NftFullListRequest, NftItem } from '@subwallet/extension-base/background/KoniTypes';
|
|
2
|
+
export interface NftHandlerResult {
|
|
3
|
+
items: NftItem[];
|
|
4
|
+
collections: NftCollection[];
|
|
5
|
+
}
|
|
6
|
+
export declare abstract class BaseNftHandler {
|
|
7
|
+
protected readonly chain: string;
|
|
8
|
+
constructor(chain: string);
|
|
9
|
+
abstract fetchPreview(addresses: string[]): Promise<NftHandlerResult>;
|
|
10
|
+
abstract filterAddresses(addresses: string[]): string[];
|
|
11
|
+
fetchFullListNftOfACollection(_request: NftFullListRequest): Promise<NftHandlerResult>;
|
|
12
|
+
fetchNftDetail(_request: NftDetailRequest): Promise<NftHandlerResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright 2019-2022 @subwallet/extension-base authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
const EMPTY_NFT_RESULT = {
|
|
5
|
+
items: [],
|
|
6
|
+
collections: []
|
|
7
|
+
};
|
|
8
|
+
export class BaseNftHandler {
|
|
9
|
+
constructor(chain) {
|
|
10
|
+
this.chain = chain;
|
|
11
|
+
}
|
|
12
|
+
// Optional method - subclasses can choose to implement or not
|
|
13
|
+
fetchFullListNftOfACollection(_request) {
|
|
14
|
+
return Promise.resolve(EMPTY_NFT_RESULT);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Optional method - subclasses can choose to implement or not
|
|
18
|
+
fetchNftDetail(_request) {
|
|
19
|
+
return Promise.resolve(EMPTY_NFT_RESULT);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NftFullListRequest } from '@subwallet/extension-base/background/KoniTypes';
|
|
2
|
+
import { BaseNftHandler, NftHandlerResult } from '../base-nft-handler';
|
|
3
|
+
export declare class EvmNftHandler extends BaseNftHandler {
|
|
4
|
+
filterAddresses(addresses: string[]): string[];
|
|
5
|
+
private mapSdkToNftItem;
|
|
6
|
+
private mapSdkToCollection;
|
|
7
|
+
fetchPreview(addresses: string[]): Promise<NftHandlerResult>;
|
|
8
|
+
fetchFullListNftOfACollection(request: NftFullListRequest): Promise<NftHandlerResult>;
|
|
9
|
+
}
|