Skip to content

Commit

Permalink
Merge pull request #464 from input-output-hk/feat/re-fetch-token-and-…
Browse files Browse the repository at this point in the history
…nft-metadata-if-provider-returns-it-as-undefined

feat(wallet): attempt to re-fetch tokenMetadata/nftMetadata if provid…
  • Loading branch information
rhyslbw committed Sep 28, 2022
2 parents be24ba8 + 2800f9c commit 4afdf79
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 24 deletions.
19 changes: 12 additions & 7 deletions packages/wallet/src/services/AssetsTracker.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Asset, Cardano } from '@cardano-sdk/core';
import { BalanceTracker } from './types';
import { Logger } from 'ts-log';
import { Observable, forkJoin, map, mergeMap, of, tap } from 'rxjs';
import { Observable, combineLatest, map, mergeMap, of, tap } from 'rxjs';
import { RetryBackoffConfig } from 'backoff-rxjs';
import { TrackedAssetProvider } from './ProviderTracker';
import { coldObservableProvider } from './util';

const isAssetInfoComplete = (assetInfo: Asset.AssetInfo): boolean =>
assetInfo.nftMetadata !== undefined && assetInfo.tokenMetadata !== undefined;

export const createAssetService =
(assetProvider: TrackedAssetProvider, retryBackoffConfig: RetryBackoffConfig) => (assetId: Cardano.AssetId) =>
coldObservableProvider({
pollUntil: isAssetInfoComplete,
provider: () =>
assetProvider.getAsset({ assetId, extraData: { history: false, nftMetadata: true, tokenMetadata: true } }),
retryBackoffConfig,
Expand Down Expand Up @@ -47,16 +51,17 @@ export const createAssetsTracker = (
// Fetch asset metadata only for assets not already present in assetsMap
map((assetIds) =>
assetIds.map((assetId) => {
if (assetsMap.has(assetId)) {
return of(assetsMap.get(assetId));
const assetInfo = assetsMap.get(assetId);
if (assetInfo && isAssetInfoComplete(assetInfo)) {
return of(assetInfo);
}
logger.debug(`Fetching metadata for asset ${assetId}`);
logger.debug('Fetching asset data for', assetId);
return assetService(assetId);
})
),
// Wait for all asset metadata fetches to complete
mergeMap((assetInfos) => forkJoin(assetInfos)),
tap((assetInfos) => logger.debug(`Done fetching metadata for ${assetInfos.length} assets`)),
// Wait for all asset metadata fetches have a value
mergeMap((assetInfos) => combineLatest(assetInfos)),
tap((assetInfos) => logger.debug(`Got metadata for ${assetInfos.length} assets`)),
map((assetInfos) => new Map(assetInfos.map((assetInfo) => [assetInfo!.assetId, assetInfo!]))),
tap((v) => (assetsMap = v))
)
Expand Down
34 changes: 31 additions & 3 deletions packages/wallet/src/services/util/coldObservableProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { NEVER, Observable, defer, distinctUntilChanged, from, of, switchMap, takeUntil } from 'rxjs';
import {
NEVER,
Observable,
concat,
defer,
distinctUntilChanged,
from,
mergeMap,
of,
switchMap,
takeUntil,
throwError
} from 'rxjs';
import { RetryBackoffConfig, retryBackoff } from 'backoff-rxjs';
import { strictEquals } from './equals';

Expand All @@ -9,6 +21,7 @@ export interface ColdObservableProviderProps<T> {
equals?: (t1: T, t2: T) => boolean;
combinator?: typeof switchMap;
cancel$?: Observable<unknown>;
pollUntil?: (v: T) => boolean;
}

export const coldObservableProvider = <T>({
Expand All @@ -17,12 +30,27 @@ export const coldObservableProvider = <T>({
trigger$ = of(true),
equals = strictEquals,
combinator = switchMap,
cancel$ = NEVER
cancel$ = NEVER,
pollUntil = () => true
}: ColdObservableProviderProps<T>) =>
new Observable<T>((subscriber) => {
const sub = trigger$
.pipe(
combinator(() => defer(() => from(provider())).pipe(retryBackoff(retryBackoffConfig))),
combinator(() =>
defer(() =>
from(provider()).pipe(
mergeMap((v) =>
pollUntil(v)
? of(v)
: // emit value, but also throw error to force retryBackoff to kick in
concat(
of(v),
throwError(() => new Error('polling'))
)
)
)
).pipe(retryBackoff(retryBackoffConfig))
),
distinctUntilChanged(equals),
takeUntil(cancel$)
)
Expand Down
4 changes: 3 additions & 1 deletion packages/wallet/test/mocks/mockAssetProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export const asset = {
}
],
name: Cardano.AssetName('54534c41'),
nftMetadata: null,
policyId: Cardano.PolicyId('7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373'),
quantity: 1000n
quantity: 1000n,
tokenMetadata: null
} as Asset.AssetInfo;

export const mockAssetProvider = () => ({
Expand Down
61 changes: 49 additions & 12 deletions packages/wallet/test/services/AssetsTracker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Asset, Cardano } from '@cardano-sdk/core';
import { AssetId, createTestScheduler } from '@cardano-sdk/util-dev';
import { AssetId, createTestScheduler, logger } from '@cardano-sdk/util-dev';
import {
AssetService,
AssetsTrackerProps,
Expand All @@ -8,20 +8,21 @@ import {
TransactionalTracker,
createAssetsTracker
} from '../../src/services';
import { dummyLogger } from 'ts-log';
import { of } from 'rxjs';

import { RetryBackoffConfig } from 'backoff-rxjs';
import { from, lastValueFrom, of, tap } from 'rxjs';

describe('createAssetsTracker', () => {
let assetTsla: Asset.AssetInfo;
let assetPxl: Asset.AssetInfo;
let assetService: AssetService;
let assetProvider: TrackedAssetProvider;
const logger = dummyLogger;
const retryBackoffConfig: RetryBackoffConfig = { initialInterval: 2 };

beforeEach(() => {
const nftMetadata = { name: 'nft' } as Asset.NftMetadata;
assetTsla = { assetId: AssetId.TSLA } as Asset.AssetInfo;
assetPxl = { assetId: AssetId.PXL, nftMetadata } as Asset.AssetInfo;
assetTsla = { assetId: AssetId.TSLA, nftMetadata: null, tokenMetadata: null } as Asset.AssetInfo;
assetPxl = { assetId: AssetId.PXL, nftMetadata, tokenMetadata: null } as Asset.AssetInfo;
assetService = jest.fn().mockReturnValueOnce(of(assetTsla)).mockReturnValueOnce(of(assetPxl));
assetProvider = {
setStatInitialized: jest.fn(),
Expand All @@ -46,9 +47,12 @@ describe('createAssetsTracker', () => {
}
} as unknown as TransactionalTracker<BalanceTracker>;

const target$ = createAssetsTracker({ assetProvider, balanceTracker, logger } as unknown as AssetsTrackerProps, {
assetService
});
const target$ = createAssetsTracker(
{ assetProvider, balanceTracker, logger, retryBackoffConfig } as unknown as AssetsTrackerProps,
{
assetService
}
);
expectObservable(target$).toBe('--b-c', {
b: new Map([[AssetId.TSLA, assetTsla]]),
c: new Map([
Expand Down Expand Up @@ -79,9 +83,12 @@ describe('createAssetsTracker', () => {
}
} as unknown as TransactionalTracker<BalanceTracker>;

const target$ = createAssetsTracker({ assetProvider, balanceTracker, logger } as unknown as AssetsTrackerProps, {
assetService
});
const target$ = createAssetsTracker(
{ assetProvider, balanceTracker, logger, retryBackoffConfig } as unknown as AssetsTrackerProps,
{
assetService
}
);
expectObservable(target$).toBe('--b-c', {
b: new Map([
[AssetId.TSLA, assetTsla],
Expand All @@ -93,4 +100,34 @@ describe('createAssetsTracker', () => {
expect(assetService).toHaveBeenCalledTimes(2);
});
});

it('polls asset info while metadata is undefined', async () => {
assetProvider = {
getAsset: jest
.fn()
.mockResolvedValueOnce({ ...assetTsla, nftMetadata: undefined })
.mockResolvedValueOnce({ ...assetTsla, tokenMetadata: undefined })
.mockResolvedValueOnce(assetTsla),
setStatInitialized: jest.fn(),
stats: {}
} as unknown as TrackedAssetProvider;

const balanceTracker = { utxo: { total$: from([{}, { assets: new Map([[AssetId.TSLA, 1n]]) }]) } };

const target$ = createAssetsTracker({
assetProvider,
balanceTracker,
logger,
retryBackoffConfig
} as unknown as AssetsTrackerProps);

const assetInfos: Map<Cardano.AssetId, Asset.AssetInfo>[] = [];
await lastValueFrom(target$.pipe(tap((ai) => assetInfos.push(ai))));
expect(assetInfos).toEqual([
new Map([[AssetId.TSLA, { ...assetTsla, nftMetadata: undefined }]]),
new Map([[AssetId.TSLA, { ...assetTsla, tokenMetadata: undefined }]]),
new Map([[AssetId.TSLA, assetTsla]])
]);
expect(assetProvider.getAsset).toBeCalledTimes(3);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BehaviorSubject, EmptyError, Subject, firstValueFrom } from 'rxjs';
import { BehaviorSubject, EmptyError, Subject, firstValueFrom, lastValueFrom, tap } from 'rxjs';
import { RetryBackoffConfig, retryBackoff } from 'backoff-rxjs';
import { coldObservableProvider } from '../../../src';

Expand Down Expand Up @@ -46,4 +46,25 @@ describe('coldObservableProvider', () => {
expect(underlyingProvider).toBeCalledTimes(2);
expect(resolvedValue).toBeTruthy();
});

it('polls the provider until the pollUntil condition is satisfied', async () => {
const underlyingProvider = jest
.fn()
.mockResolvedValueOnce('a')
.mockResolvedValueOnce('b')
.mockResolvedValueOnce('c')
.mockResolvedValue('Never reached');
const backoffConfig: RetryBackoffConfig = { initialInterval: 1 };

const provider$ = coldObservableProvider({
pollUntil: (v) => v === 'c',
provider: underlyingProvider,
retryBackoffConfig: backoffConfig
});

const providerValues: unknown[] = [];
await lastValueFrom(provider$.pipe(tap((v) => providerValues.push(v))));
expect(providerValues).toEqual(['a', 'b', 'c']);
expect(underlyingProvider).toBeCalledTimes(3);
});
});

0 comments on commit 4afdf79

Please sign in to comment.