Skip to content

Commit

Permalink
refactor(core)!: changes value sent and received inspectors
Browse files Browse the repository at this point in the history
  • Loading branch information
lgobbi-atix committed Apr 25, 2022
1 parent 47efa0e commit bdecf31
Show file tree
Hide file tree
Showing 17 changed files with 538 additions and 50 deletions.
12 changes: 7 additions & 5 deletions packages/core/src/Address/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Address, TxAlonzo } from '../Cardano';
import { Address, TxAlonzo, TxIn } from '../Cardano';
import { parseCslAddress } from '../CSL';

/**
Expand All @@ -15,8 +15,10 @@ export const isAddressWithin =
addresses.includes(address!);

/**
* Receives a transaction and a set of addresses to check if the transaction is outgoing,
* i.e., some of the addresses are included in the transaction inputs
* Receives a transaction and a set of addresses to check if
* some of them are included in the transaction inputs
*
* @returns {TxIn[]} array of inputs that contain any of the addresses
*/
export const isOutgoing = (tx: TxAlonzo, ownAddresses: Address[]): boolean =>
tx.body.inputs.some(isAddressWithin(ownAddresses));
export const inputsWithAddresses = (tx: TxAlonzo, ownAddresses: Address[]): TxIn[] =>
tx.body.inputs.filter(isAddressWithin(ownAddresses));
2 changes: 2 additions & 0 deletions packages/core/src/Asset/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './assetId';
export * from './metadatumToCip25';
export * from './coalesceTokenMaps';
export * from './removeNegativesFromTokenMap';
export * from './subtractTokenMaps';
18 changes: 18 additions & 0 deletions packages/core/src/Asset/util/removeNegativesFromTokenMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TokenMap } from '../../Cardano';

/**
* Remove all negative quantities from a TokenMap.
* Does not modify the original TokenMap
*
* @param {TokenMap} assets TokenMap to remove negative quantities
* @returns {TokenMap} a copy of `assets` with negative quantities removed, could be empty
*/
export const removeNegativesFromTokenMap = (assets: TokenMap): TokenMap => {
const result: TokenMap = new Map(assets);
for (const [assetId, assetQuantity] of result) {
if (assetQuantity < 0) {
result.delete(assetId);
}
}
return result;
};
22 changes: 22 additions & 0 deletions packages/core/src/Asset/util/subtractTokenMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TokenMap } from '../../Cardano';
import { util } from '../../util';

/**
* Subtract asset quantities in order
*/
export const subtractTokenMaps = (assets: (TokenMap | undefined)[]): TokenMap | undefined => {
if (assets.length <= 0 || !util.isNotNil(assets[0])) return undefined;
const result: TokenMap = assets[0];
const rest: TokenMap[] = assets.slice(1).filter(util.isNotNil);
for (const assetTotals of rest) {
for (const [assetId, assetQuantity] of assetTotals.entries()) {
const total = result.get(assetId) ?? 0n;
const diff = total - assetQuantity;
diff === 0n ? result.delete(assetId) : result.set(assetId, diff);
}
}
if (result.size === 0) {
return undefined;
}
return result;
};
2 changes: 2 additions & 0 deletions packages/core/src/Cardano/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './estimateStakePoolAPY';
export * from './primitives';
export * as metadatum from './metadatum';
export * from './txSubmissionErrors';
export * from './resolveInputValue';
export * from './subtractValueQuantities';
13 changes: 13 additions & 0 deletions packages/core/src/Cardano/util/resolveInputValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TxAlonzo, TxIn, Value } from '../types';

/**
* Resolves the value of an input by looking for the matching output in a list of transactions
*
* @param {TxIn} input input to resolve value for
* @param {TxAlonzo[]} transactions list of transactions to find the matching output
* @returns {Value | undefined} input value or undefined if not found
*/
export const resolveInputValue = (input: TxIn, transactions: TxAlonzo[]): Value | undefined => {
const tx = transactions.find((transaction) => transaction.id === input.txId);
return tx?.body.outputs[input.index]?.value;
};
10 changes: 10 additions & 0 deletions packages/core/src/Cardano/util/subtractValueQuantities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Asset, BigIntMath } from '../..';
import { Value } from '../types';

/**
* Subtract all quantities
*/
export const subtractValueQuantities = (quantities: Value[]) => ({
assets: Asset.util.subtractTokenMaps(quantities.map(({ assets }) => assets)),
coins: BigIntMath.subtract(quantities.map(({ coins }) => coins))
});
4 changes: 4 additions & 0 deletions packages/core/src/util/BigIntMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const BigIntMath = {
}
return max;
},
subtract(arr: bigint[]): bigint {
if (arr.length === 0) return 0n;
return arr.reduce((result, num) => result - num);
},
sum(arr: bigint[]): bigint {
return arr.reduce((result, num) => result + num, 0n);
}
Expand Down
120 changes: 107 additions & 13 deletions packages/core/src/util/txInspector.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import {
Address,
Certificate,
CertificateType,
Ed25519KeyHash,
Lovelace,
RewardAccount,
StakeAddressCertificate,
StakeDelegationCertificate,
TokenMap,
TxAlonzo,
TxIn,
Value
} from '../Cardano';
import { BigIntMath } from './BigIntMath';
import { coalesceValueQuantities } from '../Cardano/util';
import { isAddressWithin, isOutgoing } from '../Address/util';
import { coalesceValueQuantities, resolveInputValue, subtractValueQuantities } from '../Cardano/util';
import { inputsWithAddresses, isAddressWithin } from '../Address/util';
import { removeNegativesFromTokenMap } from '../Asset/util';

type Inspector<Inspection> = (tx: TxAlonzo) => Inspection;
type Inspectors = { [k: string]: Inspector<unknown> };
Expand All @@ -22,37 +28,125 @@ export type SendReceiveValueInspection = Value;
export type DelegationInspection = StakeDelegationCertificate[];
export type StakeKeyRegistrationInspection = StakeAddressCertificate[];
export type WithdrawalInspection = Lovelace;
export interface SentInspection {
inputs: TxIn[];
certificates: Certificate[];
}

// Inspector types
interface SentInspectorArgs {
addresses?: Address[];
rewardAccounts?: RewardAccount[];
}
export type SentInspector = (args: SentInspectorArgs) => Inspector<SentInspection>;
export type TotalAddressInputsValueInspector = (
ownAddresses: Address[],
getHistoricalTxs: () => TxAlonzo[]
) => Inspector<SendReceiveValueInspection>;
export type SendReceiveValueInspector = (ownAddresses: Address[]) => Inspector<SendReceiveValueInspection>;
export type DelegationInspector = Inspector<DelegationInspection>;
export type StakeKeyRegistrationInspector = Inspector<StakeKeyRegistrationInspection>;
export type WithdrawalInspector = Inspector<WithdrawalInspection>;

/**
* Inspects a transaction for value (coins + assets) sent by the provided addresses.
* Inspects a transaction for values (coins + assets) in inputs
* containing any of the provided addresses.
*
* @param {Address[]} ownAddresses own wallet's addresses
* @returns {Value} total value sent
* @param {() => TxAlonzo[]} getHistoricalTxs wallet's historical transactions
* @returns {Value} total value in inputs
*/
export const valueSentInspector: SendReceiveValueInspector = (ownAddresses: Address[]) => (tx: TxAlonzo) => {
if (!isOutgoing(tx, ownAddresses)) return { coins: 0n };
const sentOutputs = tx.body.outputs.filter((out) => !isAddressWithin(ownAddresses)(out));
return coalesceValueQuantities(sentOutputs.map((output) => output.value));
};
export const totalAddressInputsValueInspector: TotalAddressInputsValueInspector =
(ownAddresses, getHistoricalTxs) => (tx) => {
const receivedInputs = tx.body.inputs.filter((input) => isAddressWithin(ownAddresses)(input));
const receivedInputsValues = receivedInputs
.map((input) => resolveInputValue(input, getHistoricalTxs()))
.filter((value): value is Value => !!value);

return coalesceValueQuantities(receivedInputsValues);
};

/**
* Inspects a transaction for value (coins + assets) received by the provided addresses.
* Inspects a transaction for values (coins + assets) in outputs
* containing any of the provided addresses.
*
* @param {Address[]} ownAddresses own wallet's addresses
* @returns {Value} total value received
* @returns {Value} total value in outputs
*/
export const valueReceivedInspector: SendReceiveValueInspector = (ownAddresses: Address[]) => (tx: TxAlonzo) => {
if (isOutgoing(tx, ownAddresses)) return { coins: 0n };
export const totalAddressOutputsValueInspector: SendReceiveValueInspector = (ownAddresses) => (tx) => {
const receivedOutputs = tx.body.outputs.filter((out) => isAddressWithin(ownAddresses)(out));
return coalesceValueQuantities(receivedOutputs.map((output) => output.value));
};

/**
* 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
*
* @param {SentInspectorArgs} args array of addresses and/or reward accounts
* @returns {SentInspection} certificates and inputs that include the addresses or reward accounts
*/
export const sentInspector: SentInspector =
({ addresses, rewardAccounts }) =>
(tx: TxAlonzo) => {
const inputs = addresses?.length ? inputsWithAddresses(tx, addresses) : [];

const stakeKeyHashes = rewardAccounts?.map((account) => Ed25519KeyHash.fromRewardAccount(account));
const stakeKeyCerts =
stakeKeyHashes && tx.body.certificates
? tx.body.certificates.filter((cert) => 'stakeKeyHash' in cert && stakeKeyHashes.includes(cert.stakeKeyHash))
: [];
const rewardAccountCerts =
rewardAccounts && tx.body.certificates
? tx.body.certificates?.filter(
(cert) =>
('rewardAccount' in cert && rewardAccounts.includes(cert.rewardAccount)) ||
('poolParameters' in cert && rewardAccounts.includes(cert.poolParameters.rewardAccount))
)
: [];
const certificates = [...stakeKeyCerts, ...rewardAccountCerts];

return { certificates, inputs };
};

/**
* Inspects a transaction for net value (coins + assets) sent by the provided addresses.
*
* @param {Address[]} ownAddresses own wallet's addresses
* @returns {Value} net value sent
*/
export const valueSentInspector: TotalAddressInputsValueInspector = (ownAddresses, historicalTxs) => (tx) => {
let assets: TokenMap = new Map();
if (sentInspector({ addresses: ownAddresses })(tx).inputs.length === 0) return { coins: 0n };
const totalOutputValue = totalAddressOutputsValueInspector(ownAddresses)(tx);
const totalInputValue = totalAddressInputsValueInspector(ownAddresses, historicalTxs)(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 {Address[]} ownAddresses own wallet's addresses
* @returns {Value} net value received
*/
export const valueReceivedInspector: TotalAddressInputsValueInspector = (ownAddresses, historicalTxs) => (tx) => {
let assets: TokenMap = new Map();
const totalOutputValue = totalAddressOutputsValueInspector(ownAddresses)(tx);
const totalInputValue = totalAddressInputsValueInspector(ownAddresses, historicalTxs)(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
};
};

/**
* Inspects a transaction for a stake delegation certificate.
*
Expand Down
16 changes: 11 additions & 5 deletions packages/core/test/Address/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Address', () => {
});
});

describe('isOutgoing', () => {
describe('inputsWithAddresses', () => {
beforeAll(() => parseCslAddressSpy.mockRestore());
const tx = {
body: {
Expand All @@ -58,12 +58,18 @@ describe('Address', () => {
}
} as Cardano.TxAlonzo;

it('returns true if any of the addresses are in some of the transaction inputs', () => {
expect(Address.util.isOutgoing(tx, addresses)).toBe(true);
it('returns the transaction inputs that contain any of the addresses', () => {
expect(Address.util.inputsWithAddresses(tx, addresses)).toEqual([
{
address: addresses[0],
index: 0,
txId: Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
}
]);
});

it('returns false if none of the addresses are in the transaction inputs', () => {
expect(Address.util.isOutgoing(tx, [addresses[1]])).toBe(false);
it('returns an empty array if none of the addresses are in the transaction inputs', () => {
expect(Address.util.inputsWithAddresses(tx, [addresses[1]])).toEqual([]);
});
});
});
Expand Down
22 changes: 22 additions & 0 deletions packages/core/test/Asset/util/removeNegativesFromTokenMap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Asset } from '@cardano-sdk/core';
import { AssetId } from '@cardano-sdk/util-dev';

describe('Asset', () => {
describe('util', () => {
describe('removeNegativesFromTokenMap', () => {
it('should delete tokens with negative quantities from a token map', () => {
const asset = new Map([
[AssetId.PXL, -100n],
[AssetId.Unit, 50n],
[AssetId.TSLA, 0n]
]);
expect(Asset.util.removeNegativesFromTokenMap(asset)).toEqual(
new Map([
[AssetId.Unit, 50n],
[AssetId.TSLA, 0n]
])
);
});
});
});
});
53 changes: 53 additions & 0 deletions packages/core/test/Asset/util/subtractTokenMaps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Asset } from '@cardano-sdk/core';
import { AssetId } from '@cardano-sdk/util-dev';

describe('Asset', () => {
describe('util', () => {
describe('subtractTokenMaps', () => {
it('should subtract quantities correctly when all assets have the same tokens', () => {
const initialAsset = new Map([
[AssetId.PXL, 100n],
[AssetId.Unit, 50n]
]);
const asset2 = new Map([
[AssetId.PXL, 23n],
[AssetId.Unit, 20n]
]);
expect(Asset.util.subtractTokenMaps([initialAsset, asset2])).toEqual(
new Map([
[AssetId.PXL, 77n],
[AssetId.Unit, 30n]
])
);
});
it('should delete tokens from result when quantity is 0', () => {
const initialAsset = new Map([
[AssetId.PXL, 100n],
[AssetId.Unit, 50n]
]);
const asset2 = new Map([
[AssetId.PXL, 23n],
[AssetId.Unit, 50n]
]);
expect(Asset.util.subtractTokenMaps([initialAsset, asset2])).toEqual(new Map([[AssetId.PXL, 77n]]));
});
it('should be able to return negative quantities', () => {
const initialAsset = new Map([
[AssetId.PXL, 100n],
[AssetId.Unit, 50n]
]);
const asset2 = new Map([
[AssetId.PXL, 173n],
[AssetId.Unit, 50n]
]);
const asset3 = new Map([[AssetId.TSLA, 44n]]);
expect(Asset.util.subtractTokenMaps([initialAsset, asset2, asset3])).toEqual(
new Map([
[AssetId.PXL, -73n],
[AssetId.TSLA, -44n]
])
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ describe('Cardano.util.coalesceValueQuantities', () => {
coins: 170n
});
});
it('returns 0 coins on empty array', () => {
expect(Cardano.util.coalesceValueQuantities([])).toEqual({ coins: 0n });
});
});
Loading

0 comments on commit bdecf31

Please sign in to comment.