Skip to content

Commit b3f3280

Browse files
fixup! feat(core): add timeout and logger to the tokenTransferInspector and transactionSummaryInspector
add promiseTimeout util, new TimeoutError and tests
1 parent f399a04 commit b3f3280

File tree

6 files changed

+215
-41
lines changed

6 files changed

+215
-41
lines changed

packages/core/src/errors.ts

Lines changed: 6 additions & 0 deletions
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+
}
Lines changed: 17 additions & 0 deletions
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

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { AssetProvider } from '../Provider';
55
import { Inspector, ResolutionResult, resolveInputs } from './txInspector';
66
import { Logger } from 'ts-log';
77
import { Milliseconds } from './time';
8+
import { TimeoutError } from '../errors';
89
import { coalesceValueQuantities } from './coalesceValueQuantities';
10+
import { promiseTimeout } from './promiseTimeout';
911
import { subtractValueQuantities } from './subtractValueQuantities';
1012
import uniq from 'lodash/uniq';
1113

@@ -157,33 +159,18 @@ const toTokenTransferValue = async (
157159
return tokenTransferValue;
158160
};
159161

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-
174162
/** Inspect a transaction and return a map of addresses and their balances. */
175163
export const tokenTransferInspector: TokenTransferInspector =
176164
({ inputResolver, fromAddressAssetProvider, toAddressAssetProvider, timeout, logger }) =>
177165
async (tx) => {
178166
let resolvedInputs: ResolutionResult['resolvedInputs'];
179167

180168
try {
181-
const inputResolution = await resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
169+
const inputResolution = await promiseTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
182170
resolvedInputs = inputResolution.resolvedInputs;
183171
} 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');
172+
if (error instanceof TimeoutError) {
173+
logger.error('Error: Inputs resolution timed out');
187174
}
188175

189176
resolvedInputs = [];

packages/core/src/util/transactionSummaryInspector.ts

Lines changed: 18 additions & 19 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, resolveWithTimeout } from './tokenTransferInspector';
4+
import { AssetInfoWithAmount } from './tokenTransferInspector';
55
import { AssetProvider } from '../Provider';
66
import {
77
AssetsMintedInspection,
@@ -16,9 +16,11 @@ import {
1616
import { BigIntMath } from '@cardano-sdk/util';
1717
import { Logger } from 'ts-log';
1818
import { Milliseconds } from './time';
19+
import { TimeoutError } from '../errors';
1920
import { coalesceTokenMaps, subtractTokenMaps } from '../Asset/util';
2021
import { coalesceValueQuantities } from './coalesceValueQuantities';
2122
import { computeImplicitCoin } from '../Cardano/util';
23+
import { promiseTimeout } from './promiseTimeout';
2224
import { subtractValueQuantities } from './subtractValueQuantities';
2325

2426
interface TransactionSummaryInspectorArgs {
@@ -29,7 +31,7 @@ interface TransactionSummaryInspectorArgs {
2931
assetProvider: AssetProvider;
3032
dRepKeyHash?: Crypto.Ed25519KeyHashHex;
3133
timeout: Milliseconds;
32-
logger?: Logger;
34+
logger: Logger;
3335
}
3436

3537
export type TransactionSummaryInspection = {
@@ -150,27 +152,24 @@ const toAssetInfoWithAmount = async (
150152
* @param {TransactionSummaryInspectorArgs} args The arguments for the inspector.
151153
*/
152154
export const transactionSummaryInspector: TransactionSummaryInspector =
153-
(args: TransactionSummaryInspectorArgs) => async (tx) => {
154-
const {
155-
inputResolver,
156-
addresses,
157-
rewardAccounts,
158-
protocolParameters,
159-
assetProvider,
160-
dRepKeyHash,
161-
timeout,
162-
logger
163-
} = args;
164-
155+
({
156+
inputResolver,
157+
addresses,
158+
rewardAccounts,
159+
protocolParameters,
160+
assetProvider,
161+
dRepKeyHash,
162+
timeout,
163+
logger
164+
}: TransactionSummaryInspectorArgs) =>
165+
async (tx) => {
165166
let resolvedInputs: ResolutionResult;
166167

167168
try {
168-
// eslint-disable-next-line sonarjs/prefer-immediate-return
169-
resolvedInputs = await resolveWithTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
169+
resolvedInputs = await promiseTimeout(resolveInputs(tx.body.inputs, inputResolver), timeout);
170170
} 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');
171+
if (error instanceof TimeoutError) {
172+
logger.error('Error: Inputs resolution timed out');
174173
}
175174

176175
resolvedInputs = {

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

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ const buildTokenTransferValue = (coins: bigint, assets: Array<[Asset.AssetInfo,
2222
coins
2323
});
2424

25-
const createMockInputResolver = (historicalTxs: Cardano.HydratedTx[]): Cardano.InputResolver => ({
25+
const createMockInputResolver = (historicalTxs: Cardano.HydratedTx[], resolveDelay = 0): Cardano.InputResolver => ({
2626
async resolveInput(input: Cardano.TxIn) {
2727
const tx = historicalTxs.find((historicalTx) => historicalTx.id === input.txId);
2828

2929
if (!tx || tx.body.outputs.length <= input.index) return Promise.resolve(null);
3030

31-
return Promise.resolve(tx.body.outputs[input.index]);
31+
return await new Promise((resolve) => {
32+
setTimeout(() => {
33+
resolve(tx.body.outputs[input.index]);
34+
}, resolveDelay);
35+
});
3236
}
3337
});
3438

@@ -1069,5 +1073,75 @@ describe('txInspector', () => {
10691073
])
10701074
);
10711075
});
1076+
1077+
it.only('has no resolved fromAddresses on timeout', async () => {
1078+
const resolveDelay = 5;
1079+
const timeoutAfter = Milliseconds(1);
1080+
1081+
const tx = buildMockTx({
1082+
inputs: [
1083+
{
1084+
address: addresses[0],
1085+
index: 0,
1086+
txId: Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
1087+
},
1088+
{
1089+
address: addresses[1],
1090+
index: 0,
1091+
txId: Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
1092+
}
1093+
],
1094+
outputs: [
1095+
{
1096+
address: addresses[0],
1097+
value: {
1098+
assets: new Map([
1099+
[AssetIds.TSLA, 100n],
1100+
[AssetIds.PXL, 50n],
1101+
[AssetIds.Unit, 30n]
1102+
]),
1103+
coins: 3_000_000n
1104+
}
1105+
}
1106+
]
1107+
});
1108+
1109+
const histTx: Cardano.HydratedTx[] = [
1110+
{
1111+
body: {
1112+
outputs: [
1113+
{
1114+
address: addresses[0],
1115+
value: {
1116+
assets: new Map([
1117+
[AssetIds.TSLA, 25n],
1118+
[AssetIds.PXL, 10n],
1119+
[AssetIds.Unit, 5n]
1120+
]),
1121+
coins: 3_000_000n
1122+
}
1123+
}
1124+
]
1125+
},
1126+
id: Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
1127+
} as unknown as Cardano.HydratedTx
1128+
];
1129+
1130+
const inspectTx = createTxInspector({
1131+
tokenTransfer: tokenTransferInspector({
1132+
fromAddressAssetProvider: assetProvider,
1133+
inputResolver: createMockInputResolver(histTx, resolveDelay),
1134+
logger,
1135+
timeout: timeoutAfter,
1136+
toAddressAssetProvider: assetProvider
1137+
})
1138+
});
1139+
1140+
// Act
1141+
const { tokenTransfer } = await inspectTx(tx);
1142+
1143+
// Assert
1144+
expect(tokenTransfer.fromAddress).toEqual(new Map([]));
1145+
});
10721146
});
10731147
});

0 commit comments

Comments
 (0)