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