@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
645 lines (587 loc) • 19.2 kB
text/typescript
import {
addAssets,
Address,
credentialToAddress,
Data,
fromHex,
fromText,
LucidEvolution,
OutRef,
paymentCredentialOf,
sortUTxOs,
toHex,
TxBuilder,
UTxO,
} from '@lucid-evolution/lucid';
import {
fromSystemParamsScriptRef,
SystemParams,
} from '../../types/system-params';
import { estimateUtxoMinLovelace } from '../../utils/lucid-utils';
import {
parseStableswapOrderDatumOrThrow,
serialiseStableswapOrderDatum,
serialiseStableswapOrderRedeemer,
StableswapOrderDatum,
} from './types-new';
import {
AssetClass,
addressFromBech32,
addressToBech32,
getInlineDatumOrThrow,
matchSingle,
mkAssetsOf,
mkLovelacesOf,
lovelacesAmt,
isSameOutRef,
assetClassValueOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
parseStableswapPoolDatumOrThrow,
serialiseCdpRedeemer,
serialiseStableswapPoolDatum,
StableswapPoolContent,
} from '../cdp/types-new';
import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
import { array as A, function as F } from 'fp-ts';
import { isEmpty } from 'fp-ts/lib/Array';
import { BASE_MAX_EXECUTION_FEE, createDestinationDatum } from './helpers';
import * as Core from '@evolution-sdk/evolution';
import { treasuryFeeTx } from '../treasury/transactions';
import {
Rational,
rationalDiv,
rationalFloor,
rationalFromInt,
rationalMul,
} from '../../types/rational';
type StableswapInfo = {
suppliedCollateralAsset: bigint;
suppliedIasset: bigint;
owedCollateralAsset: bigint;
owedIasset: bigint;
mintingFee: bigint;
redemptionFee: bigint;
};
type StableswapOrderInfo = {
utxo: UTxO;
datum: StableswapOrderDatum;
swapInfo: StableswapInfo;
};
export async function createStableswapOrder(
iasset: string,
collateralAsset: AssetClass,
amount: bigint,
minting: boolean,
poolDatum: StableswapPoolContent,
params: SystemParams,
lucid: LucidEvolution,
destinationAddress?: Address,
destinationInlineDatum?: Core.Data.Data,
maxExecutionFee: bigint = BASE_MAX_EXECUTION_FEE,
additionalLovelaces: bigint = 0n,
maxFeeRatio?: Rational,
): Promise<TxBuilder> {
const myAddress = await lucid.wallet().address();
const pkh = paymentCredentialOf(myAddress);
const datum: StableswapOrderDatum = {
iasset: fromHex(fromText(iasset)),
collateralAsset: collateralAsset,
owner: fromHex(pkh.hash),
destination: addressFromBech32(destinationAddress ?? myAddress),
destinationInlineDatum: destinationInlineDatum ?? null,
maxExecutionFee: maxExecutionFee,
maxFeeRatio:
maxFeeRatio ??
(minting ? poolDatum.mintingFeeRatio : poolDatum.redemptionFeeRatio),
};
const assetsToSwap = minting
? mkAssetsOf(collateralAsset, amount)
: mkAssetsOf(
{
currencySymbol: fromHex(
params.stableswapParams.iassetSymbol.unCurrencySymbol,
),
tokenName: fromHex(fromText(iasset)),
},
amount,
);
// This is an approximation of the amount of lovelace that will be needed to pay for the output.
const expectedOutputLovelaces = estimateUtxoMinLovelace(
lucid.config().protocolParameters!,
myAddress,
assetsToSwap,
{
kind: 'inline',
value: createDestinationDatum(destinationInlineDatum ?? null, {
txHash:
'0000000000000000000000000000000000000000000000000000000000000000',
outputIndex: 0,
}),
},
);
const roundedExpectedOutputLovelaces =
(expectedOutputLovelaces / 1_000_000n + 1n) * 1_000_000n;
return lucid.newTx().pay.ToContract(
credentialToAddress(lucid.config().network!, {
hash: params.validatorHashes.stableswapHash,
type: 'Script',
}),
{
kind: 'inline',
value: serialiseStableswapOrderDatum(datum),
},
addAssets(
assetsToSwap,
mkLovelacesOf(
roundedExpectedOutputLovelaces + maxExecutionFee + additionalLovelaces,
),
),
);
}
export async function cancelStableswapOrder(
stableswapOrderOref: OutRef,
sysParams: SystemParams,
lucid: LucidEvolution,
): Promise<TxBuilder> {
const stableswapScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
sysParams.scriptReferences.stableswapValidatorRef,
),
]),
(_) => new Error('Expected a single Stableswap Ref Script UTXO'),
);
const stableswapOrderUtxo = matchSingle(
await lucid.utxosByOutRef([stableswapOrderOref]),
(_) => new Error('Expected a single Stableswap Order UTXO.'),
);
const stableswapOrderDatum = parseStableswapOrderDatumOrThrow(
getInlineDatumOrThrow(stableswapOrderUtxo),
);
return lucid
.newTx()
.readFrom([stableswapScriptRefUtxo])
.collectFrom(
[stableswapOrderUtxo],
serialiseStableswapOrderRedeemer('CancelStableswapOrder'),
)
.addSignerKey(toHex(stableswapOrderDatum.owner));
}
export async function batchProcessStableswapOrders(
stableswapOrderOrefs: OutRef[],
stableswapPoolOref: OutRef,
treasuryOref: OutRef,
sysParams: SystemParams,
lucid: LucidEvolution,
): Promise<TxBuilder> {
const stableswapScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
sysParams.scriptReferences.stableswapValidatorRef,
),
]),
(_) => new Error('Expected a single Stableswap Ref Script UTXO'),
);
const cdpScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
]),
(_) => new Error('Expected a single CDP Ref Script UTXO'),
);
const iAssetTokenPolicyRefScriptUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
sysParams.scriptReferences.iAssetTokenPolicyRef,
),
]),
(_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
);
if (isEmpty(stableswapOrderOrefs)) {
throw new Error('At least one order must be provided.');
}
const stableswapOrderUtxos = await lucid.utxosByOutRef(stableswapOrderOrefs);
const sortedStableswapOrderUtxos = sortUTxOs(
stableswapOrderUtxos,
'Canonical',
);
if (sortedStableswapOrderUtxos.length !== stableswapOrderOrefs.length) {
throw new Error('Expected certain number of orders');
}
const mainOrderUtxo = sortedStableswapOrderUtxos[0];
const mainOrderDatum = parseStableswapOrderDatumOrThrow(
getInlineDatumOrThrow(mainOrderUtxo),
);
const iassetAc = {
currencySymbol: fromHex(
sysParams.stableswapParams.iassetSymbol.unCurrencySymbol,
),
tokenName: mainOrderDatum.iasset,
};
const collateralAc = mainOrderDatum.collateralAsset;
const stableswapPoolUtxo = matchSingle(
await lucid.utxosByOutRef([stableswapPoolOref]),
(_) => new Error('Expected a single cdp UTXO'),
);
const stableswapPoolDatum = parseStableswapPoolDatumOrThrow(
getInlineDatumOrThrow(stableswapPoolUtxo),
);
const ordersInfo: StableswapOrderInfo[] = sortedStableswapOrderUtxos.map(
(orderUtxo) => {
const orderDatum = parseStableswapOrderDatumOrThrow(
getInlineDatumOrThrow(orderUtxo),
);
if (
toHex(orderDatum.iasset) != toHex(mainOrderDatum.iasset) ||
toHex(orderDatum.collateralAsset.currencySymbol) !=
toHex(mainOrderDatum.collateralAsset.currencySymbol) ||
toHex(orderDatum.collateralAsset.tokenName) !=
toHex(mainOrderDatum.collateralAsset.tokenName)
) {
throw new Error('Wrong batch of orders');
}
const suppliedIasset = assetClassValueOf(orderUtxo.assets, iassetAc);
const suppliedCollateralAsset = assetClassValueOf(
orderUtxo.assets,
collateralAc,
);
if (
(suppliedIasset != 0n && suppliedCollateralAsset != 0n) ||
(suppliedIasset == 0n && suppliedCollateralAsset == 0n)
) {
throw new Error(
'An order must supply either iAsset or collateral asset',
);
}
const isMinting = suppliedCollateralAsset > 0n;
const isOneToOne =
stableswapPoolDatum.collateralToIassetRatio.numerator ===
stableswapPoolDatum.collateralToIassetRatio.denominator;
if (isMinting) {
// Mint order with one to one ratio case.
if (isOneToOne) {
const fee = calculateFeeFromRatio(
stableswapPoolDatum.mintingFeeRatio,
suppliedCollateralAsset,
);
return {
utxo: orderUtxo,
datum: orderDatum,
swapInfo: {
suppliedCollateralAsset: suppliedCollateralAsset,
suppliedIasset: 0n,
owedCollateralAsset: 0n,
owedIasset: suppliedCollateralAsset - fee,
mintingFee: fee,
redemptionFee: 0n,
},
};
// Mint order with any ratio case.
} else {
const iAssetConversion = rationalFloor(
rationalDiv(
rationalFromInt(suppliedCollateralAsset),
stableswapPoolDatum.collateralToIassetRatio,
),
);
const attemptedNormalizedCollateral = rationalFloor(
rationalMul(
rationalFromInt(iAssetConversion),
stableswapPoolDatum.collateralToIassetRatio,
),
);
const normalizedCollateralSupplied =
rationalFloor(
rationalDiv(
rationalFromInt(attemptedNormalizedCollateral),
stableswapPoolDatum.collateralToIassetRatio,
),
) != iAssetConversion
? suppliedCollateralAsset
: attemptedNormalizedCollateral;
const fee = calculateFeeFromRatio(
stableswapPoolDatum.mintingFeeRatio,
iAssetConversion,
);
return {
utxo: orderUtxo,
datum: orderDatum,
swapInfo: {
suppliedCollateralAsset: normalizedCollateralSupplied,
suppliedIasset: 0n,
owedCollateralAsset: 0n,
owedIasset: iAssetConversion - fee,
mintingFee: fee,
redemptionFee: 0n,
},
};
}
// Redeem order case
} else {
const fee = calculateFeeFromRatio(
stableswapPoolDatum.redemptionFeeRatio,
suppliedIasset,
);
const effectiveSuppliedIasset = suppliedIasset - fee;
// Redeem order with one to one ratio case
if (isOneToOne) {
return {
utxo: orderUtxo,
datum: orderDatum,
swapInfo: {
suppliedCollateralAsset: 0n,
suppliedIasset: effectiveSuppliedIasset,
owedCollateralAsset: effectiveSuppliedIasset,
owedIasset: 0n,
mintingFee: 0n,
redemptionFee: fee,
},
};
// Redeem order with any ratio case
} else {
const collateralConversion = rationalFloor(
rationalMul(
rationalFromInt(effectiveSuppliedIasset),
stableswapPoolDatum.collateralToIassetRatio,
),
);
const attemptedNormalizedEffectiveIasset = rationalFloor(
rationalDiv(
rationalFromInt(collateralConversion),
stableswapPoolDatum.collateralToIassetRatio,
),
);
const normalizedEffectiveIasset =
rationalFloor(
rationalMul(
rationalFromInt(attemptedNormalizedEffectiveIasset),
stableswapPoolDatum.collateralToIassetRatio,
),
) != collateralConversion
? effectiveSuppliedIasset
: attemptedNormalizedEffectiveIasset;
return {
utxo: orderUtxo,
datum: orderDatum,
swapInfo: {
suppliedCollateralAsset: 0n,
suppliedIasset: normalizedEffectiveIasset,
owedCollateralAsset: rationalFloor(
rationalMul(
rationalFromInt(effectiveSuppliedIasset),
stableswapPoolDatum.collateralToIassetRatio,
),
),
owedIasset: 0n,
mintingFee: 0n,
redemptionFee: fee,
},
};
}
}
},
);
const totalSwapInfo = F.pipe(
ordersInfo,
A.reduce<StableswapOrderInfo, StableswapInfo>(
{
suppliedCollateralAsset: 0n,
suppliedIasset: 0n,
owedCollateralAsset: 0n,
owedIasset: 0n,
mintingFee: 0n,
redemptionFee: 0n,
},
(acc, orderInfo) => {
return {
suppliedCollateralAsset:
acc.suppliedCollateralAsset +
orderInfo.swapInfo.suppliedCollateralAsset,
suppliedIasset:
acc.suppliedIasset + orderInfo.swapInfo.suppliedIasset,
owedCollateralAsset:
acc.owedCollateralAsset + orderInfo.swapInfo.owedCollateralAsset,
owedIasset: acc.owedIasset + orderInfo.swapInfo.owedIasset,
mintingFee: acc.mintingFee + orderInfo.swapInfo.mintingFee,
redemptionFee: acc.redemptionFee + orderInfo.swapInfo.redemptionFee,
};
},
),
);
const collateralAmtChangePool =
totalSwapInfo.suppliedCollateralAsset - totalSwapInfo.owedCollateralAsset;
const amountToMint =
totalSwapInfo.owedIasset -
totalSwapInfo.suppliedIasset +
totalSwapInfo.mintingFee;
const fee = totalSwapInfo.mintingFee + totalSwapInfo.redemptionFee;
const tx = lucid
.newTx()
.readFrom([
stableswapScriptRefUtxo,
cdpScriptRefUtxo,
iAssetTokenPolicyRefScriptUtxo,
])
.collectFrom([stableswapPoolUtxo], {
kind: 'selected',
makeRedeemer: (inputIndices) =>
serialiseCdpRedeemer({
Stableswap: {
forwardingInputIndex: inputIndices[0],
},
}),
inputs: [mainOrderUtxo],
})
.pay.ToContract(
stableswapPoolUtxo.address,
{
kind: 'inline',
value: serialiseStableswapPoolDatum(stableswapPoolDatum),
},
collateralAmtChangePool != 0n
? addAssets(
stableswapPoolUtxo.assets,
mkAssetsOf(collateralAc, collateralAmtChangePool),
)
: stableswapPoolUtxo.assets,
)
// This has to be added as otherwise there is the following error:
// TxBuilderError: { Complete: RedeemerBuilder: Coin selection had to be updated
// after building redeemers, possibly leading to incorrect indices. Try setting
// a minimum fee of 1761019 lovelaces. }
// Trying to set it as low as possible to reduce costs.
.setMinFee(stableswapOrderOrefs.length > 1 ? 1_498_875n : 1_030_000n);
if (amountToMint !== 0n) {
tx.mintAssets(mkAssetsOf(iassetAc, amountToMint), Data.void());
}
F.pipe(
ordersInfo,
A.reduce<StableswapOrderInfo, TxBuilder>(tx, (acc, orderInfo) => {
return acc
.collectFrom(
[orderInfo.utxo],
isSameOutRef(orderInfo.utxo, mainOrderUtxo)
? serialiseStableswapOrderRedeemer('BatchProcessStableswapOrders')
: {
kind: 'selected',
makeRedeemer: (inputIndices: bigint[]) => {
return serialiseStableswapOrderRedeemer({
BatchAuxiliary: {
ownInputIndex: inputIndices[0],
mainOrderInputIndex: inputIndices[1],
},
});
},
inputs: [orderInfo.utxo, mainOrderUtxo],
},
)
.pay.ToAddressWithData(
addressToBech32(orderInfo.datum.destination, lucid.config().network!),
{
kind: 'inline',
value: createDestinationDatum(
orderInfo.datum.destinationInlineDatum ?? null,
orderInfo.utxo,
),
},
addAssets(
// Currently, we always take the max execution fee from the order utxo.
// This can be improved so that we take the actual execution fee.
mkLovelacesOf(
lovelacesAmt(orderInfo.utxo.assets) -
orderInfo.datum.maxExecutionFee,
),
orderInfo.swapInfo.owedIasset > 0
? mkAssetsOf(iassetAc, orderInfo.swapInfo.owedIasset)
: mkAssetsOf(
collateralAc,
orderInfo.swapInfo.owedCollateralAsset,
),
),
);
}),
);
if (fee > 0) {
await treasuryFeeTx(
iassetAc,
fee,
0n,
lucid,
sysParams,
tx,
stableswapPoolOref,
treasuryOref,
);
}
return tx;
}
export async function updateStableswapPoolFees(
stableswapPoolOutRef: OutRef,
stableswapFeeOutRef: OutRef,
newMintingFeeRatio: Rational | null,
newRedemptionFeeRatio: Rational | null,
sysParams: SystemParams,
lucid: LucidEvolution,
): Promise<TxBuilder> {
const stableswapScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(
sysParams.scriptReferences.stableswapValidatorRef,
),
]),
(_) => new Error('Expected a single Stableswap Ref Script UTXO'),
);
const cdpScriptRefUtxo = matchSingle(
await lucid.utxosByOutRef([
fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef),
]),
(_) => new Error('Expected a single CDP Ref Script UTXO'),
);
const stableswapPool = matchSingle(
await lucid.utxosByOutRef([stableswapPoolOutRef]),
(_) => new Error('Expected a single Stableswap Pool UTXO.'),
);
const stableswapFeeUtxo = matchSingle(
await lucid.utxosByOutRef([stableswapFeeOutRef]),
(_) => new Error('Expected a single Stableswap Fee UTXO.'),
);
const stableswapPoolDatum = parseStableswapPoolDatumOrThrow(
getInlineDatumOrThrow(stableswapPool),
);
const newStableswapPoolDatum = {
...stableswapPoolDatum,
mintingFeeRatio: newMintingFeeRatio ?? stableswapPoolDatum.mintingFeeRatio,
redemptionFeeRatio:
newRedemptionFeeRatio ?? stableswapPoolDatum.redemptionFeeRatio,
};
if (!stableswapPoolDatum.feeManager) {
throw new Error('Stableswap pool fee manager is not set');
}
return lucid
.newTx()
.readFrom([stableswapScriptRefUtxo, cdpScriptRefUtxo])
.collectFrom([stableswapPool], {
kind: 'selected',
makeRedeemer: (inputIndices) =>
serialiseCdpRedeemer({
Stableswap: {
forwardingInputIndex: inputIndices[0],
},
}),
inputs: [stableswapFeeUtxo],
})
.collectFrom(
[stableswapFeeUtxo],
serialiseStableswapOrderRedeemer('UpdateFees'),
)
.pay.ToContract(
stableswapPool.address,
{
kind: 'inline',
value: serialiseStableswapPoolDatum(newStableswapPoolDatum),
},
stableswapPool.assets,
)
.addSignerKey(toHex(stableswapPoolDatum.feeManager))
.setMinFee(1038402n);
}