Skip to content

Commit f399a04

Browse files
committed
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 c1ea23f commit f399a04

7 files changed

+115
-15
lines changed

packages/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@cardano-ogmios/schema": "5.6.0",
6060
"@cardano-sdk/crypto": "workspace:~",
6161
"@cardano-sdk/util": "workspace:~",
62+
"@cardano-sdk/util-dev": "workspace:~",
6263
"@foxglove/crc": "^0.0.3",
6364
"@scure/base": "^1.1.1",
6465
"fraction.js": "4.0.1",

packages/core/src/util/tokenTransferInspector.ts

+38-3
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';
4-
import { Inspector, resolveInputs } from './txInspector';
5+
import { Inspector, ResolutionResult, 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';
@@ -28,6 +31,12 @@ 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

3342
export type TokenTransferInspector = (args: TokenTransferInspectorArgs) => Inspector<TokenTransferInspection>;
@@ -148,11 +157,37 @@ const toTokenTransferValue = async (
148157
return tokenTransferValue;
149158
};
150159

160+
/**
161+
* Resolve inputs with a timeout so that if the inputs fail to resolve in n Miliseconds
162+
* we throw an error so the UI consumer can handle situation accordingly.
163+
* In this case we create a race condition in which we race to send the resolved inputs before the
164+
* timeout, and timeout only if the inputs aren't there.
165+
*/
166+
export const resolveWithTimeout = <T>(promise: Promise<T>, timeout: Milliseconds): Promise<T> =>
167+
Promise.race([
168+
promise,
169+
new Promise<T>((_, reject) =>
170+
setTimeout(() => reject(new Error('Timeout: failed to resolve inputs in time')), timeout)
171+
)
172+
]);
173+
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 resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
182+
resolvedInputs = inputResolution.resolvedInputs;
183+
} catch (error) {
184+
if (error === 'Timeout: failed to resolve inputs in time') {
185+
// Handle timeout error specifically
186+
logger.log('Error: Inputs resolution timed out');
187+
}
188+
189+
resolvedInputs = [];
190+
}
156191

157192
const coalescedInputsByAddress = coalesceByAddress(resolvedInputs);
158193
const coalescedOutputsByAddress = coalesceByAddress(tx.body.outputs);

packages/core/src/util/transactionSummaryInspector.ts

+33-3
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 = {
@@ -147,8 +151,34 @@ const toAssetInfoWithAmount = async (
147151
*/
148152
export const transactionSummaryInspector: TransactionSummaryInspector =
149153
(args: TransactionSummaryInspectorArgs) => async (tx) => {
150-
const { inputResolver, addresses, rewardAccounts, protocolParameters, assetProvider, dRepKeyHash } = args;
151-
const resolvedInputs = await resolveInputs(tx.body.inputs, inputResolver);
154+
const {
155+
inputResolver,
156+
addresses,
157+
rewardAccounts,
158+
protocolParameters,
159+
assetProvider,
160+
dRepKeyHash,
161+
timeout,
162+
logger
163+
} = args;
164+
165+
let resolvedInputs: ResolutionResult;
166+
167+
try {
168+
// eslint-disable-next-line sonarjs/prefer-immediate-return
169+
resolvedInputs = await resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
170+
} catch (error) {
171+
if (error === 'Timeout: failed to resolve inputs in time') {
172+
// Handle timeout error specifically
173+
logger?.log('Error: Inputs resolution timed out');
174+
}
175+
176+
resolvedInputs = {
177+
resolvedInputs: [],
178+
unresolvedInputs: tx.body.inputs
179+
};
180+
}
181+
152182
const fee = tx.body.fee;
153183

154184
const implicit = computeImplicitCoin(

packages/core/test/util/tokenTransferInspector.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
AssetInfoWithAmount,
77
AssetProvider,
88
HealthCheckResponse,
9+
Milliseconds,
910
TokenTransferValue,
1011
createTxInspector,
1112
tokenTransferInspector
1213
} from '../../src';
1314
import { Ed25519KeyHashHex, Ed25519PublicKeyHex, Ed25519SignatureHex } from '@cardano-sdk/crypto';
1415
import { jsonToMetadatum } from '../../src/util/metadatum';
16+
import { logger } from '@cardano-sdk/util-dev';
1517

1618
const buildTokenTransferValue = (coins: bigint, assets: Array<[Asset.AssetInfo, bigint]>): TokenTransferValue => ({
1719
assets: new Map<Cardano.AssetId, AssetInfoWithAmount>(
@@ -221,6 +223,8 @@ describe('txInspector', () => {
221223

222224
const assetProvider = createMockAssetProvider(assetInfos);
223225

226+
const timeout = 6000 as Milliseconds;
227+
224228
describe('Token Transfer Inspector', () => {
225229
it('does not include addresses which net difference is 0 (assets and coins)', async () => {
226230
// Arrange
@@ -302,6 +306,8 @@ describe('txInspector', () => {
302306
tokenTransfer: tokenTransferInspector({
303307
fromAddressAssetProvider: assetProvider,
304308
inputResolver: createMockInputResolver(histTx),
309+
logger,
310+
timeout,
305311
toAddressAssetProvider: assetProvider
306312
})
307313
});
@@ -365,6 +371,8 @@ describe('txInspector', () => {
365371
tokenTransfer: tokenTransferInspector({
366372
fromAddressAssetProvider: assetProvider,
367373
inputResolver: createMockInputResolver(histTx),
374+
logger,
375+
timeout,
368376
toAddressAssetProvider: assetProvider
369377
})
370378
});
@@ -423,6 +431,8 @@ describe('txInspector', () => {
423431
tokenTransfer: tokenTransferInspector({
424432
fromAddressAssetProvider: assetProvider,
425433
inputResolver: createMockInputResolver(histTx),
434+
logger,
435+
timeout,
426436
toAddressAssetProvider: assetProvider
427437
})
428438
});
@@ -500,6 +510,8 @@ describe('txInspector', () => {
500510
tokenTransfer: tokenTransferInspector({
501511
fromAddressAssetProvider: assetProvider,
502512
inputResolver: createMockInputResolver(histTx),
513+
logger,
514+
timeout,
503515
toAddressAssetProvider: assetProvider
504516
})
505517
});
@@ -568,6 +580,8 @@ describe('txInspector', () => {
568580
tokenTransfer: tokenTransferInspector({
569581
fromAddressAssetProvider: assetProvider,
570582
inputResolver: createMockInputResolver(histTx),
583+
logger,
584+
timeout,
571585
toAddressAssetProvider: assetProvider
572586
})
573587
});
@@ -720,6 +734,8 @@ describe('txInspector', () => {
720734
tokenTransfer: tokenTransferInspector({
721735
fromAddressAssetProvider: assetProvider,
722736
inputResolver: createMockInputResolver(histTx),
737+
logger,
738+
timeout,
723739
toAddressAssetProvider: assetProvider
724740
})
725741
});
@@ -856,6 +872,8 @@ describe('txInspector', () => {
856872
tokenTransfer: tokenTransferInspector({
857873
fromAddressAssetProvider: assetProvider,
858874
inputResolver: createMockInputResolver(histTx),
875+
logger,
876+
timeout,
859877
toAddressAssetProvider: assetProvider
860878
})
861879
});
@@ -999,6 +1017,8 @@ describe('txInspector', () => {
9991017
tokenTransfer: tokenTransferInspector({
10001018
fromAddressAssetProvider: createMockAssetProvider(oldAssetInfos),
10011019
inputResolver: createMockInputResolver(histTx),
1020+
logger,
1021+
timeout,
10021022
toAddressAssetProvider: assetProvider
10031023
})
10041024
});

packages/core/test/util/transactionSummaryInspector.test.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AssetInfoWithAmount,
77
AssetProvider,
88
HealthCheckResponse,
9+
Milliseconds,
910
createTxInspector,
1011
transactionSummaryInspector
1112
} from '../../src';
@@ -226,6 +227,8 @@ const AssetInfoIdx = {
226227

227228
const assetProvider = createMockAssetProvider(assetInfos);
228229

230+
const timeout = 6000 as Milliseconds;
231+
229232
describe('Transaction Summary Inspector', () => {
230233
it('computes the correct asset and coin difference', async () => {
231234
// Arrange
@@ -328,7 +331,8 @@ describe('Transaction Summary Inspector', () => {
328331
assetProvider,
329332
inputResolver: createMockInputResolver(histTx),
330333
protocolParameters,
331-
rewardAccounts
334+
rewardAccounts,
335+
timeout
332336
})
333337
});
334338

@@ -425,7 +429,8 @@ describe('Transaction Summary Inspector', () => {
425429
assetProvider,
426430
inputResolver: createMockInputResolver(histTx),
427431
protocolParameters,
428-
rewardAccounts
432+
rewardAccounts,
433+
timeout
429434
})
430435
});
431436

@@ -517,7 +522,8 @@ describe('Transaction Summary Inspector', () => {
517522
assetProvider,
518523
inputResolver: createMockInputResolver(histTx),
519524
protocolParameters,
520-
rewardAccounts
525+
rewardAccounts,
526+
timeout
521527
})
522528
});
523529

@@ -601,7 +607,8 @@ describe('Transaction Summary Inspector', () => {
601607
assetProvider,
602608
inputResolver: createMockInputResolver(histTx),
603609
protocolParameters,
604-
rewardAccounts
610+
rewardAccounts,
611+
timeout
605612
})
606613
});
607614

@@ -668,7 +675,8 @@ describe('Transaction Summary Inspector', () => {
668675
assetProvider,
669676
inputResolver: createMockInputResolver(histTx),
670677
protocolParameters,
671-
rewardAccounts
678+
rewardAccounts,
679+
timeout
672680
})
673681
});
674682

@@ -735,7 +743,8 @@ describe('Transaction Summary Inspector', () => {
735743
assetProvider,
736744
inputResolver: createMockInputResolver(histTx),
737745
protocolParameters,
738-
rewardAccounts
746+
rewardAccounts,
747+
timeout
739748
})
740749
});
741750

@@ -811,7 +820,8 @@ describe('Transaction Summary Inspector', () => {
811820
assetProvider,
812821
inputResolver: createMockInputResolver(histTx),
813822
protocolParameters,
814-
rewardAccounts
823+
rewardAccounts,
824+
timeout
815825
})
816826
});
817827

@@ -887,7 +897,8 @@ describe('Transaction Summary Inspector', () => {
887897
assetProvider,
888898
inputResolver: createMockInputResolver(histTx),
889899
protocolParameters,
890-
rewardAccounts
900+
rewardAccounts,
901+
timeout
891902
})
892903
});
893904

@@ -974,7 +985,8 @@ describe('Transaction Summary Inspector', () => {
974985
assetProvider,
975986
inputResolver: createMockInputResolver(histTx),
976987
protocolParameters,
977-
rewardAccounts
988+
rewardAccounts,
989+
timeout
978990
})
979991
});
980992

yarn-project.nix

+1
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"; };

yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -3207,6 +3207,7 @@ __metadata:
32073207
"@cardano-ogmios/schema": 5.6.0
32083208
"@cardano-sdk/crypto": "workspace:~"
32093209
"@cardano-sdk/util": "workspace:~"
3210+
"@cardano-sdk/util-dev": "workspace:~"
32103211
"@foxglove/crc": ^0.0.3
32113212
"@scure/base": ^1.1.1
32123213
"@types/lodash": ^4.14.182

0 commit comments

Comments
 (0)