// import { OutRef } from '@lucid-evolution/lucid';
import {
  addAssets,
  Assets,
  fromHex,
  fromText,
  LucidEvolution,
  OutRef,
  paymentCredentialOf,
  slotToUnixTime,
  toHex,
  TxBuilder,
  UTxO,
} from '@lucid-evolution/lucid';

import { matchSingle } from '../../utils/utils';
import {
  createScriptAddress,
  getInlineDatumOrThrow,
} from '../../utils/lucid-utils';

import {
  adaAssetClass,
  assetClassValueOf,
  isAssetsZero,
  isSameAssetClass,
  lovelacesAmt,
  mkAssetsOf,
  mkLovelacesOf,
  negateAssets,
} from '@3rd-eye-labs/cardano-offchain-common';
import { Data } from '@lucid-evolution/lucid';
import { pipe } from 'fp-ts/lib/function';
import { array as A, option as O, function as F } from 'fp-ts';
import { match, P } from 'ts-pattern';
import {
  fromSysParamsCredential,
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import { ONE_SECOND } from '../../utils/time-helpers';
import {
  parseStakingPositionOrThrow,
  serialiseStakingDatum,
  serialiseStakingRedeemer,
  StakingPosLockedAmt,
} from '../staking/types-new';
import { updateStakingLockedAmount } from '../staking/helpers';
import { pollPassQuorum } from '../poll/helpers';
import {
  collateralAssetCreationDatumHelper,
  createValueFromWithdrawal,
  findFirstCollateralAsset,
  findRelativeCollateralAssetForInsertion,
  findRelativeIAssetForInsertion,
  iassetCreationDatumHelper,
  proposalDeposit,
} from './helpers';
import { initSpState } from '../stability-pool/helpers';
import { serialiseVersionRecordDatum } from '../version-registry/types-new';
import {
  parseGovDatumOrThrow,
  ProposalContent,
  serialiseGovDatum,
  serialiseGovRedeemer,
  TreasuryWithdrawal,
} from './types-new';
import { serialiseStabilityPoolDatum } from '../stability-pool/types-new';
import {
  parsePollManagerOrThrow,
  parsePollShardOrThrow,
  PollShardContent,
  PollStatus,
  serialisePollDatum,
  serialisePollManagerRedeemer,
  serialisePollShardRedeemer,
  VoteOption,
} from '../poll/types-poll-new';
import {
  parseExecuteDatumOrThrow,
  serialiseExecuteDatum,
} from '../execute/types-new';
import {
  addressFromBech32,
  addressToBech32,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  serialiseTreasuryRedeemer,
  serialiseWithdrawalOutputDatum,
} from '../treasury/types-new';
import {
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
  serialiseIAssetDatum,
  serialiseIAssetRedeemer,
} from '../iasset/types';
import { serialiseCDPCreatorRedeemer } from '../cdp-creator/types-new';
import {
  serialiseCdpRedeemer,
  serialiseStableswapPoolDatum,
} from '../cdp/types-new';

/**
 * Returns the new PollId.
 */
export async function createProposal(
  proposalContent: ProposalContent,
  treasuryWithdrawal: TreasuryWithdrawal | null,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
  govOref: OutRef,
  /**
   * This has to be passed only in case of createAsset proposal
   */
  allIAssetOrefs: OutRef[] | undefined,
): Promise<[TxBuilder, bigint]> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();
  const pkh = paymentCredentialOf(ownAddr);

  const govRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.governanceValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single Gov Ref Script UTXO'),
  );
  const pollAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.pollManagerTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single poll auth token policy ref Script UTXO'),
  );
  const govUtxo = matchSingle(
    await lucid.utxosByOutRef([govOref]),
    (_) => new Error('Expected a single Gov UTXO'),
  );

  const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo));

  const votingEndTime = currentTime + govDatum.protocolParams.votingPeriod;
  const expirationTime =
    votingEndTime + govDatum.protocolParams.expirationPeriod;
  const proposingEndTime =
    currentTime + govDatum.protocolParams.proposingPeriod;

  const pollNftValue = mkAssetsOf(
    fromSystemParamsAsset(sysParams.govParams.pollToken),
    1n,
  );

  const newPollId = govDatum.currentProposal + 1n;

  const tx = lucid.newTx();

  // Add iAsset ref input when Propose asset proposal
  await match(proposalContent)
    .with({ ProposeIAsset: { asset: P.select() } }, async (newAsset) => {
      if (allIAssetOrefs === undefined) {
        throw new Error('Missing iAsset orefs');
      }

      const relativeIAsset = await findRelativeIAssetForInsertion(
        newAsset,
        allIAssetOrefs,
        lucid,
      );

      pipe(
        relativeIAsset,
        O.match(
          () => {
            if (govDatum.iassetsCount !== 0n) {
              throw new Error(
                'Has to find relative iAsset when there are iAssets.',
              );
            }
          },
          (relative) => {
            tx.readFrom([relative.utxo]);
          },
        ),
      );
    })
    .otherwise(() => {});

  return [
    tx
      .mintAssets(pollNftValue, Data.void())
      // Ref scripts
      .readFrom([govRefScriptUtxo, pollAuthTokenPolicyRefScriptUtxo])
      .collectFrom(
        [govUtxo],
        serialiseGovRedeemer({
          CreatePoll: {
            content: proposalContent,
            currentTime: currentTime,
            proposalOwner: fromHex(pkh.hash),
            treasuryWithdrawal: treasuryWithdrawal,
          },
        }),
      )
      .pay.ToContract(
        govUtxo.address,
        {
          kind: 'inline',
          value: serialiseGovDatum({
            ...govDatum,
            currentProposal: govDatum.currentProposal + 1n,
            activeProposals: govDatum.activeProposals + 1n,
          }),
        },
        govUtxo.assets,
      )
      .pay.ToContract(
        createScriptAddress(network, sysParams.validatorHashes.pollManagerHash),
        {
          kind: 'inline',
          value: serialisePollDatum({
            PollManager: {
              pollId: newPollId,
              pollOwner: fromHex(pkh.hash),
              content: proposalContent,
              treasuryWithdrawal: treasuryWithdrawal,
              status: { yesVotes: 0n, noVotes: 0n },
              votingEndTime: votingEndTime,
              createdShardsCount: 0n,
              talliedShardsCount: 0n,
              totalShardsCount: govDatum.protocolParams.totalShards,
              proposingEndTime: proposingEndTime,
              expirationTime: expirationTime,
              protocolVersion: govDatum.currentVersion,
              minimumQuorum: govDatum.protocolParams.minimumQuorum,
            },
          }),
        },
        addAssets(
          pollNftValue,
          mkAssetsOf(
            fromSystemParamsAsset(sysParams.govParams.indyAsset),
            proposalDeposit(
              govDatum.protocolParams.proposalDeposit,
              govDatum.activeProposals,
            ),
          ),
        ),
      )
      .validFrom(Number(currentTime) - ONE_SECOND)
      .validTo(
        Number(currentTime) +
          Number(sysParams.govParams.gBiasTime) -
          ONE_SECOND,
      )
      .addSigner(ownAddr),
    newPollId,
  ];
}

/**
 * Builds transaction creating shards of count chunk size.
 */
export async function createShardsChunks(
  /**
   * This gets automatically capped to total shards count.
   */
  chunkSize: bigint,
  pollManagerOref: OutRef,
  sysParams: SystemParams,
  currentSlot: number,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();

  const pollManagerUtxo = matchSingle(
    await lucid.utxosByOutRef([pollManagerOref]),
    (_) => new Error('Expected a single Poll manager UTXO'),
  );

  const pollManager = parsePollManagerOrThrow(
    getInlineDatumOrThrow(pollManagerUtxo),
  );

  if (pollManager.createdShardsCount >= pollManager.totalShardsCount) {
    throw new Error('All shards already created.');
  }

  const pollAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.pollManagerTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single poll auth token policy ref Script UTXO'),
  );
  const pollManagerRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.pollManagerValidatorRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single poll auth token policy ref Script UTXO'),
  );

  const shardsCount = BigInt(
    Math.min(
      Number(chunkSize),
      Number(pollManager.totalShardsCount - pollManager.createdShardsCount),
    ),
  );

  const pollNft = fromSystemParamsAsset(sysParams.govParams.pollToken);

  const tx = lucid
    .newTx()
    .validFrom(Number(currentTime) - ONE_SECOND)
    .validTo(
      Number(currentTime) +
        Number(sysParams.pollManagerParams.pBiasTime) -
        ONE_SECOND,
    )
    .mintAssets(mkAssetsOf(pollNft, shardsCount), Data.void())
    // Ref scripts
    .readFrom([pollAuthTokenPolicyRefScriptUtxo, pollManagerRefScriptUtxo])
    .collectFrom(
      [pollManagerUtxo],
      serialisePollManagerRedeemer({ CreateShards: { currentTime } }),
    )
    .pay.ToContract(
      pollManagerUtxo.address,
      {
        kind: 'inline',
        value: serialisePollDatum({
          PollManager: {
            ...pollManager,
            createdShardsCount: pollManager.createdShardsCount + shardsCount,
          },
        }),
      },
      pollManagerUtxo.assets,
    )
    .addSigner(ownAddr);

  for (let idx = 0; idx < shardsCount; idx++) {
    tx.pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.pollShardHash),
      {
        kind: 'inline',
        value: serialisePollDatum({
          PollShard: {
            pollId: pollManager.pollId,
            status: { yesVotes: 0n, noVotes: 0n },
            votingEndTime: pollManager.votingEndTime,
            managerAddress: addressFromBech32(pollManagerUtxo.address),
          },
        }),
      },
      mkAssetsOf(pollNft, 1n),
    );
  }

  return tx;
}

/**
 * Updates both locked amount and poll status based on the vote.
 */
function voteHelper(
  stakingPosLockedAmt: StakingPosLockedAmt,
  pollShard: PollShardContent,
  voteOption: VoteOption,
  currentTime: bigint,
  indyStakedAmt: bigint,
): [StakingPosLockedAmt, PollStatus] {
  const newPollStatus = match(voteOption)
    .returnType<PollStatus>()
    .with('Yes', () => ({
      ...pollShard.status,
      yesVotes: pollShard.status.yesVotes + indyStakedAmt,
    }))
    .with('No', () => ({
      ...pollShard.status,
      noVotes: pollShard.status.noVotes + indyStakedAmt,
    }))
    .exhaustive();

  const newLockedAmt: StakingPosLockedAmt = [
    ...updateStakingLockedAmount(stakingPosLockedAmt, currentTime),
    [
      BigInt(pollShard.pollId),
      { voteAmt: indyStakedAmt, votingEnd: pollShard.votingEndTime },
    ],
  ];

  return [newLockedAmt, newPollStatus];
}

export async function vote(
  voteOption: VoteOption,
  pollShardOref: OutRef,
  stakingPositionOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();

  const pollShardRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.pollShardValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single poll shard ref Script UTXO'),
  );
  const stakingRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.stakingValidatorRef),
    ]),
    (_) => new Error('Expected a single staking ref Script UTXO'),
  );

  const pollShardUtxo = matchSingle(
    await lucid.utxosByOutRef([pollShardOref]),
    (_) => new Error('Expected a single Poll shard UTXO'),
  );
  const pollShardDatum = parsePollShardOrThrow(
    getInlineDatumOrThrow(pollShardUtxo),
  );

  const stakingPosUtxo = matchSingle(
    await lucid.utxosByOutRef([stakingPositionOref]),
    (_) => new Error('Expected a single staking position UTXO'),
  );
  const stakingPosDatum = parseStakingPositionOrThrow(
    getInlineDatumOrThrow(stakingPosUtxo),
  );

  const indyStakedAmt = assetClassValueOf(
    stakingPosUtxo.assets,
    fromSystemParamsAsset(sysParams.govParams.indyAsset),
  );

  const validityFrom = Number(currentTime) - ONE_SECOND;

  if (
    stakingPosDatum.lockedAmount.find((x) => x[0] === pollShardDatum.pollId) !==
    undefined
  ) {
    throw new Error('Already voted for that proposal.');
  }

  const [newLockedAmt, newVoteStatus] = voteHelper(
    stakingPosDatum.lockedAmount,
    pollShardDatum,
    voteOption,
    BigInt(validityFrom),
    indyStakedAmt,
  );

  return lucid
    .newTx()
    .validFrom(validityFrom)
    .validTo(Number(pollShardDatum.votingEndTime) - ONE_SECOND)
    .readFrom([stakingRefScriptUtxo, pollShardRefScriptUtxo])
    .collectFrom([stakingPosUtxo], serialiseStakingRedeemer('Lock'))
    .collectFrom(
      [pollShardUtxo],
      serialisePollShardRedeemer({ Vote: voteOption }),
    )
    .pay.ToContract(
      pollShardUtxo.address,
      {
        kind: 'inline',
        value: serialisePollDatum({
          PollShard: {
            ...pollShardDatum,
            status: newVoteStatus,
          },
        }),
      },
      pollShardUtxo.assets,
    )
    .pay.ToContract(
      stakingPosUtxo.address,
      {
        kind: 'inline',
        value: serialiseStakingDatum({
          ...stakingPosDatum,
          lockedAmount: newLockedAmt,
        }),
      },
      stakingPosUtxo.assets,
    )
    .addSigner(ownAddr);
}

export async function mergeShards(
  pollManagerOref: OutRef,
  shardsOutRefs: OutRef[],
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();

  const pollShardRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.pollShardValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single poll shard ref Script UTXO'),
  );
  const pollManagerRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.pollManagerValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single poll shard ref Script UTXO'),
  );

  const pollAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.pollManagerTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single poll auth token policy ref Script UTXO'),
  );

  const pollManagerUtxo = matchSingle(
    await lucid.utxosByOutRef([pollManagerOref]),
    (_) => new Error('Expected a single Poll manager UTXO'),
  );

  const pollManagerDatum = parsePollManagerOrThrow(
    getInlineDatumOrThrow(pollManagerUtxo),
  );

  const shardUtxos = await lucid.utxosByOutRef(shardsOutRefs);

  const aggregatedStatus: PollStatus = F.pipe(
    shardUtxos,
    A.map((utxo) => parsePollShardOrThrow(getInlineDatumOrThrow(utxo))),
    A.reduce<PollShardContent, PollStatus>(
      pollManagerDatum.status,
      (acc, shard) => ({
        yesVotes: acc.yesVotes + shard.status.yesVotes,
        noVotes: acc.noVotes + shard.status.noVotes,
      }),
    ),
  );

  const shardsAggregatedAda = A.reduce<UTxO, Assets>({}, (acc, utxo) =>
    addAssets(acc, mkLovelacesOf(lovelacesAmt(utxo.assets))),
  )(shardUtxos);

  const pollNft = fromSystemParamsAsset(sysParams.govParams.pollToken);

  return lucid
    .newTx()
    .validFrom(Number(currentTime) - ONE_SECOND)
    .validTo(
      Number(currentTime) +
        Number(sysParams.pollManagerParams.pBiasTime) -
        ONE_SECOND,
    )
    .readFrom([
      pollShardRefScriptUtxo,
      pollManagerRefScriptUtxo,
      pollAuthTokenPolicyRefScriptUtxo,
    ])
    .mintAssets(mkAssetsOf(pollNft, -BigInt(shardsOutRefs.length)), Data.void())
    .collectFrom(
      [pollManagerUtxo],
      serialisePollManagerRedeemer({
        MergeShardsManager: { currentTime: currentTime },
      }),
    )
    .collectFrom(
      shardUtxos,
      serialisePollShardRedeemer({
        MergeShards: {
          currentTime: currentTime,
          pollManagerRef: {
            outputIndex: BigInt(pollManagerUtxo.outputIndex),
            txHash: fromHex(pollManagerUtxo.txHash),
          },
        },
      }),
    )
    .pay.ToContract(
      pollManagerUtxo.address,
      {
        kind: 'inline',
        value: serialisePollDatum({
          PollManager: {
            ...pollManagerDatum,
            talliedShardsCount:
              pollManagerDatum.talliedShardsCount +
              BigInt(shardsOutRefs.length),
            status: aggregatedStatus,
          },
        }),
      },
      addAssets(pollManagerUtxo.assets, shardsAggregatedAda),
    )
    .addSigner(ownAddr);
}

export async function endProposal(
  pollManagerOref: OutRef,
  govOref: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();

  const pollManagerRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.pollManagerValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single poll shard ref Script UTXO'),
  );
  const govRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.governanceValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single Gov Ref Script UTXO'),
  );
  const pollAuthTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.pollManagerTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single poll auth token policy ref Script UTXO'),
  );
  const upgradeTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.upgradeTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single upgrade auth token policy ref Script UTXO'),
  );

  const pollManagerUtxo = matchSingle(
    await lucid.utxosByOutRef([pollManagerOref]),
    (_) => new Error('Expected a single Poll manager UTXO'),
  );
  const pollManager = parsePollManagerOrThrow(
    getInlineDatumOrThrow(pollManagerUtxo),
  );

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

  const pollNft = fromSystemParamsAsset(sysParams.govParams.pollToken);
  const indyAsset = fromSystemParamsAsset(
    sysParams.pollManagerParams.indyAsset,
  );

  const proposalExpired = currentTime > pollManager.expirationTime;
  const proposalPassed =
    !proposalExpired &&
    pollPassQuorum(
      pollManager.status,
      govDatum.protocolParams.electorate,
      pollManager.minimumQuorum,
    );

  const upgradeTokenVal = mkAssetsOf(
    fromSystemParamsAsset(sysParams.govParams.upgradeToken),
    1n,
  );

  const tx = lucid
    .newTx()
    .validFrom(Number(currentTime) - ONE_SECOND)
    .validTo(
      Number(currentTime) +
        Number(sysParams.pollManagerParams.pBiasTime) -
        ONE_SECOND,
    )
    .readFrom([
      pollManagerRefScriptUtxo,
      govRefScriptUtxo,
      pollAuthTokenPolicyRefScriptUtxo,
      upgradeTokenPolicyRefScriptUtxo,
    ])
    .collectFrom(
      [pollManagerUtxo],
      serialisePollManagerRedeemer({ EndPoll: { currentTime: currentTime } }),
    )
    .collectFrom(
      [govUtxo],
      serialiseGovRedeemer({ WitnessEndPoll: { currentTime: currentTime } }),
    )
    .mintAssets(mkAssetsOf(pollNft, -1n), Data.void())
    .pay.ToContract(
      govUtxo.address,
      {
        kind: 'inline',
        value: serialiseGovDatum({
          ...govDatum,
          activeProposals: govDatum.activeProposals - 1n,
        }),
      },
      govUtxo.assets,
    )
    .addSigner(ownAddr);

  if (proposalPassed) {
    tx.pay
      .ToContract(
        createScriptAddress(network, sysParams.validatorHashes.executeHash),
        {
          kind: 'inline',
          value: serialiseExecuteDatum({
            id: pollManager.pollId,
            content: pollManager.content,
            passedTime: currentTime,
            votingEndTime: pollManager.votingEndTime,
            protocolVersion: pollManager.protocolVersion,
            treasuryWithdrawal: pollManager.treasuryWithdrawal,
          }),
        },
        upgradeTokenVal,
      )
      .mintAssets(upgradeTokenVal, Data.void());
  } else {
    tx.pay.ToContract(
      createScriptAddress(network, sysParams.validatorHashes.treasuryHash),
      { kind: 'inline', value: Data.void() },
      mkAssetsOf(
        indyAsset,
        assetClassValueOf(pollManagerUtxo.assets, indyAsset),
      ),
    );
  }

  return tx;
}

export async function executeProposal(
  executeOref: OutRef,
  govOref: OutRef,
  treasuryWithdrawalOref: OutRef | null,
  allIAssetOrefs: OutRef[] | null,
  /**
   * This for either Propose IAsset, Modify IAsset or Add Collateral asset proposals.
   */
  iAssetOref: OutRef | null,
  /**
   * This is when adding new collateral asset. It should be all collateral assets of the given iAsset
   * where it's adding the new collateral asset.
   */
  allCollateralAssetsOrefsOfIAsset: OutRef[] | null,
  collateralAssetOref: OutRef | null,
  stableswapPoolOrCdpCreatorOref: OutRef | null,
  sysParams: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const ownAddr = await lucid.wallet().address();

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

  const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo));

  const govRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.governanceValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single Gov Ref Script UTXO'),
  );
  const executeRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.executeValidatorRef),
    ]),
    (_) => new Error('Expected a single execute Ref Script UTXO'),
  );

  const upgradeTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.authTokenPolicies.upgradeTokenRef,
      ),
    ]),
    (_) =>
      new Error('Expected a single upgrade auth token policy ref Script UTXO'),
  );

  const executeUtxo = matchSingle(
    await lucid.utxosByOutRef([executeOref]),
    (_) => new Error('Expected a single execute UTXO'),
  );

  const executeDatum = parseExecuteDatumOrThrow(
    getInlineDatumOrThrow(executeUtxo),
  );

  const tx = lucid.newTx();

  // Handle treasury withdrawal
  await pipe(
    O.fromNullable(executeDatum.treasuryWithdrawal),
    O.match(
      () => {
        if (treasuryWithdrawalOref) {
          throw new Error('Cannot provide withdrawal oref when no withdrawal.');
        }
        return Promise.resolve();
      },
      async (withdrawal) => {
        if (!treasuryWithdrawalOref) {
          throw new Error('Have to provide withdrawal oref when withdrawal.');
        }

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

        const treasuryWithdrawalUtxo = matchSingle(
          await lucid.utxosByOutRef([treasuryWithdrawalOref]),
          (_) => new Error('Expected a single withdrawal UTXO'),
        );

        const withdrawalVal = createValueFromWithdrawal(withdrawal);
        const withdrawalChangeVal = addAssets(
          treasuryWithdrawalUtxo.assets,
          negateAssets(withdrawalVal),
        );

        if (!isAssetsZero(withdrawalChangeVal)) {
          tx.pay.ToContract(
            createScriptAddress(
              network,
              sysParams.validatorHashes.treasuryHash,
              sysParams.treasuryParams.treasuryUtxosStakeCredential
                ? fromSysParamsCredential(
                    sysParams.treasuryParams.treasuryUtxosStakeCredential,
                  )
                : undefined,
            ),
            { kind: 'inline', value: Data.void() },
            withdrawalChangeVal,
          );
        }

        tx.readFrom([treasuryRefScriptUtxo])
          .collectFrom(
            [treasuryWithdrawalUtxo],
            serialiseTreasuryRedeemer('Withdraw'),
          )
          .pay.ToAddressWithData(
            addressToBech32(withdrawal.destination, lucid.config().network!),
            {
              kind: 'inline',
              value: serialiseWithdrawalOutputDatum([
                fromHex(fromText('IndigoTreasuryWithdrawal')),
                {
                  txHash: fromHex(executeUtxo.txHash),
                  outputIndex: BigInt(executeUtxo.outputIndex),
                },
              ]),
            },
            withdrawalVal,
          );
      },
    ),
  );

  await match(executeDatum.content)
    .with({ ProposeIAsset: P.select() }, async (proposeContent) => {
      const iassetTokenPolicyRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies.iAssetAuthTokenRef,
          ),
        ]),
        (_) =>
          new Error(
            'Expected a single iasset auth token policy ref Script UTXO',
          ),
      );
      const stabilityPoolTokenPolicyRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies
              .stabilityPoolAuthTokenRef,
          ),
        ]),
        (_) =>
          new Error('Expected a single SP auth token policy ref Script UTXO'),
      );

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

      if (!allIAssetOrefs) {
        throw new Error('Have to provide all iasset orefs when propose asset.');
      }

      const iassetToReference = await findRelativeIAssetForInsertion(
        proposeContent.asset,
        allIAssetOrefs,
        lucid,
      );

      const { newIAsset, newReferencedIAsset } = iassetCreationDatumHelper(
        proposeContent,
        F.pipe(
          iassetToReference,
          O.map((i) => i.datum),
        ),
      );

      const iassetAuthVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.executeParams.iAssetToken),
        1n,
      );
      const spAuthVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.executeParams.stabilityPoolToken),
        1n,
      );

      tx.readFrom([
        govRefScriptUtxo,
        iassetRefScriptUtxo,
        iassetTokenPolicyRefScriptUtxo,
        stabilityPoolTokenPolicyRefScriptUtxo,
      ])
        .mintAssets(spAuthVal, Data.void())
        .mintAssets(iassetAuthVal, Data.void())
        .collectFrom([govUtxo], serialiseGovRedeemer('UpgradeGov'))
        .pay.ToContract(
          govUtxo.address,
          {
            kind: 'inline',
            value: serialiseGovDatum({
              ...govDatum,
              iassetsCount: govDatum.iassetsCount + 1n,
            }),
          },
          govUtxo.assets,
        )
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.iassetHash),
          {
            kind: 'inline',
            value: serialiseIAssetDatum({ IAssetContent: newIAsset }),
          },
          iassetAuthVal,
        )
        .pay.ToContract(
          createScriptAddress(
            network,
            sysParams.validatorHashes.stabilityPoolHash,
          ),
          {
            kind: 'inline',
            value: serialiseStabilityPoolDatum(
              {
                StabilityPool: {
                  iasset: proposeContent.asset,
                  state: initSpState,
                  assetStates: [],
                },
              },
              true,
            ),
          },
          spAuthVal,
        );

      F.pipe(
        iassetToReference,
        O.match(
          () => {
            // no action
          },
          (i) =>
            F.pipe(
              newReferencedIAsset,
              O.match(
                () => {
                  throw new Error('Expected some referenced iasset.');
                },
                (newRefIAsset) => {
                  tx.collectFrom([i.utxo], {
                    kind: 'selected',
                    makeRedeemer: (inputIndices) =>
                      serialiseIAssetRedeemer({
                        UpdateOrInsertAsset: {
                          executeInputIdx: inputIndices[0],
                        },
                      }),
                    inputs: [executeUtxo],
                  }).pay.ToContract(
                    i.utxo.address,
                    {
                      kind: 'inline',
                      value: serialiseIAssetDatum({
                        IAssetContent: newRefIAsset,
                      }),
                    },
                    i.utxo.assets,
                  );
                },
              ),
            ),
        ),
      );
    })
    .with({ ModifyIAsset: P.select() }, async (modifyContent) => {
      const iassetRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.iassetValidatorRef,
          ),
        ]),
        (_) => new Error('Expected a single CDP Ref Script UTXO'),
      );

      if (!iAssetOref) {
        throw new Error('Have to provide iasset oref when modify asset.');
      }

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

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

      tx.readFrom([iassetRefScriptUtxo])
        .collectFrom([iassetUtxo], {
          kind: 'selected',
          makeRedeemer: (inputIndices) =>
            serialiseIAssetRedeemer({
              UpdateOrInsertAsset: {
                executeInputIdx: inputIndices[0],
              },
            }),
          inputs: [executeUtxo],
        })
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.iassetHash),
          {
            kind: 'inline',
            value: serialiseIAssetDatum({
              IAssetContent: {
                assetName: iassetDatum.assetName,
                collateralAssetsCount: iassetDatum.collateralAssetsCount,
                debtMintingFeeRatio: modifyContent.newDebtMintingFeeRatio,
                liquidationProcessingFeeRatio:
                  modifyContent.newLiquidationProcessingFeeRatio,
                stabilityPoolWithdrawalFeeRatio:
                  modifyContent.newStabilityPoolWithdrawalFeeRatio,
                redemptionReimbursementRatio:
                  modifyContent.newRedemptionReimbursementRatio,
                redemptionProcessingFeeRatio:
                  modifyContent.newRedemptionProcessingFeeRatio,
                firstIAsset: iassetDatum.firstIAsset,
                nextIAsset: iassetDatum.nextIAsset,
              },
            }),
          },
          iassetUtxo.assets,
        )
        .readFrom([govUtxo])
        .setMinFee(900_000n);
    })
    .with({ AddCollateralAsset: P.select() }, async (addCollateralContent) => {
      const collateralAssetTokenPolicyRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies
              .collateralAssetTokenRef,
          ),
        ]),
        (_) =>
          new Error(
            'Expected a single collateral asset auth token policy ref Script UTXO',
          ),
      );

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

      if (!iAssetOref) {
        throw new Error(
          'Have to provide iasset oref when add collateral asset.',
        );
      }

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

      if (!allCollateralAssetsOrefsOfIAsset) {
        throw new Error(
          'Have to provide all collateral asset orefs when propose asset.',
        );
      }

      const collateralAssetToReference =
        await findRelativeCollateralAssetForInsertion(
          addCollateralContent.collateralAsset,
          allCollateralAssetsOrefsOfIAsset,
          lucid,
        );

      const { newCollateralAsset, newReferencedCollateralAsset } =
        collateralAssetCreationDatumHelper(
          addCollateralContent,
          F.pipe(
            collateralAssetToReference,
            O.map((i) => i.datum),
          ),
        );

      const collateralAssetAuthVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.executeParams.collateralAssetToken),
        1n,
      );

      const firstCollateralAsset = await findFirstCollateralAsset(
        allCollateralAssetsOrefsOfIAsset,
        lucid,
      );

      tx.readFrom([
        iassetRefScriptUtxo,
        collateralAssetTokenPolicyRefScriptUtxo,
      ]);
      tx.readFrom([govUtxo])
        .mintAssets(collateralAssetAuthVal, Data.void())
        .collectFrom([iassetUtxo], {
          kind: 'selected',
          makeRedeemer: (inputIndices) =>
            serialiseIAssetRedeemer({
              UpdateOrInsertAsset: {
                executeInputIdx: inputIndices[0],
              },
            }),
          inputs: [executeUtxo],
        })
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.iassetHash),
          {
            kind: 'inline',
            value: serialiseIAssetDatum({
              IAssetContent: {
                ...iassetDatum,
                collateralAssetsCount: iassetDatum.collateralAssetsCount + 1n,
              },
            }),
          },
          iassetUtxo.assets,
        )
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.iassetHash),
          {
            kind: 'inline',
            value: serialiseIAssetDatum({
              CollateralAssetContent: newCollateralAsset,
            }),
          },
          collateralAssetAuthVal,
        );

      F.pipe(
        collateralAssetToReference,
        O.match(
          () => {
            // no action
          },
          (i) => {
            F.pipe(
              newReferencedCollateralAsset,
              O.match(
                () => {
                  throw new Error('Expected some referenced collateral asset');
                },
                (newRefCollateralAsset) => {
                  tx.collectFrom([i.utxo], {
                    kind: 'selected',
                    makeRedeemer: (inputIndices) =>
                      serialiseIAssetRedeemer({
                        UpdateOrInsertAsset: {
                          executeInputIdx: inputIndices[0],
                        },
                      }),
                    inputs: [executeUtxo],
                  }).pay.ToContract(
                    createScriptAddress(
                      network,
                      sysParams.validatorHashes.iassetHash,
                    ),
                    {
                      kind: 'inline',
                      value: serialiseIAssetDatum({
                        CollateralAssetContent: newRefCollateralAsset,
                      }),
                    },
                    i.utxo.assets,
                  );
                },
              ),
            );
          },
        ),
      );

      // Provide proof of existence or non-existence of ADA collateral.
      if (
        !isSameAssetClass(newCollateralAsset.collateralAsset, adaAssetClass)
      ) {
        F.pipe(
          newReferencedCollateralAsset,
          O.match(
            () => {},
            (referencedAsset) =>
              referencedAsset.firstCollateralAsset
                ? {}
                : F.pipe(
                    firstCollateralAsset,
                    O.match(
                      () => {},
                      (firstAsset) => {
                        tx.readFrom([firstAsset.utxo]);
                      },
                    ),
                  ),
          ),
        );
      }
    })
    .with(
      { UpdateCollateralAsset: P.select() },
      async (updateCollateralContent) => {
        const iassetRefScriptUtxo = matchSingle(
          await lucid.utxosByOutRef([
            fromSystemParamsScriptRef(
              sysParams.scriptReferences.iassetValidatorRef,
            ),
          ]),
          (_) => new Error('Expected a single iasset Ref Script UTXO'),
        );

        if (!collateralAssetOref) {
          throw new Error(
            'Have to provide collateral asset oref when add collateral asset.',
          );
        }

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

        if (
          !isSameAssetClass(
            collateralAssetDatum.collateralAsset,
            updateCollateralContent.collateralAsset,
          ) ||
          toHex(collateralAssetDatum.iasset) !==
            toHex(updateCollateralContent.correspondingIAsset)
        ) {
          throw new Error('Wrong collateral asset.');
        }

        tx.readFrom([iassetRefScriptUtxo])
          .collectFrom([collateralAssetUtxo], {
            kind: 'selected',
            makeRedeemer: (inputIndices) =>
              serialiseIAssetRedeemer({
                UpdateOrInsertAsset: {
                  executeInputIdx: inputIndices[0],
                },
              }),
            inputs: [executeUtxo],
          })
          .pay.ToContract(
            createScriptAddress(network, sysParams.validatorHashes.iassetHash),
            {
              kind: 'inline',
              value: serialiseIAssetDatum({
                CollateralAssetContent: {
                  ...collateralAssetDatum,
                  priceInfo: updateCollateralContent.newAssetPriceInfo,
                  interestOracleNft:
                    updateCollateralContent.newInterestOracleNft,
                  redemptionRatio: updateCollateralContent.newRedemptionRatio,
                  maintenanceRatio: updateCollateralContent.newMaintenanceRatio,
                  liquidationRatio: updateCollateralContent.newLiquidationRatio,
                  minCollateralAmt: updateCollateralContent.newMinCollateralAmt,
                },
              }),
            },
            collateralAssetUtxo.assets,
          )
          .readFrom([govUtxo])
          .setMinFee(900_000n);
      },
    )
    .with({ TextProposal: P.any }, () => {
      tx.readFrom([govUtxo]);
    })
    .with({ ProposeStableswapPool: P.select() }, async (proposeContent) => {
      const cdpCreatorRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.cdpCreatorValidatorRef,
          ),
        ]),
        (_) => new Error('Expected a single CDP Creator Ref Script UTXO'),
      );

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

      if (!iAssetOref) {
        throw new Error(
          'Have to provide iasset oref when proposing stableswap pool.',
        );
      }

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

      const cdpAuthVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.executeParams.cdpToken),
        1n,
      );

      if (!stableswapPoolOrCdpCreatorOref) {
        throw new Error(
          'Have to provide cdp creator oref when propose stableswap pool.',
        );
      }

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

      tx.readFrom([
        cdpCreatorRefScriptUtxo,
        cdpTokenPolicyRefScriptUtxo,
        govUtxo,
        iassetUtxo,
      ])
        .mintAssets(cdpAuthVal, Data.void())
        .collectFrom(
          [cdpCreatorUtxo],
          serialiseCDPCreatorRedeemer('CreateStableswapPool'),
        )
        .pay.ToContract(
          createScriptAddress(
            network,
            sysParams.validatorHashes.cdpCreatorHash,
          ),
          { kind: 'inline', value: Data.void() },
          cdpCreatorUtxo.assets,
        )
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.cdpHash),
          {
            kind: 'inline',
            value: serialiseStableswapPoolDatum({
              iasset: proposeContent.iasset,
              collateralAsset: proposeContent.collateralAsset,
              collateralToIassetRatio: proposeContent.collateralToIassetRatio,
              mintingFeeRatio: proposeContent.mintingFeeRatio,
              redemptionFeeRatio: proposeContent.redemptionFeeRatio,
              mintingEnabled: true,
              redemptionEnabled: true,
              feeManager: proposeContent.feeManager,
              minMintOrderAmount: proposeContent.minMintingAmount,
              minRedemptionOrderAmount: proposeContent.minRedemptionAmount,
              stableswapValHash: proposeContent.stableswapValHash,
            }),
          },
          cdpAuthVal,
        );
    })
    .with({ ModifyStableswapPool: P.select() }, async (modifyContent) => {
      const cdpRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
        ]),
        (_) => new Error('Expected a single CDP Ref Script UTXO'),
      );

      if (!stableswapPoolOrCdpCreatorOref) {
        throw new Error(
          'Have to provide stableswap pool oref when propose stableswap pool.',
        );
      }

      const stableswapPoolUtxo = matchSingle(
        await lucid.utxosByOutRef([stableswapPoolOrCdpCreatorOref]),
        (_) => new Error('Expected a single stableswap pool UTXO'),
      );

      tx.readFrom([govRefScriptUtxo, cdpRefScriptUtxo, govUtxo])
        .collectFrom([stableswapPoolUtxo], {
          kind: 'selected',
          makeRedeemer: (inputIndices) =>
            serialiseCdpRedeemer({
              Stableswap: {
                forwardingInputIndex: inputIndices[0],
              },
            }),
          inputs: [executeUtxo],
        })
        .pay.ToContract(
          createScriptAddress(network, sysParams.validatorHashes.cdpHash),
          {
            kind: 'inline',
            value: serialiseStableswapPoolDatum({
              iasset: modifyContent.iasset,
              collateralAsset: modifyContent.collateralAsset,
              collateralToIassetRatio: modifyContent.newCollateralToIassetRatio,
              mintingFeeRatio: modifyContent.newMintingFeeRatio,
              redemptionFeeRatio: modifyContent.newRedemptionFeeRatio,
              mintingEnabled: modifyContent.newMintingEnabled,
              redemptionEnabled: modifyContent.newRedemptionEnabled,
              feeManager: modifyContent.newFeeManager,
              minMintOrderAmount: modifyContent.newMinMintingAmount,
              minRedemptionOrderAmount: modifyContent.newMinRedemptionAmount,
              stableswapValHash: modifyContent.newStableswapValHash,
            }),
          },
          stableswapPoolUtxo.assets,
        )
        // This has to be added as otherwise there is the following error:
        // TxBuilderError: { Complete: RedeemerBuilder: Coin selection had to be updated
        // after building redeemers, possibly leading to incorrect indices. Try setting
        // a minimum fee of 1416197 lovelaces. }
        .setMinFee(1416197n);
    })
    .with(
      { ModifyProtocolParams: { newParams: P.select() } },
      (newProtocolParams) => {
        tx.readFrom([govRefScriptUtxo])
          .collectFrom([govUtxo], serialiseGovRedeemer('UpgradeGov'))
          .pay.ToContract(
            govUtxo.address,
            {
              kind: 'inline',
              value: serialiseGovDatum({
                ...govDatum,
                protocolParams: newProtocolParams,
              }),
            },
            govUtxo.assets,
          );
      },
    )
    .with({ UpgradeProtocol: P.select() }, async (d) => {
      const versionRecordTokenPolicyRefScriptUtxo = matchSingle(
        await lucid.utxosByOutRef([
          fromSystemParamsScriptRef(
            sysParams.scriptReferences.authTokenPolicies
              .versionRecordTokenPolicyRef,
          ),
        ]),
        (_) =>
          new Error(
            'Expected a single version record token policy ref Script UTXO',
          ),
      );

      const versionRecordNftVal = mkAssetsOf(
        fromSystemParamsAsset(sysParams.executeParams.versionRecordToken),
        1n,
      );

      tx.readFrom([govRefScriptUtxo, versionRecordTokenPolicyRefScriptUtxo])
        .mintAssets(versionRecordNftVal, Data.void())
        .pay.ToContract(
          createScriptAddress(
            network,
            sysParams.validatorHashes.versionRegistryHash,
          ),
          {
            kind: 'inline',
            value: serialiseVersionRecordDatum({
              upgradeId: d.content.upgradeId,
              upgradePaths: d.content.upgradePaths.map(([h1, h2]) => [
                h1,
                h2.upgradeSymbol,
              ]),
            }),
          },
          versionRecordNftVal,
        )
        .collectFrom([govUtxo], serialiseGovRedeemer('UpgradeGov'))
        .pay.ToContract(
          govUtxo.address,
          {
            kind: 'inline',
            value: serialiseGovDatum({
              ...govDatum,
              currentVersion: govDatum.currentVersion + 1n,
            }),
          },
          govUtxo.assets,
        );
    })
    .otherwise(() => {});

  tx.readFrom([upgradeTokenPolicyRefScriptUtxo, executeRefScriptUtxo])
    .validFrom(Number(currentTime) - ONE_SECOND)
    .validTo(
      Number(currentTime) + Number(sysParams.govParams.gBiasTime) - ONE_SECOND,
    )
    .collectFrom([executeUtxo], Data.void())
    .mintAssets(
      mkAssetsOf(fromSystemParamsAsset(sysParams.govParams.upgradeToken), -1n),
      Data.void(),
    )
    .addSigner(ownAddr)
    .setMinFee(922932n);

  return tx;
}
