@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
372 lines (339 loc) • 10.6 kB
text/typescript
import {
LucidEvolution,
TxBuilder,
OutRef,
UTxO,
addAssets,
slotToUnixTime,
Data,
fromHex,
getInputIndices,
} from '@lucid-evolution/lucid';
import {
addrDetails,
createScriptAddress,
getInlineDatumOrThrow,
} from '../../utils/lucid-utils';
import { serialiseCdpDatum } from '../cdp/types-new';
import { matchSingle } from '../../utils/utils';
import {
fromSystemParamsAsset,
fromSystemParamsScriptRef,
SystemParams,
} from '../../types/system-params';
import { parseInterestOracleDatum } from '../interest-oracle/types-new';
import {
serialiseCDPCreatorDatum,
serialiseCDPCreatorRedeemer,
} from '../cdp-creator/types-new';
import { calculateUnitaryInterestSinceOracleLastUpdated } from '../interest-oracle/helpers';
import {
approximateLeverageRedemptions,
summarizeActualLeverageRedemptions,
calculateLeverageFromCollateralRatio,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
} from './helpers';
import {
parseCollateralAssetDatumOrThrow,
parseIAssetDatumOrThrow,
} from '../iasset/types';
import { mkAssetsOf } from '@3rd-eye-labs/cardano-offchain-common';
import { RobDatum } from '../rob/types-new';
import { rationalToFloat } from '../../types/rational';
import {
buildRedemptionsTx,
randomRobsSubsetSatisfyingTargetCollateral,
} from '../rob/helpers';
import { treasuryFeeTx } from '../treasury/transactions';
import Decimal from 'decimal.js';
import { fromDecimal } from '../../utils/bigint-utils';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { retrieveAdjustedPrice } from '../../utils/oracle-helpers';
import { attachOracle } from '../iasset/helpers';
import { OracleIdx } from '../price-oracle/types-new';
export async function leverageCdpWithRob(
leverage: number,
baseCollateral: bigint,
priceOracleOutRef: OutRef | undefined,
iassetOutRef: OutRef,
collateralAssetOutRef: OutRef,
cdpCreatorOref: OutRef,
interestOracleOref: OutRef,
treasuryOref: OutRef,
sysParams: SystemParams,
lucid: LucidEvolution,
allRobs: [UTxO, RobDatum][],
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 robScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef),
]),
(_) => new Error('Expected a single ROB Ref Script UTXO'),
);
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 cdpCreatorUtxo = matchSingle(
await lucid.utxosByOutRef([cdpCreatorOref]),
(_) => new Error('Expected a single CDP creator UTXO'),
);
const interestOracleUtxo = matchSingle(
await lucid.utxosByOutRef([interestOracleOref]),
(_) => new Error('Expected a single interest oracle UTXO'),
);
const interestOracleDatum = parseInterestOracleDatum(
getInlineDatumOrThrow(interestOracleUtxo),
);
const iassetUtxo = matchSingle(
await lucid.utxosByOutRef([iassetOutRef]),
(_) => new Error('Expected a single IAsset UTXO'),
);
const iassetDatum = parseIAssetDatumOrThrow(
getInlineDatumOrThrow(iassetUtxo),
);
const collateralAssetUtxo = matchSingle(
await lucid.utxosByOutRef([collateralAssetOutRef]),
(_) => new Error('Expected a single collateral asset UTXO'),
);
const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
getInlineDatumOrThrow(collateralAssetUtxo),
);
const [price, _] = await retrieveAdjustedPrice(
iassetDatum.assetName,
collateralAssetDatum.collateralAsset,
collateralAssetDatum.priceInfo,
collateralAssetDatum.extraDecimals,
priceOracleOutRef,
pythMessage,
sysParams.pythConfig,
lucid,
);
const maxLeverage = calculateLeverageFromCollateralRatio(
collateralAssetDatum.iasset,
collateralAssetDatum.collateralAsset,
collateralAssetDatum.maintenanceRatio,
baseCollateral,
price,
iassetDatum.debtMintingFeeRatio,
iassetDatum.redemptionReimbursementRatio,
allRobs,
);
if (!maxLeverage) {
throw new Error("Can't calculate max leverage with those parameters.");
}
const leverageSummary = approximateLeverageRedemptions(
baseCollateral,
leverage,
iassetDatum.redemptionProcessingFeeRatio,
iassetDatum.debtMintingFeeRatio,
);
if (maxLeverage < leverageSummary.leverage) {
throw new Error("Can't use more leverage than max.");
}
if (
rationalToFloat(leverageSummary.collateralRatio) <
rationalToFloat(collateralAssetDatum.maintenanceRatio)
) {
throw new Error(
"Can't have collateral ratio smaller than maintenance ratio",
);
}
const redemptionDetails = summarizeActualLeverageRedemptions(
leverageSummary.redeemedCollateral,
iassetDatum.redemptionReimbursementRatio,
price,
randomRobsSubsetSatisfyingTargetCollateral(
iassetDatum.assetName,
collateralAssetDatum.collateralAsset,
leverageSummary.redeemedCollateral,
price,
allRobs,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
);
// payout / (1 - debtMintingFee) = total minted amount
const mintedAmt = fromDecimal(
Decimal(redemptionDetails.totalIAssetPayout)
.div(
Decimal(1).minus(
Decimal(iassetDatum.debtMintingFeeRatio.numerator).div(
iassetDatum.debtMintingFeeRatio.denominator,
),
),
)
.floor(),
);
const debtMintingFee = calculateFeeFromRatio(
iassetDatum.debtMintingFeeRatio,
mintedAmt,
);
const collateralAmt =
redemptionDetails.totalRedeemedCollateral + baseCollateral;
const cdpNftVal = mkAssetsOf(
fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
1n,
);
const iassetClass = {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: iassetDatum.assetName,
};
const iassetTokensVal = mkAssetsOf(iassetClass, mintedAmt);
const refScripts = [
cdpCreatorRefScriptUtxo,
cdpAuthTokenPolicyRefScriptUtxo,
iAssetTokenPolicyRefScriptUtxo,
robScriptRefUtxo,
];
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: mintedAmt,
cdpFees: {
ActiveCDPInterestTracking: {
lastSettled: currentTime,
unitaryInterestSnapshot:
calculateUnitaryInterestSinceOracleLastUpdated(
currentTime,
interestOracleDatum,
) + interestOracleDatum.unitaryInterest,
},
},
}),
},
addAssets(
cdpNftVal,
mkAssetsOf(collateralAssetDatum.collateralAsset, collateralAmt),
),
)
.pay.ToContract(
cdpCreatorUtxo.address,
{
kind: 'inline',
value: serialiseCDPCreatorDatum({
creatorInputOref: {
outputIndex: BigInt(cdpCreatorUtxo.outputIndex),
txHash: fromHex(cdpCreatorUtxo.txHash),
},
}),
},
cdpCreatorUtxo.assets,
);
const treasuryRefScriptUtxo =
debtMintingFee > 0n
? await treasuryFeeTx(
iassetClass,
debtMintingFee,
0n,
lucid,
sysParams,
tx,
cdpCreatorUtxo,
treasuryOref,
)
: undefined;
const { interval, referenceInputs: refInputs } = await attachOracle(
iassetDatum.assetName,
collateralAssetDatum.collateralAsset,
collateralAssetDatum.priceInfo,
priceOracleOutRef,
pythStateOref,
pythMessage,
sysParams.pythConfig,
sysParams.cdpCreatorParams.biasTime,
currentSlot,
lucid,
tx,
);
const referenceInputs = [
interestOracleUtxo,
iassetUtxo,
collateralAssetUtxo,
...refInputs,
];
const refInputsIndices = getInputIndices(referenceInputs, [
...referenceInputs,
...refScripts,
...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []),
]);
const oracleIdx: OracleIdx =
priceOracleOutRef !== undefined
? { OracleRefInputIdx: refInputsIndices[3] }
: 'OracleVoid';
tx.validFrom(interval.validFrom)
.validTo(interval.validTo)
.readFrom(referenceInputs)
.collectFrom([cdpCreatorUtxo], {
kind: 'self',
makeRedeemer: (inputIdx) => {
return serialiseCDPCreatorRedeemer({
CreateCDP: {
cdpOwner: fromHex(pkh.hash),
minted: mintedAmt,
collateralAmt: collateralAmt,
currentTime: currentTime,
creatorInputIdx: inputIdx,
creatorOutputIdx: 1n,
cdpOutputIdx: 0n,
iassetRefInputIdx: refInputsIndices[1],
collateralAssetRefInputIdx: refInputsIndices[2],
interestOracleRefInputIdx: refInputsIndices[0],
priceOracleIdx: oracleIdx,
},
});
},
});
buildRedemptionsTx(
redemptionDetails.redemptions.map((r) => [r.utxo, r.iassetsPayoutAmt]),
iassetDatum.assetName,
collateralAssetDatum.collateralAsset,
price,
iassetDatum.redemptionReimbursementRatio,
sysParams,
tx,
2n + (debtMintingFee > 0n ? 1n : 0n),
refInputsIndices[2],
refInputsIndices[1],
oracleIdx,
);
return tx;
}