11import { Asset , Cardano } from '@cardano-sdk/core' ;
2+ import { Assets } from '../types' ;
3+ import { BalanceTracker , Milliseconds , TransactionsTracker } from './types' ;
24import { Logger } from 'ts-log' ;
35import {
46 Observable ,
@@ -8,6 +10,7 @@ import {
810 debounceTime ,
911 distinctUntilChanged ,
1012 filter ,
13+ firstValueFrom ,
1114 map ,
1215 of ,
1316 share ,
@@ -17,7 +20,6 @@ import {
1720} from 'rxjs' ;
1821import { RetryBackoffConfig } from 'backoff-rxjs' ;
1922import { TrackedAssetProvider } from './ProviderTracker' ;
20- import { TransactionsTracker } from './types' ;
2123import { coldObservableProvider , concatAndCombineLatest } from '@cardano-sdk/util-rxjs' ;
2224import { deepEquals , isNotNil } from '@cardano-sdk/util' ;
2325import { newTransactions$ } from './TransactionsTracker' ;
@@ -35,11 +37,102 @@ const bufferTick =
3537 source$ . pipe ( connect ( ( shared$ ) => shared$ . pipe ( buffer ( shared$ . pipe ( debounceTime ( 1 ) ) ) ) ) ) ;
3638
3739const ASSET_INFO_FETCH_CHUNK_SIZE = 100 ;
40+ const ONE_WEEK = 7 * 24 * 60 * 60 * 1000 ;
41+
42+ const isInBalance = ( assetId : Cardano . AssetId , balance : Cardano . Value ) : boolean =>
43+ balance . assets ?. has ( assetId ) ?? false ;
44+
45+ /**
46+ * Splits a list of asset IDs into cached and uncached groups based on their presence in the cache,
47+ * their freshness, and their balance status:
48+ *
49+ * 1. Assets not in Balance:
50+ * - Always use the cached version if present in the cache, ignoring freshness.
51+ * 2. Assets in Balance:
52+ * - Use the cached version only if it exists and its `staleAt` timestamp already expired.
53+ * 3. Uncached Assets:
54+ * - If an asset is not in the cache or does not meet the above criteria, mark it as uncached.
55+ */
56+ const splitCachedAndUncachedAssets = (
57+ cache : Assets ,
58+ balance : Cardano . Value ,
59+ assetIds : Cardano . AssetId [ ]
60+ ) : { cachedAssets : Assets ; uncachedAssetIds : Cardano . AssetId [ ] } => {
61+ const cachedAssets : Assets = new Map ( ) ;
62+ const uncachedAssetIds : Cardano . AssetId [ ] = [ ] ;
63+ const now = new Date ( ) ;
64+
65+ for ( const id of assetIds ) {
66+ const cachedAssetInfo = cache . get ( id ) ;
67+
68+ if ( ! cachedAssetInfo ) {
69+ uncachedAssetIds . push ( id ) ;
70+ continue ;
71+ }
72+
73+ const { staleAt } = cachedAssetInfo ;
74+
75+ const expired = ! staleAt || new Date ( staleAt ) < now ;
76+
77+ const mustFetch = ! isAssetInfoComplete ( cachedAssetInfo ) || ( isInBalance ( id , balance ) && expired ) ;
78+
79+ if ( mustFetch ) {
80+ uncachedAssetIds . push ( id ) ;
81+ } else {
82+ cachedAssets . set ( id , cachedAssetInfo ) ;
83+ }
84+ }
85+
86+ return { cachedAssets, uncachedAssetIds } ;
87+ } ;
88+
89+ const getAssetsWithCache = async (
90+ assetIdsChunk : Cardano . AssetId [ ] ,
91+ assetCache$ : Observable < Assets > ,
92+ totalBalance$ : Observable < Cardano . Value > ,
93+ assetProvider : TrackedAssetProvider ,
94+ maxAssetInfoCacheAge : Milliseconds
95+ ) : Promise < Asset . AssetInfo [ ] > => {
96+ const [ cache , totalValue ] = await Promise . all ( [ firstValueFrom ( assetCache$ ) , firstValueFrom ( totalBalance$ ) ] ) ;
97+
98+ const { cachedAssets, uncachedAssetIds } = splitCachedAndUncachedAssets ( cache , totalValue , assetIdsChunk ) ;
99+
100+ if ( uncachedAssetIds . length === 0 ) {
101+ // If all assets are cached we wont perform any fetches from assetProvider, but still need to
102+ // mark it as initialized.
103+ if ( ! assetProvider . stats . getAsset$ . value . initialized ) {
104+ assetProvider . setStatInitialized ( assetProvider . stats . getAsset$ ) ;
105+ }
106+
107+ return [ ...cachedAssets . values ( ) ] ;
108+ }
109+
110+ const fetchedAssets = await assetProvider . getAssets ( {
111+ assetIds : uncachedAssetIds ,
112+ extraData : { nftMetadata : true , tokenMetadata : true }
113+ } ) ;
114+
115+ const now = Date . now ( ) ;
116+ const updatedFetchedAssets = fetchedAssets . map ( ( asset ) => {
117+ const randomDelta = Math . floor ( Math . random ( ) * 2 * 24 * 60 * 60 * 1000 ) ; // Random time between 0 and 2 days
118+ return {
119+ ...asset ,
120+ staleAt : new Date ( now + maxAssetInfoCacheAge + randomDelta )
121+ } ;
122+ } ) ;
123+
124+ return [ ...cachedAssets . values ( ) , ...updatedFetchedAssets ] ;
125+ } ;
126+
38127export const createAssetService =
39128 (
40129 assetProvider : TrackedAssetProvider ,
130+ assetCache$ : Observable < Assets > ,
131+ totalBalance$ : Observable < Cardano . Value > ,
41132 retryBackoffConfig : RetryBackoffConfig ,
42- onFatalError ?: ( value : unknown ) => void
133+ onFatalError ?: ( value : unknown ) => void ,
134+ maxAssetInfoCacheAge : Milliseconds = ONE_WEEK
135+ // eslint-disable-next-line max-params
43136 ) =>
44137 ( assetIds : Cardano . AssetId [ ] ) =>
45138 concatAndCombineLatest (
@@ -48,23 +141,24 @@ export const createAssetService =
48141 onFatalError,
49142 pollUntil : isEveryAssetInfoComplete ,
50143 provider : ( ) =>
51- assetProvider . getAssets ( {
52- assetIds : assetIdsChunk ,
53- extraData : { nftMetadata : true , tokenMetadata : true }
54- } ) ,
144+ getAssetsWithCache ( assetIdsChunk , assetCache$ , totalBalance$ , assetProvider , maxAssetInfoCacheAge ) ,
55145 retryBackoffConfig,
56146 trigger$ : of ( true ) // fetch only once
57147 } )
58148 )
59- ) . pipe ( map ( ( arr ) => arr . flat ( ) ) ) ; // concat the chunk results
149+ ) . pipe ( map ( ( arr ) => arr . flat ( ) ) ) ; // Concatenate the chunk results
150+
60151export type AssetService = ReturnType < typeof createAssetService > ;
61152
62153export interface AssetsTrackerProps {
63154 transactionsTracker : TransactionsTracker ;
64155 assetProvider : TrackedAssetProvider ;
65156 retryBackoffConfig : RetryBackoffConfig ;
66157 logger : Logger ;
158+ assetsCache$ : Observable < Assets > ;
159+ balanceTracker : BalanceTracker ;
67160 onFatalError ?: ( value : unknown ) => void ;
161+ maxAssetInfoCacheAge ?: Milliseconds ;
68162}
69163
70164interface AssetsTrackerInternals {
@@ -76,8 +170,28 @@ const uniqueAssetIds = ({ body: { outputs } }: Cardano.OnChainTx) =>
76170const flatUniqueAssetIds = ( txes : Cardano . OnChainTx [ ] ) => uniq ( txes . flatMap ( uniqueAssetIds ) ) ;
77171
78172export const createAssetsTracker = (
79- { assetProvider, transactionsTracker : { history$ } , retryBackoffConfig, logger, onFatalError } : AssetsTrackerProps ,
80- { assetService = createAssetService ( assetProvider , retryBackoffConfig , onFatalError ) } : AssetsTrackerInternals = { }
173+ {
174+ assetProvider,
175+ assetsCache$,
176+ transactionsTracker : { history$ } ,
177+ balanceTracker : {
178+ utxo : { total$ }
179+ } ,
180+ retryBackoffConfig,
181+ logger,
182+ onFatalError,
183+ maxAssetInfoCacheAge
184+ } : AssetsTrackerProps ,
185+ {
186+ assetService = createAssetService (
187+ assetProvider ,
188+ assetsCache$ ,
189+ total$ ,
190+ retryBackoffConfig ,
191+ onFatalError ,
192+ maxAssetInfoCacheAge
193+ )
194+ } : AssetsTrackerInternals = { }
81195) =>
82196 new Observable < Map < Cardano . AssetId , Asset . AssetInfo > > ( ( subscriber ) => {
83197 let fetchedAssetInfoMap = new Map < Cardano . AssetId , Asset . AssetInfo > ( ) ;
0 commit comments