Skip to content

Commit d662e77

Browse files
committed
chore: add cleanup for expired non evm historical prices
1 parent b3c7eff commit d662e77

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.test.ts

+94-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ const fakeHistoricalPrices: OnAssetHistoricalPriceResponse = {
9999
],
100100
},
101101
updateTime: 1737542312,
102-
expirationTime: 1737542312,
102+
// expirationTime is in 1Hour based on current Date.now()
103+
expirationTime: Date.now() + 1000 * 60 * 60,
103104
},
104105
};
105106

@@ -627,5 +628,97 @@ describe('MultichainAssetsRatesController', () => {
627628

628629
expect(snapHandler).toHaveBeenCalledTimes(1);
629630
});
631+
632+
it('does not clean up any of the prices if none of them have expired', async () => {
633+
const testCurrency = 'EUR';
634+
const testNativeAssetPrices = {
635+
intervals: {},
636+
updateTime: Date.now(),
637+
expirationTime: Date.now() + 1000, // not expired
638+
};
639+
const testTokenAssetPrices = {
640+
intervals: {},
641+
updateTime: Date.now(),
642+
expirationTime: Date.now() + 1000, // not expired
643+
};
644+
const { controller, messenger } = setupController({
645+
config: {
646+
state: {
647+
historicalPrices: {
648+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
649+
[testCurrency]: testNativeAssetPrices,
650+
},
651+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:testToken1': {
652+
[testCurrency]: testTokenAssetPrices,
653+
},
654+
},
655+
},
656+
},
657+
});
658+
659+
const snapHandler = jest.fn().mockResolvedValue(fakeHistoricalPrices);
660+
messenger.registerActionHandler(
661+
'SnapController:handleRequest',
662+
snapHandler,
663+
);
664+
665+
await controller.fetchHistoricalPricesForAsset(
666+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
667+
);
668+
669+
expect(snapHandler).toHaveBeenCalledTimes(1);
670+
expect(controller.state.historicalPrices).toStrictEqual({
671+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
672+
USD: fakeHistoricalPrices.historicalPrice,
673+
EUR: testNativeAssetPrices,
674+
},
675+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:testToken1': {
676+
EUR: testTokenAssetPrices,
677+
},
678+
});
679+
});
680+
681+
it('cleans up all historical prices that have expired', async () => {
682+
const testCurrency = 'EUR';
683+
const { controller, messenger } = setupController({
684+
config: {
685+
state: {
686+
historicalPrices: {
687+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
688+
[testCurrency]: {
689+
intervals: {},
690+
updateTime: Date.now(),
691+
expirationTime: Date.now() - 1000, // expired
692+
},
693+
},
694+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:testToken1': {
695+
[testCurrency]: {
696+
intervals: {},
697+
updateTime: Date.now(),
698+
expirationTime: Date.now() - 1000, // expired
699+
},
700+
},
701+
},
702+
},
703+
},
704+
});
705+
706+
const snapHandler = jest.fn().mockResolvedValue(fakeHistoricalPrices);
707+
messenger.registerActionHandler(
708+
'SnapController:handleRequest',
709+
snapHandler,
710+
);
711+
712+
await controller.fetchHistoricalPricesForAsset(
713+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
714+
);
715+
716+
expect(snapHandler).toHaveBeenCalledTimes(1);
717+
expect(controller.state.historicalPrices).toStrictEqual({
718+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
719+
USD: fakeHistoricalPrices.historicalPrice,
720+
},
721+
});
722+
});
630723
});
631724
});

packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.ts

+42
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
import { HandlerType } from '@metamask/snaps-utils';
2929
import { Mutex } from 'async-mutex';
3030
import type { Draft } from 'immer';
31+
import { isEqual } from 'lodash';
3132

3233
import { MAP_CAIP_CURRENCIES } from './constant';
3334
import type {
@@ -389,6 +390,8 @@ export class MultichainAssetsRatesController extends StaticIntervalPollingContro
389390
},
390391
};
391392
});
393+
// cleanup all historical prices that have expired
394+
this.#cleanupHistoricalPrices();
392395
} catch {
393396
throw new Error(
394397
`Failed to fetch historical prices for asset: ${asset}`,
@@ -399,6 +402,45 @@ export class MultichainAssetsRatesController extends StaticIntervalPollingContro
399402
});
400403
}
401404

405+
#cleanupHistoricalPrices() {
406+
const allHistoricalPrices = this.state.historicalPrices;
407+
const cleanedHistoricalPrices =
408+
this.#removeExpiredEntries(allHistoricalPrices);
409+
410+
// do not update state if no changes
411+
if (isEqual(allHistoricalPrices, cleanedHistoricalPrices)) {
412+
return;
413+
}
414+
415+
this.update((state) => {
416+
state.historicalPrices = cleanedHistoricalPrices;
417+
});
418+
}
419+
420+
#removeExpiredEntries(
421+
data: Record<CaipAssetType, Record<string, HistoricalPrice>>,
422+
): Record<CaipAssetType, Record<string, HistoricalPrice>> {
423+
const now = Date.now();
424+
const result: Record<CaipAssetType, Record<string, HistoricalPrice>> = {};
425+
426+
Object.entries(data).forEach(([assetId, currencies]) => {
427+
const validCurrencies: Record<string, HistoricalPrice> = {};
428+
429+
Object.entries(currencies).forEach(([currency, details]) => {
430+
const exp = details.expirationTime;
431+
if (exp === undefined || exp > now) {
432+
validCurrencies[currency] = details;
433+
}
434+
});
435+
436+
if (Object.keys(validCurrencies).length > 0) {
437+
result[assetId as CaipAssetType] = validCurrencies;
438+
}
439+
});
440+
441+
return result;
442+
}
443+
402444
/**
403445
* Returns the array of CAIP-19 assets for the given account ID.
404446
* If none are found, returns an empty array.

0 commit comments

Comments
 (0)