@exodus/assets-feature 9.2.6 → 9.3.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 +12 -0
- package/constants.js +5 -0
- package/index.js +2 -0
- package/module/assets-module.js +167 -33
- package/module/dynamic-combined-assets.js +106 -0
- package/package.json +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.3.1](https://github.com/ExodusMovement/exodus/compare/@exodus/assets-feature@9.3.0...@exodus/assets-feature@9.3.1) (2026-08-26)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **assets:** accumulate changes in load() before single flush ([#44228](https://github.com/ExodusMovement/exodus/issues/44228)) ([401680e](https://github.com/ExodusMovement/exodus/commit/401680ef721d736a349d54e89b044ec54a469b0c))
|
|
11
|
+
|
|
12
|
+
## [9.3.0](https://github.com/ExodusMovement/exodus/compare/@exodus/assets-feature@9.2.6...@exodus/assets-feature@9.3.0) (2026-08-25)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **assets:** add dynamic combined assets generation ([#44197](https://github.com/ExodusMovement/exodus/issues/44197)) ([f107abe](https://github.com/ExodusMovement/exodus/commit/f107abece571af407df3b7154518aa689b8c1a04))
|
|
17
|
+
|
|
6
18
|
## [9.2.6](https://github.com/ExodusMovement/exodus/compare/@exodus/assets-feature@9.2.5...@exodus/assets-feature@9.2.6) (2026-07-30)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
package/constants.js
CHANGED
|
@@ -6,7 +6,12 @@ export const disabledPurposesDefaultConfig = {
|
|
|
6
6
|
defaults: Object.create(null),
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export const assetsModuleDefaultConfig = {
|
|
10
|
+
useDynamicCombinedAssets: false,
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export const defaultConfig = {
|
|
10
14
|
multiAddressMode: multiAddressModeDefaultConfig,
|
|
11
15
|
disabledPurposes: disabledPurposesDefaultConfig,
|
|
16
|
+
assetsModule: assetsModuleDefaultConfig,
|
|
12
17
|
}
|
package/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const assets = (config = Object.create(null)) => {
|
|
|
19
19
|
compatibilityModeGapLimits,
|
|
20
20
|
compatibilityModeMultiAddressMode,
|
|
21
21
|
searchBasedAssetLoaderPlugin,
|
|
22
|
+
assetsModule,
|
|
22
23
|
} = { ...defaultConfig, ...config }
|
|
23
24
|
|
|
24
25
|
return {
|
|
@@ -52,6 +53,7 @@ const assets = (config = Object.create(null)) => {
|
|
|
52
53
|
definition: assetModuleDefinition,
|
|
53
54
|
// storage: { namespace: 'customTokens' },
|
|
54
55
|
aliases: [{ implementationId: 'customTokensStorage', interfaceId: 'storage' }],
|
|
56
|
+
config: assetsModule,
|
|
55
57
|
},
|
|
56
58
|
{ definition: assetPreferencesDefinition },
|
|
57
59
|
{ if: { registered: ['customTokensStorage'] }, definition: customTokensMonitorDefinition },
|
package/module/assets-module.js
CHANGED
|
@@ -26,6 +26,11 @@ import createFetchival from '@exodus/fetch/create-fetchival'
|
|
|
26
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
|
+
import {
|
|
30
|
+
createDynamicCombinedAssetDefinitions,
|
|
31
|
+
shouldTokenJoinCombinedAsset,
|
|
32
|
+
findDynamicCombinedAssetForToken,
|
|
33
|
+
} from './dynamic-combined-assets.js'
|
|
29
34
|
|
|
30
35
|
// oxlint-disable-next-line @exodus/oxlint-plugin-restricted-imports/prefer-basic-utils
|
|
31
36
|
const { get, isEmpty, once, uniq, chunk, pick } = lodash
|
|
@@ -79,6 +84,7 @@ export class AssetsModule {
|
|
|
79
84
|
#fetchival
|
|
80
85
|
#logger
|
|
81
86
|
#invalidTokenNames
|
|
87
|
+
#useDynamicCombinedAssets
|
|
82
88
|
|
|
83
89
|
constructor({
|
|
84
90
|
storage,
|
|
@@ -118,6 +124,7 @@ export class AssetsModule {
|
|
|
118
124
|
),
|
|
119
125
|
...(config.invalidTokenNames || []),
|
|
120
126
|
])
|
|
127
|
+
this.#useDynamicCombinedAssets = config.useDynamicCombinedAssets ?? false
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
#setRegistry = (registry) => {
|
|
@@ -161,11 +168,22 @@ export class AssetsModule {
|
|
|
161
168
|
console.warn(`Incorrectly referenced supported asset ${name}. Expected ${asset.name}.`)
|
|
162
169
|
})
|
|
163
170
|
.filter(Boolean)
|
|
171
|
+
|
|
172
|
+
const combinedAssets = this.#useDynamicCombinedAssets ? [] : this.#combinedAssetsList
|
|
173
|
+
|
|
164
174
|
this.#setRegistry(
|
|
165
175
|
createAssetRegistry({
|
|
166
|
-
supportedAssetsList: [...assetsList, ...
|
|
176
|
+
supportedAssetsList: [...assetsList, ...combinedAssets],
|
|
167
177
|
})
|
|
168
178
|
)
|
|
179
|
+
|
|
180
|
+
// Only flush static combined assets - dynamic ones are flushed in load()
|
|
181
|
+
if (!this.#useDynamicCombinedAssets) {
|
|
182
|
+
const allCombined = Object.values(this.#registry.getAssets()).filter((a) => a.isCombined)
|
|
183
|
+
if (allCombined.length > 0) {
|
|
184
|
+
this.#flushChanges({ added: allCombined })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
169
187
|
})
|
|
170
188
|
|
|
171
189
|
getAssets = () => this.#registry.getAssets()
|
|
@@ -186,8 +204,12 @@ export class AssetsModule {
|
|
|
186
204
|
getBaseAssetNames = () => this.#getAssetNamesBy((asset) => asset.baseAsset.name === asset.name)
|
|
187
205
|
|
|
188
206
|
load = once(async () => {
|
|
207
|
+
const allAdded = []
|
|
208
|
+
const allUpdated = []
|
|
209
|
+
|
|
189
210
|
try {
|
|
190
211
|
const storedTokens = await this.#readCustomTokens()
|
|
212
|
+
|
|
191
213
|
const tokens = pickBy(storedTokens, ({ baseAssetName }) => {
|
|
192
214
|
const baseAsset = this.getAsset(baseAssetName)
|
|
193
215
|
return baseAsset?.api?.features?.customTokens
|
|
@@ -199,12 +221,48 @@ export class AssetsModule {
|
|
|
199
221
|
// 3. both of the above from a network that has feature flag customTokens === false
|
|
200
222
|
// It is OK to add all of these to the assets registry
|
|
201
223
|
const { added, updated } = this.#handleFetchedTokens(Object.values(tokens))
|
|
202
|
-
|
|
224
|
+
allAdded.push(...added)
|
|
225
|
+
allUpdated.push(...updated)
|
|
203
226
|
} catch (e) {
|
|
204
227
|
this.#logger.error('error reading custom tokens from storage: ' + e.message, e)
|
|
205
228
|
}
|
|
229
|
+
|
|
230
|
+
if (this.#useDynamicCombinedAssets) {
|
|
231
|
+
try {
|
|
232
|
+
const { added, updated } = this.#createDynamicCombinedAssets()
|
|
233
|
+
allAdded.push(...added)
|
|
234
|
+
allUpdated.push(...updated)
|
|
235
|
+
} catch (e) {
|
|
236
|
+
this.#logger.error('error creating dynamic combined assets: ' + e.message, e)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.#flushChanges({ added: allAdded, updated: allUpdated })
|
|
206
241
|
})
|
|
207
242
|
|
|
243
|
+
#createDynamicCombinedAssets = () => {
|
|
244
|
+
const allAssets = this.getAssets()
|
|
245
|
+
const definitions = createDynamicCombinedAssetDefinitions(allAssets)
|
|
246
|
+
|
|
247
|
+
const newlyCreatedNames = []
|
|
248
|
+
for (const definition of definitions) {
|
|
249
|
+
const existingCombined = this.getAsset(definition.name)
|
|
250
|
+
if (existingCombined) continue
|
|
251
|
+
|
|
252
|
+
this.#registry.addCombinedAsset(definition)
|
|
253
|
+
newlyCreatedNames.push(definition.name)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (newlyCreatedNames.length > 0) {
|
|
257
|
+
const newCombinedAssets = newlyCreatedNames.map((name) => this.getAsset(name)).filter(Boolean)
|
|
258
|
+
const childAssetNames = new Set(newCombinedAssets.flatMap((c) => c.combinedAssetNames))
|
|
259
|
+
const childAssets = [...childAssetNames].map((name) => this.getAsset(name)).filter(Boolean)
|
|
260
|
+
return { added: newCombinedAssets, updated: childAssets }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { added: [], updated: [] }
|
|
264
|
+
}
|
|
265
|
+
|
|
208
266
|
// Custom tokens API
|
|
209
267
|
|
|
210
268
|
fetchToken = async (assetId, baseAssetName) => {
|
|
@@ -316,16 +374,16 @@ export class AssetsModule {
|
|
|
316
374
|
|
|
317
375
|
try {
|
|
318
376
|
const token = await this.fetchToken(assetId, baseAssetName)
|
|
319
|
-
const { asset, isAdded,
|
|
320
|
-
const updated =
|
|
377
|
+
const { asset, isAdded, combinedUpdates, combinedCreated } = this.#handleFetchedToken(token)
|
|
378
|
+
const updated = combinedUpdates.map(this.getAsset)
|
|
379
|
+
const added = isAdded ? [asset, ...combinedCreated] : combinedCreated
|
|
321
380
|
|
|
322
381
|
if (isAdded) {
|
|
323
382
|
await this.#storeCustomTokens([token])
|
|
324
|
-
this.#flushChanges({ added: [asset], updated })
|
|
325
|
-
} else {
|
|
326
|
-
this.#flushChanges({ updated: [asset, ...updated] })
|
|
327
383
|
}
|
|
328
384
|
|
|
385
|
+
this.#flushChanges({ added, updated: isAdded ? updated : [asset, ...updated] })
|
|
386
|
+
|
|
329
387
|
return asset
|
|
330
388
|
} catch (err) {
|
|
331
389
|
this.#logger.warn(`Add custom token ${assetId} error on ${baseAssetName}`, err.message)
|
|
@@ -577,6 +635,10 @@ export class AssetsModule {
|
|
|
577
635
|
}
|
|
578
636
|
|
|
579
637
|
#handleCombinedParents = ({ name, parents }) => {
|
|
638
|
+
if (this.#useDynamicCombinedAssets) {
|
|
639
|
+
return []
|
|
640
|
+
}
|
|
641
|
+
|
|
580
642
|
if (!parents) return []
|
|
581
643
|
assert(Array.isArray(parents), 'Array expected')
|
|
582
644
|
|
|
@@ -603,59 +665,121 @@ export class AssetsModule {
|
|
|
603
665
|
return uniq(updatedCombinedAssetNames)
|
|
604
666
|
}
|
|
605
667
|
|
|
668
|
+
#handleDynamicCombinedAssetUpdate = (token) => {
|
|
669
|
+
if (!this.#useDynamicCombinedAssets) return { updated: [], created: [], members: [] }
|
|
670
|
+
if (!shouldTokenJoinCombinedAsset(token)) return { updated: [], created: [], members: [] }
|
|
671
|
+
|
|
672
|
+
const allAssets = this.getAssets()
|
|
673
|
+
const combinedAssets = Object.values(allAssets).filter((a) => a.isCombined)
|
|
674
|
+
const matchingCombined = findDynamicCombinedAssetForToken(token, combinedAssets)
|
|
675
|
+
|
|
676
|
+
if (matchingCombined) {
|
|
677
|
+
if (!matchingCombined.combinedAssetNames.includes(token.name)) {
|
|
678
|
+
this.#registry.updateCombinedAsset(matchingCombined.name, { addMembers: [token.name] })
|
|
679
|
+
return { updated: [matchingCombined.name], created: [], members: [] }
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
const matchingAssets = Object.values(allAssets).filter(
|
|
683
|
+
(a) =>
|
|
684
|
+
!a.isCombined &&
|
|
685
|
+
a.displayTicker?.toLowerCase() === token.displayTicker?.toLowerCase() &&
|
|
686
|
+
a.displayName?.toLowerCase() === token.displayName?.toLowerCase() &&
|
|
687
|
+
shouldTokenJoinCombinedAsset(a)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if (matchingAssets.length >= 2) {
|
|
691
|
+
const definitions = createDynamicCombinedAssetDefinitions(matchingAssets)
|
|
692
|
+
if (definitions.length > 0) {
|
|
693
|
+
const newCombined = this.#registry.addCombinedAsset(definitions[0])
|
|
694
|
+
const memberNames = matchingAssets.map((a) => a.name).filter((n) => n !== token.name)
|
|
695
|
+
return { updated: [], created: [newCombined], members: memberNames }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return { updated: [], created: [], members: [] }
|
|
701
|
+
}
|
|
702
|
+
|
|
606
703
|
#handleFetchedToken = (token) => {
|
|
607
704
|
const asset = this.getAsset(token.name)
|
|
608
|
-
if (asset)
|
|
705
|
+
if (asset) {
|
|
706
|
+
const dynamicResult = this.#handleDynamicCombinedAssetUpdate(asset)
|
|
707
|
+
return {
|
|
708
|
+
asset,
|
|
709
|
+
isAdded: false,
|
|
710
|
+
combinedUpdates: dynamicResult.updated,
|
|
711
|
+
combinedCreated: dynamicResult.created,
|
|
712
|
+
combinedMembers: dynamicResult.members,
|
|
713
|
+
}
|
|
714
|
+
}
|
|
609
715
|
|
|
610
|
-
const { name } = this.#registry.addCustomToken(token)
|
|
611
|
-
const
|
|
716
|
+
const { name } = this.#registry.addCustomToken(token)
|
|
717
|
+
const staticUpdates = this.#handleCombinedParents(token)
|
|
718
|
+
const dynamicResult = this.#handleDynamicCombinedAssetUpdate(this.getAsset(name))
|
|
719
|
+
const combinedUpdates = [...staticUpdates, ...dynamicResult.updated]
|
|
612
720
|
|
|
613
721
|
this.#logger.log('Custom token added:', name)
|
|
614
|
-
return {
|
|
722
|
+
return {
|
|
723
|
+
asset: this.getAsset(name),
|
|
724
|
+
isAdded: true,
|
|
725
|
+
combinedUpdates,
|
|
726
|
+
combinedCreated: dynamicResult.created,
|
|
727
|
+
combinedMembers: dynamicResult.members,
|
|
728
|
+
}
|
|
615
729
|
}
|
|
616
730
|
|
|
617
731
|
#handleFetchedTokens = (tokens) => {
|
|
618
732
|
const fetchedAndHandledTokens = []
|
|
619
733
|
const added = []
|
|
620
734
|
const updated = []
|
|
621
|
-
const
|
|
735
|
+
const combinedUpdateNames = []
|
|
736
|
+
const combinedCreated = []
|
|
737
|
+
const combinedMemberNames = []
|
|
738
|
+
|
|
622
739
|
for (const token of tokens) {
|
|
623
740
|
try {
|
|
624
|
-
const
|
|
741
|
+
const result = this.#handleFetchedToken(token)
|
|
625
742
|
|
|
626
|
-
if (isAdded) {
|
|
627
|
-
added.push(asset)
|
|
743
|
+
if (result.isAdded) {
|
|
744
|
+
added.push(result.asset)
|
|
628
745
|
fetchedAndHandledTokens.push(token)
|
|
629
746
|
} else {
|
|
630
|
-
updated.push(asset)
|
|
747
|
+
updated.push(result.asset)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (result.combinedUpdates.length > 0) {
|
|
751
|
+
combinedUpdateNames.push(...result.combinedUpdates)
|
|
631
752
|
}
|
|
632
753
|
|
|
633
|
-
if (
|
|
634
|
-
|
|
754
|
+
if (result.combinedCreated.length > 0) {
|
|
755
|
+
combinedCreated.push(...result.combinedCreated)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (result.combinedMembers.length > 0) {
|
|
759
|
+
combinedMemberNames.push(...result.combinedMembers)
|
|
635
760
|
}
|
|
636
761
|
} catch (err) {
|
|
637
762
|
this.#logger.error('Handle fetched custom tokens error: ' + err.message, err)
|
|
638
763
|
}
|
|
639
764
|
}
|
|
640
765
|
|
|
641
|
-
|
|
766
|
+
const createdNames = new Set(combinedCreated.map((a) => a.name))
|
|
767
|
+
const dedupedUpdateNames = uniq(combinedUpdateNames).filter((name) => !createdNames.has(name))
|
|
768
|
+
const dedupedMemberNames = uniq(combinedMemberNames).filter((name) => !createdNames.has(name))
|
|
769
|
+
updated.push(...dedupedUpdateNames.map(this.getAsset).filter(Boolean))
|
|
770
|
+
updated.push(...dedupedMemberNames.map(this.getAsset).filter(Boolean))
|
|
771
|
+
added.push(...combinedCreated.map((a) => this.getAsset(a.name)).filter(Boolean))
|
|
642
772
|
|
|
643
773
|
return { fetchedAndHandledTokens, added, updated }
|
|
644
774
|
}
|
|
645
775
|
|
|
646
|
-
#
|
|
776
|
+
#fetchIconToken = memoizeLruCache(
|
|
647
777
|
async (assetName) => {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
'tokens'
|
|
654
|
-
)
|
|
655
|
-
const token = tokens[0]
|
|
656
|
-
if (token?.icon) {
|
|
657
|
-
return this.#iconsStorage.unzipIcon(token.icon)
|
|
658
|
-
}
|
|
778
|
+
const tokens = await this.#fetchTokensByNames({
|
|
779
|
+
tokenNames: [assetName],
|
|
780
|
+
allowedStatusList: ['c', 'v', 'u'],
|
|
781
|
+
})
|
|
782
|
+
return tokens[0]
|
|
659
783
|
},
|
|
660
784
|
(assetName) => assetName,
|
|
661
785
|
{ max: 100 }
|
|
@@ -666,11 +790,21 @@ export class AssetsModule {
|
|
|
666
790
|
|
|
667
791
|
const asset = await this.getAsset(assetName)
|
|
668
792
|
if (asset) {
|
|
669
|
-
|
|
793
|
+
try {
|
|
794
|
+
const storedIcon = await this.#iconsStorage.getIcon(assetName)
|
|
795
|
+
if (storedIcon) return storedIcon
|
|
796
|
+
} catch (e) {
|
|
797
|
+
this.#logger.warn(`Failed to read stored icon for ${assetName}. ${e.message}`, e)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (asset.isBuiltIn) return
|
|
670
801
|
}
|
|
671
802
|
|
|
672
803
|
try {
|
|
673
|
-
|
|
804
|
+
const token = await this.#fetchIconToken(assetName)
|
|
805
|
+
if (!token?.icon) return
|
|
806
|
+
if (asset) await this.#storeIcons([token])
|
|
807
|
+
return this.#iconsStorage.unzipIcon(token.icon)
|
|
674
808
|
} catch (e) {
|
|
675
809
|
this.#logger.error(`Cannot load icon for ${assetName} from CTR. ${e.message}`, e)
|
|
676
810
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const TRUSTED_LIFECYCLE = new Set(['c'])
|
|
2
|
+
const MIN_ASSETS_TO_COMBINE = 2
|
|
3
|
+
|
|
4
|
+
const normalizeForKey = (str) => str.toLowerCase().trim()
|
|
5
|
+
|
|
6
|
+
const normalizeForName = (str) => str.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
7
|
+
|
|
8
|
+
const createGroupKey = (displayTicker, displayName) =>
|
|
9
|
+
`${normalizeForKey(displayTicker)}::${normalizeForKey(displayName)}`
|
|
10
|
+
|
|
11
|
+
const groupAssetsByTickerAndName = (assets) => {
|
|
12
|
+
const groups = Object.create(null)
|
|
13
|
+
|
|
14
|
+
for (const asset of assets) {
|
|
15
|
+
if (asset.isCombined || !asset.displayTicker || !asset.displayName) continue
|
|
16
|
+
|
|
17
|
+
const key = createGroupKey(asset.displayTicker, asset.displayName)
|
|
18
|
+
if (!groups[key]) groups[key] = []
|
|
19
|
+
groups[key].push(asset)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return groups
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const selectPrimaryAsset = (assets) => {
|
|
26
|
+
const sorted = [...assets].sort((a, b) => a.name.localeCompare(b.name))
|
|
27
|
+
return sorted[0]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const resolveDecimals = (assets) => {
|
|
31
|
+
const decimals = assets.map((a) => a.currency?.defaultUnit?.power ?? 8)
|
|
32
|
+
return Math.max(...decimals)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const createDynamicCombinedAssetDefinition = (assetGroup) => {
|
|
36
|
+
const primary = selectPrimaryAsset(assetGroup)
|
|
37
|
+
const sortedAssets = [...assetGroup].sort((a, b) => a.name.localeCompare(b.name))
|
|
38
|
+
|
|
39
|
+
const normalizedTicker = normalizeForName(primary.displayTicker)
|
|
40
|
+
const normalizedDisplayName = normalizeForName(primary.displayName)
|
|
41
|
+
const decimals = resolveDecimals(assetGroup)
|
|
42
|
+
|
|
43
|
+
const name = `_combined_${normalizedTicker}_${normalizedDisplayName}`
|
|
44
|
+
const ticker = name.toUpperCase()
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
ticker,
|
|
49
|
+
baseAssetName: name,
|
|
50
|
+
displayName: primary.displayName,
|
|
51
|
+
displayTicker: primary.displayTicker,
|
|
52
|
+
assetType: 'MULTI_NETWORK_ASSET',
|
|
53
|
+
units: {
|
|
54
|
+
[`${name}_base`]: 0,
|
|
55
|
+
[ticker]: decimals,
|
|
56
|
+
},
|
|
57
|
+
combinedAssetNames: sortedAssets.map((a) => a.name),
|
|
58
|
+
info: primary.info || { description: '' },
|
|
59
|
+
primaryColor: primary.primaryColor || '#888888',
|
|
60
|
+
gradientColors: primary.gradientColors || ['#888888', '#666666'],
|
|
61
|
+
chainBadgeColors: primary.chainBadgeColors || primary.gradientColors || ['#888888', '#666666'],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const isTrustedAsset = (asset) => {
|
|
66
|
+
// Built-in assets (bundled with app, not fetched from CTR) are trusted
|
|
67
|
+
if (asset.isBuiltIn === true && asset.isCustomToken !== true) return true
|
|
68
|
+
|
|
69
|
+
// Curated custom tokens from CTR are trusted (Exodus controls curation)
|
|
70
|
+
if (TRUSTED_LIFECYCLE.has(asset.lifecycleStatus)) return true
|
|
71
|
+
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const createDynamicCombinedAssetDefinitions = (assets) => {
|
|
76
|
+
const assetList = Array.isArray(assets) ? assets : Object.values(assets)
|
|
77
|
+
const trustedAssets = assetList.filter(isTrustedAsset)
|
|
78
|
+
const groups = groupAssetsByTickerAndName(trustedAssets)
|
|
79
|
+
|
|
80
|
+
const definitions = []
|
|
81
|
+
for (const assetGroup of Object.values(groups)) {
|
|
82
|
+
if (assetGroup.length >= MIN_ASSETS_TO_COMBINE) {
|
|
83
|
+
definitions.push(createDynamicCombinedAssetDefinition(assetGroup))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return definitions
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const findDynamicCombinedAssetForToken = (token, existingCombinedAssets) => {
|
|
91
|
+
if (!token.displayTicker || !token.displayName) return null
|
|
92
|
+
|
|
93
|
+
const tokenKey = createGroupKey(token.displayTicker, token.displayName)
|
|
94
|
+
|
|
95
|
+
for (const combined of existingCombinedAssets) {
|
|
96
|
+
if (!combined.isCombined) continue
|
|
97
|
+
if (!combined.displayTicker || !combined.displayName) continue
|
|
98
|
+
|
|
99
|
+
const combinedKey = createGroupKey(combined.displayTicker, combined.displayName)
|
|
100
|
+
if (combinedKey === tokenKey) return combined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const shouldTokenJoinCombinedAsset = (token) => isTrustedAsset(token)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/assets-feature",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.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",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"@exodus/asset-schema-validation": "^1.2.0",
|
|
41
41
|
"@exodus/asset-types": "^0.5.0",
|
|
42
42
|
"@exodus/assets": "^11.3.0",
|
|
43
|
-
"@exodus/atoms": "^10.3.
|
|
44
|
-
"@exodus/basic-utils": "^5.
|
|
43
|
+
"@exodus/atoms": "^10.3.4",
|
|
44
|
+
"@exodus/basic-utils": "^5.2.0",
|
|
45
45
|
"@exodus/fetch": "^1.3.0",
|
|
46
46
|
"@exodus/fusion-atoms": "^1.4.0",
|
|
47
|
-
"@exodus/timer": "^1.
|
|
47
|
+
"@exodus/timer": "^1.2.2",
|
|
48
48
|
"lodash": "^4.17.21",
|
|
49
49
|
"make-concurrent": "^5.4.0",
|
|
50
50
|
"minimalistic-assert": "^1.0.1",
|
|
@@ -52,17 +52,16 @@
|
|
|
52
52
|
"reselect": "^3.0.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@exodus/available-assets": "^8.11.
|
|
55
|
+
"@exodus/available-assets": "^8.11.1",
|
|
56
56
|
"@exodus/bip32": "^4.3.0",
|
|
57
57
|
"@exodus/bip39": "^1.1.0",
|
|
58
|
-
"@exodus/bip44-constants": "^195.0.
|
|
58
|
+
"@exodus/bip44-constants": "^195.0.2",
|
|
59
59
|
"@exodus/bitcoin-meta": "^2.0.0",
|
|
60
60
|
"@exodus/bitcoin-plugin": "^2.0.0",
|
|
61
61
|
"@exodus/bitcoinregtest-plugin": "^2.0.0",
|
|
62
62
|
"@exodus/bitcointestnet-plugin": "^2.0.0",
|
|
63
63
|
"@exodus/blockchain-metadata": "^17.2.0",
|
|
64
64
|
"@exodus/cardano-lib": "^4.0.0",
|
|
65
|
-
"@exodus/combined-assets-meta": "^3.7.0",
|
|
66
65
|
"@exodus/cosmos-plugin": "^1.3.3",
|
|
67
66
|
"@exodus/ethereum-lib": "^5.0.0",
|
|
68
67
|
"@exodus/ethereum-meta": "^2.0.0",
|
|
@@ -75,8 +74,8 @@
|
|
|
75
74
|
"@exodus/public-key-provider": "^4.3.1",
|
|
76
75
|
"@exodus/redux-dependency-injection": "^5.0.0",
|
|
77
76
|
"@exodus/storage-memory": "^2.4.0",
|
|
78
|
-
"@exodus/wallet-accounts": "^20.4.
|
|
79
|
-
"@exodus/wild-emitter": "^1.
|
|
77
|
+
"@exodus/wallet-accounts": "^20.4.11",
|
|
78
|
+
"@exodus/wild-emitter": "^1.1.2",
|
|
80
79
|
"events": "^3.3.0",
|
|
81
80
|
"msw": "^2.0.0",
|
|
82
81
|
"redux": "^4.0.0"
|
|
@@ -85,5 +84,5 @@
|
|
|
85
84
|
"access": "public",
|
|
86
85
|
"provenance": false
|
|
87
86
|
},
|
|
88
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "69a95445c733ef5519ac3fde6632fa9afc399b76"
|
|
89
88
|
}
|