UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

692 lines (630 loc) 16.4 kB
import { TxBuilder, Credential, stakeCredentialOf, toHex, UTxO, toText, fromText, Assets, addAssets, fromHex, } from '@lucid-evolution/lucid'; import { addrDetails, burnCdp, closeCdp, depositCdp, freezeCdp, fromSystemParamsAsset, liquidateCdp, mintCdp, mkTreasuryAddr, openCdp, redeemCdp, SystemParams, withdrawCdp, } from '../../src'; import { findAllActiveCdps, findAllNecessaryOrefs, findCdp, findFrozenCDPs, findPrice, findPriceOracleFromCollateralAsset, } from './cdp-queries'; import { LucidContext, repeat, runAndAwaitTx, runAndAwaitTxBuilder, } from '../test-helpers'; import { AssetInfo } from '../endpoints/initialize'; import { assert, expect } from 'vitest'; import { feedPriceOracleTx } from '../../src/contracts/price-oracle/transactions'; import { adaAssetClass, AssetClass, assetClassToUnit, assetClassValueOf, getInlineDatumOrThrow, matchSingle, mkAssetsOf, } from '@3rd-eye-labs/cardano-offchain-common'; import { parseCdpDatumOrThrow } from '../../src/contracts/cdp/types-new'; import { match, P } from 'ts-pattern'; import { findPriceOracle } from '../price-oracle/price-oracle-queries'; import { findCollateralAsset } from '../queries/iasset-queries'; import { runFeedPriceToOracle } from '../price-oracle/actions'; import { rationalFromInt, rationalMul } from '../../src/types/rational'; import { array as A } from 'fp-ts'; import { sendValueTo } from '../utils'; import { findRandomTreasuryUtxoWithAsset } from '../treasury/treasury-queries'; // Selects users wallet and opens a CDP with the given initial collateral and mint amount export async function runOpenCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, initialCollateral: bigint, initialMint: bigint, pythMessage?: string, directTreasuryPayment: boolean = false, ): Promise<TxBuilder> { const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, orefs.collateralAsset, ); const pythStateOref = pythMessage ? await context.lucid.utxoByUnit( assetClassToUnit( fromSystemParamsAsset(sysParams.pythConfig.pythStateAssetClass), ), ) : undefined; return openCdp( initialCollateral, initialMint, sysParams, orefs.cdpCreatorUtxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, directTreasuryPayment ? undefined : orefs.treasuryUtxo, context.lucid, context.emulator.slot, pythMessage, pythStateOref, ); } export async function runDepositCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, amount: bigint = 1_000_000n, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); return await depositCdp( amount, cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, orefs.interestOracleUtxo, orefs.treasuryUtxo, orefs.interestCollectorUtxo, sysParams, context.lucid, context.emulator.slot, ); } export async function runWithdrawCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, amount: bigint = 1_000_000n, pythMessage?: string, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => findPriceOracle(context.lucid, oracleNft), ) .otherwise(() => undefined); const pythStateOref = pythMessage ? await context.lucid.utxoByUnit( sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol + fromText('Pyth State'), ) : undefined; return await withdrawCdp( amount, cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, orefs.treasuryUtxo, orefs.interestCollectorUtxo, sysParams, context.lucid, context.emulator.slot, pythMessage, pythStateOref, ); } export async function runMintCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, amount: bigint = 100_000n, pythMessage?: string, directTreasuryPayment: boolean = false, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => findPriceOracle(context.lucid, oracleNft), ) .otherwise(() => undefined); const pythStateOref = pythMessage ? await context.lucid.utxoByUnit( sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol + fromText('Pyth State'), ) : undefined; return await mintCdp( amount, cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, directTreasuryPayment ? undefined : orefs.treasuryUtxo, orefs.interestCollectorUtxo, sysParams, context.lucid, context.emulator.slot, pythMessage, pythStateOref, ); } export async function runBurnCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, amount: bigint = 100_000n, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); return await burnCdp( amount, cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, orefs.interestOracleUtxo, orefs.treasuryUtxo, orefs.interestCollectorUtxo, sysParams, context.lucid, context.emulator.slot, ); } export async function runCloseCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); return await closeCdp( cdp.utxo, orefs.collateralAsset.utxo, orefs.interestOracleUtxo, orefs.interestCollectorUtxo, sysParams, context.lucid, context.emulator.slot, ); } export async function runRedeemCdp( context: LucidContext, sysParams: SystemParams, iusdAssetInfo: AssetInfo, collateralAsset: AssetClass, pkh: string, skh: Credential | undefined, directTreasuryPayment: boolean = false, ): Promise<TxBuilder> { const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, collateralAsset, ); const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => findPriceOracle(context.lucid, oracleNft), ) .otherwise(() => undefined); return await redeemCdp( cdp.datum.mintedAmt, cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, orefs.interestCollectorUtxo, directTreasuryPayment ? undefined : orefs.treasuryUtxo, orefs.govUtxo, sysParams, context.lucid, context.emulator.slot, ); } export async function runFreezeCdp( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, pkh: string, skh: Credential | undefined, pythMessage?: string, ): Promise<TxBuilder> { const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh, skh, ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => findPriceOracle(context.lucid, oracleNft), ) .otherwise(() => undefined); const pythStateOref = pythMessage ? await context.lucid.utxoByUnit( sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol + fromText('Pyth State'), ) : undefined; return freezeCdp( cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, sysParams, context.lucid, context.emulator.slot, pythMessage, pythStateOref, ); } export async function runCreateAndFreezeCdps( context: LucidContext, sysParams: SystemParams, assetInfo: AssetInfo, numberOfCdps: number, iasset: string, collateralAsset: AssetClass, ): Promise<void> { const collateralAssetOutput = await findCollateralAsset( context.lucid, sysParams, fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken), iasset, collateralAsset, ); let priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, collateralAssetOutput, ); await repeat(numberOfCdps, async () => { const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, iasset, collateralAsset, ); await runAndAwaitTx( context.lucid, openCdp( 12_000_000n, 6_000_000n, sysParams, orefs.cdpCreatorUtxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, orefs.treasuryUtxo, context.lucid, context.emulator.slot, ), ); }); await runAndAwaitTx( context.lucid, feedPriceOracleTx( context.lucid, priceOracleUtxo!, { numerator: 18n, denominator: 10n }, // 1.8 assetInfo.collateralAssets[0].oracleParams!, context.emulator.slot, ), ); priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, collateralAssetOutput, ); { const activeCdps = await findAllActiveCdps( context.lucid, sysParams, iasset, stakeCredentialOf(context.users.admin.address), ); expect( activeCdps.length === numberOfCdps, `Expected ${numberOfCdps} cdps`, ).toBeTruthy(); for (const cdp of activeCdps) { const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, iasset, collateralAsset, ); await runAndAwaitTx( context.lucid, freezeCdp( cdp.utxo, orefs.iasset.utxo, orefs.collateralAsset.utxo, priceOracleUtxo, orefs.interestOracleUtxo, sysParams, context.lucid, context.emulator.slot, ), ); } } } export async function runLiquidateCdp( context: LucidContext, sysParams: SystemParams, frozenCdpUtxo: UTxO, treasuryUtxo?: UTxO, directTreasuryPayment: boolean = false, ): Promise<TxBuilder> { const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(frozenCdpUtxo)); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, toText(toHex(cdpDatum.iasset)), cdpDatum.collateralAsset, ); return liquidateCdp( frozenCdpUtxo, orefs.stabilityPoolUtxo, orefs.interestCollectorUtxo, directTreasuryPayment ? undefined : treasuryUtxo ? treasuryUtxo : orefs.treasuryUtxo, sysParams, context.lucid, ); } /** * Expecting price double will make CDP liquidatable. */ export async function executeLiquidation( context: LucidContext, sysParams: SystemParams, liquidatedDebt: bigint, liquidatedCollateral: bigint, collateralAsset: AssetClass, assetInfo: AssetInfo, liquidateWrapper: (liquidateTx: TxBuilder) => Promise<void> = async (tx) => { await runAndAwaitTxBuilder(context.lucid, tx); }, directTreasuryPayment: boolean = false, ) { const price = await findPrice( context.lucid, sysParams, assetInfo.iassetTokenNameAscii, collateralAsset, ); const [pkh, skh] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, assetInfo.iassetTokenNameAscii, collateralAsset, liquidatedCollateral, liquidatedDebt, ), ); const user4Value = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users['user4'].address)); const iassetAc: AssetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: fromHex(fromText(assetInfo.iassetTokenNameAscii)), }; // Send iAssets to treasury so liquidation is performed from a clean address. await sendValueTo( mkTreasuryAddr(context.lucid, sysParams), mkAssetsOf(iassetAc, assetClassValueOf(user4Value, iassetAc)), context.lucid, ); const user4collateralAssetAmount = assetClassValueOf( user4Value, collateralAsset, ); // Send collateral asset to treasury so liquidation is performed from a clean address. if (collateralAsset !== adaAssetClass && user4collateralAssetAmount > 0n) { await sendValueTo( mkTreasuryAddr(context.lucid, sysParams), mkAssetsOf(collateralAsset, user4collateralAssetAmount), context.lucid, ); } const user4ValueBeforeLiquidation = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users['user4'].address)); assert(Object.keys(user4ValueBeforeLiquidation).length === 1); context.emulator.awaitSlot(1000); await runFeedPriceToOracle( context, sysParams, assetInfo, collateralAsset, rationalMul(price, rationalFromInt(2n)), ); await runAndAwaitTx( context.lucid, runFreezeCdp( context, sysParams, assetInfo.iassetTokenNameAscii, collateralAsset, pkh.hash, skh, ), ); const frozenCdp = matchSingle( await findFrozenCDPs( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), assetInfo.iassetTokenNameAscii, ), (_) => new Error('Expected only single frozen CDP'), ); let treasuryUtxo = undefined; if (!directTreasuryPayment) { treasuryUtxo = await findRandomTreasuryUtxoWithAsset( context.lucid, sysParams, collateralAsset, ); // This treasury UTxO should contain ADA and collateral asset. if (collateralAsset !== adaAssetClass) { assert(Object.keys(treasuryUtxo.assets).length === 2); } } await liquidateWrapper( await runLiquidateCdp( context, sysParams, frozenCdp.utxo, treasuryUtxo, directTreasuryPayment, ), ); await runFeedPriceToOracle( context, sysParams, assetInfo, collateralAsset, price, ); }