ndtr-dexter
Version:
Customizable Typescript SDK for interacting with Cardano DEXs
175 lines (174 loc) • 8.34 kB
JavaScript
import { BaseDex } from './base-dex';
import { Asset } from './models/asset';
import { LiquidityPool } from './models/liquidity-pool';
import { DefinitionBuilder } from '../definition-builder';
import { correspondingReserves } from '../utils';
import { AddressType, DatumParameterKey } from '../constants';
import pool from './definitions/muesliswap/pool';
import order from './definitions/muesliswap/order';
import { MuesliSwapApi } from './api/muesliswap-api';
export class MuesliSwap extends BaseDex {
constructor(requestConfig = {}) {
super();
this.name = 'MuesliSwap';
/**
* On-Chain constants.
*/
this.orderAddress = 'addr1zyq0kyrml023kwjk8zr86d5gaxrt5w8lxnah8r6m6s4jp4g3r6dxnzml343sx8jweqn4vn3fz2kj8kgu9czghx0jrsyqqktyhv';
this.lpTokenPolicyId = 'af3d70acf4bd5b3abb319a7d75c89fb3e56eafcdd46b2e9b57a2557f';
this.poolNftPolicyId = '909133088303c49f3a30f1cc8ed553a73857a29779f6c6561cd8093f';
this.factoryToken = 'de9b756719341e79785aa13c164e7fe68c189ed04d61c9876b2fe53f4d7565736c69537761705f414d4d';
this.api = new MuesliSwapApi(this, requestConfig);
}
async liquidityPoolAddresses(provider) {
const validityAsset = Asset.fromId(this.factoryToken);
const assetAddresses = await provider.assetAddresses(validityAsset);
return Promise.resolve([...new Set(assetAddresses.map((assetAddress) => assetAddress.address))]);
}
async liquidityPools(provider) {
const validityAsset = Asset.fromId(this.factoryToken);
const poolAddresses = await this.liquidityPoolAddresses(provider);
const addressPromises = poolAddresses.map(async (address) => {
const utxos = await provider.utxos(address, validityAsset);
return await Promise.all(utxos.map(async (utxo) => {
return await this.liquidityPoolFromUtxo(provider, utxo);
}))
.then((liquidityPools) => {
return liquidityPools.filter((liquidityPool) => {
return liquidityPool !== undefined;
});
});
});
return Promise.all(addressPromises)
.then((liquidityPools) => liquidityPools.flat());
}
async liquidityPoolFromUtxo(provider, utxo) {
if (!utxo.datumHash) {
return Promise.resolve(undefined);
}
const relevantAssets = utxo.assetBalances.filter((assetBalance) => {
const assetBalanceId = assetBalance.asset === 'lovelace' ? 'lovelace' : assetBalance.asset.id();
return !assetBalanceId.startsWith(this.factoryToken.slice(0, 56))
&& !assetBalanceId.startsWith(this.poolNftPolicyId);
});
// Irrelevant UTxO
if (relevantAssets.length < 2) {
return Promise.resolve(undefined);
}
// Could be ADA/X or X/X pool
const assetAIndex = relevantAssets.length === 2 ? 0 : 1;
const assetBIndex = relevantAssets.length === 2 ? 1 : 2;
const liquidityPool = new LiquidityPool(this.name, relevantAssets[assetAIndex].asset, relevantAssets[assetBIndex].asset, relevantAssets[assetAIndex].quantity, relevantAssets[assetBIndex].quantity, utxo.address, this.orderAddress, this.orderAddress);
// Load additional pool information
const lpToken = utxo.assetBalances.find((assetBalance) => {
return assetBalance.asset !== 'lovelace' && assetBalance.asset.policyId === this.poolNftPolicyId;
})?.asset;
if (lpToken) {
lpToken.policyId = this.lpTokenPolicyId;
liquidityPool.lpToken = lpToken;
}
try {
const builder = await (new DefinitionBuilder())
.loadDefinition(pool);
const datum = await provider.datumValue(utxo.datumHash);
const parameters = builder.pullParameters(datum);
liquidityPool.totalLpTokens = typeof parameters.TotalLpTokens === 'number'
? BigInt(parameters.TotalLpTokens)
: 0n;
liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number'
? parameters.LpFee / 100
: 0;
}
catch (e) {
return liquidityPool;
}
return liquidityPool;
}
estimatedGive(liquidityPool, swapOutToken, swapOutAmount) {
const [reserveOut, reserveIn] = correspondingReserves(liquidityPool, swapOutToken);
const receive = (Number(reserveIn) * Number(reserveOut)) / (Number(reserveOut) - Number(swapOutAmount)) - Number(reserveIn);
return BigInt(Math.floor(Number(receive) * (1 + liquidityPool.poolFeePercent / 100)));
}
estimatedReceive(liquidityPool, swapInToken, swapInAmount) {
const [reserveIn, reserveOut] = correspondingReserves(liquidityPool, swapInToken);
const swapFee = ((swapInAmount * BigInt(liquidityPool.poolFeePercent * 100)) + BigInt(10000) - 1n) / 10000n;
const adjustedSwapInAmount = swapInAmount - swapFee;
const estimatedReceive = Number(reserveOut) - (Number(reserveIn) * Number(reserveOut)) / (Number(reserveIn) + Number(adjustedSwapInAmount));
return BigInt(Math.floor(estimatedReceive));
}
priceImpactPercent(liquidityPool, swapInToken, swapInAmount) {
const [reserveIn, reserveOut] = correspondingReserves(liquidityPool, swapInToken);
const estimatedReceive = this.estimatedReceive(liquidityPool, swapInToken, swapInAmount);
const oldPrice = Number(reserveIn) / Number(reserveOut);
const swapPrice = Number(swapInAmount) / Number(estimatedReceive);
return (swapPrice - oldPrice) / oldPrice * 100;
}
async buildSwapOrder(liquidityPool, swapParameters) {
const matchMakerFee = this.swapOrderFees().find((fee) => fee.id === 'matchmakerFee');
const deposit = this.swapOrderFees().find((fee) => fee.id === 'deposit');
if (!matchMakerFee || !deposit || !swapParameters[DatumParameterKey.MinReceive]) {
return Promise.reject('Parameters for datum are not set.');
}
swapParameters = {
...swapParameters,
[DatumParameterKey.TotalFees]: matchMakerFee.value + deposit.value,
[DatumParameterKey.AllowPartialFill]: 1,
};
// Asset -> ADA swap
if (!swapParameters[DatumParameterKey.SwapOutTokenPolicyId]) {
swapParameters[DatumParameterKey.MinReceive] -= matchMakerFee.value;
}
const datumBuilder = new DefinitionBuilder();
await datumBuilder.loadDefinition(order)
.then((builder) => {
builder.pushParameters(swapParameters);
});
return [
this.buildSwapOrderPayment(swapParameters, {
address: this.orderAddress,
addressType: AddressType.Contract,
assetBalances: [
{
asset: 'lovelace',
quantity: matchMakerFee.value + deposit.value,
},
],
datum: datumBuilder.getCbor(),
})
];
}
async buildCancelSwapOrder(txOutputs, returnAddress) {
const relevantUtxo = txOutputs.find((utxo) => {
return utxo.address === this.orderAddress;
});
if (!relevantUtxo) {
return Promise.reject('Unable to find relevant UTxO for cancelling the swap order.');
}
return [
{
address: returnAddress,
addressType: AddressType.Base,
assetBalances: relevantUtxo.assetBalances,
spendUtxos: [relevantUtxo],
}
];
}
swapOrderFees() {
return [
{
id: 'matchmakerFee',
title: 'Matchmaker Fee',
description: 'Fee to cover costs for the order matchmakers.',
value: 950000n,
isReturned: false,
},
{
id: 'deposit',
title: 'Deposit',
description: 'This amount of ADA will be held as minimum UTxO ADA and will be returned when your order is processed or cancelled.',
value: 1700000n,
isReturned: true,
},
];
}
}