Skip to content

LW-12164 Feat/cache data from blockfrost locally #1569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type BlockfrostTx = Pick<Responses['address_transactions_content'][0], 'block_he
const compareTx = (a: BlockfrostTx, b: BlockfrostTx) => a.block_height - b.block_height || a.tx_index - b.tx_index;

export class BlockfrostChainHistoryProvider extends BlockfrostProvider implements ChainHistoryProvider {
private readonly cache: Map<string, Cardano.HydratedTx> = new Map();
Copy link
Member

@mkazlauskas mkazlauskas Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better if cache objects were injected (e.g. type Cache<K, V> = { get: (key: K) => Promise<T>, set: (key: K, value: V) => Promise<void> }), then it would be easier to update/swap it with a more sophisticated cache (lru, persistent etc.) and control it with FFs. Anyway happy to merge as-is to mitigate the current issue quickly.

private networkInfoProvider: NetworkInfoProvider;

constructor(client: BlockfrostClient, networkInfoProvider: NetworkInfoProvider, logger: Logger) {
Expand Down Expand Up @@ -475,11 +476,20 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement
}

public async transactionsByHashes({ ids }: TransactionsByIdsArgs): Promise<Cardano.HydratedTx[]> {
try {
return Promise.all(ids.map((id) => this.fetchTransaction(id)));
} catch (error) {
throw this.toProviderError(error);
}
return Promise.all(
ids.map(async (id) => {
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
try {
const fetchedTransaction = await this.fetchTransaction(id);
this.cache.set(id, fetchedTransaction);
return fetchedTransaction;
} catch (error) {
throw this.toProviderError(error);
}
})
);
}

// eslint-disable-next-line sonarjs/cognitive-complexity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Cardano, Serialization, UtxoByAddressesArgs, UtxoProvider } from '@card
import type { Responses } from '@blockfrost/blockfrost-js';

export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoProvider {
private readonly cache: Map<string, Cardano.Tx> = new Map();

protected async fetchUtxos(addr: Cardano.PaymentAddress, paginationQueryString: string): Promise<Cardano.Utxo[]> {
const queryString = `addresses/${addr.toString()}/utxos?${paginationQueryString}`;
const utxos = await this.request<Responses['address_utxo_content']>(queryString);
Expand All @@ -27,7 +29,11 @@ export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoPr
});
}
protected async fetchDetailsFromCBOR(hash: string) {
return this.fetchCBOR(hash)
if (this.cache.has(hash)) {
return this.cache.get(hash);
}

const result = await this.fetchCBOR(hash)
.then((cbor) => {
const tx = Serialization.Transaction.fromCbor(Serialization.TxCBOR(cbor)).toCore();
this.logger.info('Fetched details from CBOR for tx', hash);
Expand All @@ -37,7 +43,15 @@ export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoPr
this.logger.warn('Failed to fetch details from CBOR for tx', hash, error);
return null;
});

if (!result) {
return null;
}

this.cache.set(hash, result);
return result;
}

public async utxoByAddresses({ addresses }: UtxoByAddressesArgs): Promise<Cardano.Utxo[]> {
try {
const utxoResults = await Promise.all(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,5 +590,25 @@ describe('blockfrostChainHistoryProvider', () => {
expect(response[0]).toEqual(expectedHydratedTxCBOR);
});
});

test('caches transactions and returns them from the cache on subsequent calls', async () => {
const firstResponse = await provider.transactionsByHashes({
ids: ['1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477' as Cardano.TransactionId]
});

expect(firstResponse).toHaveLength(1);
expect(firstResponse[0]).toEqual(expectedHydratedTxCBOR);
expect(request).toHaveBeenCalled();

request.mockClear();

const secondResponse = await provider.transactionsByHashes({
ids: ['1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477' as Cardano.TransactionId]
});

expect(secondResponse).toHaveLength(1);
expect(secondResponse[0]).toEqual(expectedHydratedTxCBOR);
expect(request).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,29 @@ describe('blockfrostUtxoProvider', () => {
}
});
});

test('does not call txs/${hash}/cbor when data is cached', async () => {
const txHash = '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5';

mockResponses(request, [
[`txs/${txHash}/cbor`, 'mockedCBORData'],
[`addresses/${address.toString()}/utxos?page=1&count=100`, generateUtxoResponseMock(1)],
[`addresses/${address.toString()}/utxos?page=2&count=100`, generateUtxoResponseMock(0)]
]);

const firstResponse = await provider.utxoByAddresses({ addresses: [address] });

expect(firstResponse).toBeTruthy();
expect(firstResponse.length).toBe(1);

expect(request).toHaveBeenCalled();
request.mockClear();

const secondResponse = await provider.utxoByAddresses({ addresses: [address] });

expect(secondResponse).toEqual(firstResponse);

expect(request).not.toHaveBeenCalledWith(`txs/${txHash}/cbor`);
});
});
});
Loading