@exodus/assets-feature 9.0.1 → 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 +16 -0
- package/api/index.d.ts +1 -0
- package/index.d.ts +1 -0
- package/index.js +9 -1
- package/module/assets-module.d.ts +37 -0
- package/module/assets-module.js +27 -3
- package/package.json +15 -14
- package/plugin/search-based-asset-loader-plugin.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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
|
+
|
|
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)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- feat(assets): add tags parameter to searchTokens (#15984)
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- fix: add protection from malformed remote asset config during startup (#15650)
|
|
21
|
+
|
|
6
22
|
## [9.0.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/assets-feature@9.0.0...@exodus/assets-feature@9.0.1) (2026-02-20)
|
|
7
23
|
|
|
8
24
|
### Bug Fixes
|
package/api/index.d.ts
CHANGED
package/index.d.ts
CHANGED
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
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BaseAsset, TokenAsset, CombinedAsset } from '@exodus/asset-types'
|
|
2
|
+
|
|
3
|
+
type Asset = BaseAsset | TokenAsset | CombinedAsset
|
|
4
|
+
type TokenDefinition = Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
export declare class AssetsModule {
|
|
7
|
+
initialize(params: {
|
|
8
|
+
assetClientInterface: unknown
|
|
9
|
+
assetsConfig?: Record<string, { config?: Record<string, unknown> }>
|
|
10
|
+
}): void
|
|
11
|
+
|
|
12
|
+
getAssets(): Record<string, Asset>
|
|
13
|
+
|
|
14
|
+
getAsset(assetName: string): Asset | undefined
|
|
15
|
+
|
|
16
|
+
getTokenNames(baseAssetName: string): string[]
|
|
17
|
+
|
|
18
|
+
getBaseAssetNames(): string[]
|
|
19
|
+
|
|
20
|
+
load(): Promise<void>
|
|
21
|
+
|
|
22
|
+
fetchToken(assetId: string, baseAssetName: string): Promise<TokenDefinition>
|
|
23
|
+
|
|
24
|
+
fetchTokens(assetDescriptors: { assetId: string; baseAssetName: string }[]): Promise<Asset[]>
|
|
25
|
+
|
|
26
|
+
addToken(assetId: string, baseAssetName: string): Promise<Asset>
|
|
27
|
+
|
|
28
|
+
addTokens(params: {
|
|
29
|
+
assetIds: string[]
|
|
30
|
+
baseAssetName: string
|
|
31
|
+
allowedStatusList?: string[]
|
|
32
|
+
}): Promise<Asset[]>
|
|
33
|
+
|
|
34
|
+
addRemoteTokens(params: { tokenNames: string[]; allowedStatusList?: string[] }): Promise<string[]>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default AssetsModule
|
package/module/assets-module.js
CHANGED
|
@@ -140,8 +140,23 @@ export class AssetsModule {
|
|
|
140
140
|
const { assetsConfig: defaultAssetsConfig = Object.create(null) } = this.#config
|
|
141
141
|
const assetsList = Object.entries(this.#assetPlugins)
|
|
142
142
|
.map(([name, assetPlugin]) => {
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
let asset
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const config = { ...defaultAssetsConfig[name], ...assetsConfig?.[name]?.config }
|
|
147
|
+
asset = assetPlugin.createAsset({ assetClientInterface, config })
|
|
148
|
+
} catch (e) {
|
|
149
|
+
this.#logger.error(
|
|
150
|
+
`failed to createAsset ${name} using custom assetsConfig: ${e.message}, using default configuration`,
|
|
151
|
+
e
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
asset = assetPlugin.createAsset({
|
|
155
|
+
assetClientInterface,
|
|
156
|
+
config: defaultAssetsConfig[name],
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
if (asset.name === name) return asset
|
|
146
161
|
console.warn(`Incorrectly referenced supported asset ${name}. Expected ${asset.name}.`)
|
|
147
162
|
})
|
|
@@ -460,6 +475,7 @@ export class AssetsModule {
|
|
|
460
475
|
baseAssetName,
|
|
461
476
|
lifecycleStatus,
|
|
462
477
|
query,
|
|
478
|
+
tags,
|
|
463
479
|
excludeTags = ['offensive'],
|
|
464
480
|
pageNumber,
|
|
465
481
|
pageSize,
|
|
@@ -482,7 +498,15 @@ export class AssetsModule {
|
|
|
482
498
|
|
|
483
499
|
const tokens = await this.#fetch(
|
|
484
500
|
'search',
|
|
485
|
-
{
|
|
501
|
+
{
|
|
502
|
+
baseAssetName: baseAssetNames,
|
|
503
|
+
lifecycleStatus,
|
|
504
|
+
query,
|
|
505
|
+
tags,
|
|
506
|
+
excludeTags,
|
|
507
|
+
pageNumber,
|
|
508
|
+
pageSize,
|
|
509
|
+
},
|
|
486
510
|
'tokens'
|
|
487
511
|
)
|
|
488
512
|
const validTokens = tokens.filter(this.#isValidCustomToken)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/assets-feature",
|
|
3
|
-
"version": "9.0
|
|
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",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@exodus/asset-legacy-token-name-mapping": "^1.4.0",
|
|
39
39
|
"@exodus/asset-lib": "^5.3.0",
|
|
40
|
-
"@exodus/asset-schema-validation": "^1.0
|
|
40
|
+
"@exodus/asset-schema-validation": "^1.2.0",
|
|
41
|
+
"@exodus/asset-types": "^0.3.0",
|
|
41
42
|
"@exodus/assets": "^11.3.0",
|
|
42
|
-
"@exodus/atoms": "^
|
|
43
|
-
"@exodus/basic-utils": "^
|
|
43
|
+
"@exodus/atoms": "^10.3.3",
|
|
44
|
+
"@exodus/basic-utils": "^5.0.0",
|
|
44
45
|
"@exodus/fetch": "^1.3.0",
|
|
45
46
|
"@exodus/fusion-atoms": "^1.4.0",
|
|
46
47
|
"@exodus/timer": "^1.1.2",
|
|
@@ -55,12 +56,12 @@
|
|
|
55
56
|
"@exodus/bip39": "^1.1.0",
|
|
56
57
|
"@exodus/bip44-constants": "^195.0.0",
|
|
57
58
|
"@exodus/bitcoin-meta": "^2.0.0",
|
|
58
|
-
"@exodus/bitcoin-plugin": "^
|
|
59
|
-
"@exodus/bitcoinregtest-plugin": "^
|
|
60
|
-
"@exodus/bitcointestnet-plugin": "^
|
|
61
|
-
"@exodus/blockchain-metadata": "^17.1.
|
|
59
|
+
"@exodus/bitcoin-plugin": "^2.0.0",
|
|
60
|
+
"@exodus/bitcoinregtest-plugin": "^2.0.0",
|
|
61
|
+
"@exodus/bitcointestnet-plugin": "^2.0.0",
|
|
62
|
+
"@exodus/blockchain-metadata": "^17.1.1",
|
|
62
63
|
"@exodus/cardano-lib": "^4.0.0",
|
|
63
|
-
"@exodus/combined-assets-meta": "^3.
|
|
64
|
+
"@exodus/combined-assets-meta": "^3.7.0",
|
|
64
65
|
"@exodus/cosmos-plugin": "^1.3.3",
|
|
65
66
|
"@exodus/ethereum-lib": "^5.0.0",
|
|
66
67
|
"@exodus/ethereum-meta": "^2.0.0",
|
|
@@ -68,12 +69,12 @@
|
|
|
68
69
|
"@exodus/fusion-local": "^2.1.0",
|
|
69
70
|
"@exodus/keychain": "^9.0.3",
|
|
70
71
|
"@exodus/logger": "^1.2.3",
|
|
71
|
-
"@exodus/models": "^
|
|
72
|
+
"@exodus/models": "^13.2.0",
|
|
72
73
|
"@exodus/osmosis-plugin": "^1.3.3",
|
|
73
74
|
"@exodus/public-key-provider": "^4.2.1",
|
|
74
|
-
"@exodus/redux-dependency-injection": "^4.
|
|
75
|
-
"@exodus/storage-memory": "^2.
|
|
76
|
-
"@exodus/wallet-accounts": "^20.
|
|
75
|
+
"@exodus/redux-dependency-injection": "^4.2.0",
|
|
76
|
+
"@exodus/storage-memory": "^2.4.0",
|
|
77
|
+
"@exodus/wallet-accounts": "^20.4.3",
|
|
77
78
|
"@exodus/wild-emitter": "^1.0.0",
|
|
78
79
|
"events": "^3.3.0",
|
|
79
80
|
"msw": "^2.0.0",
|
|
@@ -83,5 +84,5 @@
|
|
|
83
84
|
"access": "public",
|
|
84
85
|
"provenance": false
|
|
85
86
|
},
|
|
86
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "3e347c272d0c2c679ee9eec9e53a2b4831ef1faf"
|
|
87
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
|