@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
692 lines (630 loc) • 16.4 kB
text/typescript
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,
);
}