@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
621 lines (569 loc) • 18.3 kB
text/typescript
import {
addAssets,
credentialToAddress,
Data,
fromHex,
fromText,
LucidEvolution,
mintingPolicyToId,
PolicyId,
SpendingValidator,
validatorToScriptHash,
} from '@lucid-evolution/lucid';
import {
AssetClass,
assetClassToUnit,
assetClassValueOf,
lovelacesAmt,
mkAssetsOf,
mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
addrDetails,
createScriptAddress,
submitTx,
} from '../../utils/lucid-utils';
import { runOneShotMintTx } from '../one-shot/transactions';
import {
fromSysParamsCredential,
getPythFeedConfig,
Input,
} from '../../types/system-params';
import type {
AssetInfo,
CollateralAssetInfo,
InitialAssetParam,
InitialCollateralAssetParam,
InitializeOptions,
} from './types';
import type {
CdpParamsSP,
CollectorParamsSP,
PythConfig,
} from '../../types/system-params';
import type {
CDPCreatorParamsSP,
TreasuryParamsSP,
} from '../../types/system-params';
import type {
IAssetParamsSP,
StabilityPoolParamsSP,
StakingParamsSP,
} from '../../types/system-params';
import type { GovParamsSP } from '../../types/system-params';
import type { InterestCollectionDatum } from '../interest-collection/types-new';
import type { PriceOracleParams } from '../price-oracle/types';
import type { InterestOracleParams } from '../interest-oracle/types';
import { mkAuthTokenPolicy } from '../../scripts/auth-token-policy';
import { mkIAssetValidatorFromSP } from '../iasset/scripts';
import { mkCollectorValidatorFromSP } from '../collector/scripts';
import { mkCDPCreatorValidatorFromSP } from '../cdp-creator/scripts';
import { mkTreasuryValidatorFromSP } from '../treasury/scripts';
import { mkStakingValidatorFromSP } from '../staking/scripts';
import { mkGovValidatorFromSP } from '../gov/scripts';
import { mkStabilityPoolValidatorFromSP } from '../stability-pool/scripts';
import { serialiseInterestCollectionDatum } from '../interest-collection/types-new';
import { startPriceOracleTx } from '../price-oracle/transactions';
import { startInterestOracle } from '../interest-oracle/transactions';
import { serialiseStakingDatum } from '../staking/types-new';
import { initSpState } from '../stability-pool/helpers';
import {
serialiseStabilityPoolDatum,
StabilityPoolContent,
} from '../stability-pool/types-new';
import {
CollateralAssetContent,
IAssetContent,
IAssetPriceInfo,
serialiseIAssetDatum,
} from '../iasset/types';
import { GovDatum, ProtocolParams, serialiseGovDatum } from '../gov/types-new';
import { match } from 'ts-pattern';
import { runAndAwaitTxBuilder } from '../../../tests/test-helpers';
import {
serialiseStableswapPoolDatum,
StableswapPoolContent,
} from '../cdp/types-new';
import { mkCdpValidatorFromSP } from '../cdp/scripts';
const alwaysFailValidatorHash =
'ea84d625650d066e1645e3e81d9c70a73f9ed837bd96dc49850ae744';
/** Default init options used by tests; users must pass explicit options to init(). */
export const DEFAULT_INIT_OPTIONS: InitializeOptions = {
numCdpCreators: 2n,
numCollectors: 2n,
numInterestCollectors: 2n,
numTreasuryUtxos: 5n,
treasuryIndyAmount: 1_000_000n,
totalIndySupply: 35000000000000n,
accountCreateFeeLovelaces: 5_000_000n,
accountProcessingCooldownMs: 300_000n,
accountProcessingBiasTime: 120_000n,
interestSettlementCooldown: 432_000_000n,
partialRedemptionExtraFeeLovelace: 10_000_000,
biasTime: 1200_000n,
gBiasTime: 120_000n,
interestCollectorUtxoLovelaces: 5_000_000n,
};
export const INIT_TOKEN_NAMES = {
indy: 'INDY',
dao: 'DAO',
multisigUtxo: 'INTEREST_COLLECTION_ADMIN',
govNft: 'GOV_NFT',
pollManager: 'POLL_MANAGER',
upgrade: 'UPGRADE',
iasset: 'IASSET',
collateralAsset: 'COLLATERAL_ASSET',
stabilityPool: 'STABILITY_POOL',
versionRecord: 'VERSION_RECORD',
cdpCreator: 'CDP_CREATOR',
cdp: 'CDP',
stakingManager: 'STAKING_MANAGER',
staking: 'STAKING_POSITION',
snapshotEpochToScaleToSum: 'SNAPSHOT_EPOCH_TO_SCALE_TO_SUM',
account: 'SP_ACCOUNT',
} as const;
export async function mintOneTimeToken(
lucid: LucidEvolution,
tokenName: string,
amount: bigint,
): Promise<PolicyId> {
const utxos = await lucid.wallet().getUtxos();
return await runOneShotMintTx(lucid, {
referenceOutRef: {
txHash: utxos[0].txHash,
outputIdx: BigInt(utxos[0].outputIndex),
},
mintAmounts: [{ tokenName: tokenName, amount: amount }],
});
}
/** Mint a one-shot token and return its AssetClass. */
export async function mintOneTimeAsset(
lucid: LucidEvolution,
tokenName: string,
amount: bigint,
): Promise<AssetClass> {
const policyId = await mintOneTimeToken(lucid, fromText(tokenName), amount);
return {
currencySymbol: fromHex(policyId),
tokenName: fromHex(fromText(tokenName)),
};
}
/** Build an auth-token AssetClass derived from a parent token. */
export function deriveAuthToken(
parent: AssetClass,
tokenName: string,
): AssetClass {
const policy = mkAuthTokenPolicy(parent, fromText(tokenName));
return {
currencySymbol: fromHex(mintingPolicyToId(policy)),
tokenName: fromHex(fromText(tokenName)),
};
}
export async function initScriptRef(
lucid: LucidEvolution,
validator: SpendingValidator,
): Promise<Input> {
const tx = lucid.newTx().pay.ToContract(
credentialToAddress(lucid.config().network!, {
hash: alwaysFailValidatorHash,
type: 'Script',
}),
undefined,
undefined,
validator,
);
const txHash = await tx
.complete()
.then((t) => t.sign.withWallet().complete())
.then((t) => t.submit());
await lucid.awaitTx(txHash);
return { transactionId: txHash, index: 0 };
}
export async function initCollector(
lucid: LucidEvolution,
collectorParams: CollectorParamsSP,
numCollectors: bigint,
): Promise<void> {
const tx = lucid.newTx();
for (let i = 0; i < Number(numCollectors); i++) {
tx.pay.ToContract(
createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkCollectorValidatorFromSP(collectorParams)),
),
{
kind: 'inline',
value: Data.void(),
},
);
}
await submitTx(lucid, tx);
}
export async function initInterestCollector(
lucid: LucidEvolution,
interestCollectionValHash: string,
multisigUtxoNft: AssetClass,
numInterestCollectors: bigint,
interestCollectorUtxoLovelaces: bigint,
): Promise<void> {
const [pkh, _] = await addrDetails(lucid);
const interestCollectionAdminDatum: InterestCollectionDatum = {
admin_permissions: {
Signature: {
keyHash: fromHex(pkh.hash),
},
},
};
await submitTx(
lucid,
lucid.newTx().pay.ToContract(
createScriptAddress(lucid.config().network!, interestCollectionValHash),
{
kind: 'inline',
value: serialiseInterestCollectionDatum(interestCollectionAdminDatum),
},
mkAssetsOf(multisigUtxoNft, 1n),
),
);
const interestCollectionUtxoDeploymentTx = lucid.newTx();
for (let i = 0; i < Number(numInterestCollectors); i++) {
interestCollectionUtxoDeploymentTx.pay.ToContract(
createScriptAddress(lucid.config().network!, interestCollectionValHash),
undefined,
mkLovelacesOf(interestCollectorUtxoLovelaces),
);
}
await submitTx(lucid, interestCollectionUtxoDeploymentTx);
}
export async function initCDPCreator(
lucid: LucidEvolution,
cdpCreatorParams: CDPCreatorParamsSP,
numCdpCreators: bigint,
): Promise<void> {
const tx = lucid.newTx();
for (let i = 0; i < Number(numCdpCreators); i++) {
tx.pay.ToContract(
credentialToAddress(lucid.config().network!, {
hash: validatorToScriptHash(
mkCDPCreatorValidatorFromSP(cdpCreatorParams),
),
type: 'Script',
}),
{ kind: 'inline', value: Data.void() },
{
[cdpCreatorParams.cdpCreatorNft[0].unCurrencySymbol +
fromText(cdpCreatorParams.cdpCreatorNft[1].unTokenName)]: 1n,
},
);
}
await submitTx(lucid, tx);
}
export async function initTreasury(
lucid: LucidEvolution,
treasuryParams: TreasuryParamsSP,
daoAsset: AssetClass,
indyAsset: AssetClass,
treasuryIndyAmount: bigint,
numTreasuryCollectors: bigint,
): Promise<void> {
const treasuryAddr = createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkTreasuryValidatorFromSP(treasuryParams)),
treasuryParams.treasuryUtxosStakeCredential != null
? fromSysParamsCredential(treasuryParams.treasuryUtxosStakeCredential)
: undefined,
);
const tx = lucid
.newTx()
.pay.ToContract(
treasuryAddr,
{ kind: 'inline', value: Data.void() },
addAssets(
mkAssetsOf(daoAsset, 1n),
mkAssetsOf(indyAsset, treasuryIndyAmount),
),
);
for (let i = 0; i < Number(numTreasuryCollectors); i++) {
tx.pay.ToContract(treasuryAddr, {
kind: 'inline',
value: Data.void(),
});
}
await submitTx(lucid, tx);
}
export async function initStakingManager(
lucid: LucidEvolution,
stakingParams: StakingParamsSP,
): Promise<void> {
const tx = lucid.newTx().pay.ToContract(
createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkStakingValidatorFromSP(stakingParams)),
),
{
kind: 'inline',
value: serialiseStakingDatum({
totalStake: 0n,
managerSnapshot: { snapshotAda: 0n },
}),
},
{
[stakingParams.stakingManagerNFT[0].unCurrencySymbol +
fromText(stakingParams.stakingManagerNFT[1].unTokenName)]: 1n,
},
);
await submitTx(lucid, tx);
}
export async function handleOracleForCollateralAsset(
lucid: LucidEvolution,
iasset: InitialAssetParam,
collateralAsset: InitialCollateralAssetParam,
pythConfig: PythConfig,
currentSlot: number,
): Promise<{ info: IAssetPriceInfo; params: PriceOracleParams | undefined }> {
return match(collateralAsset.priceOracle)
.returnType<
Promise<{ info: IAssetPriceInfo; params: PriceOracleParams | undefined }>
>()
.with({ tag: '_indigo_oracle_nft' }, async (val) => {
const [pkh, _] = await addrDetails(lucid);
const priceOracleParams: PriceOracleParams = {
owner: pkh.hash,
biasTime: val.params.biasTime,
expirationPeriod: val.params.expirationPeriod,
};
const [priceOracleStartTx, priceOracleNft] = await startPriceOracleTx(
lucid,
val.tokenName,
val.startPrice,
priceOracleParams,
currentSlot,
);
await runAndAwaitTxBuilder(lucid, priceOracleStartTx);
return { info: { OracleNft: priceOracleNft }, params: priceOracleParams };
})
.with({ tag: '_pyth_oracle_nft' }, () => {
return Promise.resolve({
info: {
DeferredValidation: {
feedValHash: fromHex(
getPythFeedConfig(
pythConfig,
fromHex(fromText(iasset.name)),
collateralAsset.collateralAsset,
).pythFeedValHash,
),
},
} satisfies IAssetPriceInfo,
params: undefined,
});
})
.exhaustive();
}
export async function initializeAsset(
lucid: LucidEvolution,
iassetParams: IAssetParamsSP,
iassetToken: AssetClass,
collateralAssetAuthToken: AssetClass,
cdpAuthToken: AssetClass,
cdpParams: CdpParamsSP,
stableswapValidatorHash: string,
stabilityPoolParams: StabilityPoolParamsSP,
stabilityPoolToken: AssetClass,
asset: InitialAssetParam,
pythConfig: PythConfig,
currentSlot: number,
): Promise<AssetInfo> {
const iassetName = fromHex(fromText(asset.name));
const collateralAssetInfos: CollateralAssetInfo[] = [];
for (const collateralAsset of asset.collateralAssets) {
const [pkh, _] = await addrDetails(lucid);
const interestOracleParams: InterestOracleParams = {
owner: pkh.hash,
biasTime: collateralAsset.interestOracle.params.biasTime,
};
const [startInterestOracleTx, interestOracleNft] =
await startInterestOracle(
0n,
collateralAsset.interestOracle.initialInterestRate,
0n,
interestOracleParams,
lucid,
collateralAsset.interestOracle.tokenName,
);
await submitTx(lucid, startInterestOracleTx);
const priceInfo = await handleOracleForCollateralAsset(
lucid,
asset,
collateralAsset,
pythConfig,
currentSlot,
);
const collateralAssetContent: CollateralAssetContent = {
iasset: iassetName,
collateralAsset: collateralAsset.collateralAsset,
extraDecimals: collateralAsset.extraDecimals,
priceInfo: priceInfo.info,
interestOracleNft: interestOracleNft,
redemptionRatio: collateralAsset.redemptionRatio,
maintenanceRatio: collateralAsset.maintenanceRatio,
liquidationRatio: collateralAsset.liquidationRatio,
minCollateralAmt: collateralAsset.minCollateralAmt,
firstCollateralAsset: collateralAsset.firstCollateralAsset,
nextCollateralAsset:
collateralAsset.nextCollateralAsset != null
? collateralAsset.nextCollateralAsset
: null,
};
const collateralAssetTx = lucid.newTx().pay.ToContract(
createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkIAssetValidatorFromSP(iassetParams)),
),
{
kind: 'inline',
value: serialiseIAssetDatum({
CollateralAssetContent: collateralAssetContent,
}),
},
mkAssetsOf(collateralAssetAuthToken, 1n),
);
await submitTx(lucid, collateralAssetTx);
collateralAssetInfos.push({
collateralAsset: collateralAsset.collateralAsset,
interestOracleNft: interestOracleNft,
interestOracleParams: interestOracleParams,
oracleParams: priceInfo.params,
});
}
for (const stableswapPool of asset.stablepools) {
const stableswapPoolDatum: StableswapPoolContent = {
collateralAsset: stableswapPool.collateralAsset,
collateralToIassetRatio: stableswapPool.collateralToIassetRatio,
mintingFeeRatio: stableswapPool.mintingFeeRatio,
redemptionFeeRatio: stableswapPool.redemptionFeeRatio,
mintingEnabled: stableswapPool.mintingEnabled,
redemptionEnabled: stableswapPool.redemptionEnabled,
feeManager: stableswapPool.feeManager
? fromHex(stableswapPool.feeManager)
: null,
minMintOrderAmount: stableswapPool.minMintOrderAmount,
minRedemptionOrderAmount: stableswapPool.minRedemptionOrderAmount,
iasset: iassetName,
stableswapValHash: fromHex(stableswapValidatorHash),
};
await submitTx(
lucid,
lucid.newTx().pay.ToContract(
createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkCdpValidatorFromSP(cdpParams)),
),
{
kind: 'inline',
value: serialiseStableswapPoolDatum(stableswapPoolDatum),
},
mkAssetsOf(cdpAuthToken, 1n),
),
);
}
const iassetDatum: IAssetContent = {
assetName: iassetName,
collateralAssetsCount: BigInt(asset.collateralAssets.length),
debtMintingFeeRatio: asset.debtMintingFeeRatio,
liquidationProcessingFeeRatio: asset.liquidationProcessingFeeRatio,
stabilityPoolWithdrawalFeeRatio: asset.stabilityPoolWithdrawalFeeRatio,
redemptionReimbursementRatio: asset.redemptionReimbursementRatio,
redemptionProcessingFeeRatio: asset.redemptionProcessingFeeRatio,
firstIAsset: asset.firstAsset,
nextIAsset: asset.nextAsset ? fromHex(fromText(asset.nextAsset)) : null,
};
const assetTx = lucid.newTx().pay.ToContract(
createScriptAddress(
lucid.config().network!,
validatorToScriptHash(mkIAssetValidatorFromSP(iassetParams)),
),
{
kind: 'inline',
value: serialiseIAssetDatum({ IAssetContent: iassetDatum }),
},
mkAssetsOf(iassetToken, 1n),
);
await submitTx(lucid, assetTx);
const stabilityPoolDatum: StabilityPoolContent = {
iasset: fromHex(fromText(asset.name)),
state: initSpState,
assetStates: [],
};
const spTx = lucid.newTx().pay.ToContract(
credentialToAddress(lucid.config().network!, {
hash: validatorToScriptHash(
mkStabilityPoolValidatorFromSP(stabilityPoolParams),
),
type: 'Script',
}),
{
kind: 'inline',
value: serialiseStabilityPoolDatum({ StabilityPool: stabilityPoolDatum }),
},
mkAssetsOf(stabilityPoolToken, 1n),
);
await submitTx(lucid, spTx);
return {
iassetTokenNameAscii: asset.name,
collateralAssets: collateralAssetInfos,
};
}
export async function initGovernance(
lucid: LucidEvolution,
governanceParams: GovParamsSP,
govToken: AssetClass,
initialAssets: InitialAssetParam[],
protocolParams: ProtocolParams,
): Promise<void> {
const datum: GovDatum = {
currentProposal: 0n,
currentVersion: 0n,
protocolParams: protocolParams,
activeProposals: 0n,
iassetsCount: BigInt(initialAssets.length),
};
const tx = lucid.newTx().pay.ToContract(
credentialToAddress(lucid.config().network!, {
hash: validatorToScriptHash(mkGovValidatorFromSP(governanceParams)),
type: 'Script',
}),
{ kind: 'inline', value: serialiseGovDatum(datum) },
mkAssetsOf(govToken, 1n),
);
await submitTx(lucid, tx);
}
export async function mintAuthTokenDirect(
lucid: LucidEvolution,
asset: AssetClass,
tokenName: string,
amount: bigint,
): Promise<void> {
const script = mkAuthTokenPolicy(asset, fromText(tokenName));
const policyId = mintingPolicyToId(script);
const address = await lucid.wallet().address();
// Only select utxos that contain exactly 1 asset of the given asset class and sort by lovelaces amount in descending order
const utxos = (await lucid.utxosAtWithUnit(address, assetClassToUnit(asset)))
.filter((utxo) => assetClassValueOf(utxo.assets, asset) === 1n)
.sort((a, b) => Number(lovelacesAmt(b.assets) - lovelacesAmt(a.assets)));
if (utxos.length === 0) {
throw new Error('No utxos found');
}
const utxo = utxos[0];
const tx = lucid
.newTx()
.attach.MintingPolicy(script)
.collectFrom([utxo])
.mintAssets(
{
[policyId + fromText(tokenName)]: amount,
},
Data.void(),
);
await submitTx(lucid, tx);
}