Skip to content

Commit 3a81510

Browse files
VanessaPCprzemyslaw-wlodek
authored andcommitted
feat(core): add timeout and logger to the tokenTransferInspector and transactionSummaryInspector
BREAKING CHANGE: add timeout prop to inspectors for them to return a timeout error
1 parent c0abc26 commit 3a81510

13 files changed

+657
-80
lines changed

packages/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@cardano-ogmios/schema": "6.3.0",
5959
"@cardano-sdk/crypto": "workspace:~",
6060
"@cardano-sdk/util": "workspace:~",
61+
"@cardano-sdk/util-dev": "workspace:~",
6162
"@foxglove/crc": "^0.0.3",
6263
"@scure/base": "^1.1.1",
6364
"fraction.js": "4.0.1",

packages/core/src/Asset/types/AssetInfo.ts

+3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ export interface AssetInfo {
1414
name: AssetName;
1515
fingerprint: AssetFingerprint;
1616
/**
17+
* `0n` if not loaded
18+
*
1719
* @deprecated Use `supply` instead
1820
*/
1921
quantity: bigint;
22+
/** `0n` if not loaded */
2023
supply: bigint;
2124
/** CIP-0035. `undefined` if not loaded, `null` if no metadata found */
2225
tokenMetadata?: TokenMetadata | null;

packages/core/src/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ export class NotImplementedError extends CustomError {
7878
super(`Not implemented: ${missingFeature}`);
7979
}
8080
}
81+
82+
export class TimeoutError extends CustomError {
83+
public constructor(message: string) {
84+
super(`Timeout: ${message}`);
85+
}
86+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable promise/param-names */
2+
3+
import { TimeoutError } from '../errors';
4+
5+
export const promiseTimeout = <T>(promise: Promise<T>, timeout: number) => {
6+
let timeoutId: NodeJS.Timeout;
7+
8+
return Promise.race([
9+
promise,
10+
new Promise<T>(
11+
(_, reject) =>
12+
(timeoutId = setTimeout(() => reject(new TimeoutError('Failed to resolve the promise in time')), timeout))
13+
)
14+
]).finally(() => {
15+
if (timeoutId) clearTimeout(timeoutId);
16+
});
17+
};

packages/core/src/util/tokenTransferInspector.ts

+56-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
/* eslint-disable promise/param-names */
12
import * as Cardano from '../Cardano';
23
import { AssetInfo } from '../Asset';
34
import { AssetProvider } from '../Provider';
4-
import { Inspector, resolveInputs } from './txInspector';
5+
import { Inspector, ResolutionResult, resolveInputs } from './txInspector';
6+
import { Logger } from 'ts-log';
7+
import { Milliseconds } from './time';
8+
import { TimeoutError } from '../errors';
59
import { coalesceValueQuantities } from './coalesceValueQuantities';
10+
import { promiseTimeout } from './promiseTimeout';
611
import { subtractValueQuantities } from './subtractValueQuantities';
7-
import uniq from 'lodash/uniq.js';
12+
import { tryGetAssetInfos } from './tryGetAssetInfos';
13+
import uniq from 'lodash/uniq';
814

915
export type AssetInfoWithAmount = { amount: Cardano.Lovelace; assetInfo: AssetInfo };
1016

@@ -28,10 +34,23 @@ export interface TokenTransferInspectorArgs {
2834

2935
/** The asset provider to resolve AssetInfo for assets in the toAddress field. */
3036
toAddressAssetProvider: AssetProvider;
37+
38+
/** Timeout provided by the app that consumes the inspector to personalise the UI response */
39+
timeout: Milliseconds;
40+
41+
/** logger */
42+
logger: Logger;
3143
}
3244

3345
export type TokenTransferInspector = (args: TokenTransferInspectorArgs) => Inspector<TokenTransferInspection>;
3446

47+
type IntoTokenTransferValueProps = {
48+
addressMap: Map<Cardano.PaymentAddress, Cardano.Value>;
49+
assetProvider: AssetProvider;
50+
timeout: Milliseconds;
51+
logger: Logger;
52+
};
53+
3554
const coalesceByAddress = <T extends { address: Cardano.PaymentAddress; value: Cardano.Value }>(
3655
elements: T[]
3756
): Map<Cardano.PaymentAddress, Cardano.Value> => {
@@ -116,10 +135,12 @@ const removeZeroBalanceEntries = (addressMap: Map<Cardano.PaymentAddress, Cardan
116135
}
117136
};
118137

119-
const toTokenTransferValue = async (
120-
assetProvider: AssetProvider,
121-
addressMap: Map<Cardano.PaymentAddress, Cardano.Value>
122-
): Promise<Map<Cardano.PaymentAddress, TokenTransferValue>> => {
138+
const intoTokenTransferValue = async ({
139+
logger,
140+
assetProvider,
141+
timeout,
142+
addressMap
143+
}: IntoTokenTransferValueProps): Promise<Map<Cardano.PaymentAddress, TokenTransferValue>> => {
123144
const tokenTransferValue = new Map<Cardano.PaymentAddress, TokenTransferValue>();
124145

125146
for (const [address, value] of addressMap.entries()) {
@@ -128,9 +149,11 @@ const toTokenTransferValue = async (
128149
const assetInfos = new Map<Cardano.AssetId, AssetInfoWithAmount>();
129150

130151
if (assetIds.length > 0) {
131-
const assets = await assetProvider.getAssets({
152+
const assets = await tryGetAssetInfos({
132153
assetIds,
133-
extraData: { nftMetadata: true, tokenMetadata: true }
154+
assetProvider,
155+
logger,
156+
timeout
134157
});
135158

136159
for (const asset of assets) {
@@ -150,9 +173,20 @@ const toTokenTransferValue = async (
150173

151174
/** Inspect a transaction and return a map of addresses and their balances. */
152175
export const tokenTransferInspector: TokenTransferInspector =
153-
({ inputResolver, fromAddressAssetProvider, toAddressAssetProvider }) =>
176+
({ inputResolver, fromAddressAssetProvider, toAddressAssetProvider, timeout, logger }) =>
154177
async (tx) => {
155-
const { resolvedInputs } = await resolveInputs(tx.body.inputs, inputResolver);
178+
let resolvedInputs: ResolutionResult['resolvedInputs'];
179+
180+
try {
181+
const inputResolution = await promiseTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
182+
resolvedInputs = inputResolution.resolvedInputs;
183+
} catch (error) {
184+
if (error instanceof TimeoutError) {
185+
logger.error('Error: Inputs resolution timed out');
186+
}
187+
188+
resolvedInputs = [];
189+
}
156190

157191
const coalescedInputsByAddress = coalesceByAddress(resolvedInputs);
158192
const coalescedOutputsByAddress = coalesceByAddress(tx.body.outputs);
@@ -168,7 +202,17 @@ export const tokenTransferInspector: TokenTransferInspector =
168202
removeZeroBalanceEntries(toAddress);
169203

170204
return {
171-
fromAddress: await toTokenTransferValue(fromAddressAssetProvider, fromAddress),
172-
toAddress: await toTokenTransferValue(toAddressAssetProvider, toAddress)
205+
fromAddress: await intoTokenTransferValue({
206+
addressMap: fromAddress,
207+
assetProvider: fromAddressAssetProvider,
208+
logger,
209+
timeout
210+
}),
211+
toAddress: await intoTokenTransferValue({
212+
addressMap: toAddress,
213+
assetProvider: toAddressAssetProvider,
214+
logger,
215+
timeout
216+
})
173217
};
174218
};

packages/core/src/util/transactionSummaryInspector.ts

+56-10
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ import {
1414
totalAddressOutputsValueInspector
1515
} from './txInspector';
1616
import { BigIntMath } from '@cardano-sdk/util';
17+
import { Logger } from 'ts-log';
18+
import { Milliseconds } from './time';
19+
import { TimeoutError } from '../errors';
1720
import { coalesceTokenMaps, subtractTokenMaps } from '../Asset/util';
1821
import { coalesceValueQuantities } from './coalesceValueQuantities';
1922
import { computeImplicitCoin } from '../Cardano/util';
23+
import { promiseTimeout } from './promiseTimeout';
2024
import { subtractValueQuantities } from './subtractValueQuantities';
25+
import { tryGetAssetInfos } from './tryGetAssetInfos';
2126

2227
interface TransactionSummaryInspectorArgs {
2328
addresses: Cardano.PaymentAddress[];
@@ -26,6 +31,8 @@ interface TransactionSummaryInspectorArgs {
2631
protocolParameters: Cardano.ProtocolParameters;
2732
assetProvider: AssetProvider;
2833
dRepKeyHash?: Crypto.Ed25519KeyHashHex;
34+
timeout: Milliseconds;
35+
logger: Logger;
2936
}
3037

3138
export type TransactionSummaryInspection = {
@@ -45,6 +52,13 @@ export type TransactionSummaryInspector = (
4552
args: TransactionSummaryInspectorArgs
4653
) => Inspector<TransactionSummaryInspection>;
4754

55+
type IntoTokenTransferValueProps = {
56+
assetProvider: AssetProvider;
57+
logger: Logger;
58+
timeout: Milliseconds;
59+
tokenMap?: TokenMap;
60+
};
61+
4862
/**
4963
* Gets the collateral specified for this transaction.
5064
*
@@ -116,19 +130,23 @@ const getUnaccountedFunds = async (
116130
return subtractValueQuantities([totalOutputs, totalInputs]);
117131
};
118132

119-
const toAssetInfoWithAmount = async (
120-
assetProvider: AssetProvider,
121-
tokenMap?: TokenMap
122-
): Promise<Map<Cardano.AssetId, AssetInfoWithAmount>> => {
133+
const intoAssetInfoWithAmount = async ({
134+
assetProvider,
135+
logger,
136+
timeout,
137+
tokenMap
138+
}: IntoTokenTransferValueProps): Promise<Map<Cardano.AssetId, AssetInfoWithAmount>> => {
123139
if (!tokenMap) return new Map();
124140

125141
const assetIds = tokenMap && tokenMap.size > 0 ? [...tokenMap.keys()] : [];
126142
const assetInfos = new Map<Cardano.AssetId, AssetInfoWithAmount>();
127143

128144
if (assetIds.length > 0) {
129-
const assets = await assetProvider.getAssets({
145+
const assets = await tryGetAssetInfos({
130146
assetIds,
131-
extraData: { nftMetadata: true, tokenMetadata: true }
147+
assetProvider,
148+
logger,
149+
timeout
132150
});
133151

134152
for (const asset of assets) {
@@ -146,9 +164,32 @@ const toAssetInfoWithAmount = async (
146164
* @param {TransactionSummaryInspectorArgs} args The arguments for the inspector.
147165
*/
148166
export const transactionSummaryInspector: TransactionSummaryInspector =
149-
(args: TransactionSummaryInspectorArgs) => async (tx) => {
150-
const { inputResolver, addresses, rewardAccounts, protocolParameters, assetProvider, dRepKeyHash } = args;
151-
const resolvedInputs = await resolveInputs(tx.body.inputs, inputResolver);
167+
({
168+
inputResolver,
169+
addresses,
170+
rewardAccounts,
171+
protocolParameters,
172+
assetProvider,
173+
dRepKeyHash,
174+
timeout,
175+
logger
176+
}: TransactionSummaryInspectorArgs) =>
177+
async (tx) => {
178+
let resolvedInputs: ResolutionResult;
179+
180+
try {
181+
resolvedInputs = await promiseTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
182+
} catch (error) {
183+
if (error instanceof TimeoutError) {
184+
logger.error('Error: Inputs resolution timed out');
185+
}
186+
187+
resolvedInputs = {
188+
resolvedInputs: [],
189+
unresolvedInputs: tx.body.inputs
190+
};
191+
}
192+
152193
const fee = tx.body.fee;
153194

154195
const implicit = computeImplicitCoin(
@@ -171,7 +212,12 @@ export const transactionSummaryInspector: TransactionSummaryInspector =
171212
};
172213

173214
return {
174-
assets: await toAssetInfoWithAmount(assetProvider, diff.assets),
215+
assets: await intoAssetInfoWithAmount({
216+
assetProvider,
217+
logger,
218+
timeout,
219+
tokenMap: diff.assets
220+
}),
175221
coins: diff.coins,
176222
collateral,
177223
deposit: implicit.deposit || 0n,
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as Cardano from '../Cardano';
2+
import { AssetInfo } from '../Asset';
3+
import { AssetProvider } from '../Provider';
4+
import { Logger } from 'ts-log';
5+
import { Milliseconds } from './time';
6+
import { promiseTimeout } from './promiseTimeout';
7+
8+
type TryGetAssetInfosProps = {
9+
assetIds: Cardano.AssetId[];
10+
assetProvider: AssetProvider;
11+
timeout: Milliseconds;
12+
logger: Logger;
13+
};
14+
15+
export const tryGetAssetInfos = async ({ assetIds, assetProvider, logger, timeout }: TryGetAssetInfosProps) => {
16+
try {
17+
return await promiseTimeout(
18+
assetProvider.getAssets({
19+
assetIds,
20+
extraData: { nftMetadata: true, tokenMetadata: true }
21+
}),
22+
timeout
23+
);
24+
} catch (error) {
25+
logger.error('Error: Failed to retrieve assets', error);
26+
27+
return assetIds.map<AssetInfo>((assetId) => {
28+
const policyId = Cardano.AssetId.getPolicyId(assetId);
29+
const name = Cardano.AssetId.getAssetName(assetId);
30+
31+
return {
32+
assetId,
33+
fingerprint: Cardano.AssetFingerprint.fromParts(policyId, name),
34+
name,
35+
policyId,
36+
quantity: 0n,
37+
supply: 0n
38+
};
39+
});
40+
}
41+
};

packages/core/test/util/mocks.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as Cardano from '../../src/Cardano';
2+
import { Asset, AssetProvider, HealthCheckResponse } from '../../src';
3+
4+
export const createMockInputResolver = (
5+
historicalTxs: Cardano.HydratedTx[],
6+
resolveDelay = 0
7+
): Cardano.InputResolver => ({
8+
async resolveInput(input: Cardano.TxIn) {
9+
const tx = historicalTxs.find((historicalTx) => historicalTx.id === input.txId);
10+
11+
if (!tx || tx.body.outputs.length <= input.index) return Promise.resolve(null);
12+
13+
return await new Promise((resolve) => {
14+
setTimeout(() => {
15+
resolve(tx.body.outputs[input.index]);
16+
}, resolveDelay);
17+
});
18+
}
19+
});
20+
21+
export const createMockAssetProvider = (assets: Asset.AssetInfo[]): AssetProvider => ({
22+
getAsset: async ({ assetId }) =>
23+
assets.find((asset) => asset.assetId === assetId) ?? Promise.reject('Asset not found'),
24+
getAssets: async ({ assetIds }) => assets.filter((asset) => assetIds.includes(asset.assetId)),
25+
healthCheck: async () => Promise.resolve({} as HealthCheckResponse)
26+
});

0 commit comments

Comments
 (0)