From 59ea629953456ca943dae0e750ff28696049097f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Jul 2024 23:56:56 -0400 Subject: [PATCH] Minswap v2 --- CHANGELOG.md | 3 + README.md | 1 + src/constants.ts | 4 + src/definition-builder.ts | 2 + src/dex/definitions/minswap-v2/order.ts | 130 +++++++++++ src/dex/definitions/minswap-v2/pool.ts | 59 +++++ src/dex/definitions/minswap/order.ts | 2 +- src/dex/logo/minswapv2.png | Bin 0 -> 2098 bytes src/dex/minswap-v2.ts | 278 ++++++++++++++++++++++++ src/dexter.ts | 2 + src/index.ts | 1 + src/utils.ts | 3 + 12 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 src/dex/definitions/minswap-v2/order.ts create mode 100644 src/dex/definitions/minswap-v2/pool.ts create mode 100644 src/dex/logo/minswapv2.png create mode 100644 src/dex/minswap-v2.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 290e391..f8cf5d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to Dexter will be documented in this file. +## [UNRELEASED] +- Minswap v2 integration + ## [v5.2.0] - Add `withMinimumReceive(minReceive: bigint)` to SwapRequest diff --git a/README.md b/README.md index fa28d47..586af9d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@

Customizable Typescript SDK for interacting with Cardano DEXs.

+ diff --git a/src/constants.ts b/src/constants.ts index e662070..01036a1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,8 @@ export enum DatumParameterKey { Action = 'Action', TokenPolicyId = 'TokenPolicyId', TokenAssetName = 'TokenAssetName', + ReserveA = 'ReserveA', + ReserveB = 'ReserveB', /** * Swap/wallet info. @@ -34,6 +36,8 @@ export enum DatumParameterKey { BatcherFee = 'BatcherFee', DepositFee = 'DepositFee', ScooperFee = 'ScooperFee', + BaseFee = 'BaseFee', + FeeSharingNumerator = 'FeeSharingNumerator', /** * LP info. diff --git a/src/definition-builder.ts b/src/definition-builder.ts index c0852c8..f31c379 100644 --- a/src/definition-builder.ts +++ b/src/definition-builder.ts @@ -109,6 +109,8 @@ export class DefinitionBuilder { * Recursively pull parameters from datum using definition template. */ private extractParameters(definedDefinition: DefinitionField, templateDefinition: DefinitionField, foundParameters: DatumParameters = {}): DatumParameters { + if (! templateDefinition) return foundParameters; + if (templateDefinition instanceof Function) { templateDefinition(definedDefinition, foundParameters); diff --git a/src/dex/definitions/minswap-v2/order.ts b/src/dex/definitions/minswap-v2/order.ts new file mode 100644 index 0000000..e7c89fc --- /dev/null +++ b/src/dex/definitions/minswap-v2/order.ts @@ -0,0 +1,130 @@ +import { DatumParameterKey } from '@app/constants'; + +/** + * https://github.com/minswap/minswap-dex-v2/blob/main/src/types/order.ts + */ +export default { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderPubKeyHash, + } + ] + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderPubKeyHash, + } + ] + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderStakingKeyHash, + } + ] + } + ] + } + ] + } + ] + }, + { + constructor: 0, + fields: [] + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderPubKeyHash, + } + ] + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderPubKeyHash, + } + ] + } + ] + } + ] + } + ] + }, + { + constructor: 0, + fields: [] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.LpTokenPolicyId + }, + { + bytes: DatumParameterKey.LpTokenAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + constructor: 1, + fields: [] + }, + { + constructor: 0, + fields: [ + { + int: DatumParameterKey.SwapInAmount + } + ] + }, + { + int: DatumParameterKey.MinReceive + }, + { + constructor: 0, + fields: [] + } + ] + }, + { + int: DatumParameterKey.BatcherFee + }, + { + constructor: 1, + fields: [] + } + ] +} \ No newline at end of file diff --git a/src/dex/definitions/minswap-v2/pool.ts b/src/dex/definitions/minswap-v2/pool.ts new file mode 100644 index 0000000..3d8cefd --- /dev/null +++ b/src/dex/definitions/minswap-v2/pool.ts @@ -0,0 +1,59 @@ +import { DatumParameterKey } from '@app/constants'; +import { DatumParameters, DefinitionField } from '@app/types'; + +/** + * https://github.com/minswap/minswap-dex-v2/blob/main/src/types/pool.ts + */ +export default { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { + return; + }, + ] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.PoolAssetAPolicyId + }, + { + bytes: DatumParameterKey.PoolAssetAAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.PoolAssetBPolicyId + }, + { + bytes: DatumParameterKey.PoolAssetBAssetName + } + ] + }, + { + int: DatumParameterKey.TotalLpTokens + }, + { + int: DatumParameterKey.ReserveA + }, + { + int: DatumParameterKey.ReserveB + }, + { + int: DatumParameterKey.BaseFee + }, + { + int: DatumParameterKey.FeeSharingNumerator + }, + (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { + return; + }, + ] +} \ No newline at end of file diff --git a/src/dex/definitions/minswap/order.ts b/src/dex/definitions/minswap/order.ts index 121b179..3f9c196 100644 --- a/src/dex/definitions/minswap/order.ts +++ b/src/dex/definitions/minswap/order.ts @@ -27,7 +27,7 @@ export default { constructor: 0, fields: [ { - bytes: DatumParameterKey.SenderStakingKeyHash, + bytes: DatumParameterKey.SenderPubKeyHash, } ] } diff --git a/src/dex/logo/minswapv2.png b/src/dex/logo/minswapv2.png new file mode 100644 index 0000000000000000000000000000000000000000..f82dd670f24a8f885304c6a3ce712fc22edb79fd GIT binary patch literal 2098 zcmZ`(c{J2(AAg3iCR|Ex>(FA)$TGH&hDjK%NkwJG8n-LkEVBL#VPqL^lecCViZG+D zG!#ZgSz4~pt}HRlNFn7Gnquy#bKY~_bKdul=kxu3Ki|*i`JD6o@z8v+nANK5RRI92 zJ@%pfWRU*tcxve{ml^o z*a=^+18ysVa%oPDlvXRvsg>q(q%^LS#*yaLNpra?{7=lT{=wJ%z$;RktdVKB($klv z*|jTVj?Bne5$D#e@HyO-8;a_GFh2z{QntFX!qa3ZOZhR%dYNS<^oKx()XFa>3kFk4 zr%4QtPxI3xdpT1GT3oYnXzkh!G(r56VpQR)Tg;GMrg8E zWQ`q`!m8n80-;bM11hK~WHxYl-RT82Jr4&Pzdut?>P`TI>VegqK#S8gQB@K zTSavL-hWUsSx4wTZdpQ;Mwl^Gg|0Y|*ahv70 zWpbaLWdhkI`(ikUIPv)2BesXpT*FfoEfk zs~3_LFOFBI=r6#t`WYW)G3-}sTb79eiSk3W0Q~;RA!k){u~KCb9r1Win$DpYTD7CA z>X`~WYw^2vv!VH(br#|mo+9pQDNdakxBPu+v%`(AF8Eu`4dS5B1JViOdgpQL-FMoT zDrm|gi&52fk=!OYf%SRF=uqP??cYx}xi35EWRE8Z#47J1nAJst^t<2Qu?_H+54Qw> zRy7Ah0METz-j(}IOI=h67$0DwL@4Sxd1OjK8Q&h1z{dc)L|_Ub=1(j8w8ss6cEEt$ zp6*zHd{~l`>y=N!kE-(BVLYQq@S966VMk9-f19HQA)9^{rY!6zSg6q)vIeCS)yiIup1Hks_eESdIMjmpK#IG96Q45+NVU=WC4ZZRzZG8 zbKRhWH5masAErG(_2La-Sbb|UU;KN)VZNt9#Yhwv_TrQV44fHGtsHtos%!PZT!h;? zF?|8n#P@+MEgaqI!)dylL})L|p`mglbiP1t&}jF%ig)ut_% zm%?;Ec;fBXj$>4=ZjR?=kNK2zaWd_Kf2)76B(!fYs^zkf>a?YK!E65O)|eJvUu<*w zBc%-W${4=%hDTFBY@)eay_ouX=nu+z;+xcPrgtFDicIA%KdU#vMrCTGc^e;Azt-WL zeD?Z{4=OXr?vhRKwdHVK8zu4%U5Q2Vy|EkAUv)0t?6@5?zTs-Z%j_|exhR&rY21+= zf!)EjQJ+30QErY-P_hrQY#5!{yBO%kleCGbV)*#>^OOBke>UBIZfOXwOnE6>tcC4= zI!qrNb03N4&eo3TLvo@@Z8~8I%Pbr#AZ|Ho7&+dpkTFqgHdy#qy7Gf<_K>sPc9El5 zy25H$A$4#JHoT_PwOG%JXsio8m1R-_7D{vLhgR50>%);)k*oJ8DW9b?Mo6Cl$LlNY_u62di=%jjA=Xpv}rm7 z_o*u1u>UvKL_c-j3T|&sYO9JrOPVg-&6DrX9^F<3@ zf7}<{+)RBjuik-dS9EB}L>w6&GVJ`lyv2KAOQ+=Kr5q|1w?xv&kj~uj!9O~WV#N%R z(#_3oWtz*6Zd(K2y~H!Pmo-Y6pQ^^%j2GuElOT@eM@y6-XkgrxqsI!%(6lJ z1`+>+mYzK~b3HI^dwW9ConwcKxdwt$C2? { + const validityAsset: Asset = Asset.fromIdentifier(this.poolValidityAsset); + const assetAddresses: AssetAddress[] = await provider.assetAddresses(validityAsset); + + return Promise.resolve([...new Set(assetAddresses.map((assetAddress: AssetAddress) => assetAddress.address))]); + } + + public async liquidityPools(provider: BaseDataProvider): Promise { + const validityAsset: Asset = Asset.fromIdentifier(this.poolValidityAsset); + const poolAddresses: string[] = await this.liquidityPoolAddresses(provider); + + const addressPromises: Promise[] = poolAddresses.map(async (address: string) => { + const utxos: UTxO[] = await provider.utxos(address, validityAsset); + + return await Promise.all( + utxos.map(async (utxo: UTxO) => { + return await this.liquidityPoolFromUtxo(provider, utxo); + }) + ) + .then((liquidityPools: (LiquidityPool | undefined)[]) => { + return liquidityPools.filter((liquidityPool?: LiquidityPool) => { + return liquidityPool !== undefined; + }) as LiquidityPool[] + }); + }); + + return Promise.all(addressPromises) + .then((liquidityPools: (Awaited)[]) => liquidityPools.flat()); + } + + public async liquidityPoolFromUtxo(provider: BaseDataProvider, utxo: UTxO): Promise { + if (! utxo.datumHash) { + return Promise.resolve(undefined); + } + + const relevantAssets: AssetBalance[] = utxo.assetBalances + .filter((assetBalance: AssetBalance) => { + const assetBalanceId: string = assetBalance.asset === 'lovelace' ? 'lovelace' : assetBalance.asset.identifier(); + + return assetBalanceId !== this.poolValidityAsset + && ! assetBalanceId.startsWith(this.lpTokenPolicyId); + }); + + // Irrelevant UTxO + if (relevantAssets.length < 2) { + return Promise.resolve(undefined); + } + + // Could be ADA/X or X/X pool + const assetAIndex: number = relevantAssets.length === 2 ? 0 : 1; + const assetBIndex: number = relevantAssets.length === 2 ? 1 : 2; + + const liquidityPool: LiquidityPool = new LiquidityPool( + MinswapV2.identifier, + relevantAssets[assetAIndex].asset, + relevantAssets[assetBIndex].asset, + relevantAssets[assetAIndex].quantity, + relevantAssets[assetBIndex].quantity, + utxo.address, + '', + '', + ); + + const lpTokenBalance: AssetBalance | undefined = utxo.assetBalances + .find((balance: AssetBalance) => { + return balance.asset !== 'lovelace' + && balance.asset.identifier() !== this.poolValidityAsset + && balance.asset.policyId === this.lpTokenPolicyId; + }); + + if (! lpTokenBalance || ! lpTokenBalance?.asset) return Promise.resolve(undefined); + + liquidityPool.lpToken = lpTokenBalance.asset as Asset; + liquidityPool.identifier = liquidityPool.lpToken.identifier(); + + try { + const builder: DefinitionBuilder = await (new DefinitionBuilder()) + .loadDefinition(pool); + const datum: DefinitionField = await provider.datumValue(utxo.datumHash); + const parameters: DatumParameters = builder.pullParameters(datum as DefinitionConstr); + + // Ignore Zap orders + if (typeof parameters.PoolAssetBPolicyId === 'string' && parameters.PoolAssetBPolicyId === this.lpTokenPolicyId) { + return undefined; + } + + liquidityPool.poolFeePercent = Number(parameters.BaseFee) / 100; + liquidityPool.totalLpTokens = typeof parameters.TotalLpTokens === 'number' + ? BigInt(parameters.TotalLpTokens) + : 0n; + } catch (e) { + return liquidityPool; + } + + return liquidityPool; + } + + estimatedGive(liquidityPool: LiquidityPool, swapOutToken: Token, swapOutAmount: bigint): bigint { + const poolFeeMultiplier: bigint = 10000n; + const poolFeeModifier: bigint = poolFeeMultiplier - BigInt(Math.round((liquidityPool.poolFeePercent / 100) * Number(poolFeeMultiplier))); + + const [reserveOut, reserveIn]: bigint[] = correspondingReserves(liquidityPool, swapOutToken); + + const swapInNumerator: bigint = swapOutAmount * reserveIn * poolFeeMultiplier; + const swapInDenominator: bigint = (reserveOut - swapOutAmount) * poolFeeModifier; + + return swapInNumerator / swapInDenominator + 1n; + } + + public estimatedReceive(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): bigint { + const poolFeeMultiplier: bigint = 10000n; + const poolFeeModifier: bigint = poolFeeMultiplier - BigInt(Math.round((liquidityPool.poolFeePercent / 100) * Number(poolFeeMultiplier))); + + const [reserveIn, reserveOut]: bigint[] = correspondingReserves(liquidityPool, swapInToken); + + const swapOutNumerator: bigint = swapInAmount * reserveOut * poolFeeModifier; + const swapOutDenominator: bigint = swapInAmount * poolFeeModifier + reserveIn * poolFeeMultiplier; + + return swapOutNumerator / swapOutDenominator; + } + + public priceImpactPercent(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): number { + const poolFeeMultiplier: bigint = 10000n; + const poolFeeModifier: bigint = poolFeeMultiplier - BigInt(Math.round((liquidityPool.poolFeePercent / 100) * Number(poolFeeMultiplier))); + + const [reserveIn, reserveOut]: bigint[] = correspondingReserves(liquidityPool, swapInToken); + + const swapOutNumerator: bigint = swapInAmount * poolFeeModifier * reserveOut; + const swapOutDenominator: bigint = swapInAmount * poolFeeModifier + reserveIn * poolFeeMultiplier; + + const priceImpactNumerator: bigint = (reserveOut * swapInAmount * swapOutDenominator * poolFeeModifier) + - (swapOutNumerator * reserveIn * poolFeeMultiplier); + const priceImpactDenominator: bigint = reserveOut * swapInAmount * swapOutDenominator * poolFeeMultiplier; + + return Number(priceImpactNumerator * 100n) / Number(priceImpactDenominator); + } + + public async buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos: SpendUTxO[] = []): Promise { + const batcherFee: SwapFee | undefined = this.swapOrderFees().find((fee: SwapFee) => fee.id === 'batcherFee'); + const deposit: SwapFee | undefined = this.swapOrderFees().find((fee: SwapFee) => fee.id === 'deposit'); + + if (! batcherFee || ! deposit) { + return Promise.reject('Parameters for datum are not set.'); + } + + swapParameters = { + ...swapParameters, + [DatumParameterKey.BatcherFee]: batcherFee.value, + [DatumParameterKey.LpTokenPolicyId]: liquidityPool.lpToken.policyId, + [DatumParameterKey.LpTokenAssetName]: liquidityPool.lpToken.nameHex, + }; + + const datumBuilder: DefinitionBuilder = new DefinitionBuilder(); + await datumBuilder.loadDefinition(order) + .then((builder: DefinitionBuilder) => { + builder.pushParameters(swapParameters); + }); + + return [ + this.buildSwapOrderPayment( + swapParameters, + { + address: lucidUtils.credentialToAddress( + { + type: 'Script', + hash: this.orderScriptHash, + }, + { + type: 'Key', + hash: swapParameters.SenderStakingKeyHash as string, + }, + ), + addressType: AddressType.Contract, + assetBalances: [ + { + asset: 'lovelace', + quantity: batcherFee.value + deposit.value, + }, + ], + datum: datumBuilder.getCbor(), + isInlineDatum: false, + spendUtxos: spendUtxos, + } + ) + ]; + } + + public async buildCancelSwapOrder(txOutputs: UTxO[], returnAddress: string): Promise { + const relevantUtxo: UTxO | undefined = txOutputs.find((utxo: UTxO) => { + const addressDetails: AddressDetails | undefined = 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, + }], + } + ]; + } + + public swapOrderFees(): SwapFee[] { + return [ + { + id: 'batcherFee', + title: 'Batcher Fee', + description: 'Fee paid for the service of off-chain Laminar batcher to process transactions.', + value: 1_000000n, + 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: 2_000000n, + isReturned: true, + }, + ]; + } + +} diff --git a/src/dexter.ts b/src/dexter.ts index 92d9749..cb5b856 100644 --- a/src/dexter.ts +++ b/src/dexter.ts @@ -18,6 +18,7 @@ import { SplitSwapRequest } from '@requests/split-swap-request'; import { TeddySwap } from '@dex/teddyswap'; import { Spectrum } from '@dex/spectrum'; import { SplitCancelSwapRequest } from '@requests/split-cancel-swap-request'; +import { MinswapV2 } from '@dex/minswap-v2'; export class Dexter { @@ -58,6 +59,7 @@ export class Dexter { this.metadataProvider = new TokenRegistryProvider(this.requestConfig); this.availableDexs = { [Minswap.identifier]: new Minswap(this.requestConfig), + [MinswapV2.identifier]: new MinswapV2(this.requestConfig), [SundaeSwap.identifier]: new SundaeSwap(this.requestConfig), [MuesliSwap.identifier]: new MuesliSwap(this.requestConfig), [WingRiders.identifier]: new WingRiders(this.requestConfig), diff --git a/src/index.ts b/src/index.ts index aca9736..3be17d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ export * from './dex/models/dex-transaction'; export * from './dex/base-dex'; export * from './dex/minswap'; +export * from './dex/minswap-v2'; export * from './dex/sundaeswap'; export * from './dex/muesliswap'; export * from './dex/wingriders'; diff --git a/src/utils.ts b/src/utils.ts index 64bf1af..125d464 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ import { Token } from '@dex/models/asset'; import { LiquidityPool } from '@dex/models/liquidity-pool'; +import { Lucid, Utils } from 'lucid-cardano'; + +export const lucidUtils: Utils = new Utils(new Lucid()); export function tokensMatch(tokenA: Token, tokenB: Token): boolean { const tokenAId: string = tokenA === 'lovelace' ? 'lovelace' : tokenA.identifier();