UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

645 lines (587 loc) 19.2 kB
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); }