Skip to content

Commit deb27de

Browse files
committed
fix: add timeout to inspectors and return null
1 parent c1ea23f commit deb27de

File tree

3 files changed

+150
-51
lines changed

3 files changed

+150
-51
lines changed

packages/core/src/util/tokenTransferInspector.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
/* eslint-disable promise/param-names */
12
import * as Cardano from '../Cardano';
23
import { AssetInfo } from '../Asset';
34
import { AssetProvider } from '../Provider';
45
import { Inspector, resolveInputs } from './txInspector';
6+
import { Logger } from 'ts-log';
7+
import { Milliseconds } from './time';
58
import { coalesceValueQuantities } from './coalesceValueQuantities';
69
import { subtractValueQuantities } from './subtractValueQuantities';
710
import uniq from 'lodash/uniq';
@@ -14,8 +17,8 @@ export type TokenTransferValue = {
1417
};
1518

1619
export type TokenTransferInspection = {
17-
fromAddress: Map<Cardano.PaymentAddress, TokenTransferValue>;
18-
toAddress: Map<Cardano.PaymentAddress, TokenTransferValue>;
20+
fromAddress: Map<Cardano.PaymentAddress | null, TokenTransferValue>;
21+
toAddress: Map<Cardano.PaymentAddress | null, TokenTransferValue>;
1922
};
2023

2124
/** Arguments for the token transfer inspector. */
@@ -28,9 +31,15 @@ export interface TokenTransferInspectorArgs {
2831

2932
/** The asset provider to resolve AssetInfo for assets in the toAddress field. */
3033
toAddressAssetProvider: AssetProvider;
34+
35+
/** Timeout provided by the app that consumes the inspector to personalise the UI response */
36+
timeout: Milliseconds;
37+
38+
/** logger */
39+
logger: Logger;
3140
}
3241

33-
export type TokenTransferInspector = (args: TokenTransferInspectorArgs) => Inspector<TokenTransferInspection>;
42+
export type TokenTransferInspector = (args: TokenTransferInspectorArgs) => Promise<TokenTransferInspection>;
3443

3544
const coalesceByAddress = <T extends { address: Cardano.PaymentAddress; value: Cardano.Value }>(
3645
elements: T[]
@@ -148,27 +157,52 @@ const toTokenTransferValue = async (
148157
return tokenTransferValue;
149158
};
150159

160+
export const resolveWithTimeout = <T>(promise: Promise<T>, timeout: Milliseconds): Promise<T> =>
161+
Promise.race([
162+
promise,
163+
new Promise<T>((_, reject) =>
164+
setTimeout(() => reject(new Error('Timeout: failed to resolve inputs in time')), timeout)
165+
)
166+
]);
167+
151168
/** Inspect a transaction and return a map of addresses and their balances. */
152169
export const tokenTransferInspector: TokenTransferInspector =
153-
({ inputResolver, fromAddressAssetProvider, toAddressAssetProvider }) =>
170+
({ inputResolver, fromAddressAssetProvider, toAddressAssetProvider, timeout, logger }) =>
154171
async (tx) => {
155-
const { resolvedInputs } = await resolveInputs(tx.body.inputs, inputResolver);
172+
const emptyInspection: TokenTransferInspection = {
173+
fromAddress: new Map(),
174+
toAddress: new Map()
175+
};
156176

157-
const coalescedInputsByAddress = coalesceByAddress(resolvedInputs);
158-
const coalescedOutputsByAddress = coalesceByAddress(tx.body.outputs);
177+
try {
178+
const { resolvedInputs } = await resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
159179

160-
const addresses = uniq([...coalescedInputsByAddress.keys(), ...coalescedOutputsByAddress.keys()]);
180+
const coalescedInputsByAddress = coalesceByAddress(resolvedInputs);
181+
const coalescedOutputsByAddress = coalesceByAddress(tx.body.outputs);
161182

162-
const fromAddress = initializeAddressMap(addresses);
163-
const toAddress = initializeAddressMap(addresses);
183+
const addresses = uniq([...coalescedInputsByAddress.keys(), ...coalescedOutputsByAddress.keys()]);
164184

165-
computeNetDifferences(coalescedInputsByAddress, coalescedOutputsByAddress, fromAddress, toAddress);
185+
const fromAddress = initializeAddressMap(addresses);
186+
const toAddress = initializeAddressMap(addresses);
166187

167-
removeZeroBalanceEntries(fromAddress);
168-
removeZeroBalanceEntries(toAddress);
188+
computeNetDifferences(coalescedInputsByAddress, coalescedOutputsByAddress, fromAddress, toAddress);
169189

170-
return {
171-
fromAddress: await toTokenTransferValue(fromAddressAssetProvider, fromAddress),
172-
toAddress: await toTokenTransferValue(toAddressAssetProvider, toAddress)
173-
};
190+
removeZeroBalanceEntries(fromAddress);
191+
removeZeroBalanceEntries(toAddress);
192+
193+
return {
194+
fromAddress: await toTokenTransferValue(fromAddressAssetProvider, fromAddress),
195+
toAddress: await toTokenTransferValue(toAddressAssetProvider, toAddress)
196+
};
197+
} catch (error) {
198+
// also, resolve this null
199+
// todo: when resolving AssetInfo fails due to provider error or timeout, resolve it without any metadata and set quantity/supply to -1. Document this behavior in doc comments.
200+
if (error === 'Timeout: failed to resolve inputs in time') {
201+
// Handle timeout error specifically
202+
logger.log('Error: Inputs resolution timed out');
203+
return { error, inspection: null };
204+
}
205+
// Bubble up any other error
206+
return { error, inspection: emptyInspection };
207+
}
174208
};

packages/core/src/util/transactionSummaryInspector.ts

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Cardano from '../Cardano';
22
import * as Crypto from '@cardano-sdk/crypto';
33
import { AssetId, TokenMap } from '../Cardano';
4-
import { AssetInfoWithAmount } from './tokenTransferInspector';
4+
import { AssetInfoWithAmount, resolveWithTimeout } from './tokenTransferInspector';
55
import { AssetProvider } from '../Provider';
66
import {
77
AssetsMintedInspection,
@@ -14,6 +14,8 @@ import {
1414
totalAddressOutputsValueInspector
1515
} from './txInspector';
1616
import { BigIntMath } from '@cardano-sdk/util';
17+
import { Logger } from 'ts-log';
18+
import { Milliseconds } from './time';
1719
import { coalesceTokenMaps, subtractTokenMaps } from '../Asset/util';
1820
import { coalesceValueQuantities } from './coalesceValueQuantities';
1921
import { computeImplicitCoin } from '../Cardano/util';
@@ -26,6 +28,8 @@ interface TransactionSummaryInspectorArgs {
2628
protocolParameters: Cardano.ProtocolParameters;
2729
assetProvider: AssetProvider;
2830
dRepKeyHash?: Crypto.Ed25519KeyHashHex;
31+
timeout: Milliseconds;
32+
logger?: Logger;
2933
}
3034

3135
export type TransactionSummaryInspection = {
@@ -140,46 +144,106 @@ const toAssetInfoWithAmount = async (
140144
return assetInfos;
141145
};
142146

147+
// todo: when timeout or provider error occurs during input resolution, return any unresolved inputs value in unresolved property;
148+
// todo: when resolving AssetInfo fails due to provider error or timeout, resolve it without any metadata and set quantity/supply to -1. Document this behavior in doc comments.
149+
143150
/**
144151
* Inspects a transaction and produces a summary.
145152
*
146153
* @param {TransactionSummaryInspectorArgs} args The arguments for the inspector.
147154
*/
148155
export const transactionSummaryInspector: TransactionSummaryInspector =
149156
(args: TransactionSummaryInspectorArgs) => async (tx) => {
150-
const { inputResolver, addresses, rewardAccounts, protocolParameters, assetProvider, dRepKeyHash } = args;
151-
const resolvedInputs = await resolveInputs(tx.body.inputs, inputResolver);
152-
const fee = tx.body.fee;
153-
154-
const implicit = computeImplicitCoin(
157+
const {
158+
inputResolver,
159+
addresses,
160+
rewardAccounts,
155161
protocolParameters,
156-
{ certificates: tx.body.certificates, withdrawals: tx.body.withdrawals },
157-
rewardAccounts || [],
158-
dRepKeyHash
159-
);
160-
161-
const collateral = await getCollateral(tx, inputResolver, addresses);
162-
163-
const totalOutputValue = await totalAddressOutputsValueInspector(addresses)(tx);
164-
const totalInputValue = await totalAddressInputsValueInspector(addresses, inputResolver)(tx);
165-
const implicitCoin = (implicit.withdrawals || 0n) + (implicit.reclaimDeposit || 0n) - (implicit.deposit || 0n);
166-
const implicitAssets = await getImplicitAssets(tx);
167-
168-
const diff = {
169-
assets: subtractTokenMaps([totalOutputValue.assets, totalInputValue.assets]),
170-
coins: totalOutputValue.coins - totalInputValue.coins
171-
};
172-
173-
return {
174-
assets: await toAssetInfoWithAmount(assetProvider, diff.assets),
175-
coins: diff.coins,
176-
collateral,
177-
deposit: implicit.deposit || 0n,
178-
fee,
179-
returnedDeposit: implicit.reclaimDeposit || 0n,
180-
unresolved: {
181-
inputs: resolvedInputs.unresolvedInputs,
182-
value: await getUnaccountedFunds(tx, resolvedInputs, implicitCoin, fee, implicitAssets)
162+
assetProvider,
163+
dRepKeyHash,
164+
timeout,
165+
logger
166+
} = args;
167+
168+
try {
169+
// eslint-disable-next-line sonarjs/prefer-immediate-return
170+
const resolvedInputs = await resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
171+
const fee = tx.body.fee;
172+
173+
const implicit = computeImplicitCoin(
174+
protocolParameters,
175+
{ certificates: tx.body.certificates, withdrawals: tx.body.withdrawals },
176+
rewardAccounts || [],
177+
dRepKeyHash
178+
);
179+
180+
const collateral = await getCollateral(tx, inputResolver, addresses);
181+
182+
const totalOutputValue = await totalAddressOutputsValueInspector(addresses)(tx);
183+
const totalInputValue = await totalAddressInputsValueInspector(addresses, inputResolver)(tx);
184+
const implicitCoin = (implicit.withdrawals || 0n) + (implicit.reclaimDeposit || 0n) - (implicit.deposit || 0n);
185+
const implicitAssets = await getImplicitAssets(tx);
186+
187+
const diff = {
188+
assets: subtractTokenMaps([totalOutputValue.assets, totalInputValue.assets]),
189+
coins: totalOutputValue.coins - totalInputValue.coins
190+
};
191+
192+
return {
193+
assets: await toAssetInfoWithAmount(assetProvider, diff.assets),
194+
coins: diff.coins,
195+
collateral,
196+
deposit: implicit.deposit || 0n,
197+
fee,
198+
returnedDeposit: implicit.reclaimDeposit || 0n,
199+
unresolved: {
200+
inputs: resolvedInputs.unresolvedInputs,
201+
value: await getUnaccountedFunds(tx, resolvedInputs, implicitCoin, fee, implicitAssets)
202+
}
203+
};
204+
} catch (error) {
205+
// also, resolve this null
206+
// todo: when resolving AssetInfo fails due to provider error or timeout, resolve it without any metadata and set quantity/supply to -1. Document this behavior in doc comments.
207+
if (error === 'Timeout: failed to resolve inputs in time') {
208+
// Handle timeout error specifically
209+
logger.log('Error: Inputs resolution timed out');
210+
} else {
211+
// Bubble up any other error
212+
throw error;
183213
}
184-
};
214+
}
215+
216+
// const fee = tx.body.fee;
217+
218+
// const implicit = computeImplicitCoin(
219+
// protocolParameters,
220+
// { certificates: tx.body.certificates, withdrawals: tx.body.withdrawals },
221+
// rewardAccounts || [],
222+
// dRepKeyHash
223+
// );
224+
225+
// const collateral = await getCollateral(tx, inputResolver, addresses);
226+
227+
// const totalOutputValue = await totalAddressOutputsValueInspector(addresses)(tx);
228+
// const totalInputValue = await totalAddressInputsValueInspector(addresses, inputResolver)(tx);
229+
// const implicitCoin = (implicit.withdrawals || 0n) + (implicit.reclaimDeposit || 0n) - (implicit.deposit || 0n);
230+
// const implicitAssets = await getImplicitAssets(tx);
231+
232+
// const diff = {
233+
// assets: subtractTokenMaps([totalOutputValue.assets, totalInputValue.assets]),
234+
// coins: totalOutputValue.coins - totalInputValue.coins
235+
// };
236+
237+
// return {
238+
// assets: await toAssetInfoWithAmount(assetProvider, diff.assets),
239+
// coins: diff.coins,
240+
// collateral,
241+
// deposit: implicit.deposit || 0n,
242+
// fee,
243+
// returnedDeposit: implicit.reclaimDeposit || 0n,
244+
// unresolved: {
245+
// inputs: resolvedInputs.unresolvedInputs,
246+
// value: await getUnaccountedFunds(tx, resolvedInputs, implicitCoin, fee, implicitAssets)
247+
// }
248+
// };
185249
};

yarn-project.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,7 @@ cacheEntries = {
14201420
"fs-minipass@npm:3.0.0" = { filename = "fs-minipass-npm-3.0.0-3692c14b65-b72e9fe426.zip"; sha512 = "b72e9fe426e39f05b35bf237c8218b7ab3f68a65f325725ad7b4e431ff5a10725946fc62883b78446c07515ab938d25fdde3d08fb5ac8693f7f9eb9990da21f0"; };
14211421
"fs.realpath@npm:1.0.0" = { filename = "fs.realpath-npm-1.0.0-c8f05d8126-99ddea01a7.zip"; sha512 = "99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0"; };
14221422
"fsevents@npm:2.3.2" = { filename = "fsevents-npm-2.3.2-a881d6ac9f-97ade64e75.zip"; sha512 = "97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f"; };
1423+
"fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7" = { filename = "fsevents-patch-3340e2eb10-8.zip"; sha512 = "edbd0fd80be379c14409605f77e52fdc78a119e17f875e8b90a220c3e5b29e54a1477c21d91fd30b957ea4866406dc3ff87b61432d2840ff8866b309e5866140"; };
14231424
"ftp@npm:0.3.10" = { filename = "ftp-npm-0.3.10-348fb9ac23-ddd313c1d4.zip"; sha512 = "ddd313c1d44eb7429f3a7d77a0155dc8fe86a4c64dca58f395632333ce4b4e74c61413c6e0ef66ea3f3d32d905952fbb6d028c7117d522f793eb1fa282e17357"; };
14241425
"function-bind@npm:1.1.1" = { filename = "function-bind-npm-1.1.1-b56b322ae9-b32fbaebb3.zip"; sha512 = "b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a"; };
14251426
"function.prototype.name@npm:1.1.5" = { filename = "function.prototype.name-npm-1.1.5-e776a642bb-acd21d733a.zip"; sha512 = "acd21d733a9b649c2c442f067567743214af5fa248dbeee69d8278ce7df3329ea5abac572be9f7470b4ec1cd4d8f1040e3c5caccf98ebf2bf861a0deab735c27"; };

0 commit comments

Comments
 (0)