-
Notifications
You must be signed in to change notification settings - Fork 54
/
txInspector.ts
427 lines (376 loc) · 15.3 KB
/
txInspector.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
import * as Crypto from '@cardano-sdk/crypto';
import {
AssetFingerprint,
AssetId,
AssetName,
Certificate,
CertificateType,
HydratedTxIn,
Lovelace,
Metadatum,
PolicyId,
PoolRegistrationCertificate,
PoolRetirementCertificate,
Script,
ScriptType,
StakeAddressCertificate,
StakeCredentialCertificateTypes,
StakeDelegationCertificate,
TokenMap,
Tx,
TxIn,
TxOut,
Value,
isCertType
} from '../Cardano/types';
import { BigIntMath } from '@cardano-sdk/util';
import { InputResolver, PaymentAddress, RewardAccount, isAddressWithin } from '../Cardano';
import { coalesceValueQuantities } from './coalesceValueQuantities';
import { nativeScriptPolicyId } from './nativeScript';
import { removeNegativesFromTokenMap } from '../Asset/util';
import { subtractValueQuantities } from './subtractValueQuantities';
export type Inspector<Inspection> = (tx: Tx) => Promise<Inspection>;
export type Inspectors = { [k: string]: Inspector<unknown> };
export type TxInspector<T extends Inspectors> = (tx: Tx) => Promise<{
[k in keyof T]: Awaited<ReturnType<T[k]>>;
}>;
// Inspection result types
export type SendReceiveValueInspection = Value;
export type DelegationInspection = StakeDelegationCertificate[];
export type StakeRegistrationInspection = StakeAddressCertificate[];
export type PoolRegistrationInspection = PoolRegistrationCertificate[];
export type PoolRetirementInspection = PoolRetirementCertificate[];
export type WithdrawalInspection = Lovelace;
export interface SentInspection {
inputs: HydratedTxIn[];
certificates: Certificate[];
}
export type SignedCertificatesInspection = Certificate[];
export interface MintedAsset {
script?: Script;
policyId: PolicyId;
fingerprint: AssetFingerprint;
assetName: AssetName;
quantity: bigint;
}
export type AssetsMintedInspection = MintedAsset[];
export type MetadataInspection = Metadatum;
// Inspector types
interface SentInspectorArgs {
addresses?: PaymentAddress[];
rewardAccounts?: RewardAccount[];
inputResolver: InputResolver;
}
export type SentInspector = (args: SentInspectorArgs) => Inspector<SentInspection>;
export type TotalAddressInputsValueInspector = (
ownAddresses: PaymentAddress[],
inputResolver: InputResolver
) => Inspector<SendReceiveValueInspection>;
export type SendReceiveValueInspector = (ownAddresses: PaymentAddress[]) => Inspector<SendReceiveValueInspection>;
export type DelegationInspector = Inspector<DelegationInspection>;
export type StakeRegistrationInspector = Inspector<StakeRegistrationInspection>;
export type WithdrawalInspector = Inspector<WithdrawalInspection>;
export type SignedCertificatesInspector = (
rewardAccounts: RewardAccount[],
certificateTypes?: CertificateType[]
) => Inspector<SignedCertificatesInspection>;
export type AssetsMintedInspector = Inspector<AssetsMintedInspection>;
export type MetadataInspector = Inspector<MetadataInspection>;
export type PoolRegistrationInspector = Inspector<PoolRegistrationInspection>;
export type PoolRetirementInspector = Inspector<PoolRetirementInspection>;
type ResolvedInput = TxIn & TxOut;
export type ResolutionResult = {
resolvedInputs: ResolvedInput[];
unresolvedInputs: TxIn[];
};
/**
* Resolves the value and address of a transaction input.
*
* @param txIns transaction inputs toi be resolved.
* @param inputResolver input resolver.
* @returns {ResolutionResult} resolved and unresolved inputs.
*/
export const resolveInputs = async (txIns: TxIn[], inputResolver: InputResolver): Promise<ResolutionResult> => {
const resolvedInputs: ResolvedInput[] = [];
const unresolvedInputs: TxIn[] = [];
for (const input of txIns) {
const resolvedInput = await inputResolver.resolveInput(input);
if (resolvedInput) {
resolvedInputs.push({
address: resolvedInput.address,
index: input.index,
txId: input.txId,
value: resolvedInput.value
});
} else {
unresolvedInputs.push(input);
}
}
return {
resolvedInputs,
unresolvedInputs
};
};
/**
* Inspects a transaction for values (coins + assets) in inputs
* containing any of the provided addresses.
*
* @param ownAddresses own wallet's addresses
* @param inputResolver input resolver.
* @returns total value in inputs
*/
export const totalAddressInputsValueInspector: TotalAddressInputsValueInspector =
(ownAddresses, inputResolver) => async (tx) => {
const { resolvedInputs } = await resolveInputs(tx.body.inputs, inputResolver);
const receivedInputs = resolvedInputs.filter((input) => isAddressWithin(ownAddresses)(input));
const receivedInputsValues = receivedInputs.map((input) => input.value);
return coalesceValueQuantities(receivedInputsValues);
};
/**
* Inspects a transaction for values (coins + assets) in outputs
* containing any of the provided addresses.
*
* @param ownAddresses own wallet's addresses
* @returns total value in outputs
*/
export const totalAddressOutputsValueInspector: SendReceiveValueInspector = (ownAddresses) => async (tx) => {
const receivedOutputs = tx.body.outputs.filter((out) => isAddressWithin(ownAddresses)(out));
return coalesceValueQuantities(receivedOutputs.map((output) => output.value));
};
/**
* Gets all certificates on the transaction that match the given reward accounts and certificate types.
*
* @param tx transaction to inspect
* @param rewardAccounts reward accounts to match
* @param certificateTypes certificate types to match
*/
export const getCertificatesByType = (
tx: Tx,
rewardAccounts: RewardAccount[],
certificateTypes?: readonly CertificateType[]
) => {
if (!tx.body.certificates || tx.body.certificates.length === 0) return [];
const certificates = certificateTypes
? tx.body.certificates?.filter((certificate) => isCertType(certificate, certificateTypes))
: tx.body.certificates;
return certificates.filter((certificate) => {
if (isCertType(certificate, StakeCredentialCertificateTypes)) {
const credHash = Crypto.Ed25519KeyHashHex(certificate.stakeCredential.hash);
return rewardAccounts.some((account) => RewardAccount.toHash(account) === credHash);
}
if (isCertType(certificate, [CertificateType.PoolRegistration]))
return rewardAccounts.includes(certificate.poolParameters.rewardAccount);
return false;
});
};
/**
* Inspects a transaction for certificates signed with the reward accounts provided.
* Is possible to specify the types of certificates to be taken into account
*
* @param rewardAccounts array of reward accounts that might have signed certificates
* @param [certificateTypes] certificates of these types will be checked. All if not provided
*/
export const signedCertificatesInspector: SignedCertificatesInspector =
(rewardAccounts: RewardAccount[], certificateTypes?: CertificateType[]) => async (tx) =>
getCertificatesByType(tx, rewardAccounts, certificateTypes);
/**
* Inspects a transaction to see if any of the addresses provided are included in a transaction input
* or if any of the rewards accounts are included in a certificate
*/
export const sentInspector: SentInspector =
({ addresses, rewardAccounts, inputResolver }) =>
async (tx) => {
const certificates = rewardAccounts?.length ? await signedCertificatesInspector(rewardAccounts)(tx) : [];
let inputs: HydratedTxIn[] = [];
if (addresses) {
const { resolvedInputs } = await resolveInputs(tx.body.inputs, inputResolver);
const sentInputs = resolvedInputs.filter((input) => isAddressWithin(addresses)(input));
inputs = sentInputs.map((input) => ({ address: input.address, index: input.index, txId: input.txId }));
}
return {
certificates,
inputs
};
};
/**
* Inspects a transaction for net value (coins + assets) sent by the provided addresses.
*
* @param ownAddresses own wallet's addresses
* @param inputResolver input resolver.
* @returns net value sent
*/
export const valueSentInspector: TotalAddressInputsValueInspector = (ownAddresses, inputResolver) => async (tx) => {
let assets: TokenMap = new Map();
if ((await sentInspector({ addresses: ownAddresses, inputResolver })(tx)).inputs.length === 0) return { coins: 0n };
const totalOutputValue = await totalAddressOutputsValueInspector(ownAddresses)(tx);
const totalInputValue = await totalAddressInputsValueInspector(ownAddresses, inputResolver)(tx);
const diff = subtractValueQuantities([totalInputValue, totalOutputValue]);
if (diff.assets) assets = removeNegativesFromTokenMap(diff.assets);
return {
assets: assets.size > 0 ? assets : undefined,
coins: diff.coins < 0n ? 0n : diff.coins
};
};
/**
* Inspects a transaction for net value (coins + assets) received by the provided addresses.
*
* @param ownAddresses own wallet's addresses
* @param inputResolver A list of historical transaction
* @returns net value received
*/
export const valueReceivedInspector: TotalAddressInputsValueInspector = (ownAddresses, inputResolver) => async (tx) => {
let assets: TokenMap = new Map();
const totalOutputValue = await totalAddressOutputsValueInspector(ownAddresses)(tx);
const totalInputValue = await totalAddressInputsValueInspector(ownAddresses, inputResolver)(tx);
const diff = subtractValueQuantities([totalOutputValue, totalInputValue]);
if (diff.assets) assets = removeNegativesFromTokenMap(diff.assets);
return {
assets: assets.size > 0 ? assets : undefined,
coins: diff.coins < 0n ? 0n : diff.coins
};
};
/**
* Generic certificate inspector.
*
* @param type The type name of the certificate.
*/
const certificateInspector =
<
T extends DelegationInspection | StakeRegistrationInspection | PoolRegistrationInspection | PoolRetirementInspection
>(
type: CertificateType
): Inspector<T> =>
async (tx) =>
(tx.body.certificates?.filter((cert) => cert.__typename === type) as T) ?? [];
/**
* Inspects a transaction for a stake delegation certificate.
*
* @param tx transaction to inspect
* @returns {DelegationInspection} array of delegation certificates
*/
export const delegationInspector: DelegationInspector = certificateInspector<DelegationInspection>(
CertificateType.StakeDelegation
);
/**
* Inspects a transaction for a stake key deregistration certificate.
*
* @param tx transaction to inspect
* @returns {StakeRegistrationInspection} array of stake key deregistration certificates
*/
export const stakeKeyDeregistrationInspector: StakeRegistrationInspector =
certificateInspector<StakeRegistrationInspection>(CertificateType.StakeDeregistration);
/**
* Inspects a transaction for a stake key registration certificate.
*
* @param tx transaction to inspect
* @returns {StakeRegistrationInspection} array of stake key registration certificates
*/
export const stakeKeyRegistrationInspector: StakeRegistrationInspector =
certificateInspector<StakeRegistrationInspection>(CertificateType.StakeRegistration);
/**
* Inspects a transaction for pool registration certificates.
*
* @param tx transaction to inspect.
* @returns {PoolRegistrationInspection} array of pool registration certificates.
*/
export const poolRegistrationInspector: PoolRegistrationInspector = certificateInspector<PoolRegistrationInspection>(
CertificateType.PoolRegistration
);
/**
* Inspects a transaction for pool retirement certificates.
*
* @param tx transaction to inspect.
* @returns {PoolRetirementInspection} array of pool retirement certificates.
*/
export const poolRetirementInspector: PoolRetirementInspector = certificateInspector<PoolRetirementInspection>(
CertificateType.PoolRetirement
);
/**
* Inspects a transaction for withdrawals.
*
* @param tx transaction to inspect
* @returns accumulated withdrawal quantities
*/
export const withdrawalInspector: WithdrawalInspector = async (tx) =>
tx.body.withdrawals?.length ? BigIntMath.sum(tx.body.withdrawals.map(({ quantity }) => quantity)) : 0n;
/**
* Matching criteria functor definition. This functor encodes a selection criteria for minted/burned assets.
* For example to get all minted assets the following criteria could be provided:
* (quantity: bigint) => quantity > 0.
*/
export interface MatchQuantityCriteria {
(quantity: bigint): boolean;
}
/**
* Inspects a transaction for minted/burned assets that match a given quantity criteria.
*
* @param matchQuantityCriteria A functor that represents a selection criteria for minted/burned assets. Will
* return <tt>true</tt> if the given criteria was met; otherwise; <tt>false</tt>.
* @returns A collection with the assets that match the given criteria.
*/
export const mintInspector =
(matchQuantityCriteria: MatchQuantityCriteria): AssetsMintedInspector =>
async (tx) => {
const assets: AssetsMintedInspection = [];
const scriptMap = new Map();
if (!tx.body.mint) return assets;
// Scripts can be embedded in transaction auxiliary data and/or the transaction witness set. If this transaction
// was built by this client the script will be present in the witness set, however, if this transaction was
// queried from a remote repository that doesn't fetch the witness data of the transaction we can still check
// if the script is present in the auxiliary data.
const scripts = [...(tx.auxiliaryData?.scripts || []), ...(tx.witness?.scripts || [])];
for (const script of scripts) {
switch (script.__type) {
case ScriptType.Native: {
const policyId = nativeScriptPolicyId(script);
if (scriptMap.has(policyId)) continue;
scriptMap.set(policyId, script);
break;
}
case ScriptType.Plutus: // TODO: Add support for plutus minting scripts.
default:
// scripts of unknown type will be ignored.
}
}
for (const [key, value] of tx.body.mint!.entries()) {
const [policyId, assetName] = [AssetId.getPolicyId(key), AssetId.getAssetName(key)];
const mintedAsset: MintedAsset = {
assetName,
fingerprint: AssetFingerprint.fromParts(policyId, assetName),
policyId,
quantity: value,
script: scriptMap.get(policyId)
};
if (matchQuantityCriteria(mintedAsset.quantity)) assets.push(mintedAsset);
}
return assets;
};
/** Inspect the transaction and retrieves all assets minted (quantity greater than 0). */
export const assetsMintedInspector: AssetsMintedInspector = mintInspector((quantity: bigint) => quantity > 0);
/** Inspect the transaction and retrieves all assets burned (quantity less than 0). */
export const assetsBurnedInspector: AssetsMintedInspector = mintInspector((quantity: bigint) => quantity < 0);
/**
* Inspects a transaction for its metadata.
*
* @param tx transaction to inspect.
*/
export const metadataInspector: MetadataInspector = async (tx) => tx.auxiliaryData?.blob ?? new Map();
/**
* Returns a function to convert lower level transaction data to a higher level object, using the provided inspectors.
*
* @param {Inspectors} inspectors inspector functions scoped to a domain concept.
*/
export const createTxInspector =
<T extends Inspectors>(inspectors: T): TxInspector<T> =>
async (tx) => {
const results = await Promise.all(
Object.entries(inspectors).map(async ([key, inspector]) => {
const result = await inspector(tx);
return { key, result };
})
);
return results.reduce((acc, { key, result }) => {
acc[key as keyof T] = result as Awaited<ReturnType<T[keyof T]>>;
return acc;
}, {} as { [K in keyof T]: Awaited<ReturnType<T[K]>> });
};