diff --git a/app/entities/idl/model/__tests__/use-idl-from-anchor-program-seed.test.ts b/app/entities/idl/model/__tests__/use-idl-from-anchor-program-seed.test.ts new file mode 100644 index 000000000..2cfdcb7f1 --- /dev/null +++ b/app/entities/idl/model/__tests__/use-idl-from-anchor-program-seed.test.ts @@ -0,0 +1,66 @@ +import { Program } from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { Cluster } from '@/app/utils/cluster'; + +import { resetCacheForTesting, useIdlFromAnchorProgramSeed } from '../use-idl-from-anchor-program-seed'; + +describe('useIdlFromAnchorProgramSeed', () => { + const url = 'https://any.rpc.address'; + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + resetCacheForTesting(); + }); + + it('should not fire duplicate /api/anchor requests for the same key while the request is in flight', () => { + const fetchMock = vi.fn(() => new Promise(() => {})); + vi.stubGlobal('fetch', fetchMock); + + const programAddress = PublicKey.unique().toBase58(); + + let firstThrown: unknown; + let secondThrown: unknown; + + try { + useIdlFromAnchorProgramSeed(programAddress, url, Cluster.MainnetBeta); + } catch (e) { + firstThrown = e; + } + try { + useIdlFromAnchorProgramSeed(programAddress, url, Cluster.MainnetBeta); + } catch (e) { + secondThrown = e; + } + + expect(firstThrown).toBeInstanceOf(Promise); + expect(secondThrown).toBe(firstThrown); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('should not call Program.fetchIdl twice for the same key while the request is in flight on a Custom cluster', () => { + const fetchIdlSpy = vi.spyOn(Program, 'fetchIdl').mockReturnValue(new Promise(() => {})); + + const programAddress = PublicKey.unique().toBase58(); + + let firstThrown: unknown; + let secondThrown: unknown; + + try { + useIdlFromAnchorProgramSeed(programAddress, url, Cluster.Custom); + } catch (e) { + firstThrown = e; + } + try { + useIdlFromAnchorProgramSeed(programAddress, url, Cluster.Custom); + } catch (e) { + secondThrown = e; + } + + expect(firstThrown).toBeInstanceOf(Promise); + expect(secondThrown).toBe(firstThrown); + expect(fetchIdlSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/entities/idl/model/use-idl-from-anchor-program-seed.ts b/app/entities/idl/model/use-idl-from-anchor-program-seed.ts index 68ad42f21..734bcde43 100644 --- a/app/entities/idl/model/use-idl-from-anchor-program-seed.ts +++ b/app/entities/idl/model/use-idl-from-anchor-program-seed.ts @@ -15,6 +15,16 @@ export function getProvider(url: string) { return new AnchorProvider(new Connection(url), new NodeWallet(Keypair.generate()), {}); } +function recordInFlight(key: string, promise: Promise) { + cachedAnchorProgramPromises[key] = { __type: 'promise', promise }; +} + +export function resetCacheForTesting() { + for (const k of Object.keys(cachedAnchorProgramPromises)) { + delete cachedAnchorProgramPromises[k]; + } +} + export function useIdlFromAnchorProgramSeed(programAddress: string, url: string, cluster?: Cluster): Idl | null { const key = `${programAddress}-${url}`; const cacheEntry = cachedAnchorProgramPromises[key]; @@ -43,6 +53,7 @@ export function useIdlFromAnchorProgramSeed(programAddress: string, url: string, .catch(_ => { cachedAnchorProgramPromises[key] = { __type: 'result', result: null }; }); + recordInFlight(key, promise); } else { const programId = new PublicKey(programAddress); promise = Program.fetchIdl(programId, getProvider(url)) @@ -59,10 +70,7 @@ export function useIdlFromAnchorProgramSeed(programAddress: string, url: string, .catch(_ => { cachedAnchorProgramPromises[key] = { __type: 'result', result: null }; }); - cachedAnchorProgramPromises[key] = { - __type: 'promise', - promise, - }; + recordInFlight(key, promise); } throw promise; } else if (cacheEntry.__type === 'promise') {