Skip to content
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

feat: src-7 src-9 #1653

Closed
wants to merge 17 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/brown-badgers-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels-wallet": patch
---

Fetch and display NFT data directly from indexer.
5 changes: 5 additions & 0 deletions .changeset/light-cooks-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels-wallet": minor
---

Support SRC-7, 9 nft asset data.
5 changes: 5 additions & 0 deletions .changeset/rotten-grapes-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels-wallet": patch
---

Fixes intermittent long loading state on the wallet
30 changes: 6 additions & 24 deletions packages/app/src/systems/Account/services/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,40 +104,22 @@ export class AccountService {
try {
const provider = await createProvider(providerUrl!);
const balances = await getBalances(provider, account.publicKey);

const assets = await AssetService.getAssets();
const balanceAssets = await AssetsCache.fetchAllAssets(
provider.getChainId(),
balances.map((balance) => balance.assetId)
);
// includes "asset" prop in balance, centralizing the complexity here instead of in rest of UI
const nextBalancesWithAssets = await balances.reduce(
async (acc, balance) => {
const prev = await acc;
const asset = {
fuel: await getFuelAssetByAssetId({
assets,
assetId: balance.assetId,
}),
};
try {
const assetCached = await AssetsCache.getInstance().getAsset({
chainId: provider.getChainId(),
assetId: balance.assetId,
provider,
});

if (assetCached && asset.fuel) {
asset.fuel = {
...asset.fuel,
...assetCached,
indexed: true,
};
}
} catch (_) {}
const cachedAsset = balanceAssets.get(balance.assetId);

return [
...prev,
{
...balance,
amount: balance.amount,
asset: asset.fuel,
asset: cachedAsset,
},
];
},
Expand Down
136 changes: 109 additions & 27 deletions packages/app/src/systems/Asset/cache/AssetsCache.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import type { AssetData } from '@fuel-wallet/types';
import type { Asset, Provider } from 'fuels';
import type { Asset, AssetFuel, Provider } from 'fuels';
import { AssetService } from '~/systems/Asset/services';
import { getFuelAssetByAssetId } from '~/systems/Asset/utils';
import { db } from '~/systems/Core/utils/database';
import { fetchNftData } from '../utils/nft';

type Endpoint = {
chainId: number;
url: string;
};

const FIVE_MINUTES = 5 * 60 * 1000;
export class AssetsCache {
private cache: { [chainId: number]: { [assetId: string]: Asset } };
private cache: {
[chainId: number]: {
[assetId: string]: Asset & { fetchedAt?: number };
};
};
private dbAssetsCache: {
[chainId: number]: Array<AssetData>;
};
private static instance: AssetsCache;
private endpoints: Endpoint[] = [
{
Expand All @@ -25,15 +34,44 @@ export class AssetsCache {

private constructor() {
this.cache = {};
this.dbAssetsCache = {};
this.storage = new IndexedAssetsDB();
}

asset = {
name: '',
symbol: '',
metadata: {},
};

private getIndexerEndpoint(chainId: number) {
return this.endpoints.find(
(endpoint: Endpoint) => endpoint.chainId === chainId
);
}

static async fetchAllAssets(chainId: number, assetsIds: string[]) {
const instance = AssetsCache.getInstance();
const assetData = new Map<string, AssetFuel>();
const dbAssets = await AssetService.getAssets();
const promises = [];
for (const assetId of assetsIds) {
promises.push(
instance
.getAsset({ chainId, assetId, dbAssets })
.then((asset) => {
assetData.set(assetId, asset);
})
.catch((e) => {
console.error('Error fetching asset from indexer', e);
assetData.set(assetId, { name: '' } as AssetFuel);
})
);
}
await Promise.all(promises);
return assetData;
}

private async fetchAssetFromIndexer(url: string, assetId: string) {
try {
const timeout = new Promise<null>((_, reject) =>
Expand All @@ -51,60 +89,104 @@ export class AssetsCache {
} catch (_e: unknown) {}
}

assetIsValid(asset: AssetData) {
return (
asset.name != null && 'fetchedAt' in asset && asset.fetchedAt != null
);
}

async getAsset({
chainId,
assetId,
provider,
}: { chainId: number; assetId: string; provider: Provider }) {
dbAssets,
save = true,
}: {
chainId: number;
assetId: string;
dbAssets: AssetData[];
save?: boolean;
}) {
if (chainId == null || !assetId) {
return;
}
const endpoint = this.getIndexerEndpoint(chainId);
if (!endpoint) return;
// try to get from memory cache first
this.cache[chainId] = this.cache[chainId] || {};
const assetFromCache = this.cache[chainId][assetId];
if (assetFromCache?.name) {
return assetFromCache;
const cachedEntry = this.cache[chainId][assetId];
const now = Date.now();

if (dbAssets?.length) {
this.dbAssetsCache[chainId] = dbAssets;
}

if (
cachedEntry?.name !== undefined &&
cachedEntry.fetchedAt &&
now - cachedEntry.fetchedAt < FIVE_MINUTES
) {
return cachedEntry;
}

// get from indexed db if not in memory
const assetFromDb = await this.storage.getItem(`${chainId}/${assetId}`);
if (assetFromDb?.name) {
if (
assetFromDb?.name &&
assetFromDb.fetchedAt &&
now - assetFromDb.fetchedAt < FIVE_MINUTES
) {
this.cache[chainId][assetId] = assetFromDb;
return assetFromDb;
}

const dbAsset = await getFuelAssetByAssetId({
assets: dbAssets.length ? dbAssets : this.dbAssetsCache[chainId],
assetId: assetId,
chainId,
}).catch((e) => {
console.error('Error fetching asset from db', e);
return undefined;
});
const assetFromIndexer = await this.fetchAssetFromIndexer(
endpoint.url,
assetId
);
).catch((e) => {
console.error('Error fetching asset from indexer', e);
return undefined;
});

console.log('asd assetFromIndexer', assetFromIndexer);
if (!assetFromIndexer) return;

const {
isNFT,
metadata,
name: indexerAssetName,
symbol: indexerAssetSymbol,
...rest
} = assetFromIndexer ?? {};
const asset = {
...assetFromIndexer,
isNft: false,
...dbAsset,
isNft: !!isNFT,
...rest,
metadata,
fetchedAt: now,
name: indexerAssetName || dbAsset?.name,
symbol: indexerAssetSymbol || dbAsset?.symbol,
};

if (assetFromIndexer.contractId) {
const nftData = await fetchNftData({
assetId,
contractId: assetFromIndexer.contractId,
provider,
});
Object.assign(asset, nftData);
if (asset.name != null) {
asset.indexed = true;
} else {
// @TODO: Remove once we have a proper caching pattern/mechanism
asset.name = '';
}

this.cache[chainId][assetId] = asset;
this.storage.setItem(`${chainId}/${assetId}`, asset);
if (save) {
this.cache[chainId][assetId] = asset;
this.storage.setItem(`${chainId}/${assetId}`, asset);
}
return asset;
}
asset = {
name: '',
symbol: '',
metadata: {},
};

static getInstance() {
if (!AssetsCache.instance) {
Expand Down
Loading
Loading