@exodus/assets-feature 9.1.0 → 9.2.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/CHANGELOG.md +6 -0
- package/index.js +9 -1
- package/package.json +2 -2
- package/plugin/search-based-asset-loader-plugin.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/assets-feature@9.1.0...@exodus/assets-feature@9.2.0) (2026-04-15)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- feat(assets-feature): add searchBasedAssetLoaderPlugin (#16016)
|
|
11
|
+
|
|
6
12
|
## [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
13
|
|
|
8
14
|
### 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/assets-feature",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
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": "3e347c272d0c2c679ee9eec9e53a2b4831ef1faf"
|
|
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
|