UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

923 lines (844 loc) 26.1 kB
import { fromText, LucidEvolution, TxBuilder, Data, UTxO, fromHex, toHex, addAssets, OutRef, slotToUnixTime, credentialToAddress, } from '@lucid-evolution/lucid'; import { fromSysParamsCredential, fromSystemParamsAsset, fromSystemParamsScriptRef, SystemParams, } from '../../types/system-params'; import { addrDetails, getInlineDatumOrThrow } from '../../utils/lucid-utils'; import { BASE_MAX_TX_FEE, createProcessRequestAccountRedeemer, findRelevantE2s2sIdxs, initSpState, liquidationHelper, partitionEpochToScaleToSums, updateAccount, updatePoolStateWhenWithdrawalFee, } from './helpers'; import { calculateFeeFromRatio } from '../../utils/indigo-helpers'; import { AccountAction, AccountContent, fromSPInteger, mkSPInteger, parseAccountDatumOrThrow, parseStabilityPoolDatumOrThrow, serialiseActionReturnDatum, serialiseStabilityPoolDatum, serialiseStabilityPoolRedeemer, spAdd, spSub, spZeroNegatives, } from './types-new'; import { adaAssetClass, addressFromBech32, addressToBech32, AssetClass, assetClassValueOf, estimateUtxoMinLovelace, lovelacesAmt, matchSingle, mkAssetsOf, mkLovelacesOf, negateAssets, } from '@3rd-eye-labs/cardano-offchain-common'; import { parseIAssetDatumOrThrow } from '../iasset/types'; import { match, P } from 'ts-pattern'; import { bigintMax, zeroNegatives } from '../../utils/bigint-utils'; export async function requestSpAccountCreation( assetAscii: string, amount: bigint, sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const [pkh, _skh] = await addrDetails(lucid); const iasset = fromHex(fromText(assetAscii)); const iassetAssetClass = { currencySymbol: fromHex( sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol, ), tokenName: iasset, }; const datum: AccountContent = { owner: fromHex(pkh.hash), iasset: iasset, state: { ...initSpState, depositVal: mkSPInteger(amount) }, assetSums: [], request: 'Create', lastRequestProcessingTime: 0n, }; return lucid .newTx() .pay.ToContract( credentialToAddress( lucid.config().network!, { hash: sysParams.validatorHashes.stabilityPoolHash, type: 'Script', }, sysParams.stabilityPoolParams.stakeCredential != null ? fromSysParamsCredential( sysParams.stabilityPoolParams.stakeCredential, ) : undefined, ), { kind: 'inline', value: serialiseStabilityPoolDatum({ Account: datum }), }, addAssets( mkAssetsOf(iassetAssetClass, amount), mkLovelacesOf( // TODO: Calculate a more accurate amount to just cover costs. // This should cover the account creation fee, // the minimum ADA for the account UTxO and the transaction fee. BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) + 3_000_000n, ), ), ) .addSignerKey(pkh.hash); } export async function requestSpAccountAdjustment( amount: bigint, accountUtxo: UTxO, sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const myAddress = await lucid.wallet().address(); const stabilityPoolScriptRef = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool Ref Script UTXO'), ); const oldAccountDatum: AccountContent = parseAccountDatumOrThrow( getInlineDatumOrThrow(accountUtxo), ); const newAccountDatum: AccountContent = { ...oldAccountDatum, request: { Adjust: { amount: amount, outputAddress: addressFromBech32(myAddress), }, }, }; return lucid .newTx() .readFrom([stabilityPoolScriptRef]) .collectFrom( [accountUtxo], serialiseStabilityPoolRedeemer({ RequestAction: { Adjust: { amount: amount, outputAddress: addressFromBech32(myAddress), }, }, }), ) .pay.ToContract( accountUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ Account: newAccountDatum, }), }, addAssets( mkLovelacesOf( // TODO: Calculate a more accurate amount to just cover costs. // This should cover the minimum ADA for 2 UTxOs (account and // rewards withdrawal) and the transaction fee. 5_000_000n, ), mkAssetsOf( fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken), 1n, ), amount > 0n ? mkAssetsOf( { currencySymbol: fromHex( sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol, ), tokenName: oldAccountDatum.iasset, }, amount, ) : {}, ), ) .addSignerKey(toHex(oldAccountDatum.owner)); } export async function requestSpAccountClosure( accountUtxo: UTxO, sysParams: SystemParams, lucid: LucidEvolution, maxTxFee: bigint = BASE_MAX_TX_FEE, ): Promise<TxBuilder> { const myAddress = await lucid.wallet().address(); const stabilityPoolScriptRef = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool Ref Script UTXO'), ); const request: AccountAction = { Close: { outputAddress: addressFromBech32(myAddress), maxTxFee: maxTxFee, }, }; const oldAccountDatum: AccountContent = parseAccountDatumOrThrow( getInlineDatumOrThrow(accountUtxo), ); const newAccountDatum: AccountContent = { ...oldAccountDatum, request: request, }; return lucid .newTx() .readFrom([stabilityPoolScriptRef]) .collectFrom( [accountUtxo], serialiseStabilityPoolRedeemer({ RequestAction: request }), ) .pay.ToContract( accountUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ Account: newAccountDatum }), }, addAssets( mkLovelacesOf( // TODO: Calculate a more accurate amount to just cover costs. // This should cover the minimum ADA for the rewards UTxO and the transaction fee. 3_000_000n, ), mkAssetsOf( fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken), 1n, ), ), ) .addSignerKey(toHex(oldAccountDatum.owner)); } async function processSpRequestAuxiliary( stabilityPoolUtxo: UTxO, accountUtxo: UTxO, iAssetUtxo: UTxO, /** * For performance provide only the ones related to the pool's iAsset. */ allE2s2sSnapshotOrefs: OutRef[], sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, txFee: bigint, ): Promise<TxBuilder> { const network = lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, currentSlot)); const stabilityPoolScriptRef = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool Ref Script UTXO'), ); const accountDatum = parseAccountDatumOrThrow( getInlineDatumOrThrow(accountUtxo), ); const stabilityPoolDatum = parseStabilityPoolDatumOrThrow( getInlineDatumOrThrow(stabilityPoolUtxo), ); const baseRefInputs = [iAssetUtxo, stabilityPoolScriptRef]; const validFrom = slotToUnixTime(network, currentSlot - 1); const tx = lucid .newTx() .validFrom(validFrom) .validTo(validFrom + sysParams.stabilityPoolParams.accountProcessingBiasMs) .readFrom(baseRefInputs) .collectFrom([stabilityPoolUtxo], { kind: 'selected', inputs: [stabilityPoolUtxo, accountUtxo], makeRedeemer: (indices) => serialiseStabilityPoolRedeemer({ ProcessRequestPool: { poolInputIdx: indices[0], accountInputIdx: indices[1], }, }), }); if (!accountDatum.request) throw new Error('Account Request is null'); const iassetAssetClass: AssetClass = { currencySymbol: fromHex( sysParams.stabilityPoolParams.assetSymbol.unCurrencySymbol, ), tokenName: stabilityPoolDatum.iasset, }; await match(accountDatum.request) .with('Create', async (_) => { const accountTokenScriptRef = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.accountTokenRef, ), ]), (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'), ); const requestDepositAmt = assetClassValueOf( accountUtxo.assets, iassetAssetClass, ); const poolAddr = credentialToAddress( lucid.config().network!, { hash: sysParams.validatorHashes.stabilityPoolHash, type: 'Script', }, sysParams.stabilityPoolParams.stakeCredential != null ? fromSysParamsCredential( sysParams.stabilityPoolParams.stakeCredential, ) : undefined, ); const accountOutputAdaAmt = lovelacesAmt(accountUtxo.assets) - BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) - txFee; const accountTokenVal = mkAssetsOf( fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken), 1n, ); const accountOutputDatum = serialiseStabilityPoolDatum({ Account: { owner: accountDatum.owner, iasset: stabilityPoolDatum.iasset, state: { ...stabilityPoolDatum.state, depositVal: mkSPInteger(requestDepositAmt), }, assetSums: stabilityPoolDatum.assetStates.map(([key, val]) => [ key, val.currentSumVal, ]), request: null, lastRequestProcessingTime: currentTime, }, }); const accountOutMinUtxoLovelace = estimateUtxoMinLovelace( lucid.config().protocolParameters!, poolAddr, accountTokenVal, { InlineDatum: { datum: accountOutputDatum } }, ); // This is to prevent spending ADA from the wallet submitting the request processing Tx. if (accountOutputAdaAmt < accountOutMinUtxoLovelace) { throw new Error("Request doesn't have enough ADA to be processed."); } tx.readFrom([accountTokenScriptRef]) .collectFrom([accountUtxo], { kind: 'selected', inputs: [stabilityPoolUtxo, accountUtxo], makeRedeemer: (indices) => serialiseStabilityPoolRedeemer({ ProcessRequestAccount: { poolInputIdx: indices[0], accountInputIdx: indices[1], e2s2sIdxs: [], currentTime: currentTime, }, }), }) .mintAssets(accountTokenVal, Data.void()) .pay.ToContract( poolAddr, { kind: 'inline', value: serialiseStabilityPoolDatum({ StabilityPool: liquidationHelper( { ...stabilityPoolDatum, state: { ...stabilityPoolDatum.state, depositVal: spAdd( stabilityPoolDatum.state.depositVal, mkSPInteger(requestDepositAmt), ), }, }, adaAssetClass, 0n, BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces), ), }), }, addAssets( stabilityPoolUtxo.assets, mkAssetsOf(iassetAssetClass, requestDepositAmt), mkLovelacesOf( BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces), ), ), ) .pay.ToContract( poolAddr, { kind: 'inline', value: accountOutputDatum, }, addAssets(accountTokenVal, mkLovelacesOf(accountOutputAdaAmt)), ) .setMinFee(txFee); }) .with({ Adjust: P.select() }, async (adjustContent) => { const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iAssetUtxo), ); const accountDepositChange = adjustContent.amount; const outputAddress = addressToBech32( adjustContent.outputAddress, lucid.config().network!, ); const e2s2sIdxs = await findRelevantE2s2sIdxs( lucid, stabilityPoolDatum, accountDatum.state, allE2s2sSnapshotOrefs, ); const { updatedAccountContent, reward } = updateAccount( stabilityPoolDatum, accountDatum, e2s2sIdxs, ); const isDepositOrRewardWithdrawal = accountDepositChange >= 0; const accountBalanceChange = isDepositOrRewardWithdrawal ? accountDepositChange : bigintMax( accountDepositChange, -fromSPInteger(updatedAccountContent.state.depositVal), ); const newPoolDepositExcludingFee = spZeroNegatives( spAdd( stabilityPoolDatum.state.depositVal, mkSPInteger(accountBalanceChange), ), ); const withdrawalFeeAmt = isDepositOrRewardWithdrawal || newPoolDepositExcludingFee.value === 0n ? 0n : calculateFeeFromRatio( iassetDatum.stabilityPoolWithdrawalFeeRatio, -accountBalanceChange, ); const newPoolState = updatePoolStateWhenWithdrawalFee(withdrawalFeeAmt, { ...stabilityPoolDatum.state, depositVal: newPoolDepositExcludingFee, }); const { e2s2sRefInputs, mkProcessRequestAccountRedeemerContent } = createProcessRequestAccountRedeemer( e2s2sIdxs, baseRefInputs, currentTime, ); if (e2s2sRefInputs.length > 0) { tx.readFrom(e2s2sRefInputs); } tx.collectFrom([accountUtxo], { kind: 'selected', inputs: [stabilityPoolUtxo, accountUtxo], makeRedeemer: (indices) => { return serialiseStabilityPoolRedeemer({ ProcessRequestAccount: mkProcessRequestAccountRedeemerContent( indices[0], indices[1], ), }); }, }).pay.ToContract( stabilityPoolUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ StabilityPool: { ...stabilityPoolDatum, state: newPoolState, }, }), }, addAssets( stabilityPoolUtxo.assets, negateAssets(reward), mkAssetsOf(iassetAssetClass, accountBalanceChange + withdrawalFeeAmt), ), ); const theoreticalOwnerOutputVal = addAssets( reward, isDepositOrRewardWithdrawal ? {} : mkAssetsOf( iassetAssetClass, -accountBalanceChange - withdrawalFeeAmt, ), ); const ownerOutputDat = serialiseActionReturnDatum({ IndigoStabilityPoolAccountAdjustment: { txHash: fromHex(accountUtxo.txHash), outputIndex: BigInt(accountUtxo.outputIndex), }, }); const ownerOutputMinUtxoLovelace = estimateUtxoMinLovelace( lucid.config().protocolParameters!, outputAddress, theoreticalOwnerOutputVal, { InlineDatum: { datum: ownerOutputDat } }, ); // Add extra lovelaces only when needed to reach minUTxo lovelaces. const extraOwnerOutputLovelacesAmt = lovelacesAmt(theoreticalOwnerOutputVal) >= ownerOutputMinUtxoLovelace ? 0n : ownerOutputMinUtxoLovelace - lovelacesAmt(theoreticalOwnerOutputVal); const actualOwnerOutputVal = addAssets( theoreticalOwnerOutputVal, mkLovelacesOf(extraOwnerOutputLovelacesAmt), ); const accountOutputDat = serialiseStabilityPoolDatum({ Account: { ...updatedAccountContent, state: { ...updatedAccountContent.state, depositVal: spAdd( updatedAccountContent.state.depositVal, mkSPInteger(accountBalanceChange), ), }, request: null, lastRequestProcessingTime: currentTime, }, }); const accountOutputValWithoutAda = mkAssetsOf( fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken), 1n, ); const accountOutMinUtxoLovelace = estimateUtxoMinLovelace( lucid.config().protocolParameters!, stabilityPoolUtxo.address, accountOutputValWithoutAda, { InlineDatum: { datum: accountOutputDat } }, ); const accountOutputAdaAmt = lovelacesAmt(accountUtxo.assets) - extraOwnerOutputLovelacesAmt - txFee; // This is to prevent spending ADA from the wallet submitting the request processing Tx. if (accountOutputAdaAmt < accountOutMinUtxoLovelace) { throw new Error("Account doesn't have enough ADA to be processed."); } tx.pay .ToContract( stabilityPoolUtxo.address, { kind: 'inline', value: accountOutputDat, }, addAssets( accountOutputValWithoutAda, mkLovelacesOf(accountOutputAdaAmt), ), ) .pay.ToAddressWithData( outputAddress, { kind: 'inline', value: ownerOutputDat, }, actualOwnerOutputVal, ) .setMinFee(txFee); }) .with({ Close: P.select() }, async (closeContent) => { if (txFee > closeContent.maxTxFee) { throw new Error("Account doesn't allow current transaction fee."); } const accountTokenScriptRef = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.accountTokenRef, ), ]), (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iAssetUtxo), ); const outputAddress = addressToBech32( closeContent.outputAddress, lucid.config().network!, ); const e2s2sIdxs = await findRelevantE2s2sIdxs( lucid, stabilityPoolDatum, accountDatum.state, allE2s2sSnapshotOrefs, ); const { updatedAccountContent, reward } = updateAccount( stabilityPoolDatum, accountDatum, e2s2sIdxs, ); const newPoolDepositExcludingFee = spZeroNegatives( spSub( stabilityPoolDatum.state.depositVal, updatedAccountContent.state.depositVal, ), ); const withdrawnAmt = zeroNegatives( fromSPInteger(updatedAccountContent.state.depositVal), ); const withdrawalFeeAmt = newPoolDepositExcludingFee.value === 0n ? 0n : calculateFeeFromRatio( iassetDatum.stabilityPoolWithdrawalFeeRatio, withdrawnAmt, ); const newPoolState = updatePoolStateWhenWithdrawalFee(withdrawalFeeAmt, { ...stabilityPoolDatum.state, depositVal: newPoolDepositExcludingFee, }); const { e2s2sRefInputs, mkProcessRequestAccountRedeemerContent } = createProcessRequestAccountRedeemer( e2s2sIdxs, [...baseRefInputs, accountTokenScriptRef], currentTime, ); if (e2s2sRefInputs.length > 0) { tx.readFrom(e2s2sRefInputs); } tx.readFrom([accountTokenScriptRef]) .collectFrom([accountUtxo], { kind: 'selected', inputs: [stabilityPoolUtxo, accountUtxo], makeRedeemer: (indices) => { return serialiseStabilityPoolRedeemer({ ProcessRequestAccount: mkProcessRequestAccountRedeemerContent( indices[0], indices[1], ), }); }, }) .mintAssets( mkAssetsOf( fromSystemParamsAsset(sysParams.stabilityPoolParams.accountToken), -1n, ), Data.void(), ) .pay.ToContract( stabilityPoolUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ StabilityPool: { ...stabilityPoolDatum, state: newPoolState, }, }), }, addAssets( stabilityPoolUtxo.assets, negateAssets(reward), mkAssetsOf(iassetAssetClass, -withdrawnAmt + withdrawalFeeAmt), ), ) .pay.ToAddressWithData( outputAddress, { kind: 'inline', value: serialiseActionReturnDatum({ IndigoStabilityPoolAccountClosure: { txHash: fromHex(accountUtxo.txHash), outputIndex: BigInt(accountUtxo.outputIndex), }, }), }, addAssets( mkLovelacesOf(lovelacesAmt(accountUtxo.assets) - txFee), reward, mkAssetsOf(iassetAssetClass, withdrawnAmt - withdrawalFeeAmt), ), ) .setMinFee(txFee); }) .exhaustive(); return tx; } export async function processSpRequest( stabilityPoolUtxo: UTxO, accountUtxo: UTxO, iAssetUtxo: UTxO, /** * For performance provide only the ones related to the pool's iAsset. */ allE2s2sSnapshotOrefs: OutRef[], sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, ): Promise<TxBuilder> { const draftTx = processSpRequestAuxiliary( stabilityPoolUtxo, accountUtxo, iAssetUtxo, allE2s2sSnapshotOrefs, sysParams, lucid, currentSlot, // Placeholder transation fee 1n, ); const fee = (await (await draftTx).complete()).toTransaction().body().fee(); return processSpRequestAuxiliary( stabilityPoolUtxo, accountUtxo, iAssetUtxo, allE2s2sSnapshotOrefs, sysParams, lucid, currentSlot, fee, ); } export async function createE2s2sSnapshots( stabilityPoolOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const stabilityPoolRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool Ref Script UTXO'), ); const snapshotE2s2sPolicyRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies .snapshotEpochToScaleToSumTokenRef, ), ]), (_) => new Error('Expected a single snapshot e2s2s policy Ref Script UTXO'), ); const spUtxo = matchSingle( await lucid.utxosByOutRef([stabilityPoolOref]), (_) => new Error('Expected a single stability pool UTXO'), ); const spDatum = parseStabilityPoolDatumOrThrow(getInlineDatumOrThrow(spUtxo)); const [newSnapshotDatums, newAssetStates] = partitionEpochToScaleToSums(spDatum); if (newSnapshotDatums.length === 0) { throw new Error('There has to be a snapshot being created.'); } const snapshotAc = fromSystemParamsAsset( sysParams.stabilityPoolParams.snapshotEpochToScaleToSumToken, ); const tx = lucid .newTx() .readFrom([stabilityPoolRefScriptUtxo, snapshotE2s2sPolicyRefScriptUtxo]) .collectFrom( [spUtxo], serialiseStabilityPoolRedeemer('RecordEpochToScaleToSum'), ) .mintAssets( mkAssetsOf(snapshotAc, BigInt(newSnapshotDatums.length)), Data.void(), ) .pay.ToContract( spUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ StabilityPool: { ...spDatum, assetStates: newAssetStates }, }), }, spUtxo.assets, ); for (const newDatum of newSnapshotDatums) { tx.pay.ToContract( spUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ SnapshotEpochToScaleToSum: newDatum, }), }, mkAssetsOf(snapshotAc, 1n), ); } return tx; } export async function annulRequest( accountUtxo: UTxO, params: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const stabilityPoolRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( params.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool Ref Script UTXO'), ); const oldAccountDatum: AccountContent = parseAccountDatumOrThrow( getInlineDatumOrThrow(accountUtxo), ); const tx = lucid .newTx() .readFrom([stabilityPoolRefScriptUtxo]) .collectFrom([accountUtxo], serialiseStabilityPoolRedeemer('AnnulRequest')) .addSignerKey(toHex(oldAccountDatum.owner)); if (oldAccountDatum.request !== 'Create') { tx.pay.ToContract( accountUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ Account: { ...oldAccountDatum, request: null, }, }), }, mkAssetsOf( fromSystemParamsAsset(params.stabilityPoolParams.accountToken), 1n, ), ); } return tx; }