@indigo-labs/dexter
Version:
Customizable Typescript SDK for interacting with Cardano DEXs
245 lines (244 loc) • 14.7 kB
JavaScript
import { LiquidityPool } from './models/liquidity-pool';
import { Asset } from './models/asset';
import { BaseDex } from './base-dex';
import { DefinitionBuilder } from '../definition-builder';
import { AddressType, DatumParameterKey } from '../constants';
import pool from './definitions/splash/pool';
import order from './definitions/splash/order';
import { bytesToHex, correspondingReserves, hexToBytes, lucidUtils, tokensMatch } from '../utils';
import { Uint64BE } from 'int64-buffer';
import blake2b from 'blake2b';
import { SplashApi } from './api/splash-api';
const MAX_INT = 9223372036854775807n;
const EXECUTOR_FEE = 1100000n;
const WORST_ORDER_STEP_COST = 900000n;
export class Splash extends BaseDex {
constructor(requestConfig = {}) {
super();
/**
* On-Chain constants.
*/
this.cancelDatum = 'd87980';
this.orderScriptHash = '464eeee89f05aff787d40045af2a40a83fd96c513197d32fbc54ff02';
this.batcherKey = '5cb2c968e5d1c7197a6ce7615967310a375545d9bc65063a964335b2';
this.orderScript = {
type: 'PlutusV2',
script: '59042d01000033232323232323222323232232253330093232533300b0041323300100137566022602460246024602460246024601c6ea8008894ccc040004528099299980719baf00d300f301300214a226600600600260260022646464a66601c6014601e6ea80044c94ccc03cc030c040dd5000899191929998090038a99980900108008a5014a066ebcc020c04cdd5001180b180b980b980b980b980b980b980b980b980b98099baa00f3375e600860246ea8c010c048dd5180a98091baa00230043012375400260286eb0c050c054c054c044dd50028b1991191980080080191299980a8008a60103d87a80001323253330143375e6016602c6ea80080144cdd2a40006603000497ae0133004004001301900230170013758600a60206ea8010c04cc040dd50008b180098079baa0052301230130013322323300100100322533301200114a0264a66602066e3cdd7180a8010020a5113300300300130150013758602060226022602260226022602260226022601a6ea8004dd71808180898089808980898089808980898089808980898069baa0093001300c37540044601e00229309b2b19299980598050008a999804180218048008a51153330083005300900114a02c2c6ea8004c8c94ccc01cc010c020dd50028991919191919191919191919191919191919191919191919299981118128010991919191924c646600200200c44a6660500022930991980180198160011bae302a0015333022301f30233754010264646464a666052605800426464931929998141812800899192999816981800109924c64a666056605000226464a66606060660042649318140008b181880098169baa0021533302b3027001132323232323253330343037002149858dd6981a800981a8011bad30330013033002375a6062002605a6ea800858c0acdd50008b181700098151baa0031533302830240011533302b302a37540062930b0b18141baa002302100316302a001302a0023028001302437540102ca666042603c60446ea802c4c8c8c8c94ccc0a0c0ac00852616375a605200260520046eb4c09c004c08cdd50058b180d006180c8098b1bac30230013023002375c60420026042004603e002603e0046eb4c074004c074008c06c004c06c008c064004c064008dd6980b800980b8011bad30150013015002375a60260026026004602200260220046eb8c03c004c03c008dd7180680098049baa0051625333007300430083754002264646464a66601c60220042930b1bae300f001300f002375c601a00260126ea8004588c94ccc01cc0100044c8c94ccc030c03c00852616375c601a00260126ea800854ccc01cc00c0044c8c94ccc030c03c00852616375c601a00260126ea800858c01cdd50009b8748008dc3a4000ae6955ceaab9e5573eae815d0aba24c0126d8799fd87a9f581c96f5c1bee23481335ff4aece32fe1dfa1aa40a944a66d2d6edc9a9a5ffff0001',
};
this.api = new SplashApi(this, requestConfig);
}
async liquidityPoolAddresses(provider) {
return Promise.resolve([
'addr1x94ec3t25egvhqy2n265xfhq882jxhkknurfe9ny4rl9k6dj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrst84slu',
'addr1x8nz307k3sr60gu0e47cmajssy4fmld7u493a4xztjrll0aj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswgxsta',
'addr1x8xw6pmmy8jcnpss6sg7za9c5lk2v9nflq684vzxyn70unaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs4tm5z7',
'addr1x8cq97k066w4rd37wprvd4qrfxctzlyd6a67us2uv6hnen9j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrsgzvahe',
'addr1xxcdveqw6g88w6cvwkf705xw30gflshu79ljc3ysrmmluadj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrscak26z',
'addr1x8mql508pa9emlqfeh0g6lmlzfmauf55eq49zmta8ny7q04j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs08z9dt',
'addr1xxw7upjedpkr4wq839wf983jsnq3yg40l4cskzd7dy8eyndj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrsgddq74',
'addr1x8zjsd5fagcwpysv2zklwu69kkqfcpwfvtxpz8s0r5kmakaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrszgx7ef',
'addr1x92m92cttwgpllls5y4c889splwgujjyy0eccl424nlezm9j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswdesh2',
'addr1xxg94wrfjcdsjncmsxtj0r87zk69e0jfl28n934sznu95tdj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs2993lw',
'addr1x9wnm7vle7al9q4aw63aw63wxz7aytnpc4h3gcjy0yufxwaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs84l0h4',
]);
}
async liquidityPools(provider) {
const poolAddresses = await this.liquidityPoolAddresses(provider);
const addressPromises = poolAddresses.map(async (address) => {
const utxos = await provider.utxos(address);
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 assetName = assetBalance.asset === 'lovelace' ? 'lovelace' : assetBalance.asset.assetName;
return !assetName?.toLowerCase()?.endsWith('_nft')
&& !assetName?.toLowerCase()?.endsWith('_lq');
});
// Irrelevant UTxO
if (![2, 3].includes(relevantAssets.length)) {
return Promise.resolve(undefined);
}
const assetAIndex = relevantAssets.length === 2 ? 0 : 1;
const assetBIndex = relevantAssets.length === 2 ? 1 : 2;
try {
const builder = await new DefinitionBuilder().loadDefinition(pool);
const datum = await provider.datumValue(utxo.datumHash);
const parameters = builder.pullParameters(datum);
const liquidityPool = new LiquidityPool(Splash.identifier, parameters.PoolAssetAPolicyId === ''
? 'lovelace'
: new Asset(parameters.PoolAssetAPolicyId, parameters.PoolAssetAAssetName), new Asset(parameters.PoolAssetBPolicyId, parameters.PoolAssetBAssetName), relevantAssets[assetAIndex].quantity - BigInt(parameters.PoolAssetATreasury), relevantAssets[assetBIndex].quantity - BigInt(parameters.PoolAssetBTreasury), utxo.address, '', '');
const [lpTokenPolicyId, lpTokenAssetName] = typeof parameters.LpTokenPolicyId === 'string' && typeof parameters.LpTokenAssetName === 'string'
? [parameters.LpTokenPolicyId, parameters.LpTokenAssetName]
: [null, null];
const lpTokenBalance = utxo.assetBalances.find((assetBalance) => {
return assetBalance.asset !== 'lovelace'
&& assetBalance.asset.policyId === lpTokenPolicyId
&& assetBalance.asset.nameHex === lpTokenAssetName;
});
const nftToken = utxo.assetBalances.find((assetBalance) => {
return assetBalance.asset.assetName?.toLowerCase()?.endsWith('_nft');
})?.asset;
if (!lpTokenBalance || !nftToken) {
return Promise.resolve(undefined);
}
liquidityPool.poolNft = nftToken;
liquidityPool.lpToken = lpTokenBalance.asset;
liquidityPool.totalLpTokens = MAX_INT - lpTokenBalance.quantity;
liquidityPool.identifier = liquidityPool.lpToken.identifier();
liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number' ? (1000 - parameters.LpFee) / 10 : 0.3;
}
catch (e) {
return undefined;
}
return undefined;
}
estimatedGive(liquidityPool, swapOutToken, swapOutAmount) {
const [reserveOut, reserveIn] = correspondingReserves(liquidityPool, swapOutToken);
return (reserveIn * reserveOut) / (reserveOut - swapOutAmount) - reserveIn;
}
estimatedReceive(liquidityPool, swapInToken, swapInAmount) {
const [reserveIn, reserveOut] = correspondingReserves(liquidityPool, swapInToken);
return reserveOut - (reserveIn * reserveOut) / (reserveIn + swapInAmount);
}
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, spendUtxos = [], dataProvider) {
const batcherFee = this.swapOrderFees().find((fee) => fee.id === 'batcherFee');
const deposit = this.swapOrderFees().find((fee) => fee.id === 'deposit');
const minReceive = swapParameters.MinReceive;
if (!batcherFee || !deposit || !minReceive) {
return Promise.reject('Parameters for datum are not set.');
}
if (!dataProvider) {
return Promise.reject('Data provider is required.');
}
const walletUtxos = await dataProvider.utxos(swapParameters[DatumParameterKey.Address], swapParameters[DatumParameterKey.SwapInTokenPolicyId] !== ''
? new Asset(swapParameters.SwapInTokenPolicyId, swapParameters.SwapInTokenAssetName)
: undefined);
const firstUtxo = walletUtxos[0];
const decimalToFractionalImproved = (decimalValue) => {
const [whole, decimals = ''] = decimalValue.toString()?.split('.');
let truncatedDecimals = decimals.slice(0, 15);
const denominator = BigInt(10 ** truncatedDecimals.length);
const numerator = BigInt(whole) * denominator + BigInt(decimals);
return [numerator, denominator];
};
const swapOutToken = swapParameters.SwapOutTokenPolicyId === 'lovelace'
? 'lovelace'
: new Asset(swapParameters.SwapOutTokenPolicyId, swapParameters.SwapOutTokenAssetName);
const outDecimals = swapOutToken === 'lovelace'
? 6
: (tokensMatch(swapOutToken, liquidityPool.assetA)) ? liquidityPool.assetA.decimals : liquidityPool.assetB.decimals;
const [numerator, denominator] = decimalToFractionalImproved(Number(minReceive) / 10 ** outDecimals);
swapParameters = {
...swapParameters,
[DatumParameterKey.Action]: '00',
[DatumParameterKey.BaseFee]: WORST_ORDER_STEP_COST,
[DatumParameterKey.ExecutionFee]: EXECUTOR_FEE,
[DatumParameterKey.LpFeeNumerator]: numerator,
[DatumParameterKey.LpFeeDenominator]: denominator,
[DatumParameterKey.Beacon]: bytesToHex(Uint8Array.from(new Array(28).fill(0))),
[DatumParameterKey.Batcher]: this.batcherKey,
};
const datumBuilder = new DefinitionBuilder();
await datumBuilder.loadDefinition(order).then((builder) => {
builder.pushParameters(swapParameters);
});
const hash = blake2b(28).update(hexToBytes(datumBuilder.getCbor())).digest('hex');
swapParameters.Beacon = this.getBeacon(firstUtxo, hash);
await datumBuilder.loadDefinition(order).then((builder) => {
builder.pushParameters(swapParameters);
});
return [
this.buildSwapOrderPayment(swapParameters, {
address: lucidUtils.credentialToAddress({
type: 'Script',
hash: this.orderScriptHash,
}, {
type: 'Key',
hash: swapParameters.SenderStakingKeyHash,
}),
addressType: AddressType.Contract,
assetBalances: [
{
asset: 'lovelace',
quantity: batcherFee?.value + deposit.value,
},
],
datum: datumBuilder.getCbor(),
isInlineDatum: true,
spendUtxos: spendUtxos.concat({ utxo: firstUtxo }),
}),
];
}
async buildCancelSwapOrder(txOutputs, returnAddress) {
const relevantUtxo = txOutputs.find((utxo) => {
const addressDetails = lucidUtils.getAddressDetails(utxo.address);
return (addressDetails.paymentCredential?.hash ?? '') === this.orderScriptHash;
});
if (!relevantUtxo) {
return Promise.reject('Unable to find relevant UTxO for cancelling the swap order.');
}
return [
{
address: returnAddress,
addressType: AddressType.Base,
assetBalances: relevantUtxo.assetBalances,
isInlineDatum: false,
spendUtxos: [{
utxo: relevantUtxo,
redeemer: this.cancelDatum,
validator: this.orderScript,
signer: returnAddress,
}],
}
];
}
swapOrderFees() {
const networkFee = 0.5;
const reward = 1;
const minNitro = 1.2;
const batcherFee = (reward + networkFee) * minNitro;
const batcherFeeInAda = BigInt(Math.round(batcherFee * 10 ** 6));
return [
{
id: 'batcherFee',
title: 'Batcher Fee',
description: 'Fee paid for the service of off-chain batcher to process transactions.',
value: batcherFeeInAda,
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: 2000000n,
isReturned: true,
},
];
}
getBeacon(utxo, datumHash) {
return blake2b(28).update(Uint8Array.from([
...hexToBytes(utxo.txHash),
...new Uint64BE(Number(utxo.outputIndex)).toArray(),
...new Uint64BE(0).toArray(),
...hexToBytes(datumHash),
])).digest('hex');
}
}
Splash.identifier = 'Splash';