ndtr-dexter
Version:
Customizable Typescript SDK for interacting with Cardano DEXs
166 lines (165 loc) • 7.56 kB
JavaScript
import { LiquidityPool } from './models/liquidity-pool';
import { BaseDex } from './base-dex';
import { DefinitionBuilder } from '../definition-builder';
import { correspondingReserves, tokensMatch } from '../utils';
import { AddressType, DatumParameterKey } from '../constants';
import pool from './definitions/sundaeswap/pool';
import order from './definitions/sundaeswap/order';
import { SundaeSwapApi } from './api/sundaeswap-api';
export class SundaeSwap extends BaseDex {
constructor(requestConfig = {}) {
super();
this.name = 'SundaeSwap';
/**
* On-Chain constants.
*/
this.orderAddress = 'addr1wxaptpmxcxawvr3pzlhgnpmzz3ql43n2tc8mn3av5kx0yzs09tqh8';
this.poolAddress = 'addr1w9qzpelu9hn45pefc0xr4ac4kdxeswq7pndul2vuj59u8tqaxdznu';
this.lpTokenPolicyId = '0029cb7c88c7567b63d1a512c0ed626aa169688ec980730c0473b913';
this.api = new SundaeSwapApi(this, requestConfig);
}
async liquidityPoolAddresses(provider) {
return Promise.resolve([this.poolAddress]);
}
async liquidityPools(provider) {
const utxos = await provider.utxos(this.poolAddress);
return await Promise.all(utxos.map(async (utxo) => {
return await this.liquidityPoolFromUtxo(provider, utxo);
}))
.then((liquidityPools) => {
return liquidityPools.filter((liquidityPool) => {
return liquidityPool !== undefined;
});
});
}
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.lpTokenPolicyId);
});
// Irrelevant UTxO
if (![2, 3].includes(relevantAssets.length)) {
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.lpTokenPolicyId;
})?.asset;
if (lpToken) {
lpToken.nameHex = '6c' + lpToken.nameHex;
liquidityPool.lpToken = lpToken;
}
try {
const builder = await (new DefinitionBuilder())
.loadDefinition(pool);
const datum = await provider.datumValue(utxo.datumHash);
const parameters = builder.pullParameters(datum);
liquidityPool.identifier = typeof parameters.PoolIdentifier === 'string'
? parameters.PoolIdentifier
: '';
liquidityPool.totalLpTokens = typeof parameters.TotalLpTokens === 'number'
? BigInt(parameters.TotalLpTokens)
: 0n;
liquidityPool.poolFeePercent = typeof parameters.LpFeeNumerator === 'number' && typeof parameters.LpFeeDenominator === 'number'
? (parameters.LpFeeNumerator / parameters.LpFeeDenominator) * 100
: 0;
}
catch (e) {
return liquidityPool;
}
return liquidityPool;
}
estimatedGive(liquidityPool, swapOutToken, swapOutAmount) {
const [reserveOut, reserveIn] = correspondingReserves(liquidityPool, swapOutToken);
const receive = (reserveIn * reserveOut) / (reserveOut - swapOutAmount) - reserveIn;
const swapFee = ((receive * BigInt(liquidityPool.poolFeePercent * 100)) + BigInt(10000) - 1n) / 10000n;
return receive + swapFee;
}
estimatedReceive(liquidityPool, swapInToken, swapInAmount) {
const [reserveIn, reserveOut] = correspondingReserves(liquidityPool, swapInToken);
const swapFee = ((swapInAmount * BigInt(liquidityPool.poolFeePercent * 100)) + BigInt(10000) - 1n) / 10000n;
return reserveOut - (reserveIn * reserveOut) / (reserveIn + swapInAmount - swapFee);
}
priceImpactPercent(liquidityPool, swapInToken, swapInAmount) {
const reserveIn = tokensMatch(swapInToken, liquidityPool.assetA)
? liquidityPool.reserveA
: liquidityPool.reserveB;
return (1 - (Number(reserveIn) / Number(reserveIn + swapInAmount))) * 100;
}
async buildSwapOrder(liquidityPool, swapParameters) {
const scooperFee = this.swapOrderFees().find((fee) => fee.id === 'scooperFee');
const deposit = this.swapOrderFees().find((fee) => fee.id === 'deposit');
if (!scooperFee || !deposit) {
return Promise.reject('Parameters for datum are not set.');
}
const swapInToken = swapParameters.SwapInTokenPolicyId + swapParameters.SwapInTokenAssetName;
const swapOutToken = swapParameters.SwapOutTokenPolicyId + swapParameters.SwapOutTokenAssetName;
const swapDirection = [swapInToken, swapOutToken].sort((a, b) => {
return a.localeCompare(b);
})[0] === swapInToken ? 0 : 1;
swapParameters = {
...swapParameters,
[DatumParameterKey.ScooperFee]: scooperFee.value,
[DatumParameterKey.Action]: swapDirection,
};
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: scooperFee.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: 'scooperFee',
title: 'Scooper Processing Fee',
description: 'An ADA fee paid to the Sundae Scooper Network for processing your order.',
value: 2500000n,
isReturned: false,
},
{
id: 'deposit',
title: 'Deposit',
description: 'A small ADA deposit that you will get back when your order is processed or cancelled.',
value: 2000000n,
isReturned: true,
},
];
}
}