UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

426 lines (387 loc) 12.1 kB
import { LucidEvolution, TxBuilder, Credential, OutRef, addAssets, fromHex, toHex, fromText, Assets, getInputIndices, } from '@lucid-evolution/lucid'; import { addrDetails, createScriptAddress, getInlineDatumOrThrow, } from '../../utils/lucid-utils'; import { readonlyArray as RA, array as A, function as F, option as O, } from 'fp-ts'; import { unzip, zip } from 'fp-ts/lib/Array'; import { adaAssetClass, AssetClass, assetClassValueOf, isSameAssetClass, mkAssetsOf, mkLovelacesOf, negateAssets, } from '@3rd-eye-labs/cardano-offchain-common'; import { matchSingle } from '../../utils/utils'; import { RobDatum, RobOrderType, parseRobDatumOrThrow, serialiseRobDatum, serialiseRobRedeemer, } from './types-new'; import { parseCollateralAssetDatumOrThrow, parseIAssetDatumOrThrow, } from '../iasset/types'; import { fromSystemParamsScriptRef, SystemParams, } from '../../types/system-params'; import { buildRedemptionsTx, isFullyRedeemed, MIN_ROB_COLLATERAL_AMT, robAmtToSpend, } from './helpers'; import { match, P } from 'ts-pattern'; import { Rational } from '../../types/rational'; import { attachOracle } from '../iasset/helpers'; import { retrieveAdjustedPrice } from '../../utils/oracle-helpers'; export async function openRob( assetTokenNameAscii: string, depositAmt: bigint, orderType: RobOrderType, lucid: LucidEvolution, sysParams: SystemParams, robStakeCredential?: Credential, ): Promise<TxBuilder> { const network = lucid.config().network!; const [ownPkh, _] = await addrDetails(lucid); const newDatum: RobDatum = { owner: fromHex(ownPkh.hash), iasset: fromHex(fromText(assetTokenNameAscii)), orderType: orderType, robRefInput: { txHash: fromHex( '0000000000000000000000000000000000000000000000000000000000000000', ), outputIndex: 0n, }, }; const depositVal = match(orderType) .with({ BuyIAssetOrder: P.select() }, (buyContent) => mkAssetsOf(buyContent.collateralAsset, depositAmt), ) .with({ SellIAssetOrder: P.any }, (_) => mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: newDatum.iasset, }, depositAmt, ), ) .exhaustive(); return lucid.newTx().pay.ToContract( createScriptAddress( network, sysParams.validatorHashes.robHash, robStakeCredential, ), { kind: 'inline', value: serialiseRobDatum(newDatum), }, addAssets(depositVal, mkLovelacesOf(MIN_ROB_COLLATERAL_AMT)), ); } export async function cancelRob( robOutRef: OutRef, sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const robScriptRefUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef), ]), (_) => new Error('Expected a single ROB Ref Script UTXO'), ); const robUtxo = matchSingle( await lucid.utxosByOutRef([robOutRef]), (_) => new Error('Expected a single ROB UTXO.'), ); const robDatum = parseRobDatumOrThrow(getInlineDatumOrThrow(robUtxo)); return lucid .newTx() .readFrom([robScriptRefUtxo]) .collectFrom([robUtxo], serialiseRobRedeemer('Cancel')) .addSignerKey(toHex(robDatum.owner)); } export async function redeemRob( /** * The tuple represents the ROB UTXO and the amount to payout for a redemption. In case of buy order, * it's denominated in iAssets, in case of sell order, it's denominated in collateral asset. */ redemptionRobsData: [OutRef, bigint][], priceOracleOutRef: OutRef | undefined, iassetOutRef: OutRef, collateralAssetOutRef: OutRef, lucid: LucidEvolution, sysParams: SystemParams, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOutRef: OutRef | undefined = undefined, ): Promise<TxBuilder> { const robScriptRefUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef), ]), (_) => new Error('Expected a single ROB Ref Script UTXO'), ); 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 [robsToRedeemOutRefs, robRedemptionIAssetAmt] = unzip(redemptionRobsData); const [adjustedPrice, _] = await retrieveAdjustedPrice( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, collateralAssetDatum.extraDecimals, priceOracleOutRef, pythMessage, sysParams.pythConfig, lucid, ); const redemptionRobs = await lucid .utxosByOutRef(robsToRedeemOutRefs) .then((val) => zip(val, robRedemptionIAssetAmt)); const allRefInputs = [robScriptRefUtxo, iassetUtxo, collateralAssetUtxo]; const tx = lucid.newTx(); const { interval, referenceInputs } = await attachOracle( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, priceOracleOutRef, pythStateOutRef, pythMessage, sysParams.pythConfig, sysParams.cdpParams.biasTime, currentSlot, lucid, tx, ); // Set the validity interval for the transaction tx.validFrom(interval.validFrom).validTo(interval.validTo); // Read from the reference inputs allRefInputs.push(...referenceInputs); const refInputIdxs = getInputIndices(allRefInputs, allRefInputs); buildRedemptionsTx( redemptionRobs, iassetDatum.assetName, collateralAssetDatum.collateralAsset, adjustedPrice, iassetDatum.redemptionReimbursementRatio, sysParams, tx, 0n, refInputIdxs[2], refInputIdxs[1], priceOracleOutRef !== undefined ? { OracleRefInputIdx: refInputIdxs[3] } : 'OracleVoid', ); tx.readFrom(allRefInputs); return tx; } /** * Create Tx adjusting the ROB and claiming the received iAssets */ export async function adjustRob( lucid: LucidEvolution, robOutRef: OutRef, /** * A positive amount increases the deposit in the ROB, * and a negative amount takes deposit from the ROB. */ adjustmentAmt: bigint, newLimitPrice: | { BuyOrder: Rational } | { SellOrder: { newLimitPrices: [AssetClass, Rational][] } } | undefined, sysParams: SystemParams, ): Promise<TxBuilder> { const robScriptRefUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef), ]), (_) => new Error('Expected a single ROB Ref Script UTXO'), ); const robUtxo = matchSingle( await lucid.utxosByOutRef([robOutRef]), (_) => new Error('Expected a single ROB UTXO.'), ); const robDatum = parseRobDatumOrThrow(getInlineDatumOrThrow(robUtxo)); // The claim case if ( adjustmentAmt === 0n && isFullyRedeemed(robUtxo.assets, robDatum.orderType, { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: robDatum.iasset, }) ) { throw new Error( "When there's no more lovelaces to spend, use close instead of claim.", ); } // Negative adjust case if ( adjustmentAmt < 0 && robAmtToSpend(robUtxo.assets, robDatum.orderType, { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: robDatum.iasset, }) <= -adjustmentAmt ) { throw new Error( "Can't adjust negatively by more than available. Also, for adjusting by exactly the amount deposited, a close action should be used instead.", ); } const iassetAc: AssetClass = { currencySymbol: fromHex( sysParams.robParams.iassetPolicyId.unCurrencySymbol, ), tokenName: robDatum.iasset, }; const [depositAsset, rewardVal] = match(robDatum.orderType) .returnType<[AssetClass, Assets]>() .with({ BuyIAssetOrder: P.select() }, (buyContent) => { const reward = assetClassValueOf(robUtxo.assets, iassetAc); return [buyContent.collateralAsset, mkAssetsOf(iassetAc, reward)]; }) .with({ SellIAssetOrder: P.select() }, (content) => { const reward = F.pipe( content.allowedCollateralAssets, RA.reduce<readonly [AssetClass, Rational], Assets>( {}, (acc, [asset, _]) => { const amt = assetClassValueOf(robUtxo.assets, asset) - // in case of ADA, the min has to stay. (isSameAssetClass(asset, adaAssetClass) ? MIN_ROB_COLLATERAL_AMT : 0n); return addAssets(acc, mkAssetsOf(asset, amt)); }, ), ); return [iassetAc, reward]; }) .exhaustive(); return lucid .newTx() .readFrom([robScriptRefUtxo]) .collectFrom([robUtxo], serialiseRobRedeemer('Cancel')) .pay.ToContract( robUtxo.address, { kind: 'inline', value: serialiseRobDatum({ ...robDatum, orderType: newLimitPrice == null ? robDatum.orderType : match(robDatum.orderType) .returnType<RobOrderType>() .with({ BuyIAssetOrder: P.select() }, (content) => { const newPrice = match(newLimitPrice) .with({ BuyOrder: P.select() }, (price) => price) .otherwise(() => { throw new Error( 'Must use buy order price change on buy order.', ); }); return { BuyIAssetOrder: { ...content, maxPrice: newPrice }, }; }) .with({ SellIAssetOrder: P.select() }, (content) => { const newPrices = match(newLimitPrice) .with( { SellOrder: { newLimitPrices: P.select() } }, (prices) => prices, ) .otherwise(() => { throw new Error( 'Must use sell order price change on sell order.', ); }); return { SellIAssetOrder: { allowedCollateralAssets: content.allowedCollateralAssets.map((entry) => F.pipe( newPrices, A.findFirst((newPrice) => isSameAssetClass(newPrice[0], entry[0]), ), O.match( () => entry, (newPrice) => [ entry[0], newPrice[1], ] satisfies typeof entry, ), ), ), }, }; }) .exhaustive(), }), }, addAssets( robUtxo.assets, mkAssetsOf(depositAsset, adjustmentAmt), negateAssets(rewardVal), ), ) .addSignerKey(toHex(robDatum.owner)); } /** * Create Tx claiming the received iAssets. */ export async function claimRob( lucid: LucidEvolution, robOutRef: OutRef, sysParams: SystemParams, ): Promise<TxBuilder> { return adjustRob(lucid, robOutRef, 0n, undefined, sysParams); }