import {
  addAssets,
  Assets,
  credentialToRewardAddress,
  Data,
  fromHex,
  getInputIndices,
  LucidEvolution,
  OutRef,
  scriptHashToCredential,
  slotToUnixTime,
  toHex,
  TxBuilder,
  UTxO,
} from '@lucid-evolution/lucid';
import {
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import {
  addrDetails,
  createScriptAddress,
  getInlineDatumOrThrow,
} from '../../utils/lucid-utils';
import { matchSingle } from '../../utils/utils';
import {
  calculateAccruedInterest,
  calculateUnitaryInterestSinceOracleLastUpdated,
} from '../interest-oracle/helpers';
import { oracleExpirationAwareValidity } from '../price-oracle/helpers';
import { match, P } from 'ts-pattern';

import { calculateMinCollateralCappedIAssetRedemptionAmt } from './helpers';
import { bigintMax, bigintMin } from '../../utils/bigint-utils';
import {
  parseStabilityPoolDatumOrThrow,
  serialiseStabilityPoolDatum,
  serialiseStabilityPoolRedeemer,
} from '../stability-pool/types-new';
import { liquidationHelper } from '../stability-pool/helpers';
import { array as A, function as F, option as O } from 'fp-ts';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { collectInterestTx } from '../interest-collection/transactions';
import {
  CDPContent,
  parseCdpDatumOrThrow,
  serialiseCdpDatum,
  serialiseCdpRedeemer,
  serialiseRedeemCdpWithdrawalRedeemer,
} from './types-new';
import { parseGovDatumOrThrow } from '../gov/types-new';
import {
  adaAssetClass,
  assetClassValueOf,
  mkAssetsOf,
  negateAssets,
} from '@3rd-eye-labs/cardano-offchain-common';
import { parsePriceOracleDatum } from '../price-oracle/types-new';
import { parseInterestOracleDatum } from '../interest-oracle/types-new';
import {
  serialiseCDPCreatorDatum,
  serialiseCDPCreatorRedeemer,
} from '../cdp-creator/types-new';
import {
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
} from '../iasset/types';
import { treasuryFeeTx } from '../treasury/transactions';
import { attachOracle } from '../iasset/helpers';
import {
  rationalFloor,
  rationalFromInt,
  rationalMul,
} from '../../types/rational';
import { retrieveAdjustedPrice } from '../../utils/oracle-helpers';

export async function openCdp(
  collateralAmount: bigint,
  mintedAmount: bigint,
  sysParams: SystemParams,
  cdpCreatorOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const [pkh, skh] = await addrDetails(lucid);

  const cdpCreatorRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpCreatorValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp creator Ref Script UTXO'),
  );
  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );
  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const cdpCreatorUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpCreatorOref]),
    (_) => new Error('Expected a single CDP creator UTXO'),
  );

  match(collateralAssetDatum.priceInfo)
    .with({ Delisted: P.any }, () => {
      throw new Error("Can't open CDP of delisted asset");
    })
    .otherwise(() => {});

  const cdpNftVal = mkAssetsOf(
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    1n,
  );

  const iassetClass = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  const iassetTokensVal = mkAssetsOf(iassetClass, mintedAmount);

  const refScripts: UTxO[] = [
    cdpCreatorRefScriptUtxo,
    cdpAuthTokenPolicyRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
  ];

  const referenceInputs: UTxO[] = [
    interestOracleUtxo,
    iassetUtxo,
    collateralAssetUtxo,
  ];

  const tx = lucid
    .newTx()
    // Ref scripts
    .readFrom(refScripts)
    .mintAssets(cdpNftVal, Data.void())
    .mintAssets(iassetTokensVal, Data.void())
    .pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh),
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          cdpOwner: fromHex(pkh.hash),
          iasset: iassetDatum.assetName,
          collateralAsset: collateralAssetDatum.collateralAsset,
          mintedAmt: mintedAmount,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpNftVal,
        mkAssetsOf(collateralAssetDatum.collateralAsset, collateralAmount),
      ),
    )
    .pay.ToContract(
      cdpCreatorUtxo.address,
      {
        kind: 'inline',
        value: serialiseCDPCreatorDatum({
          creatorInputOref: {
            outputIndex: BigInt(cdpCreatorUtxo.outputIndex),
            txHash: fromHex(cdpCreatorUtxo.txHash),
          },
        }),
      },
      cdpCreatorUtxo.assets,
    );

  const { interval, referenceInputs: refInputs } = await attachOracle(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    priceOracleOref,
    pythStateOref,
    pythMessage,
    sysParams.pythConfig,
    sysParams.cdpCreatorParams.biasTime,
    currentSlot,
    lucid,
    tx,
  );

  // Set the validity interval for the transaction
  tx.validFrom(interval.validFrom).validTo(interval.validTo);

  // Read from the reference inputs
  referenceInputs.push(...refInputs);

  tx.readFrom(referenceInputs);

  const debtMintingFee = calculateFeeFromRatio(
    iassetDatum.debtMintingFeeRatio,
    mintedAmount,
  );

  const treasuryRefScriptUtxo =
    debtMintingFee > 0
      ? await treasuryFeeTx(
          iassetClass,
          debtMintingFee,
          0n,
          lucid,
          sysParams,
          tx,
          cdpCreatorUtxo,
          treasuryOref,
        )
      : undefined;

  // We need to take into account the treasury ref script as well.
  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...refScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
  ]);

  tx.collectFrom([cdpCreatorUtxo], {
    kind: 'self',
    makeRedeemer: (inputIdx) => {
      return serialiseCDPCreatorRedeemer({
        CreateCDP: {
          cdpOwner: fromHex(pkh.hash),
          minted: mintedAmount,
          collateralAmt: collateralAmount,
          currentTime: currentTime,
          creatorInputIdx: inputIdx,
          creatorOutputIdx: 1n,
          cdpOutputIdx: 0n,
          iassetRefInputIdx: refInputsIndices[1],
          collateralAssetRefInputIdx: refInputsIndices[2],
          interestOracleRefInputIdx: refInputsIndices[0],
          priceOracleIdx:
            priceOracleOref !== undefined
              ? { OracleRefInputIdx: refInputsIndices[3] }
              : 'OracleVoid',
        },
      });
    },
  });

  return tx;
}

export async function adjustCdp(
  collateralAdjustment: bigint,
  debtAdjustment: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  interestCollectorOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );

  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const iAssetAc = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: iassetDatum.assetName,
  };

  const isMintOrWithdraw = debtAdjustment > 0n || collateralAdjustment < 0n;

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const mintedAmountChange = debtAdjustment + interestAmt;

  const referenceScripts = [cdpRefScriptUtxo];
  let referenceInputs = [iassetUtxo, collateralAssetUtxo, interestOracleUtxo];

  let treasuryFee = 0n;

  const tx = lucid.newTx().readFrom(referenceScripts);

  if (isMintOrWithdraw) {
    const { interval, referenceInputs: oracleRefInputs } = await attachOracle(
      iassetDatum.assetName,
      collateralAssetDatum.collateralAsset,
      collateralAssetDatum.priceInfo,
      priceOracleOref,
      pythStateOref,
      pythMessage,
      sysParams.pythConfig,
      sysParams.cdpParams.biasTime,
      currentSlot,
      lucid,
      tx,
    );

    referenceInputs = [...oracleRefInputs, ...referenceInputs];

    // Set the validity interval for the transaction
    tx.validFrom(interval.validFrom).validTo(interval.validTo);
  } else {
    // Set the validity interval for the transaction
    tx.validFrom(
      Number(currentTime - BigInt(sysParams.cdpParams.biasTime) / 2n),
    ).validTo(Number(currentTime + BigInt(sysParams.cdpParams.biasTime) / 2n));
  }

  // when mint
  if (debtAdjustment > 0n) {
    treasuryFee += calculateFeeFromRatio(
      iassetDatum.debtMintingFeeRatio,
      debtAdjustment,
    );
  }

  const treasuryRefScriptUtxo =
    treasuryFee > 0
      ? await treasuryFeeTx(
          iAssetAc,
          treasuryFee,
          0n,
          lucid,
          sysParams,
          tx,
          cdpUtxo,
          treasuryOref,
        )
      : undefined;

  const interestCollectorRefScriptUtxo =
    interestAmt > 0n
      ? await collectInterestTx(
          mkAssetsOf(iAssetAc, interestAmt),
          lucid,
          sysParams,
          tx,
          interestCollectorOref,
        )
      : undefined;

  const iAssetTokenPolicyRefScriptUtxo =
    mintedAmountChange !== 0n
      ? matchSingle(
          await lucid.utxosByOutRef([
            fromSystemParamsScriptRef(
              sysParams.scriptReferences.iAssetTokenPolicyRef,
            ),
          ]),
          (_) =>
            new Error('Expected a single iasset token policy Ref Script UTXO'),
        )
      : undefined;

  // We need to take into account the treasury, interest collector
  // and iAsset policy ref scripts as well.
  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...referenceScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
    ...(interestCollectorRefScriptUtxo != null
      ? [interestCollectorRefScriptUtxo]
      : []),
    ...(iAssetTokenPolicyRefScriptUtxo != null
      ? [iAssetTokenPolicyRefScriptUtxo]
      : []),
  ]);

  tx.readFrom(referenceInputs)
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({
        AdjustCdp: {
          currentTime: currentTime,
          debtAdjustment: debtAdjustment,
          collateralAdjustment,
          priceOracleIdx:
            priceOracleOref !== undefined
              ? { OracleRefInputIdx: refInputsIndices[0] }
              : 'OracleVoid',
        },
      }),
    )
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: cdpDatum.mintedAmt + mintedAmountChange,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ) + interestOracleDatum.unitaryInterest,
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        mkAssetsOf(cdpDatum.collateralAsset, collateralAdjustment),
      ),
    );

  if (mintedAmountChange !== 0n) {
    const iassetTokensVal = mkAssetsOf(iAssetAc, mintedAmountChange);

    tx.readFrom([iAssetTokenPolicyRefScriptUtxo as UTxO]).mintAssets(
      iassetTokensVal,
      Data.void(),
    );
  }

  if (!cdpDatum.cdpOwner) {
    throw new Error('Expected active CDP');
  }

  tx.addSignerKey(toHex(cdpDatum.cdpOwner));

  return tx;
}

export async function depositCdp(
  amount: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  interestCollectorOref: OutRef,
  params: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  return adjustCdp(
    amount,
    0n,
    cdpOref,
    iassetOref,
    collateralAssetOref,
    undefined,
    interestOracleOref,
    treasuryOref,
    interestCollectorOref,
    params,
    lucid,
    currentSlot,
  );
}

export async function withdrawCdp(
  amount: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  interestCollectorOref: OutRef,
  params: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  return adjustCdp(
    -amount,
    0n,
    cdpOref,
    iassetOref,
    collateralAssetOref,
    priceOracleOref,
    interestOracleOref,
    treasuryOref,
    interestCollectorOref,
    params,
    lucid,
    currentSlot,
    pythMessage,
    pythStateOref,
  );
}

export async function mintCdp(
  amount: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  interestCollectorOref: OutRef,
  params: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  return adjustCdp(
    0n,
    amount,
    cdpOref,
    iassetOref,
    collateralAssetOref,
    priceOracleOref,
    interestOracleOref,
    treasuryOref,
    interestCollectorOref,
    params,
    lucid,
    currentSlot,
    pythMessage,
    pythStateOref,
  );
}

export async function burnCdp(
  amount: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  interestOracleOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  interestCollectorOref: OutRef,
  params: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  return adjustCdp(
    0n,
    -amount,
    cdpOref,
    iassetOref,
    collateralAssetOref,
    undefined,
    interestOracleOref,
    treasuryOref,
    interestCollectorOref,
    params,
    lucid,
    currentSlot,
  );
}

export async function closeCdp(
  cdpOref: OutRef,
  collateralAssetOref: OutRef,
  interestOracleOref: OutRef,
  interestCollectorOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const validateFrom = slotToUnixTime(network, currentSlot - 1);
  const validateTo = validateFrom + Number(sysParams.cdpCreatorParams.biasTime);

  const tx = lucid
    .newTx()
    .readFrom([
      cdpRefScriptUtxo,
      iAssetTokenPolicyRefScriptUtxo,
      cdpAuthTokenPolicyRefScriptUtxo,
    ])
    .readFrom([collateralAssetUtxo, interestOracleUtxo])
    .validFrom(validateFrom)
    .validTo(validateTo)
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: collateralDatum.iasset,
        },
        -cdpDatum.mintedAmt,
      ),
      Data.void(),
    )
    .mintAssets(
      mkAssetsOf(fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), -1n),
      Data.void(),
    )
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({ CloseCdp: { currentTime: currentTime } }),
    );

  if (!cdpDatum.cdpOwner) {
    throw new Error('Expected active CDP');
  }

  tx.addSignerKey(toHex(cdpDatum.cdpOwner));

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  if (interestAmt > 0n) {
    await collectInterestTx(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: collateralDatum.iasset,
        },
        interestAmt,
      ),
      lucid,
      sysParams,
      tx,
      interestCollectorOref,
    );
  }

  return tx;
}

export async function redeemCdp(
  /**
   * When the goal is to redeem the maximum possible, just pass in the total minted amount of the CDP.
   * The logic will automatically cap the amount to the max.
   */
  attemptedRedemptionIAssetAmt: bigint,
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  interestCollectorOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  govOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  _pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpRedeemRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.cdpRedeemValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp redeem Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const isDelisted = match(collateralAssetDatum.priceInfo)
    .with({ Delisted: P.any }, () => true)
    .otherwise(() => false);

  if (!isDelisted && priceOracleOref === undefined) {
    throw new Error('Missing price oracle');
  }

  const [adjustedPrice, priceOracleUtxo] = await retrieveAdjustedPrice(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    collateralAssetDatum.extraDecimals,
    priceOracleOref,
    pythMessage,
    sysParams.pythConfig,
    lucid,
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const govUtxo = matchSingle(
    await lucid.utxosByOutRef([govOref]),
    (_) => new Error('Expected a single gov UTXO'),
  );
  const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo));

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const collateralAmt = assetClassValueOf(
    cdpUtxo.assets,
    cdpDatum.collateralAsset,
  );

  const totalCdpDebt = cdpDatum.mintedAmt + interestAmt;

  const [isPartial, redemptionIAssetAmt] = (() => {
    const res = calculateMinCollateralCappedIAssetRedemptionAmt(
      collateralAmt,
      totalCdpDebt,
      adjustedPrice,
      collateralAssetDatum.redemptionRatio,
      iassetDatum.redemptionReimbursementRatio,
      BigInt(collateralAssetDatum.minCollateralAmt),
    );

    const redemptionAmt = bigintMin(
      attemptedRedemptionIAssetAmt,
      res.cappedIAssetRedemptionAmt,
    );

    return [redemptionAmt < res.cappedIAssetRedemptionAmt, redemptionAmt];
  })();

  if (redemptionIAssetAmt <= 0) {
    throw new Error("There's no iAssets available for redemption.");
  }

  const redemptionCollateralAmt = rationalFloor(
    rationalMul(adjustedPrice, rationalFromInt(redemptionIAssetAmt)),
  );

  const processingFee = calculateFeeFromRatio(
    iassetDatum.redemptionProcessingFeeRatio,
    redemptionCollateralAmt,
  );

  const reimburstmentFee = calculateFeeFromRatio(
    iassetDatum.redemptionReimbursementRatio,
    redemptionCollateralAmt,
  );

  const referenceScripts = [
    cdpRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
    cdpRedeemRefScriptUtxo,
  ];

  const referenceInputs = [
    iassetUtxo,
    collateralAssetUtxo,
    interestOracleUtxo,
    govUtxo,
  ];

  const tx = lucid.newTx().readFrom(referenceScripts);

  if (priceOracleUtxo !== undefined) {
    const priceOracleDatum = parsePriceOracleDatum(
      getInlineDatumOrThrow(priceOracleUtxo),
    );

    const txValidity = oracleExpirationAwareValidity(
      currentSlot,
      Number(sysParams.cdpCreatorParams.biasTime),
      Number(priceOracleDatum.expirationTime),
      network,
    );

    referenceInputs.push(priceOracleUtxo);

    tx.validFrom(txValidity.validFrom).validTo(txValidity.validTo);
  } else {
    const validateFrom = slotToUnixTime(network, currentSlot - 1);
    const validateTo =
      validateFrom + Number(sysParams.cdpCreatorParams.biasTime);

    tx.validFrom(validateFrom).validTo(validateTo);
  }

  const partialRedemptionFee = F.pipe(
    govDatum.protocolParams.cdpRedemptionRequiredSignature,
    O.fromNullable,
    O.match(
      // When public redemptions
      () => {
        return isPartial
          ? BigInt(sysParams.cdpRedeemParams.partialRedemptionExtraFeeLovelace)
          : 0n;
      },
      // When private redemptions
      (requiredSignature) => {
        tx.addSignerKey(toHex(requiredSignature));
        return 0n;
      },
    ),
  );

  const treasuryRefScriptUtxo =
    processingFee > 0n
      ? await treasuryFeeTx(
          cdpDatum.collateralAsset,
          processingFee,
          partialRedemptionFee,
          lucid,
          sysParams,
          tx,
          cdpOref,
          treasuryOref,
        )
      : partialRedemptionFee > 0n
        ? await treasuryFeeTx(
            adaAssetClass,
            partialRedemptionFee,
            0n,
            lucid,
            sysParams,
            tx,
            cdpOref,
            treasuryOref,
          )
        : undefined;

  const interestCollectorRefScriptUtxo =
    interestAmt > 0n
      ? await collectInterestTx(
          mkAssetsOf(
            {
              currencySymbol: fromHex(
                sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
              ),
              tokenName: iassetDatum.assetName,
            },
            interestAmt,
          ),
          lucid,
          sysParams,
          tx,
          interestCollectorOref,
        )
      : undefined;

  // We need to take into account the treasury and interest collector ref scripts as well.
  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...referenceScripts,
    ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
    ...(interestCollectorRefScriptUtxo != null
      ? [interestCollectorRefScriptUtxo]
      : []),
  ]);

  tx
    // Ref inputs
    .readFrom(referenceInputs)
    // Trigger CDP Redeem Withdrawal validator
    .withdraw(
      credentialToRewardAddress(
        lucid.config().network!,
        scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash),
      ),
      0n,
      serialiseRedeemCdpWithdrawalRedeemer({
        cdpOutReference: {
          txHash: fromHex(cdpUtxo.txHash),
          outputIndex: BigInt(cdpUtxo.outputIndex),
        },
        currentTime: currentTime,
        priceOracleIdx:
          priceOracleOref !== undefined
            ? { OracleRefInputIdx: refInputsIndices[4] }
            : 'OracleVoid',
      }),
    )
    .collectFrom([cdpUtxo], serialiseCdpRedeemer('RedeemCdp'))
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: iassetDatum.assetName,
        },
        interestAmt - redemptionIAssetAmt,
      ),
      Data.void(),
    )
    .pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: totalCdpDebt - redemptionIAssetAmt,
          cdpFees: {
            ActiveCDPInterestTracking: {
              lastSettled: currentTime,
              unitaryInterestSnapshot:
                interestOracleDatum.unitaryInterest +
                calculateUnitaryInterestSinceOracleLastUpdated(
                  currentTime,
                  interestOracleDatum,
                ),
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        mkAssetsOf(
          cdpDatum.collateralAsset,
          -redemptionCollateralAmt + reimburstmentFee,
        ),
      ),
    );

  return tx;
}

export async function freezeCdp(
  cdpOref: OutRef,
  iassetOref: OutRef,
  collateralAssetOref: OutRef,
  priceOracleOref: OutRef | undefined,
  interestOracleOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOref: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );
  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOref]),
    (_) => new Error('Expected a single iasset UTXO'),
  );
  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );
  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOref]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );
  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const [adjustedPrice, _] = await retrieveAdjustedPrice(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    collateralAssetDatum.extraDecimals,
    priceOracleOref,
    pythMessage,
    sysParams.pythConfig,
    lucid,
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([interestOracleOref]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const interestAmt = match(cdpDatum.cdpFees)
    .with({ FrozenCDPAccumulatedFees: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
      return calculateAccruedInterest(
        currentTime,
        interest.unitaryInterestSnapshot,
        cdpDatum.mintedAmt,
        interest.lastSettled,
        interestOracleDatum,
      );
    })
    .exhaustive();

  const inputCollateral = assetClassValueOf(
    cdpUtxo.assets,
    cdpDatum.collateralAsset,
  );

  const cdpDebtCollateralValue = rationalFloor(
    rationalMul(
      rationalFromInt(cdpDatum.mintedAmt + interestAmt),
      adjustedPrice,
    ),
  );

  const liquidationProcessingFee = bigintMin(
    calculateFeeFromRatio(
      iassetDatum.liquidationProcessingFeeRatio,
      inputCollateral,
    ),
    bigintMax(0n, inputCollateral - cdpDebtCollateralValue),
  );

  let referenceInputs = [iassetUtxo, collateralAssetUtxo, interestOracleUtxo];
  const referenceScripts = [cdpRefScriptUtxo];

  const tx = lucid.newTx();

  const { interval, referenceInputs: oracleRefInputs } = await attachOracle(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    priceOracleOref,
    pythStateOref,
    pythMessage,
    sysParams.pythConfig,
    sysParams.cdpParams.biasTime,
    currentSlot,
    lucid,
    tx,
  );

  referenceInputs = [...oracleRefInputs, ...referenceInputs];

  const refInputsIndices = getInputIndices(referenceInputs, [
    ...referenceInputs,
    ...referenceScripts,
  ]);

  return tx
    .readFrom(referenceScripts)
    .readFrom(referenceInputs)
    .validFrom(interval.validFrom)
    .validTo(interval.validTo)
    .collectFrom(
      [cdpUtxo],
      serialiseCdpRedeemer({
        FreezeCdp: {
          currentTime: currentTime,
          priceOracleIdx: { OracleRefInputIdx: refInputsIndices[0] },
        },
      }),
    )
    .pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.cdpHash),
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          cdpOwner: null,
          cdpFees: {
            FrozenCDPAccumulatedFees: {
              iassetInterest: interestAmt,
              collateralTreasury: liquidationProcessingFee,
            },
          },
        }),
      },
      cdpUtxo.assets,
    );
}

export async function liquidateCdp(
  cdpOref: OutRef,
  stabilityPoolOref: OutRef,
  interestCollectorOref: OutRef,
  /**
   * `undefined` in case using direct treasury payment.
   */
  treasuryOref: OutRef | undefined,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );
  const stabilityPoolRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.stabilityPoolValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single stability pool Ref Script UTXO'),
  );
  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.iAssetTokenPolicyRef,
      ),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );
  const cdpAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef,
      ),
    ]),
    (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'),
  );

  const cdpUtxo = matchSingle(
    await lucid.utxosByOutRef([cdpOref]),
    (_) => new Error('Expected a single cdp UTXO'),
  );
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));

  const spUtxo = matchSingle(
    await lucid.utxosByOutRef([stabilityPoolOref]),
    (_) => new Error('Expected a single stability pool UTXO'),
  );
  const spDatum = parseStabilityPoolDatumOrThrow(getInlineDatumOrThrow(spUtxo));

  const [frozenInterest, collateralForTreasury] = match(cdpDatum.cdpFees)
    .returnType<[bigint, bigint]>()
    .with({ FrozenCDPAccumulatedFees: P.select() }, (fees) => [
      fees.iassetInterest,
      fees.collateralTreasury,
    ])
    .with({ ActiveCDPInterestTracking: P.any }, () => {
      throw new Error('CDP fees wrong');
    })
    .exhaustive();

  const cdpNftAc = fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken);
  const iassetsAc = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: cdpDatum.iasset,
  };

  const spIassetAmt = assetClassValueOf(spUtxo.assets, iassetsAc);

  const totalCdpDebt = cdpDatum.mintedAmt + frozenInterest;

  // Take from the SP enough as much iAsset as possible to pay out the debt.
  const iassetUsedAmt = bigintMin(totalCdpDebt, spIassetAmt);

  const collateralAvailable = assetClassValueOf(
    cdpUtxo.assets,
    cdpDatum.collateralAsset,
  );
  const collateralAvailMinusFees = collateralAvailable - collateralForTreasury;
  const collateralAbsorbed =
    (collateralAvailMinusFees * iassetUsedAmt) / totalCdpDebt;

  const isPartial = spIassetAmt < totalCdpDebt;

  // Interest partially paid if the liquidity in the SP is not enough to cover it.
  const remainingInterest = bigintMax(frozenInterest - spIassetAmt, 0n);

  const payableInterest = frozenInterest - remainingInterest;

  // All the iAsset taken from the SP and not used to pay out the interest must be burnt.
  const iassetBurnAmt = iassetUsedAmt - payableInterest;

  const tx = lucid
    .newTx()
    .readFrom([
      cdpRefScriptUtxo,
      stabilityPoolRefScriptUtxo,
      iAssetTokenPolicyRefScriptUtxo,
      cdpAuthTokenPolicyRefScriptUtxo,
    ])
    .collectFrom([spUtxo], {
      kind: 'selected',
      makeRedeemer: (inputIndices) =>
        serialiseStabilityPoolRedeemer({
          LiquidateCDP: {
            cdpIdx: inputIndices[0],
          },
        }),
      inputs: [cdpUtxo],
    })
    .collectFrom([cdpUtxo], serialiseCdpRedeemer('Liquidate'))
    .mintAssets(mkAssetsOf(iassetsAc, -iassetBurnAmt), Data.void())
    .pay.ToContract(
      spUtxo.address,
      {
        kind: 'inline',
        value: serialiseStabilityPoolDatum({
          StabilityPool: liquidationHelper(
            spDatum,
            cdpDatum.collateralAsset,
            iassetUsedAmt,
            collateralAbsorbed,
          ),
        }),
      },
      addAssets(
        spUtxo.assets,
        mkAssetsOf(cdpDatum.collateralAsset, collateralAbsorbed),
        mkAssetsOf(iassetsAc, -iassetUsedAmt),
      ),
    );

  if (collateralForTreasury > 0n) {
    await treasuryFeeTx(
      cdpDatum.collateralAsset,
      collateralForTreasury,
      0n,
      lucid,
      sysParams,
      tx,
      cdpOref,
      treasuryOref,
    );
  }

  if (isPartial) {
    tx.pay.ToContract(
      cdpUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          ...cdpDatum,
          mintedAmt: cdpDatum.mintedAmt - iassetBurnAmt,
          cdpFees: {
            FrozenCDPAccumulatedFees: {
              iassetInterest: remainingInterest,
              collateralTreasury: 0n,
            },
          },
        }),
      },
      addAssets(
        cdpUtxo.assets,
        negateAssets(
          mkAssetsOf(
            cdpDatum.collateralAsset,
            collateralForTreasury + collateralAbsorbed,
          ),
        ),
      ),
    );
  } else {
    tx.mintAssets(
      mkAssetsOf(cdpNftAc, -assetClassValueOf(cdpUtxo.assets, cdpNftAc)),
      Data.void(),
    );
  }

  if (payableInterest > 0) {
    await collectInterestTx(
      mkAssetsOf(iassetsAc, payableInterest),
      lucid,
      sysParams,
      tx,
      interestCollectorOref,
    );
  }

  return tx;
}

export async function mergeCdps(
  cdpsToMergeUtxos: OutRef[],
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const cdpUtxos = await lucid.utxosByOutRef(cdpsToMergeUtxos);
  const cdpDatums = cdpUtxos.map((utxo) =>
    parseCdpDatumOrThrow(getInlineDatumOrThrow(utxo)),
  );

  if (cdpUtxos.length !== cdpsToMergeUtxos.length) {
    throw new Error('Expected certain number of CDPs');
  }

  const aggregatedVal = F.pipe(
    cdpUtxos,
    A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets)),
  );

  const aggregatedMintedAmt = F.pipe(
    cdpDatums,
    A.reduce<CDPContent, bigint>(0n, (acc, cdpDat) => acc + cdpDat.mintedAmt),
  );

  type AggregatedFees = {
    aggregatedCollateralTreasury: bigint;
    aggregatedInterest: bigint;
  };

  const { aggregatedInterest, aggregatedCollateralTreasury } = F.pipe(
    cdpDatums,
    A.reduce<CDPContent, AggregatedFees>(
      { aggregatedCollateralTreasury: 0n, aggregatedInterest: 0n },
      (acc, cdpDat) =>
        match(cdpDat.cdpFees)
          .returnType<AggregatedFees>()
          .with({ FrozenCDPAccumulatedFees: P.select() }, (fees) => ({
            aggregatedCollateralTreasury:
              acc.aggregatedCollateralTreasury + fees.collateralTreasury,
            aggregatedInterest: acc.aggregatedInterest + fees.iassetInterest,
          }))
          .otherwise(() => acc),
    ),
  );

  const [[mainMergeUtxo, mainCdpDatum], otherMergeUtxos] = match(
    A.zip(cdpUtxos, cdpDatums),
  )
    .returnType<[[UTxO, CDPContent], UTxO[]]>()
    .with([P._, ...P.array()], ([main, ...other]) => [
      main,
      other.map((a) => a[0]),
    ])
    .otherwise(() => {
      throw new Error('Expects more CDPs for merging');
    });

  return lucid
    .newTx()
    .readFrom([cdpRefScriptUtxo])
    .collectFrom([mainMergeUtxo], serialiseCdpRedeemer('MergeCdps'))
    .collectFrom(
      otherMergeUtxos,
      serialiseCdpRedeemer({
        MergeAuxiliary: {
          mainMergeUtxo: {
            outputIndex: BigInt(mainMergeUtxo.outputIndex),
            txHash: fromHex(mainMergeUtxo.txHash),
          },
        },
      }),
    )
    .pay.ToContract(
      mainMergeUtxo.address,
      {
        kind: 'inline',
        value: serialiseCdpDatum({
          cdpOwner: null,
          iasset: mainCdpDatum.iasset,
          collateralAsset: mainCdpDatum.collateralAsset,
          mintedAmt: aggregatedMintedAmt,
          cdpFees: {
            FrozenCDPAccumulatedFees: {
              collateralTreasury: aggregatedCollateralTreasury,
              iassetInterest: aggregatedInterest,
            },
          },
        }),
      },
      aggregatedVal,
    );
}
