All files / tr8-script/validators/rules allocation-policy-rule.ts

100% Statements 39/39
100% Branches 18/18
100% Functions 12/12
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                          1x   42x   42x 42x   42x 42x 42x 42x     42x                 42x   42x   42x 54x 24x     84x 84x   84x 78x 54x   84x 8x               76x 30x 54x 36x     30x 18x 18x                   12x 12x                   46x           42x 42x 42x                         132x 64x     68x          
import { add, equal, lessThanOrEqual } from "dinero.js";
import { autoInjectable } from "tsyringe";
 
import {
  LedgerEvent,
  LedgerEventAbsRefTx,
  LedgerEventRelRefTx,
} from "../../domain/ledger-event.js";
import { atd } from "../../domain/utils/math-low-level.js";
import { Amount } from "../../domain/value.js";
import { LogicalValidationRule, ValidationResult } from "../validator.js";
 
@autoInjectable()
class AllocationPolicyRule implements LogicalValidationRule {
  validate(event: LedgerEvent): ValidationResult {
    const { txs } = event;
 
    let isValid = true;
    const errors: string[] = [];
 
    for (const tx of txs) {
      const result = this.validateAmountsInTx(tx);
      isValid &&= result.isValid;
      errors.push(...result.errors);
    }
 
    return {
      isValid,
      errors,
    };
  }
 
  private validateAmountsInTx(
    tx: LedgerEventAbsRefTx | LedgerEventRelRefTx,
  ): ValidationResult {
    const { sources, destinations } = tx;
 
    const refValue = atd(this.getAmount(tx.refValue.amount));
 
    return [
      sources.map((s) => s.allocationPolicy),
      destinations.map((d) => d.allocationPolicy),
    ]
      .map((policies, idx) => {
        const name = idx === 0 ? "source" : "destination";
        const policyTypes = new Set(policies.map((item) => item.policy));
 
        const allPositiveAmounts = policies
          .filter((item) => item.policy === "split" || item.policy === "max")
          .every((p) => this.getAmount(p.amount).amount > 0n);
 
        if (!allPositiveAmounts) {
          return {
            isValid: false,
            errors: [
              `All split/max amounts for ${name} must be greater than 0.`,
            ],
          };
        }
 
        if (policyTypes.has("split")) {
          const splitMaxSums = policies
            .filter((p) => p.policy === "split" || p.policy === "max")
            .map((s) => atd(this.getAmount(s.amount)))
            .reduce(add, atd({ amount: 0n, scale: 0n }));
 
          if (policyTypes.has("remaining")) {
            const isValid = lessThanOrEqual(splitMaxSums, refValue);
            return {
              isValid,
              errors: isValid
                ? []
                : [
                    `The sum of split and/or max amounts for ${name} cannot be more than the ref value amount.`,
                  ],
            };
          }
 
          const isValid = equal(splitMaxSums, refValue);
          return {
            isValid,
            errors: isValid
              ? []
              : [
                  `The sum of split amounts for ${name} must be equal to the ref value amount.`,
                ],
          };
        }
 
        return {
          isValid: true,
          errors: [],
        };
      })
      .reduce((acc, curr) => {
        acc.isValid &&= curr.isValid;
        acc.errors.push(...curr.errors);
        return acc;
      });
  }
 
  /**
   * Helper method to extract the amount from:
   * Amount (in absolute ref)
   * or
   * [Amount | Amount] (in relative ref)
   */
  private getAmount(amount: Amount | [Amount, Amount]): Amount {
    // Since denominator of ref value and all amounts are assumed to be the same,
    // we just need to use the numerator in performing logical validation.
    if (Array.isArray(amount)) {
      return amount[0];
    }
 
    return amount;
  }
}
 
export { AllocationPolicyRule };