@exodus/assets-feature 9.1.0 → 9.2.1
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/CHANGELOG.md +10 -0
- package/index.js +9 -1
- package/module/assets-module.js +15 -28
- package/package.json +2 -2
- package/plugin/search-based-asset-loader-plugin.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [9.2.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/assets-feature@9.2.0...@exodus/assets-feature@9.2.1) (2026-04-16)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/assets-feature
|
|
9
|
+
|
|
10
|
+
## [9.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/assets-feature@9.1.0...@exodus/assets-feature@9.2.0) (2026-04-15)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- feat(assets-feature): add searchBasedAssetLoaderPlugin (#16016)
|
|
15
|
+
|
|
6
16
|
## [9.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/assets-feature@9.0.1...@exodus/assets-feature@9.1.0) (2026-04-14)
|
|
7
17
|
|
|
8
18
|
### Features
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assetsClientInterfaceDefinition from './client/index.js'
|
|
2
2
|
import assetsPluginDefinition from './plugin/index.js'
|
|
3
|
+
import searchBasedAssetLoaderPluginDefinition from './plugin/search-based-asset-loader-plugin.js'
|
|
3
4
|
import multiAddressModeAtomDefinition from './atoms/multi-address-mode.js'
|
|
4
5
|
import legacyAddressModeAtomDefinition from './atoms/legacy-address-mode.js'
|
|
5
6
|
import taprootAddressModeAtomDefinition from './atoms/taproot-address-mode.js'
|
|
@@ -17,6 +18,7 @@ const assets = (config = Object.create(null)) => {
|
|
|
17
18
|
disabledPurposes,
|
|
18
19
|
compatibilityModeGapLimits,
|
|
19
20
|
compatibilityModeMultiAddressMode,
|
|
21
|
+
searchBasedAssetLoaderPlugin,
|
|
20
22
|
} = { ...defaultConfig, ...config }
|
|
21
23
|
|
|
22
24
|
return {
|
|
@@ -53,10 +55,16 @@ const assets = (config = Object.create(null)) => {
|
|
|
53
55
|
},
|
|
54
56
|
{ definition: assetPreferencesDefinition },
|
|
55
57
|
{ if: { registered: ['customTokensStorage'] }, definition: customTokensMonitorDefinition },
|
|
58
|
+
searchBasedAssetLoaderPlugin && {
|
|
59
|
+
if: { registered: ['customTokensStorage'] },
|
|
60
|
+
definition: searchBasedAssetLoaderPluginDefinition,
|
|
61
|
+
storage: { namespace: 'assetPreferences' },
|
|
62
|
+
config: searchBasedAssetLoaderPlugin,
|
|
63
|
+
},
|
|
56
64
|
// This report was intentionally omitted as it did not provide useful value in practice.
|
|
57
65
|
// We prefer less data over more when it's not meaningful, keeping them commented for reference.
|
|
58
66
|
// { definition: assetsReportDefinition },
|
|
59
|
-
],
|
|
67
|
+
].filter(Boolean),
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
package/module/assets-module.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
isDisabledCustomToken,
|
|
24
24
|
} from './utils.js'
|
|
25
25
|
import createFetchival from '@exodus/fetch/create-fetchival'
|
|
26
|
-
import { validateCustomToken
|
|
26
|
+
import { validateCustomToken } from '@exodus/asset-schema-validation'
|
|
27
27
|
import makeConcurrent from 'make-concurrent'
|
|
28
28
|
import oldToNewStyleTokenNames from '@exodus/asset-legacy-token-name-mapping'
|
|
29
29
|
|
|
@@ -234,17 +234,10 @@ export class AssetsModule {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
const _token = await fetchTokenAndCacheError()
|
|
237
|
-
if (this.#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.#logger.warn(
|
|
242
|
-
`Token did not pass validation ${baseAssetName} ${assetId}. Error: ${e.message}`
|
|
243
|
-
)
|
|
244
|
-
const err = new Error('Token did not pass validation')
|
|
245
|
-
this.#setCache(key, { cachedError: err })
|
|
246
|
-
throw err
|
|
247
|
-
}
|
|
237
|
+
if (!this.#isValidCustomToken(_token)) {
|
|
238
|
+
const err = new Error('Token did not pass validation')
|
|
239
|
+
this.#setCache(key, { cachedError: err })
|
|
240
|
+
throw err
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
const token = normalizeToken(_token)
|
|
@@ -277,21 +270,7 @@ export class AssetsModule {
|
|
|
277
270
|
'tokens'
|
|
278
271
|
)
|
|
279
272
|
|
|
280
|
-
|
|
281
|
-
if (this.#shouldValidateCustomToken) {
|
|
282
|
-
for (const token of _tokens) {
|
|
283
|
-
try {
|
|
284
|
-
validateCustomToken(token)
|
|
285
|
-
validTokens.push(token)
|
|
286
|
-
} catch (e) {
|
|
287
|
-
this.#logger.warn(
|
|
288
|
-
`Token did not pass validation ${token.baseAssetName} ${token.assetId}. Error: ${e.message}`
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
validTokens = _tokens
|
|
294
|
-
}
|
|
273
|
+
const validTokens = _tokens.filter(this.#isValidCustomToken)
|
|
295
274
|
|
|
296
275
|
const tokens = validTokens.map((token) => normalizeToken(token))
|
|
297
276
|
|
|
@@ -775,7 +754,15 @@ export class AssetsModule {
|
|
|
775
754
|
return true
|
|
776
755
|
}
|
|
777
756
|
|
|
778
|
-
|
|
757
|
+
try {
|
|
758
|
+
validateCustomToken(token)
|
|
759
|
+
return true
|
|
760
|
+
} catch (e) {
|
|
761
|
+
this.#logger.warn(
|
|
762
|
+
`Token ${token.name ?? token.assetName} did not pass validation: ${e.message}`
|
|
763
|
+
)
|
|
764
|
+
return false
|
|
765
|
+
}
|
|
779
766
|
}
|
|
780
767
|
|
|
781
768
|
clear = async () =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/assets-feature",
|
|
3
|
-
"version": "9.1
|
|
3
|
+
"version": "9.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "This Exodus SDK feature provides access to instances of all blockchain asset adapters supported by the wallet, and enables you to search for and add custom tokens at runtime.",
|
|
6
6
|
"type": "module",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"access": "public",
|
|
85
85
|
"provenance": false
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "9819576371d9361b2710dd8eae2fc6f4ef05d0b9"
|
|
88
88
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import ms from 'ms'
|
|
2
|
+
|
|
3
|
+
const toArray = (value) => (Array.isArray(value) ? value : [value])
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'loadedSearches'
|
|
6
|
+
const DEFAULT_REFRESH_INTERVAL = '8h'
|
|
7
|
+
|
|
8
|
+
const isValidSearchConfig = ({ tags, baseAssetName, lifecycleStatus }) =>
|
|
9
|
+
Array.isArray(tags) && tags.length > 0 && baseAssetName && lifecycleStatus
|
|
10
|
+
|
|
11
|
+
const getSearchHash = ({ tags, baseAssetName, lifecycleStatus }) => {
|
|
12
|
+
const sortedTags = [...tags].sort().join(',')
|
|
13
|
+
const sortedStatus = [...toArray(lifecycleStatus)].sort().join(',')
|
|
14
|
+
return `${sortedTags}:${baseAssetName}:${sortedStatus}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const createSearchBasedAssetLoaderPlugin = ({
|
|
18
|
+
assetsModule,
|
|
19
|
+
logger,
|
|
20
|
+
storage,
|
|
21
|
+
config: { searches = [], refreshInterval = DEFAULT_REFRESH_INTERVAL, pageSize } = {},
|
|
22
|
+
}) => {
|
|
23
|
+
const refreshIntervalMs =
|
|
24
|
+
typeof refreshInterval === 'string' ? ms(refreshInterval) : refreshInterval
|
|
25
|
+
|
|
26
|
+
const getLoadedSearches = async () => {
|
|
27
|
+
return (await storage.get(STORAGE_KEY)) || {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const markSearchAsLoaded = async (hash) => {
|
|
31
|
+
const loadedSearches = await getLoadedSearches()
|
|
32
|
+
await storage.set(STORAGE_KEY, { ...loadedSearches, [hash]: Date.now() })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isSearchStale = (loadedSearches, hash) => {
|
|
36
|
+
const lastLoaded = loadedSearches[hash]
|
|
37
|
+
return !lastLoaded || Date.now() - lastLoaded > refreshIntervalMs
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const loadTokensForSearch = async (searchConfig, loadedSearches) => {
|
|
41
|
+
if (!isValidSearchConfig(searchConfig)) {
|
|
42
|
+
logger.error('Invalid search config, skipping:', JSON.stringify(searchConfig))
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { tags, baseAssetName, lifecycleStatus } = searchConfig
|
|
47
|
+
const lifecycleStatusList = toArray(lifecycleStatus)
|
|
48
|
+
const hash = getSearchHash(searchConfig)
|
|
49
|
+
|
|
50
|
+
if (!isSearchStale(loadedSearches, hash)) {
|
|
51
|
+
logger.info(`Search still fresh, skipping: ${hash}`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
logger.info(
|
|
57
|
+
`Fetching tokens: tags=${tags.join(',')}, baseAsset=${baseAssetName}, status=${lifecycleStatusList.join(',')}`
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const tokens = await assetsModule.searchTokens({
|
|
61
|
+
tags,
|
|
62
|
+
baseAssetName,
|
|
63
|
+
lifecycleStatus: lifecycleStatusList,
|
|
64
|
+
pageSize,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (tokens.length === 0) {
|
|
68
|
+
logger.info('No tokens found for search criteria')
|
|
69
|
+
await markSearchAsLoaded(hash)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tokenNames = tokens.map((t) => t.name)
|
|
74
|
+
await assetsModule.addRemoteTokens({
|
|
75
|
+
tokenNames,
|
|
76
|
+
allowedStatusList: lifecycleStatusList,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await markSearchAsLoaded(hash)
|
|
80
|
+
logger.info(`Loaded ${tokens.length} tokens`)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error('Failed to load tokens:', error.message, error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const onLoad = async () => {
|
|
87
|
+
const loadedSearches = await getLoadedSearches()
|
|
88
|
+
for (const searchConfig of searches) {
|
|
89
|
+
await loadTokensForSearch(searchConfig, loadedSearches)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const onClear = async () => {
|
|
94
|
+
await storage.delete(STORAGE_KEY)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { onLoad, onClear }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const searchBasedAssetLoaderPluginDefinition = {
|
|
101
|
+
id: 'searchBasedAssetLoaderPlugin',
|
|
102
|
+
type: 'plugin',
|
|
103
|
+
factory: createSearchBasedAssetLoaderPlugin,
|
|
104
|
+
dependencies: ['assetsModule', 'logger', 'storage', 'config?'],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default searchBasedAssetLoaderPluginDefinition
|