Skip to content

Commit 63b0095

Browse files
authored
Merge pull request #1565 from input-output-hk/fix/blockfrost-chain-history-slice
fixes: blockfrost chain history transactions limit per address & balance token info
2 parents f6a4480 + b3c871f commit 63b0095

File tree

4 files changed

+133
-52
lines changed

4 files changed

+133
-52
lines changed

packages/cardano-services-client/src/ChainHistoryProvider/BlockfrostChainHistoryProvider.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,17 +522,15 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement
522522

523523
const allTransactions = addressTransactions.flat(1);
524524

525-
const dedupedSortedTransactions = uniq(
525+
const dedupedSortedTransactionsIds = uniq(
526526
allTransactions
527527
.filter(({ block_height }) => block_height >= lowerBound && block_height <= upperBound)
528528
.sort(pagination.order === 'desc' ? (a, b) => compareTx(b, a) : compareTx)
529529
.map(({ tx_hash }) => Cardano.TransactionId(tx_hash))
530530
);
531-
const ids = dedupedSortedTransactions.slice(pagination.startAt, pagination.limit);
531+
const pageResults = await this.transactionsByHashes({ ids: dedupedSortedTransactionsIds });
532532

533-
const pageResults = await this.transactionsByHashes({ ids });
534-
535-
return { pageResults, totalResultCount: dedupedSortedTransactions.length };
533+
return { pageResults, totalResultCount: dedupedSortedTransactionsIds.length };
536534
} catch (error) {
537535
throw this.toProviderError(error);
538536
}

packages/cardano-services-client/test/ChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ describe('blockfrostChainHistoryProvider', () => {
1111
let provider: BlockfrostChainHistoryProvider;
1212
let networkInfoProvider: NetworkInfoProvider;
1313

14+
const txId1 = Cardano.TransactionId('1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477');
15+
const txId2 = Cardano.TransactionId('2e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477');
16+
const address1 = Cardano.PaymentAddress('2cWKMJemoBai9J7kVvRTukMmdfxtjL9z7c396rTfrrzfAZ6EeQoKLC2y1k34hswwm4SVr');
17+
const address2 = Cardano.PaymentAddress(
18+
'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x'
19+
);
1420
const txsUtxosResponse = {
15-
hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6',
21+
hash: txId1,
1622
inputs: [
1723
{
1824
address:
@@ -58,13 +64,13 @@ describe('blockfrostChainHistoryProvider', () => {
5864
}
5965
]
6066
};
61-
const mockedTxResponse = {
67+
const mockedTx1Response = {
6268
asset_mint_or_burn_count: 5,
6369
block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940',
6470
block_height: 123_456,
6571
delegation_count: 0,
6672
fees: '182485',
67-
hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477',
73+
hash: txId1,
6874
index: 1,
6975
invalid_before: null,
7076
invalid_hereafter: '13885913',
@@ -89,6 +95,10 @@ describe('blockfrostChainHistoryProvider', () => {
8995
valid_contract: true,
9096
withdrawal_count: 1
9197
};
98+
const mockedTx2Response = {
99+
...mockedTx1Response,
100+
hash: txId2
101+
};
92102
const mockedMetadataResponse = [
93103
{
94104
json_metadata: {
@@ -186,11 +196,20 @@ describe('blockfrostChainHistoryProvider', () => {
186196
unit_steps: '476468'
187197
}
188198
];
199+
189200
const mockedAddressTransactionResponse: Responses['address_transactions_content'] = [
190201
{
191202
block_height: 123,
192203
block_time: 131_322,
193-
tx_hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477',
204+
tx_hash: txId1,
205+
tx_index: 0
206+
}
207+
];
208+
const mockedAddress2TransactionResponse: Responses['address_transactions_content'] = [
209+
{
210+
block_height: 124,
211+
block_time: 131_322,
212+
tx_hash: txId2,
194213
tx_index: 0
195214
}
196215
];
@@ -352,25 +371,30 @@ describe('blockfrostChainHistoryProvider', () => {
352371
])
353372
} as unknown as NetworkInfoProvider;
354373
provider = new BlockfrostChainHistoryProvider(client, networkInfoProvider, logger);
355-
const txId = Cardano.TransactionId('1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477');
356-
const id = txId.toString();
357374
mockResponses(request, [
358-
[`txs/${id}/utxos`, txsUtxosResponse],
359-
[`txs/${id}`, mockedTxResponse],
360-
[`txs/${id}/metadata`, mockedMetadataResponse],
361-
[`txs/${id}/mirs`, mockedMirResponse],
362-
[`txs/${id}/pool_updates`, mockedPoolUpdateResponse],
363-
[`txs/${id}/pool_retires`, mockedPoolRetireResponse],
364-
[`txs/${id}/stakes`, mockedStakeResponse],
365-
[`txs/${id}/delegations`, mockedDelegationResponse],
366-
[`txs/${id}/withdrawals`, mockedWithdrawalResponse],
367-
[`txs/${id}/redeemers`, mockedReedemerResponse],
368-
[
369-
`addresses/${Cardano.PaymentAddress(
370-
'2cWKMJemoBai9J7kVvRTukMmdfxtjL9z7c396rTfrrzfAZ6EeQoKLC2y1k34hswwm4SVr'
371-
).toString()}/transactions?page=1&count=20`,
372-
mockedAddressTransactionResponse
373-
],
375+
[`txs/${txId1}/utxos`, txsUtxosResponse],
376+
[`txs/${txId1}`, mockedTx1Response],
377+
[`txs/${txId1}/metadata`, mockedMetadataResponse],
378+
[`txs/${txId1}/mirs`, mockedMirResponse],
379+
[`txs/${txId1}/pool_updates`, mockedPoolUpdateResponse],
380+
[`txs/${txId1}/pool_retires`, mockedPoolRetireResponse],
381+
[`txs/${txId1}/stakes`, mockedStakeResponse],
382+
[`txs/${txId1}/delegations`, mockedDelegationResponse],
383+
[`txs/${txId1}/withdrawals`, mockedWithdrawalResponse],
384+
[`txs/${txId1}/redeemers`, mockedReedemerResponse],
385+
[`txs/${txId2}/utxos`, txsUtxosResponse],
386+
[`txs/${txId2}`, mockedTx2Response],
387+
[`txs/${txId2}/metadata`, mockedMetadataResponse],
388+
[`txs/${txId2}/mirs`, mockedMirResponse],
389+
[`txs/${txId2}/pool_updates`, mockedPoolUpdateResponse],
390+
[`txs/${txId2}/pool_retires`, mockedPoolRetireResponse],
391+
[`txs/${txId2}/stakes`, mockedStakeResponse],
392+
[`txs/${txId2}/delegations`, mockedDelegationResponse],
393+
[`txs/${txId2}/withdrawals`, mockedWithdrawalResponse],
394+
[`txs/${txId2}/redeemers`, mockedReedemerResponse],
395+
[`addresses/${address1}/transactions?page=1&count=20`, mockedAddressTransactionResponse],
396+
[`addresses/${address1}/transactions?page=1&count=1`, mockedAddressTransactionResponse],
397+
[`addresses/${address2}/transactions?page=1&count=1`, mockedAddress2TransactionResponse],
374398
[
375399
`addresses/${Cardano.PaymentAddress(
376400
'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x'
@@ -382,7 +406,7 @@ describe('blockfrostChainHistoryProvider', () => {
382406
mockedAddressTransactionDescResponse
383407
],
384408
['epochs/420000/parameters', mockedEpochParametersResponse],
385-
[`txs/${id}/cbor`, new Error('CBOR is null')]
409+
[`txs/${txId1}/cbor`, new Error('CBOR is null')]
386410
]);
387411
});
388412

@@ -394,7 +418,7 @@ describe('blockfrostChainHistoryProvider', () => {
394418
pagination: { limit: 20, startAt: 0 }
395419
});
396420

397-
expect(response.totalResultCount).toBe(1);
421+
expect(response.totalResultCount).toBe(mockedAddressTransactionResponse.length);
398422
expect(response.pageResults[0]).toEqual(expectedHydratedTx);
399423
});
400424
test('supports desc order', async () => {
@@ -418,6 +442,16 @@ describe('blockfrostChainHistoryProvider', () => {
418442
expect(response.pageResults).toHaveLength(mockedAddressTransactionResponse.length);
419443
expect(response.totalResultCount).toBe(mockedAddressTransactionResponse.length);
420444
});
445+
test('returns up to the {limit*addresses.length} number of transactions', async () => {
446+
const response = await provider.transactionsByAddresses({
447+
addresses: [address1, address2],
448+
pagination: { limit: 1, startAt: 0 }
449+
});
450+
451+
const totalResultCount = mockedAddressTransactionResponse.length + mockedAddress2TransactionResponse.length;
452+
expect(response.totalResultCount).toBe(totalResultCount);
453+
expect(response.pageResults.length).toBe(totalResultCount);
454+
});
421455
});
422456

423457
describe('transactionsByHashes', () => {
@@ -514,7 +548,7 @@ describe('blockfrostChainHistoryProvider', () => {
514548
const id = txId.toString();
515549
mockResponses(request, [
516550
[`txs/${id}/utxos`, txsUtxosResponse],
517-
[`txs/${id}`, mockedTxResponse],
551+
[`txs/${id}`, mockedTx1Response],
518552
[`txs/${id}/metadata`, mockedMetadataResponse],
519553
[`txs/${id}/mirs`, mockedMirResponse],
520554
[`txs/${id}/pool_updates`, mockedPoolUpdateResponse],
@@ -541,7 +575,7 @@ describe('blockfrostChainHistoryProvider', () => {
541575
pagination: { limit: 20, startAt: 0 }
542576
});
543577

544-
expect(response.totalResultCount).toBe(1);
578+
expect(response.totalResultCount).toBe(mockedAddressTransactionResponse.length);
545579
expect(response.pageResults[0]).toEqual(expectedHydratedTxCBOR);
546580
});
547581
});

packages/wallet/src/services/AssetsTracker.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Logger } from 'ts-log';
55
import {
66
Observable,
77
buffer,
8+
combineLatest,
89
concat,
910
connect,
1011
debounceTime,
@@ -151,14 +152,19 @@ export const createAssetService =
151152
).pipe(map((arr) => arr.flat())); // Concatenate the chunk results
152153

153154
export type AssetService = ReturnType<typeof createAssetService>;
155+
export type UtxoTotalBalance = {
156+
utxo: {
157+
total$: BalanceTracker['utxo']['total$'];
158+
};
159+
};
154160

155161
export interface AssetsTrackerProps {
156162
transactionsTracker: TransactionsTracker;
157163
assetProvider: TrackedAssetProvider;
158164
retryBackoffConfig: RetryBackoffConfig;
159165
logger: Logger;
160166
assetsCache$: Observable<Assets>;
161-
balanceTracker: BalanceTracker;
167+
balanceTracker: UtxoTotalBalance;
162168
maxAssetInfoCacheAge?: Milliseconds;
163169
}
164170

@@ -198,13 +204,14 @@ export const createAssetsTracker = (
198204
const allAssetIds = new Set<Cardano.AssetId>();
199205
const sharedHistory$ = history$.pipe(share());
200206
return concat(
201-
sharedHistory$.pipe(
202-
map((historyTxs) => uniq(historyTxs.flatMap(uniqueAssetIds))),
207+
combineLatest([
208+
sharedHistory$.pipe(map((historyTxs) => uniq(historyTxs.flatMap(uniqueAssetIds)))),
209+
total$.pipe(map((balance) => [...(balance.assets?.keys() || [])]))
210+
]).pipe(
211+
map(([txAssets, balanceAssets]) => uniq([...txAssets, ...balanceAssets])),
203212
tap((assetIds) =>
204213
logger.debug(
205-
assetIds.length > 0
206-
? `Historical total assets: ${assetIds.length}`
207-
: 'Setting assetProvider stats as initialized'
214+
assetIds.length > 0 ? `Total assets: ${assetIds.length}` : 'Setting assetProvider stats as initialized'
208215
)
209216
),
210217
tap((assetIds) => assetIds.length === 0 && assetProvider.setStatInitialized(assetProvider.stats.getAsset$)),

packages/wallet/test/services/AssetsTracker.test.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ import { AssetId, createTestScheduler, generateRandomHexString, logger } from '@
33
import {
44
AssetService,
55
AssetsTrackerProps,
6-
BalanceTracker,
76
TrackedAssetProvider,
87
TransactionsTracker,
8+
UtxoTotalBalance,
99
createAssetService,
1010
createAssetsTracker
1111
} from '../../src/services';
1212

13-
import { Observable, firstValueFrom, from, lastValueFrom, of, tap } from 'rxjs';
13+
import { Observable, concat, firstValueFrom, lastValueFrom, of, tap, timer } from 'rxjs';
1414
import { RetryBackoffConfig } from 'backoff-rxjs';
15+
import { toEmpty } from '@cardano-sdk/util-rxjs';
1516

1617
const createTxWithValues = (values: Partial<Cardano.Value>[]): Cardano.HydratedTx =>
1718
({ body: { outputs: values.map((value) => ({ value })) }, id: generateRandomHexString(64) } as Cardano.HydratedTx);
@@ -55,7 +56,7 @@ const assetInfo = {
5556
describe('createAssetsTracker', () => {
5657
let assetService: AssetService;
5758
let assetProvider: TrackedAssetProvider;
58-
let balanceTracker: BalanceTracker;
59+
let balanceTracker: UtxoTotalBalance;
5960
let assetsCache$: Observable<Map<Cardano.AssetId, Asset.AssetInfo>>;
6061
const retryBackoffConfig: RetryBackoffConfig = { initialInterval: 2 };
6162

@@ -86,14 +87,8 @@ describe('createAssetsTracker', () => {
8687
} as unknown as TrackedAssetProvider;
8788

8889
balanceTracker = {
89-
rewardAccounts: {
90-
deposit$: of(0n),
91-
rewards$: of(0n)
92-
},
9390
utxo: {
94-
available$: of({ coins: 0n }),
95-
total$: of({ coins: 0n }),
96-
unspendable$: of({ coins: 0n })
91+
total$: of({ coins: 0n })
9792
}
9893
};
9994

@@ -144,6 +139,52 @@ describe('createAssetsTracker', () => {
144139
});
145140
});
146141

142+
it('fetches asset info for assets in balance but not in transaction history', () => {
143+
createTestScheduler().run(({ cold, expectObservable, flush }) => {
144+
const transactionsTracker: Partial<TransactionsTracker> = {
145+
history$: cold('a', {
146+
a: [createTxWithValues([{ assets: new Map([[AssetId.TSLA, 1n]]) }])]
147+
})
148+
};
149+
150+
balanceTracker = {
151+
utxo: {
152+
total$: cold('a', {
153+
a: {
154+
assets: new Map([
155+
[AssetId.TSLA, 1n],
156+
[AssetId.PXL, 2n]
157+
]),
158+
coins: 1n
159+
}
160+
})
161+
}
162+
};
163+
164+
const target$ = createAssetsTracker(
165+
{
166+
assetProvider,
167+
assetsCache$,
168+
balanceTracker,
169+
logger,
170+
retryBackoffConfig,
171+
transactionsTracker
172+
} as unknown as AssetsTrackerProps,
173+
{
174+
assetService
175+
}
176+
);
177+
expectObservable(target$).toBe('a', {
178+
a: new Map([
179+
[AssetId.TSLA, assetInfo.TSLA],
180+
[AssetId.PXL, assetInfo.PXL]
181+
])
182+
});
183+
flush();
184+
expect(assetService).toHaveBeenCalledTimes(1);
185+
});
186+
});
187+
147188
it('re-fetches asset info when there is a cip68 reference nft in some tx history output', () => {
148189
createTestScheduler().run(({ cold, expectObservable, flush }) => {
149190
const transactionsTracker: Partial<TransactionsTracker> = {
@@ -349,9 +390,10 @@ describe('createAssetsTracker', () => {
349390
} as unknown as TrackedAssetProvider;
350391

351392
const transactionsTracker: Partial<TransactionsTracker> = {
352-
history$: from([
353-
[createTxWithValues([{}])],
354-
[
393+
history$: concat(
394+
of([createTxWithValues([{}])]),
395+
timer(1).pipe(toEmpty),
396+
of([
355397
createTxWithValues([
356398
{
357399
assets: new Map([
@@ -360,8 +402,8 @@ describe('createAssetsTracker', () => {
360402
])
361403
}
362404
])
363-
]
364-
])
405+
])
406+
)
365407
};
366408

367409
const target$ = createAssetsTracker({

0 commit comments

Comments
 (0)