Skip to content

Commit

Permalink
feat(core): introduces transaction inspection utility
Browse files Browse the repository at this point in the history
Inspections for:
 - valueSent
 - valueReceived
 - delegation
 - stakeKeyRegistration
 - stakeKeyDeregistration
 - withdrawal
  • Loading branch information
rhyslbw committed Apr 14, 2022
1 parent 65003b5 commit a887733
Show file tree
Hide file tree
Showing 3 changed files with 359 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/src/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './BigIntMath';
export * as util from './misc';
export * from './slotCalc';
export * from './txInspector';
113 changes: 113 additions & 0 deletions packages/core/src/util/txInspector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
Address,
CertificateType,
Lovelace,
StakeAddressCertificate,
StakeDelegationCertificate,
TxAlonzo,
Value
} from '../Cardano';
import { BigIntMath } from './BigIntMath';
import { coalesceValueQuantities } from '../Cardano/util';
import { isAddressWithin, isOutgoing } from '../Address/util';

type Inspector<Inspection> = (tx: TxAlonzo) => Inspection;
type Inspectors = { [k: string]: Inspector<unknown> };
type TxInspector<T extends Inspectors> = (tx: TxAlonzo) => {
[k in keyof T]: ReturnType<T[k]>;
};

// Inspectors result types
export type SendReceiveValueInspection = Value;
export type DelegationInspection = StakeDelegationCertificate[];
export type StakeKeyRegistrationInspection = StakeAddressCertificate[];
export type WithdrawalInspection = Lovelace;

/**
* Inspects a transaction for value (coins + assets) sent by the provided addresses.
*
* @param {Address[]} ownAddresses own wallet's addresses
* @returns {Value} total value sent
*/
export const valueSentInspector =
(ownAddresses: Address[]): Inspector<Value> =>
(tx: TxAlonzo): SendReceiveValueInspection => {
if (!isOutgoing(tx, ownAddresses)) return { coins: 0n };
const sentOutputs = tx.body.outputs.filter((out) => !isAddressWithin(ownAddresses)(out));
return coalesceValueQuantities(sentOutputs.map((output) => output.value));
};

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

/**
* Inspects a transaction for a stake delegation certificate.
*
* @param {TxAlonzo} tx transaction to inspect
* @returns {DelegationInspection} array of delegation certificates
*/
export const delegationInspector: Inspector<DelegationInspection> = (tx: TxAlonzo) =>
(tx.body.certificates?.filter(
(cert) => cert.__typename === CertificateType.StakeDelegation
) as StakeDelegationCertificate[]) ?? [];

/**
* Inspects a transaction for a stake key registration certificate.
*
* @param {TxAlonzo} tx transaction to inspect
* @returns {StakeKeyRegistrationInspection} array of stake key registration certificates
*/
export const stakeKeyRegistrationInspector: Inspector<StakeKeyRegistrationInspection> = (tx: TxAlonzo) =>
(tx.body.certificates?.filter(
(cert) => cert.__typename === CertificateType.StakeKeyRegistration
) as StakeAddressCertificate[]) ?? [];

/**
* Inspects a transaction for a stake key deregistration certificate.
*
* @param {TxAlonzo} tx transaction to inspect
* @returns {StakeKeyRegistrationInspection} array of stake key deregistration certificates
*/
export const stakeKeyDeregistrationInspector: Inspector<StakeKeyRegistrationInspection> = (tx: TxAlonzo) =>
(tx.body.certificates?.filter(
(cert) => cert.__typename === CertificateType.StakeKeyDeregistration
) as StakeAddressCertificate[]) ?? [];

/**
* Inspects a transaction for withdrawals.
*
* @param {TxAlonzo} tx transaction to inspect
* @returns {WithdrawalInspection} accumulated withdrawal quantities
*/
export const withdrawalInspector: Inspector<WithdrawalInspection> = (tx: TxAlonzo) =>
tx.body.withdrawals?.length ? BigIntMath.sum(tx.body.withdrawals.map(({ quantity }) => quantity)) : 0n;

/**
* 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> =>
(tx: TxAlonzo) =>
Object.keys(inspectors).reduce(
(result, key) => {
const inspector = inspectors[key];
result[key as keyof T] = inspector(tx) as ReturnType<T[keyof T]>;
return result;
},
{} as {
[k in keyof T]: ReturnType<T[k]>;
}
);
245 changes: 245 additions & 0 deletions packages/core/test/util/txInspector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import {
Address,
BlockId,
Certificate,
CertificateType,
Ed25519KeyHash,
PoolId,
RewardAccount,
StakeAddressCertificate,
StakeDelegationCertificate,
TransactionId,
TxAlonzo,
TxIn,
TxOut,
Withdrawal
} from '../../src/Cardano';
import { AssetId } from '@cardano-sdk/util-dev';
import {
createTxInspector,
delegationInspector,
stakeKeyDeregistrationInspector,
stakeKeyRegistrationInspector,
valueReceivedInspector,
valueSentInspector,
withdrawalInspector
} from '../../src/util/txInspector';

describe('txInspector', () => {
const sendingAddress = Address(
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
);
const receivingAddress = Address(
'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz'
);

const delegationCert: StakeDelegationCertificate = {
__typename: CertificateType.StakeDelegation,
poolId: PoolId('pool1euf2nh92ehqfw7rpd4s9qgq34z8dg4pvfqhjmhggmzk95gcd402'),
stakeKeyHash: Ed25519KeyHash('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed5')
};
const keyRegistrationCert: StakeAddressCertificate = {
__typename: CertificateType.StakeKeyRegistration,
stakeKeyHash: Ed25519KeyHash('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed5')
};
const keyDeregistrationCert: StakeAddressCertificate = {
__typename: CertificateType.StakeKeyDeregistration,
stakeKeyHash: Ed25519KeyHash('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed5')
};
const withdrawals: Withdrawal[] = [
{
quantity: 2_000_000n,
stakeAddress: RewardAccount('stake_test1upqykkjq3zhf4085s6n70w8cyp57dl87r0ezduv9rnnj2uqk5zmdv')
},
{
quantity: 7_000_000n,
stakeAddress: RewardAccount('stake_test1upqykkjq3zhf4085s6n70w8cyp57dl87r0ezduv9rnnj2uqk5zmdv')
}
];

const buildMockTx = (
args: { inputs?: TxIn[]; outputs?: TxOut[]; certificates?: Certificate[]; withdrawals?: Withdrawal[] } = {}
): TxAlonzo =>
({
blockHeader: {
blockNo: 200,
hash: BlockId('0dbe461fb5f981c0d01615332b8666340eb1a692b3034f46bcb5f5ea4172b2ed'),
slot: 1000
},
body: {
certificates: args.certificates,
fee: 170_000n,
inputs: args.inputs ?? [
{
address: sendingAddress,
index: 0,
txId: TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
}
],
outputs: args.outputs ?? [
{
address: receivingAddress,
value: { coins: 5_000_000n }
},
{
address: receivingAddress,
value: {
assets: new Map([
[AssetId.PXL, 3n],
[AssetId.TSLA, 4n]
]),
coins: 2_000_000n
}
},
{
address: receivingAddress,
value: {
assets: new Map([[AssetId.PXL, 6n]]),
coins: 2_000_000n
}
},
{
address: sendingAddress,
value: {
assets: new Map([[AssetId.PXL, 1n]]),
coins: 2_000_000n
}
}
],
validityInterval: {},
withdrawals: args.withdrawals
},
id: TransactionId('e3a443363eb6ee3d67c5e75ec10b931603787581a948d68fa3b2cd3ff2e0d2ad'),
index: 0
} as TxAlonzo);

describe('sent and received value inspectors', () => {
test('an outgoing transaction produces an inspection containing total sent coins and not received coins', () => {
const tx = buildMockTx();
const inspectTx = createTxInspector({
valueReceived: valueReceivedInspector([sendingAddress]),
valueSent: valueSentInspector([sendingAddress])
});
const txProperties = inspectTx(tx);

expect(txProperties.valueSent.coins).toEqual(9_000_000n);
expect(txProperties.valueSent.assets).toEqual(
new Map([
[AssetId.PXL, 9n],
[AssetId.TSLA, 4n]
])
);
expect(txProperties.valueReceived).toEqual({ coins: 0n });
});

test('an incoming transaction produces an inspection containing total received coins and not sent coins', () => {
const tx = buildMockTx();
const inspectTx = createTxInspector({
valueReceived: valueReceivedInspector([receivingAddress]),
valueSent: valueSentInspector([receivingAddress])
});
const txProperties = inspectTx(tx);

expect(txProperties.valueSent).toEqual({ coins: 0n });
expect(txProperties.valueReceived.coins).toEqual(9_000_000n);
expect(txProperties.valueReceived.assets).toEqual(
new Map([
[AssetId.PXL, 9n],
[AssetId.TSLA, 4n]
])
);
});
});

describe('delegation inspector', () => {
test(
'a transaction containing delegations produces an inspection ' +
'containing an array with the key hashes and pool ids',
() => {
const tx = buildMockTx({ certificates: [delegationCert] });
const inspectTx = createTxInspector({ delegation: delegationInspector });
const txProperties = inspectTx(tx);

expect(txProperties.delegation[0].stakeKeyHash).toEqual(delegationCert.stakeKeyHash);
expect(txProperties.delegation[0].poolId).toEqual(delegationCert.poolId);
}
);

test('a transaction with no delegations produces an inspection containing an empty array', () => {
const tx = buildMockTx({ certificates: [] });
const inspectTx = createTxInspector({ delegation: delegationInspector });
const txProperties = inspectTx(tx);

expect(txProperties.delegation).toEqual([]);
});
});

describe('stake key registration inspector', () => {
test(
'a transaction containing stake key registrations produces an inspection ' +
'containing an array with the key hashes',
() => {
const tx = buildMockTx({ certificates: [keyRegistrationCert] });
const inspectTx = createTxInspector({
stakeKeyRegistration: stakeKeyRegistrationInspector
});
const txProperties = inspectTx(tx);

expect(txProperties.stakeKeyRegistration[0].stakeKeyHash).toEqual(keyRegistrationCert.stakeKeyHash);
}
);

test('a transaction with no stake key registrations produces an inspection containing an empty array', () => {
const tx = buildMockTx({ certificates: [] });
const inspectTx = createTxInspector({
stakeKeyRegistration: stakeKeyRegistrationInspector
});
const txProperties = inspectTx(tx);

expect(txProperties.stakeKeyRegistration).toEqual([]);
});
});

describe('stake key deregistration inspector', () => {
test(
'a transaction containing stake key deregistrations produces an inspection ' +
'containing an array with the key hashes',
() => {
const tx = buildMockTx({
certificates: [keyDeregistrationCert]
});
const inspectTx = createTxInspector({
stakeKeyDeregistration: stakeKeyDeregistrationInspector
});
const txProperties = inspectTx(tx);
expect(txProperties.stakeKeyDeregistration[0].stakeKeyHash).toEqual(keyDeregistrationCert.stakeKeyHash);
}
);

test('a transaction with no stake key deregistrations produces an inspection containing an empty array', () => {
const tx = buildMockTx({ certificates: [] });
const inspectTx = createTxInspector({
stakeKeyDeregistration: stakeKeyDeregistrationInspector
});
const txProperties = inspectTx(tx);

expect(txProperties.stakeKeyDeregistration).toEqual([]);
});
});

describe('withdrawal inspector', () => {
test('a transaction containing withdrawals produces an inspection containing the accumulated withdrawals', () => {
const tx = buildMockTx({ withdrawals });
const inspectTx = createTxInspector({ totalWithdrawals: withdrawalInspector });
const txProperties = inspectTx(tx);
expect(txProperties.totalWithdrawals).toEqual(9_000_000n);
});

test('a transaction with no withdrawals produces an inspection with total withdrawals equal to 0', () => {
const tx = buildMockTx({ withdrawals: [] });
const inspectTx = createTxInspector({ totalWithdrawals: withdrawalInspector });
const txProperties = inspectTx(tx);
expect(txProperties.totalWithdrawals).toEqual(0n);
});
});
});

0 comments on commit a887733

Please sign in to comment.