@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
437 lines (395 loc) • 12.4 kB
text/typescript
import {
Data,
LucidEvolution,
OutRef,
toHex,
TxBuilder,
UTxO,
addAssets,
Assets,
fromHex,
slotToUnixTime,
sortUTxOs,
getInputIndices,
} from '@lucid-evolution/lucid';
import {
fromSystemParamsAsset,
fromSystemParamsScriptRef,
SystemParams,
} from '../../types/system-params';
import { matchSingle } from '../../utils/utils';
import {
parseInterestCollectionDatum,
serialiseInterestCollectionDatum,
serialiseInterestCollectionRedeemer,
} from './types-new';
import {
assetClassValueOf,
getInlineDatumOrThrow,
mkAssetsOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import { createScriptAddress } from '../../utils/lucid-utils';
import {
CDPContent,
parseCdpDatumOrThrow,
serialiseCdpDatum,
serialiseCdpRedeemer,
} from '../cdp/types-new';
import { parseInterestOracleDatum } from '../interest-oracle/types-new';
import {
calculateAccruedInterest,
calculateUnitaryInterestSinceOracleLastUpdated,
} from '../interest-oracle/helpers';
import { match, P } from 'ts-pattern';
import { array as A, function as F } from 'fp-ts';
import { parseCollateralAssetDatumOrThrow } from '../iasset/types';
import { Multisig } from '../../types/multisig';
type CDPInfo = {
utxo: UTxO;
datum: CDPContent;
accruedInterest: bigint;
};
export async function batchCollectInterest(
collateralAssetOref: OutRef,
interestCollectorOutRef: OutRef,
interestOracleOutRef: OutRef,
cdpOutRefs: OutRef[],
params: SystemParams,
lucid: LucidEvolution,
currentSlot: number,
): Promise<TxBuilder> {
const network = lucid.config().network!;
const currentTime = BigInt(slotToUnixTime(network, currentSlot));
const interestCollectionRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
params.scriptReferences.interestCollectionValidatorRef,
),
]),
(_) => new Error('Expected a single interest collection Ref Script UTXO'),
);
const cdpRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(params.scriptReferences.cdpValidatorRef),
]),
(_) => new Error('Expected a single cdp Ref Script UTXO'),
);
const iAssetTokenPolicyRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(params.scriptReferences.iAssetTokenPolicyRef),
]),
(_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
);
const collateralAssetUtxo = matchSingle(
await lucid.utxosByOutRef([collateralAssetOref]),
(_) => new Error('Expected a single iasset UTXO'),
);
const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
getInlineDatumOrThrow(collateralAssetUtxo),
);
const interestCollectorUtxo: UTxO = matchSingle(
await lucid.utxosByOutRef([interestCollectorOutRef]),
(_) => new Error('Expected a single interest collector UTXO'),
);
// Confirm that the interest collector UTXO is NOT of InterestCollectorDatum type
if (
assetClassValueOf(
interestCollectorUtxo.assets,
fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
)
) {
throw new Error(
'Interest collector UTXO is of InterestCollectorDatum type',
);
}
const interestOracleUtxo: UTxO = matchSingle(
await lucid.utxosByOutRef([interestOracleOutRef]),
(_) => new Error('Expected a single interest oracle UTXO'),
);
const interestOracleDatum = parseInterestOracleDatum(
getInlineDatumOrThrow(interestOracleUtxo),
);
const cdpUtxos = await lucid.utxosByOutRef(cdpOutRefs);
const sortedCdpUtxos = sortUTxOs(cdpUtxos, 'Canonical');
if (sortedCdpUtxos.length !== cdpOutRefs.length) {
throw new Error('Expected certain number of CDPs');
}
function getAccruedInterest(cdpDatum: CDPContent): bigint {
return match(cdpDatum.cdpFees)
.with({ FrozenCDPAccumulatedFees: P.any }, () => {
throw new Error('CDP fees wrong');
})
.with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
return calculateAccruedInterest(
currentTime,
interest.unitaryInterestSnapshot,
cdpDatum.mintedAmt,
interest.lastSettled,
interestOracleDatum,
);
})
.exhaustive();
}
const cdpsInfo: CDPInfo[] = sortedCdpUtxos.map((cdpUtxo) => {
const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));
return {
utxo: cdpUtxo,
datum: cdpDatum,
accruedInterest: getAccruedInterest(cdpDatum),
};
});
const accumulatedInterest = F.pipe(
cdpsInfo,
A.reduce<CDPInfo, bigint>(
0n,
(acc, cdpInfo) => acc + cdpInfo.accruedInterest,
),
);
const updatedUnitarySnapshot =
calculateUnitaryInterestSinceOracleLastUpdated(
currentTime,
interestOracleDatum,
) + interestOracleDatum.unitaryInterest;
const referenceInputs = [
collateralAssetUtxo,
interestOracleUtxo,
interestCollectionRefScriptUtxo,
cdpRefScriptUtxo,
iAssetTokenPolicyRefScriptUtxo,
];
const referenceInputIndices = getInputIndices(
[collateralAssetUtxo, interestOracleUtxo],
referenceInputs,
false,
);
const validateFrom = slotToUnixTime(network, currentSlot - 1);
const validateTo = validateFrom + Number(params.cdpParams.biasTime);
const tx = lucid
.newTx()
.validFrom(validateFrom)
.validTo(validateTo)
.readFrom(referenceInputs)
.collectFrom([interestCollectorUtxo], {
kind: 'selected',
makeRedeemer: (inputIndices: bigint[]) => {
return serialiseInterestCollectionRedeemer({
BatchCollectInterest: {
currentTime: currentTime,
ownInputIndex: inputIndices[0],
collateralAssetRefInputIndex: referenceInputIndices[0],
interestOracleRefInputIndex: referenceInputIndices[1],
},
});
},
inputs: [interestCollectorUtxo],
})
.mintAssets(
mkAssetsOf(
{
currencySymbol: fromHex(
params.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: collateralAssetDatum.iasset,
},
accumulatedInterest,
),
Data.void(),
)
.pay.ToContract(
createScriptAddress(
lucid.config().network!,
params.validatorHashes.interestCollectionHash,
),
{ kind: 'inline', value: Data.void() },
addAssets(
interestCollectorUtxo.assets,
mkAssetsOf(
{
currencySymbol: fromHex(
params.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: collateralAssetDatum.iasset,
},
accumulatedInterest,
),
),
);
F.pipe(
cdpsInfo,
A.reduce<CDPInfo, TxBuilder>(tx, (acc, cdpInfo) =>
acc
.collectFrom([cdpInfo.utxo], {
kind: 'selected',
makeRedeemer: (inputIndices: bigint[]) => {
return serialiseCdpRedeemer({
SettleInterest: { interestCollectorInputIndex: inputIndices[0] },
});
},
inputs: [interestCollectorUtxo],
})
.pay.ToContract(
cdpInfo.utxo.address,
{
kind: 'inline',
value: serialiseCdpDatum({
...cdpInfo.datum,
mintedAmt: cdpInfo.datum.mintedAmt + cdpInfo.accruedInterest,
cdpFees: {
ActiveCDPInterestTracking: {
lastSettled: currentTime,
unitaryInterestSnapshot: updatedUnitarySnapshot,
},
},
}),
},
cdpInfo.utxo.assets,
),
),
);
return tx;
}
export async function collectInterestTx(
value: Assets,
lucid: LucidEvolution,
sysParams: SystemParams,
tx: TxBuilder,
interestCollectorOref: OutRef,
): Promise<UTxO> {
const interestCollectorUtxo = matchSingle(
await lucid.utxosByOutRef([interestCollectorOref]),
(_) => new Error('Expected a single interest collector UTXO'),
);
const interestCollectorRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
sysParams.scriptReferences.interestCollectionValidatorRef,
),
]),
(_) => new Error('Expected a single interest collector Ref Script UTXO'),
);
tx.readFrom([interestCollectorRefScriptUtxo])
.collectFrom(
[interestCollectorUtxo],
serialiseInterestCollectionRedeemer('CollectInterest'),
)
.pay.ToContract(
interestCollectorUtxo.address,
{ kind: 'inline', value: Data.void() },
addAssets(interestCollectorUtxo.assets, value),
);
return interestCollectorRefScriptUtxo;
}
export async function distributeInterest(
interestCollectorOutRefs: OutRef[],
interestAdminOutRef: OutRef,
params: SystemParams,
lucid: LucidEvolution,
): Promise<TxBuilder> {
const interestCollectorUtxos = [];
for (const outRef of interestCollectorOutRefs) {
interestCollectorUtxos.push(
matchSingle(
await lucid.utxosByOutRef([outRef]),
(_) => new Error('Expected a single UTXO with that reference'),
),
);
}
const interestAdminUtxo: UTxO = matchSingle(
(await lucid.utxosByOutRef([interestAdminOutRef])).filter((utxo) =>
assetClassValueOf(
utxo.assets,
fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
),
),
(_) => new Error('Expected a single interest admin UTXO'),
);
const interestAdminDatum = parseInterestCollectionDatum(
getInlineDatumOrThrow(interestAdminUtxo),
);
// Find the script reference UTXO for the interest collection validator
const interestCollectionRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
params.scriptReferences.interestCollectionValidatorRef,
),
]),
(_) => new Error('Expected a single interest collection Ref Script UTXO'),
);
const tx = lucid
.newTx()
.readFrom([interestCollectionRefScriptUtxo, interestAdminUtxo])
.collectFrom(
interestCollectorUtxos,
serialiseInterestCollectionRedeemer('Distribute'),
);
// TODO: Handle tx contruction for Multisig
if ('Signature' in interestAdminDatum.admin_permissions) {
tx.addSignerKey(
toHex(interestAdminDatum.admin_permissions.Signature.keyHash),
);
} else {
// TODO: Handle other admin permissions types
throw new Error('Unsupported admin permissions type');
}
return tx;
}
export async function updatePermissions(
interestAdminOutRef: OutRef,
govOref: OutRef,
newAdminPermissions: Multisig,
expectedSigners: string[],
params: SystemParams,
lucid: LucidEvolution,
): Promise<TxBuilder> {
const interestAdminUtxo: UTxO = matchSingle(
(await lucid.utxosByOutRef([interestAdminOutRef])).filter((utxo) =>
assetClassValueOf(
utxo.assets,
fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
),
),
(_) => new Error('Expected a single interest admin UTXO'),
);
const govUtxo: UTxO = matchSingle(
(await lucid.utxosByOutRef([govOref])).filter((utxo) =>
assetClassValueOf(
utxo.assets,
fromSystemParamsAsset(params.interestCollectionParams.govAuthTk),
),
),
(_) => new Error('Expected a single interest admin UTXO'),
);
// Find the script reference UTXO for the interest collection validator
const interestCollectionRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
params.scriptReferences.interestCollectionValidatorRef,
),
]),
(_) => new Error('Expected a single interest collection Ref Script UTXO'),
);
const tx = lucid
.newTx()
.readFrom([interestCollectionRefScriptUtxo, govUtxo])
.collectFrom(
[interestAdminUtxo],
serialiseInterestCollectionRedeemer('UpdatePermissions'),
)
.pay.ToContract(
createScriptAddress(
lucid.config().network!,
params.validatorHashes.interestCollectionHash,
),
{
kind: 'inline',
value: serialiseInterestCollectionDatum({
admin_permissions: newAdminPermissions,
}),
},
interestAdminUtxo.assets,
);
for (const signer of expectedSigners) {
tx.addSignerKey(signer);
}
return tx;
}