From 332dee92ecb68075ec8e7e905db15688ffc4a04a Mon Sep 17 00:00:00 2001 From: Sluder Date: Thu, 14 Dec 2023 11:41:57 -0500 Subject: [PATCH 1/4] Spectrum integration --- CHANGELOG.md | 1 + README.md | 1 + src/dex/api/spectrum-api.ts | 62 +++++++ src/dex/definitions/spectrum/order.ts | 85 ++++++++++ src/dex/definitions/spectrum/pool.ts | 62 +++++++ src/dex/logo/spectrum.png | Bin 0 -> 420 bytes src/dex/spectrum.ts | 235 ++++++++++++++++++++++++++ src/dexter.ts | 2 + src/index.ts | 1 + tests/spectrum.test.ts | 100 +++++++++++ 10 files changed, 549 insertions(+) create mode 100644 src/dex/api/spectrum-api.ts create mode 100644 src/dex/definitions/spectrum/order.ts create mode 100644 src/dex/definitions/spectrum/pool.ts create mode 100644 src/dex/logo/spectrum.png create mode 100644 src/dex/spectrum.ts create mode 100644 tests/spectrum.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab5071..5d1d0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to Dexter will be documented in this file. ## [UNRELEASED] - TeddySwap integration +- Spectrum integration ## [v4.2.0] - Fix WR price impact formula for 0 decimals diff --git a/README.md b/README.md index 07a9745..ba14b81 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ + ### What You Can Do diff --git a/src/dex/api/spectrum-api.ts b/src/dex/api/spectrum-api.ts new file mode 100644 index 0000000..fb4909a --- /dev/null +++ b/src/dex/api/spectrum-api.ts @@ -0,0 +1,62 @@ +import { BaseApi } from './base-api'; +import { Asset, Token } from '../models/asset'; +import { LiquidityPool } from '../models/liquidity-pool'; +import axios, { AxiosInstance } from 'axios'; +import { RequestConfig } from '@app/types'; +import { appendSlash, tokensMatch } from '@app/utils'; +import { TeddySwap } from '@dex/teddyswap'; +import { Spectrum } from '@dex/spectrum'; + +export class SpectrumApi extends BaseApi { + + protected readonly api: AxiosInstance; + protected readonly dex: TeddySwap; + + constructor(dex: TeddySwap, requestConfig: RequestConfig) { + super(); + + this.dex = dex; + this.api = axios.create({ + timeout: requestConfig.timeout, + baseURL: `${appendSlash(requestConfig.proxyUrl)}https://analytics-balanced.spectrum.fi/cardano`, + headers: { + 'Content-Type': 'application/json', + } + }); + } + + liquidityPools(assetA: Token, assetB?: Token): Promise { + return this.api.get('/front/pools', ).then((response: any) => { + return response.data.map((poolResponse: any) => { + const tokenA: Token = poolResponse.lockedX.asset.currencySymbol !== '' + ? new Asset(poolResponse.lockedX.asset.currencySymbol, Buffer.from(poolResponse.lockedX.asset.tokenName, 'utf8').toString('hex')) + : 'lovelace'; + const tokenB: Token = poolResponse.lockedY.asset.currencySymbol !== '' + ? new Asset(poolResponse.lockedY.asset.currencySymbol, Buffer.from(poolResponse.lockedY.asset.tokenName, 'utf8').toString('hex')) + : 'lovelace'; + + if (! tokensMatch(tokenA, assetA) || (assetB && ! tokensMatch(tokenB, assetB))) { + return undefined; + } + + let liquidityPool: LiquidityPool = new LiquidityPool( + Spectrum.identifier, + tokenA, + tokenB, + BigInt(poolResponse.lockedX.amount), + BigInt(poolResponse.lockedY.amount), + '', // Not supplied + this.dex.orderAddress, + this.dex.orderAddress, + ); + + liquidityPool.lpToken = new Asset(poolResponse.lockedLQ.asset.currencySymbol, Buffer.from(poolResponse.lockedLQ.asset.tokenName, 'utf8').toString('hex')); + liquidityPool.poolFeePercent = (1 - (poolResponse.poolFeeNum / poolResponse.poolFeeDenum)) * 10; + liquidityPool.identifier = liquidityPool.lpToken.identifier(); + + return liquidityPool; + }).filter((pool: LiquidityPool | undefined) => pool !== undefined) as LiquidityPool[]; + }); + } + +} diff --git a/src/dex/definitions/spectrum/order.ts b/src/dex/definitions/spectrum/order.ts new file mode 100644 index 0000000..2b25d08 --- /dev/null +++ b/src/dex/definitions/spectrum/order.ts @@ -0,0 +1,85 @@ +import { DatumParameterKey } from '@app/constants'; +import { DatumParameters, DefinitionField } from '@app/types'; + +export default { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SwapInTokenPolicyId + }, + { + bytes: DatumParameterKey.SwapInTokenAssetName + } + ], + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SwapOutTokenPolicyId + }, + { + bytes: DatumParameterKey.SwapOutTokenAssetName + } + ], + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.TokenPolicyId // Pool NFT + }, + { + bytes: DatumParameterKey.TokenAssetName + } + ], + }, + { + int: DatumParameterKey.LpFee + }, + { + int: DatumParameterKey.LpFeeNumerator // Execution fee numerator + }, + { + int: DatumParameterKey.LpFeeDenominator // Execution fee denominator + }, + { + bytes: DatumParameterKey.SenderPubKeyHash + }, + (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { + if (! shouldExtract) { + const stakeKeyHash: string = parameters[DatumParameterKey.SenderStakingKeyHash] as string ?? null; + + if (! stakeKeyHash) return; + + return { + constructor: 0, + fields: [ + { + bytes: stakeKeyHash, + } + ], + }; + } + + if ('fields' in field) { + if (field.constructor === 1) return; + + if (field.fields.length > 0 && 'bytes' in field.fields[0]) { + parameters[DatumParameterKey.SenderStakingKeyHash] = field.fields[0].bytes; + } + } + + return; + }, + { + int: DatumParameterKey.SwapInAmount + }, + { + int: DatumParameterKey.MinReceive + } + ], +} diff --git a/src/dex/definitions/spectrum/pool.ts b/src/dex/definitions/spectrum/pool.ts new file mode 100644 index 0000000..e59b246 --- /dev/null +++ b/src/dex/definitions/spectrum/pool.ts @@ -0,0 +1,62 @@ +import { DatumParameterKey } from '@app/constants'; + +export default { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.TokenPolicyId // Pool NFT + }, + { + bytes: DatumParameterKey.TokenAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.PoolAssetAPolicyId + }, + { + bytes: DatumParameterKey.PoolAssetAAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.PoolAssetBPolicyId + }, + { + bytes: DatumParameterKey.PoolAssetBAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.LpTokenPolicyId + }, + { + bytes: DatumParameterKey.LpTokenAssetName + } + ] + }, + { + int: DatumParameterKey.LpFee + }, + [ + { + bytes: DatumParameterKey.StakeAdminPolicy + } + ], + { + int: DatumParameterKey.LqBound + } + ] +} diff --git a/src/dex/logo/spectrum.png b/src/dex/logo/spectrum.png new file mode 100644 index 0000000000000000000000000000000000000000..c63b757b967a5ee744c033d95e2dc9193650dca8 GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYc!3HGV7iFISQk(@Ik;M!Qe1}1p@p%4<6riAF ziEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$QVa}?hMq2tArY-_XDsw`aTICW zUn;J@x+z1*FjM#oXW4Amg9*aDY>PMCe4wo(!ILG>ZPyT5!)`uHC+R|hka*o`wVN|{ zKIZ(c^}KoVKjZX$k~55WJ+I8{T*Vr+V}YpnmLRtRqvSZoGpSZv8m9)#5PHKjy>p)0 z1;$?oj5kQWjAb!vP=3HA!61L2GQ&~Z=#78{s~po$tqaU$4L=Qk@@(K=)1SpVqxa2p z-5s+}Y;S1x*vB&GDc9d8@ivpZ3LUleGLEvY1>5teDJz#7QV|UEj2e0($`3f&sruqXzn8DN4 K&t;ucLK6V#Bc-eW literal 0 HcmV?d00001 diff --git a/src/dex/spectrum.ts b/src/dex/spectrum.ts new file mode 100644 index 0000000..9012784 --- /dev/null +++ b/src/dex/spectrum.ts @@ -0,0 +1,235 @@ +import { LiquidityPool } from './models/liquidity-pool'; +import { BaseDataProvider } from '@providers/data/base-data-provider'; +import { Asset, Token } from './models/asset'; +import { BaseDex } from './base-dex'; +import { + AssetBalance, + DatumParameters, + DefinitionConstr, + DefinitionField, + PayToAddress, + RequestConfig, + SwapFee, + UTxO +} from '@app/types'; +import { DefinitionBuilder } from '@app/definition-builder'; +import { AddressType, DatumParameterKey } from '@app/constants'; +import { BaseApi } from '@dex/api/base-api'; +import pool from './definitions/spectrum/pool'; +import order from './definitions/spectrum/order'; +import { correspondingReserves, tokensMatch } from '@app/utils'; +import { SpectrumApi } from '@dex/api/spectrum-api'; + +const MAX_INT: bigint = 9_223_372_036_854_775_807n; + +export class Spectrum extends BaseDex { + + public static readonly identifier: string = 'Spectrum'; + public readonly api: BaseApi; + + /** + * On-Chain constants. + */ + public readonly orderAddress: string = 'addr1wynp362vmvr8jtc946d3a3utqgclfdl5y9d3kn849e359hsskr20n'; + + constructor(requestConfig: RequestConfig = {}) { + super(); + + this.api = new SpectrumApi(this, requestConfig); + } + + public async liquidityPoolAddresses(provider: BaseDataProvider): Promise { + return Promise.resolve([ + 'addr1x94ec3t25egvhqy2n265xfhq882jxhkknurfe9ny4rl9k6dj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrst84slu', + 'addr1x8nz307k3sr60gu0e47cmajssy4fmld7u493a4xztjrll0aj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswgxsta', + ]); + } + + async liquidityPools(provider: BaseDataProvider): Promise { + const poolAddresses: string[] = await this.liquidityPoolAddresses(provider); + + const addressPromises: Promise[] = poolAddresses.map(async (address: string) => { + const utxos: UTxO[] = await provider.utxos(address); + + 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 = utxo.assetBalances.filter((assetBalance: AssetBalance) => { + const assetName = assetBalance.asset === 'lovelace' ? 'lovelace' : assetBalance.asset.assetName; + return !assetName?.toLowerCase()?.endsWith('_nft') + && !assetName?.toLowerCase()?.endsWith('_identity') + && !assetName?.toLowerCase()?.endsWith('_lq'); + }); + + // Irrelevant UTxO + if (![2, 3].includes(relevantAssets.length)) { + 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( + Spectrum.identifier, + relevantAssets[assetAIndex].asset, + relevantAssets[assetBIndex].asset, + relevantAssets[assetAIndex].quantity, + relevantAssets[assetBIndex].quantity, + utxo.address, + this.orderAddress, + this.orderAddress + ); + + 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); + + const [lpTokenPolicyId, lpTokenAssetName] = typeof parameters.LpTokenPolicyId === 'string' && typeof parameters.LpTokenAssetName === 'string' + ? [parameters.LpTokenPolicyId, parameters.LpTokenAssetName] + : [null, null]; + const lpTokenBalance: AssetBalance | undefined = utxo.assetBalances.find((assetBalance: AssetBalance) => { + return assetBalance.asset !== 'lovelace' + && assetBalance.asset.policyId === lpTokenPolicyId + && assetBalance.asset.nameHex === lpTokenAssetName; + }); + + if (! lpTokenBalance) { + return Promise.resolve(undefined); + } + + liquidityPool.lpToken = lpTokenBalance.asset as Asset; + liquidityPool.totalLpTokens = MAX_INT - lpTokenBalance.quantity; + liquidityPool.identifier = liquidityPool.lpToken.identifier(); + liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number' ? (1000 - parameters.LpFee) / 10 : 0.3; + console.log(liquidityPool) + } catch (e) { + return liquidityPool; + } + + return liquidityPool; + } + + estimatedGive(liquidityPool: LiquidityPool, swapOutToken: Token, swapOutAmount: bigint): bigint { + const [reserveOut, reserveIn]: bigint[] = correspondingReserves(liquidityPool, swapOutToken); + + const receive: bigint = (reserveIn * reserveOut) / (reserveOut - swapOutAmount) - reserveIn; + const swapFee: bigint = ((receive * BigInt(Math.floor(liquidityPool.poolFeePercent * 100))) + BigInt(10000) - 1n) / 10000n; + + return receive + swapFee; + } + + estimatedReceive(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): bigint { + const [reserveIn, reserveOut]: bigint[] = correspondingReserves(liquidityPool, swapInToken); + + const swapFee: bigint = ((swapInAmount * BigInt(Math.floor(liquidityPool.poolFeePercent * 100))) + BigInt(10000) - 1n) / 10000n; + + return reserveOut - (reserveIn * reserveOut) / (reserveIn + swapInAmount - swapFee); + } + + priceImpactPercent(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): number { + const reserveIn: bigint = tokensMatch(swapInToken, liquidityPool.assetA) + ? liquidityPool.reserveA + : liquidityPool.reserveB; + + return (1 - (Number(reserveIn) / Number(reserveIn + swapInAmount))) * 100; + } + + public async buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos: UTxO[] = []): 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'); + const minReceive = swapParameters.MinReceive as bigint; + + if (!batcherFee || !deposit || !minReceive) { + return Promise.reject('Parameters for datum are not set.'); + } + + const decimalToFractionalImproved = (decimalValue: bigint | number): [bigint, bigint] => { + const [whole, decimals = ''] = decimalValue.toString()?.split('.'); + let truncatedDecimals = decimals.slice(0, 15); + const denominator = 10n ** BigInt(truncatedDecimals.length); + const numerator = BigInt(whole + truncatedDecimals); + return [numerator, denominator]; + } + + const batcherFeeForToken = Number(batcherFee.value) / Number(minReceive); + const [numerator, denominator] = decimalToFractionalImproved(batcherFeeForToken); + const lpfee: number = 1000 - liquidityPool.poolFeePercent * 10; + + swapParameters = { + ...swapParameters, + [DatumParameterKey.TokenPolicyId]: liquidityPool.lpToken.policyId, + [DatumParameterKey.TokenAssetName]: liquidityPool.lpToken.nameHex, + [DatumParameterKey.LpFee]: lpfee, + [DatumParameterKey.LpFeeNumerator]: numerator, + [DatumParameterKey.LpFeeDenominator]: denominator, + }; + + const datumBuilder: DefinitionBuilder = new DefinitionBuilder(); + await datumBuilder.loadDefinition(order).then((builder: DefinitionBuilder) => { + builder.pushParameters(swapParameters); + }); + + return [ + this.buildSwapOrderPayment(swapParameters, { + address: this.orderAddress, + addressType: AddressType.Contract, + assetBalances: [ + { + asset: 'lovelace', + quantity: batcherFee?.value + deposit.value, + }, + ], + datum: datumBuilder.getCbor(), + spendUtxos: spendUtxos, + }), + ]; + } + + public buildCancelSwapOrder(txOutputs: UTxO[], returnAddress: string): Promise { + throw new Error('Method not implemented.'); + } + + public swapOrderFees(): SwapFee[] { + const networkFee: number = 0.5; + const reward: number = 1; + const minNitro: number = 1.2; + const batcherFee: number = (reward + networkFee) * minNitro; + const batcherFeeInAda: bigint = 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: 2_000000n, + isReturned: true, + }, + ]; + } +} diff --git a/src/dexter.ts b/src/dexter.ts index cef6057..c6c3f9a 100644 --- a/src/dexter.ts +++ b/src/dexter.ts @@ -16,6 +16,7 @@ import axios from "axios"; import axiosRetry from "axios-retry"; import { SplitSwapRequest } from '@requests/split-swap-request'; import { TeddySwap } from '@dex/teddyswap'; +import { Spectrum } from '@dex/spectrum'; export class Dexter { @@ -61,6 +62,7 @@ export class Dexter { [WingRiders.identifier]: new WingRiders(this.requestConfig), [VyFinance.identifier]: new VyFinance(this.requestConfig), [TeddySwap.identifier]: new TeddySwap(this.requestConfig), + [Spectrum.identifier]: new Spectrum(this.requestConfig), }; } diff --git a/src/index.ts b/src/index.ts index d57761b..946a0e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,3 +44,4 @@ export * from './dex/muesliswap'; export * from './dex/wingriders'; export * from './dex/vyfinance'; export * from './dex/teddyswap'; +export * from './dex/spectrum'; diff --git a/tests/spectrum.test.ts b/tests/spectrum.test.ts new file mode 100644 index 0000000..d08fb85 --- /dev/null +++ b/tests/spectrum.test.ts @@ -0,0 +1,100 @@ +import { + Asset, + Dexter, + LiquidityPool, + MockDataProvider, + SwapRequest, + MockWalletProvider, + DatumParameters, + DatumParameterKey, + PayToAddress, + AddressType, +} from '../src'; +import { Spectrum } from '../src/dex/spectrum'; + +describe('Spectrum', () => { + + const walletProvider: MockWalletProvider = new MockWalletProvider(); + walletProvider.loadWalletFromSeedPhrase(['']); + const dexter: Dexter = (new Dexter()) + .withDataProvider(new MockDataProvider()) + .withWalletProvider(walletProvider); + const asset: Asset = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344', 6); + + describe('Set Swap In', () => { + + const liquidityPool: LiquidityPool = new LiquidityPool( + Spectrum.identifier, + 'lovelace', + asset, + 15853604203n, + 2999947840n, + 'addr1', + ); + liquidityPool.poolFeePercent = 0.3; + liquidityPool.lpToken = new Asset('f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880', '69555344'); + + const swapRequest: SwapRequest = dexter.newSwapRequest() + .forLiquidityPool(liquidityPool) + .withSwapInToken('lovelace') + .withSwapInAmount(10_000_000000n) + .withSlippagePercent(1.0); + + it('Can calculate swap parameters', () => { + expect(+swapRequest.getPriceImpactPercent().toFixed(2)).toEqual(38.68); + expect(swapRequest.getEstimatedReceive()).toEqual(1158222522n); + expect(swapRequest.getMinimumReceive()).toEqual(1146754972n); + }); + + it('Can build swap order', () => { + const spectrum: Spectrum = new Spectrum(); + const defaultSwapParameters: DatumParameters = { + [DatumParameterKey.PoolIdentifier]: '1234', + [DatumParameterKey.SenderPubKeyHash]: walletProvider.publicKeyHash(), + [DatumParameterKey.SenderStakingKeyHash]: walletProvider.stakingKeyHash(), + [DatumParameterKey.ReceiverPubKeyHash]: walletProvider.publicKeyHash(), + [DatumParameterKey.ReceiverStakingKeyHash]: walletProvider.stakingKeyHash(), + [DatumParameterKey.SwapInAmount]: swapRequest.swapInAmount, + [DatumParameterKey.MinReceive]: swapRequest.getMinimumReceive(), + [DatumParameterKey.SwapInTokenPolicyId]: '', + [DatumParameterKey.SwapInTokenAssetName]: '', + [DatumParameterKey.SwapOutTokenPolicyId]: asset.policyId, + [DatumParameterKey.SwapOutTokenAssetName]: asset.nameHex, + }; + + return spectrum.buildSwapOrder(liquidityPool, defaultSwapParameters) + .then((payments: PayToAddress[]) => { + expect(() => { spectrum.buildSwapOrder(liquidityPool, defaultSwapParameters); }).not.toThrowError(); + expect(payments[0].addressType).toBe(AddressType.Contract); + expect(payments[0].assetBalances[0].quantity).toEqual(10003800000n); + expect(payments[0].datum).toBe('d8799fd8799f4040ffd8799f581cf66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b698804469555344ffd8799f581cf66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b698804469555344ff1903e51b0000016d7639cd581b00038d7ea4c6800042ed56d8799f42bac6ff1b00000002540be4001a445a179cff'); + }); + }); + + }); + + describe('Set Swap Out', () => { + + const liquidityPool: LiquidityPool = new LiquidityPool( + Spectrum.identifier, + 'lovelace', + asset, + 15853604203n, + 2999947840n, + 'addr1', + ); + liquidityPool.poolFeePercent = 0.3; + + const swapRequest: SwapRequest = dexter.newSwapRequest() + .forLiquidityPool(liquidityPool) + .withSwapInToken('lovelace') + .withSwapOutAmount(1_000_000000n) + .withSlippagePercent(1.0); + + it('Can calculate swap parameters', () => { + expect(swapRequest.swapInAmount).toEqual(7950789864n); + }); + + }); + +}); From 8c35d9f201e655e33bb0585742140e41a3e20c39 Mon Sep 17 00:00:00 2001 From: Sluder Date: Thu, 14 Dec 2023 12:00:26 -0500 Subject: [PATCH 2/4] Inline datum order --- src/dex/spectrum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dex/spectrum.ts b/src/dex/spectrum.ts index 9012784..fdb9a79 100644 --- a/src/dex/spectrum.ts +++ b/src/dex/spectrum.ts @@ -120,7 +120,6 @@ export class Spectrum extends BaseDex { liquidityPool.totalLpTokens = MAX_INT - lpTokenBalance.quantity; liquidityPool.identifier = liquidityPool.lpToken.identifier(); liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number' ? (1000 - parameters.LpFee) / 10 : 0.3; - console.log(liquidityPool) } catch (e) { return liquidityPool; } @@ -199,6 +198,7 @@ export class Spectrum extends BaseDex { }, ], datum: datumBuilder.getCbor(), + isInlineDatum: false, spendUtxos: spendUtxos, }), ]; From dff04043a52a593ace93158b4a36bd6963c1eb15 Mon Sep 17 00:00:00 2001 From: Sluder Date: Thu, 14 Dec 2023 12:04:47 -0500 Subject: [PATCH 3/4] Fix inline --- src/dex/spectrum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dex/spectrum.ts b/src/dex/spectrum.ts index fdb9a79..9617058 100644 --- a/src/dex/spectrum.ts +++ b/src/dex/spectrum.ts @@ -198,7 +198,7 @@ export class Spectrum extends BaseDex { }, ], datum: datumBuilder.getCbor(), - isInlineDatum: false, + isInlineDatum: true, spendUtxos: spendUtxos, }), ]; From 9a5903c90d0b00f972ee3a0685fc1b08fab4b89f Mon Sep 17 00:00:00 2001 From: Sluder Date: Thu, 14 Dec 2023 16:27:23 -0500 Subject: [PATCH 4/4] Pool NFT --- src/dex/api/spectrum-api.ts | 4 +++- src/dex/models/liquidity-pool.ts | 1 + src/dex/spectrum.ts | 21 ++++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/dex/api/spectrum-api.ts b/src/dex/api/spectrum-api.ts index fb4909a..6e82199 100644 --- a/src/dex/api/spectrum-api.ts +++ b/src/dex/api/spectrum-api.ts @@ -50,8 +50,10 @@ export class SpectrumApi extends BaseApi { this.dex.orderAddress, ); + const [poolNftPolicyId, poolNftName] = poolResponse.id.split('.'); + liquidityPool.poolNft = new Asset(poolNftPolicyId, Buffer.from(poolNftName, 'utf8').toString('hex')); liquidityPool.lpToken = new Asset(poolResponse.lockedLQ.asset.currencySymbol, Buffer.from(poolResponse.lockedLQ.asset.tokenName, 'utf8').toString('hex')); - liquidityPool.poolFeePercent = (1 - (poolResponse.poolFeeNum / poolResponse.poolFeeDenum)) * 10; + liquidityPool.poolFeePercent = (1 - (poolResponse.poolFeeNum / poolResponse.poolFeeDenum)) * 100; liquidityPool.identifier = liquidityPool.lpToken.identifier(); return liquidityPool; diff --git a/src/dex/models/liquidity-pool.ts b/src/dex/models/liquidity-pool.ts index 70703c8..e5039a2 100644 --- a/src/dex/models/liquidity-pool.ts +++ b/src/dex/models/liquidity-pool.ts @@ -12,6 +12,7 @@ export class LiquidityPool { limitOrderAddress: string; lpToken: Asset; + poolNft: Asset; identifier: string = ''; poolFeePercent: number = 0; totalLpTokens: bigint = 0n; diff --git a/src/dex/spectrum.ts b/src/dex/spectrum.ts index 9617058..268fdc4 100644 --- a/src/dex/spectrum.ts +++ b/src/dex/spectrum.ts @@ -111,11 +111,15 @@ export class Spectrum extends BaseDex { && assetBalance.asset.policyId === lpTokenPolicyId && assetBalance.asset.nameHex === lpTokenAssetName; }); + const nftToken: Asset | undefined = utxo.assetBalances.find((assetBalance) => { + return (assetBalance.asset as Asset).assetName?.toLowerCase()?.endsWith('_nft'); + })?.asset as Asset | undefined; - if (! lpTokenBalance) { + if (! lpTokenBalance || ! nftToken) { return Promise.resolve(undefined); } + liquidityPool.poolNft = nftToken; liquidityPool.lpToken = lpTokenBalance.asset as Asset; liquidityPool.totalLpTokens = MAX_INT - lpTokenBalance.quantity; liquidityPool.identifier = liquidityPool.lpToken.identifier(); @@ -157,26 +161,29 @@ export class Spectrum extends BaseDex { const deposit: SwapFee | undefined = this.swapOrderFees().find((fee: SwapFee) => fee.id === 'deposit'); const minReceive = swapParameters.MinReceive as bigint; - if (!batcherFee || !deposit || !minReceive) { + if (! batcherFee || ! deposit || ! minReceive) { return Promise.reject('Parameters for datum are not set.'); } + if (! liquidityPool.poolNft) { + return Promise.reject('Pool NFT is required.'); + } const decimalToFractionalImproved = (decimalValue: bigint | number): [bigint, bigint] => { const [whole, decimals = ''] = decimalValue.toString()?.split('.'); let truncatedDecimals = decimals.slice(0, 15); - const denominator = 10n ** BigInt(truncatedDecimals.length); - const numerator = BigInt(whole + truncatedDecimals); + const denominator: bigint = BigInt(10 ** truncatedDecimals.length); + const numerator = BigInt(whole) * denominator + BigInt(decimals); return [numerator, denominator]; } const batcherFeeForToken = Number(batcherFee.value) / Number(minReceive); const [numerator, denominator] = decimalToFractionalImproved(batcherFeeForToken); - const lpfee: number = 1000 - liquidityPool.poolFeePercent * 10; + const lpfee: bigint = BigInt(1000 - Math.floor(liquidityPool.poolFeePercent * 10)); swapParameters = { ...swapParameters, - [DatumParameterKey.TokenPolicyId]: liquidityPool.lpToken.policyId, - [DatumParameterKey.TokenAssetName]: liquidityPool.lpToken.nameHex, + [DatumParameterKey.TokenPolicyId]: liquidityPool.poolNft.policyId, + [DatumParameterKey.TokenAssetName]: liquidityPool.poolNft.nameHex, [DatumParameterKey.LpFee]: lpfee, [DatumParameterKey.LpFeeNumerator]: numerator, [DatumParameterKey.LpFeeDenominator]: denominator,