UNPKG

ndtr-dexter

Version:

Customizable Typescript SDK for interacting with Cardano DEXs

175 lines (174 loc) 8.34 kB
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, }, ]; } }