Skip to content

Commit 44db6b5

Browse files
feat(wallet): add a new util createWalletAssetProvider that creates a new assetProvider that uses local cache
1 parent 591ea29 commit 44db6b5

File tree

3 files changed

+941
-0
lines changed

3 files changed

+941
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { Asset, AssetProvider, Cardano, GetAssetArgs, GetAssetsArgs, HealthCheckResponse } from '@cardano-sdk/core';
2+
import { Assets } from '../types';
3+
import { Logger } from 'ts-log';
4+
import { Observable, firstValueFrom } from 'rxjs';
5+
import { isNotNil } from '@cardano-sdk/util';
6+
7+
export interface AssetProviderContext {
8+
assetProvider: AssetProvider;
9+
assetInfo$: Observable<Assets>;
10+
tx?: Cardano.Tx;
11+
logger: Logger;
12+
}
13+
14+
const tryCip68NftMetadata = (
15+
policyId: Cardano.PolicyId,
16+
name: Cardano.AssetName,
17+
tx: Cardano.Tx,
18+
logger: Logger
19+
): Asset.NftMetadata | null => {
20+
const decoded = Asset.AssetNameLabel.decode(name);
21+
22+
if (decoded?.label === Asset.AssetNameLabelNum.UserNFT) {
23+
const referenceAssetId = Cardano.AssetId.fromParts(
24+
policyId,
25+
Asset.AssetNameLabel.encode(decoded.content, Asset.AssetNameLabelNum.ReferenceNFT)
26+
);
27+
28+
// TODO: It is possible that the reference NFT is not in one of the outputs of the transaction and was previously minted. We
29+
// need a way to find the reference NFT TxOut from the current active UTXO set on the network.
30+
for (const output of tx.body.outputs) {
31+
if (output.value.assets?.get(referenceAssetId)) {
32+
return Asset.NftMetadata.fromPlutusData(output.datum, logger);
33+
}
34+
}
35+
}
36+
37+
return null;
38+
};
39+
40+
const getNftMetadata = (
41+
name: Cardano.AssetName,
42+
policyId: Cardano.PolicyId,
43+
tx: Cardano.Tx,
44+
logger: Logger
45+
): Asset.NftMetadata | null => {
46+
// First, try CIP-68
47+
let metadata = tryCip68NftMetadata(policyId, name, tx, logger);
48+
49+
// If metadata is not found, try CIP-25
50+
if (!metadata) {
51+
metadata = tx.auxiliaryData?.blob
52+
? Asset.NftMetadata.fromMetadatum({ name, policyId }, tx.auxiliaryData.blob, logger)
53+
: null;
54+
}
55+
56+
return metadata;
57+
};
58+
59+
const createAssetInfo = (assetId: Cardano.AssetId, amount: bigint, tx: Cardano.Tx, logger: Logger): Asset.AssetInfo => {
60+
const name = Cardano.AssetId.getAssetName(assetId);
61+
const policyId = Cardano.AssetId.getPolicyId(assetId);
62+
const assetInfo: Asset.AssetInfo = {
63+
assetId,
64+
fingerprint: Cardano.AssetFingerprint.fromParts(policyId, name),
65+
name,
66+
policyId,
67+
quantity: amount,
68+
supply: amount
69+
};
70+
71+
assetInfo.nftMetadata = getNftMetadata(name, policyId, tx, logger);
72+
73+
return assetInfo;
74+
};
75+
76+
const getMintedAssetInfosFromTx = async (tx: Cardano.Tx, logger: Logger): Promise<Asset.AssetInfo[] | null> => {
77+
const mints = tx.body.mint;
78+
79+
if (!mints) return null;
80+
81+
return [...mints.entries()]
82+
.filter(([_, amount]) => amount > 0)
83+
.map(([assetId, amount]) => createAssetInfo(assetId, amount, tx, logger));
84+
};
85+
86+
const fetchAssetsFromProvider = async (
87+
provider: AssetProvider,
88+
assetIds: Cardano.AssetId[],
89+
logger: Logger
90+
): Promise<Asset.AssetInfo[]> => {
91+
const assetsFromProvider: Asset.AssetInfo[] = [];
92+
93+
// We need to fetch assets one by one because the provider will throw if any of the assets requests to the getAssets endpoint is not found.
94+
// We want to fetch the ones we can and return a simplified AssetInfo for the ones we can't.
95+
for (const assetId of assetIds) {
96+
try {
97+
const fetchedAsset = await provider.getAsset({
98+
assetId,
99+
extraData: { nftMetadata: true, tokenMetadata: true }
100+
});
101+
assetsFromProvider.push(fetchedAsset);
102+
} catch (error) {
103+
logger.error(error);
104+
}
105+
}
106+
107+
return assetsFromProvider;
108+
};
109+
110+
const createFallbackAsset = (assetId: Cardano.AssetId): Asset.AssetInfo => {
111+
const name = Cardano.AssetId.getAssetName(assetId);
112+
const policyId = Cardano.AssetId.getPolicyId(assetId);
113+
return {
114+
assetId,
115+
fingerprint: Cardano.AssetFingerprint.fromParts(policyId, name),
116+
name,
117+
policyId,
118+
quantity: 0n,
119+
supply: 0n
120+
};
121+
};
122+
123+
const mergeAssets = (
124+
assetIds: Cardano.AssetId[],
125+
cachedAssetsInfo: Map<Cardano.AssetId, Asset.AssetInfo>,
126+
assetsFromProvider: Asset.AssetInfo[],
127+
mintedAssets: Asset.AssetInfo[] | null
128+
): Asset.AssetInfo[] =>
129+
assetIds.map((assetId) => {
130+
const asset = cachedAssetsInfo.get(assetId) || assetsFromProvider.find((a) => a.assetId === assetId);
131+
const mintedAsset = mintedAssets?.find((info) => info.assetId === assetId);
132+
133+
if (!asset && !mintedAsset) {
134+
return createFallbackAsset(assetId);
135+
}
136+
137+
if (!asset && mintedAsset) {
138+
return mintedAsset;
139+
}
140+
141+
if (asset && mintedAsset) {
142+
asset.supply += mintedAsset.supply;
143+
asset.quantity = asset.supply;
144+
145+
if (mintedAsset.nftMetadata) {
146+
asset.nftMetadata = mintedAsset.nftMetadata;
147+
}
148+
}
149+
150+
return asset!;
151+
});
152+
153+
/**
154+
* Creates a wallet asset provider. This provider will try to first fetch the asset from the local cache (assetInfo$),
155+
* then from the provider and finally from the transaction if it was minted in the transaction. If the asset can not be found
156+
* it will return a dummy AssetInfo with both supply and quantity set to 0.
157+
*/
158+
export const createWalletAssetProvider = ({
159+
assetProvider,
160+
assetInfo$,
161+
tx,
162+
logger
163+
}: AssetProviderContext): AssetProvider => ({
164+
async getAsset({ assetId }: GetAssetArgs): Promise<Asset.AssetInfo> {
165+
const mintedAssets = tx ? await getMintedAssetInfosFromTx(tx, logger) : [];
166+
const cachedAssetsInfo = await firstValueFrom(assetInfo$);
167+
168+
let asset = cachedAssetsInfo.get(assetId);
169+
170+
if (!asset) {
171+
try {
172+
asset = await assetProvider.getAsset({ assetId, extraData: { nftMetadata: true, tokenMetadata: true } });
173+
} catch (error) {
174+
logger.error(error);
175+
}
176+
}
177+
178+
const mintedAsset = mintedAssets?.find((info) => info.assetId === assetId);
179+
180+
// Let's create dummy AssetInfo for the unresolved asset. This is probably better than throwing as the UI can still present it as regular token.
181+
if (!asset && !mintedAsset) {
182+
const name = Cardano.AssetId.getAssetName(assetId);
183+
const policyId = Cardano.AssetId.getPolicyId(assetId);
184+
return {
185+
assetId,
186+
fingerprint: Cardano.AssetFingerprint.fromParts(policyId, name),
187+
name,
188+
policyId,
189+
quantity: 0n,
190+
supply: 0n
191+
};
192+
}
193+
194+
if (!asset) return mintedAsset!;
195+
196+
if (mintedAsset) {
197+
asset.supply += mintedAsset.supply;
198+
199+
// We give preference to the metadata in the transaction if preset as this would be the most up to date.
200+
if (mintedAsset.nftMetadata) {
201+
asset.nftMetadata = mintedAsset.nftMetadata;
202+
}
203+
}
204+
205+
const cip68NftMetadata = tx ? tryCip68NftMetadata(asset.policyId, asset.name, tx, logger) : null;
206+
if (cip68NftMetadata) asset.nftMetadata = cip68NftMetadata;
207+
208+
return asset;
209+
},
210+
211+
async getAssets({ assetIds }: GetAssetsArgs): Promise<Asset.AssetInfo[]> {
212+
const cachedAssetsInfo = await firstValueFrom(assetInfo$);
213+
const mintedAssets = tx ? await getMintedAssetInfosFromTx(tx, logger) : [];
214+
const missingAssetIds = assetIds.filter((assetId) => !cachedAssetsInfo.has(assetId));
215+
const assetsFromProvider = await fetchAssetsFromProvider(assetProvider, missingAssetIds, logger);
216+
const mergedAssets = mergeAssets(assetIds, cachedAssetsInfo, assetsFromProvider, mintedAssets);
217+
218+
const assets = mergedAssets.filter(isNotNil);
219+
220+
if (tx) {
221+
for (const asset of assets) {
222+
const cip68NftMetadata = tryCip68NftMetadata(asset.policyId, asset.name, tx, logger);
223+
if (cip68NftMetadata) asset.nftMetadata = cip68NftMetadata;
224+
}
225+
}
226+
227+
return mergedAssets.filter(isNotNil);
228+
},
229+
230+
healthCheck(): Promise<HealthCheckResponse> {
231+
return assetProvider.healthCheck();
232+
}
233+
});

packages/wallet/src/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from './AddressDiscovery';
1717
export * from './HandlesTracker';
1818
export * from './ChangeAddress';
1919
export * from './AddressTracker';
20+
export * from './WalletAssetProvider';

0 commit comments

Comments
 (0)