diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf19da..f24b800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to Dexter will be documented in this file. +## [v5.4.9] +- Splash integration + ## [v5.4.0] - SundaeSwap v3 integration diff --git a/README.md b/README.md index 8f787b2..c845837 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ - - + ### What You Can Do diff --git a/package-lock.json b/package-lock.json index 0686198..0441087 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,23 @@ { "name": "@indigo-labs/dexter", - "version": "5.4.2", + "version": "5.4.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@indigo-labs/dexter", - "version": "5.4.2", + "version": "5.4.8", "license": "MIT", "dependencies": { + "@types/blake2b": "^2.1.3", "@types/crypto-js": "^4.1.1", "axios": "^0.26.1", "axios-retry": "^3.5.1", + "blake2b": "^2.1.4", "bottleneck": "^2.19.5", "crypto-js": "^4.1.1", + "int64-buffer": "^1.0.1", + "js-encoding-utils": "^0.7.3", "lodash": "^4.17.21", "lucid-cardano": "^0.10.9" }, @@ -2698,6 +2702,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/blake2b": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/blake2b/-/blake2b-2.1.3.tgz", + "integrity": "sha512-MFCdX0MNxFBP/xEILO5Td0kv6nI7+Q2iRWZbTL/yzH2/eDVZS5Wd1LHdsmXClvsCyzqaZfHFzZaN6BUeUCfSDA==" + }, "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", @@ -2915,6 +2924,11 @@ "is-retry-allowed": "^2.2.0" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -3136,6 +3150,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/blake2b": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz", + "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==", + "dependencies": { + "blake2b-wasm": "^2.4.0", + "nanoassert": "^2.0.0" + } + }, + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -4119,6 +4151,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/int64-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.0.1.tgz", + "integrity": "sha512-+3azY4pXrjAupJHU1V9uGERWlhoqNswJNji6aD/02xac7oxol508AsMC5lxKhEqyZeDFy3enq5OGWXF4u75hiw==", + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5960,6 +6000,11 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-encoding-utils": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-encoding-utils/-/js-encoding-utils-0.7.3.tgz", + "integrity": "sha512-cfjcyPOzkZ2esMAi6eAjuto7GiT6YpPan5xIeQyN/CFqFHTt1sdqP0PJPgzi3HqAqXKN9j9hduynkgwk+AAJOw==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6194,6 +6239,11 @@ "url": "https://github.com/sponsors/raouldeheer" } }, + "node_modules/nanoassert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index 044721a..ec6e1df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@indigo-labs/dexter", - "version": "5.4.8", + "version": "5.4.9", "license": "MIT", "author": "Zachary Sluder", "keywords": [ @@ -22,11 +22,15 @@ "prepublishOnly": "npm run test" }, "dependencies": { + "@types/blake2b": "^2.1.3", "@types/crypto-js": "^4.1.1", "axios": "^0.26.1", "axios-retry": "^3.5.1", + "blake2b": "^2.1.4", "bottleneck": "^2.19.5", "crypto-js": "^4.1.1", + "int64-buffer": "^1.0.1", + "js-encoding-utils": "^0.7.3", "lodash": "^4.17.21", "lucid-cardano": "^0.10.9" }, diff --git a/src/constants.ts b/src/constants.ts index e3722dd..c502033 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,6 +18,7 @@ export enum DatumParameterKey { /** * Swap/wallet info. */ + Address = 'Address', SenderPubKeyHash = 'SenderPubKeyHash', SenderStakingKeyHash = 'SenderStakingKeyHash', SenderKeyHashes = 'SenderKeyHashes', @@ -32,6 +33,10 @@ export enum DatumParameterKey { Expiration = 'Expiration', AllowPartialFill = 'AllowPartialFill', Direction = 'Direction', + FeePaymentKeyHash = 'FeePaymentKeyHash', + Beacon = 'Beacon', + Batcher = 'Batcher', + InToken = 'InToken', /** * Trading fees. @@ -41,6 +46,7 @@ export enum DatumParameterKey { DepositFee = 'DepositFee', ScooperFee = 'ScooperFee', BaseFee = 'BaseFee', + ExecutionFee = 'ExecutionFee', FeeSharingNumerator = 'FeeSharingNumerator', OpeningFee = 'OpeningFee', FinalFee = 'FinalFee', diff --git a/src/dex/api/spectrum-api.ts b/src/dex/api/spectrum-api.ts deleted file mode 100644 index 7c27a31..0000000 --- a/src/dex/api/spectrum-api.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 { Spectrum } from '@dex/spectrum'; - -export class SpectrumApi extends BaseApi { - - protected readonly api: AxiosInstance; - protected readonly dex: Spectrum; - - constructor(dex: Spectrum, 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, - ); - - 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)) * 100; - liquidityPool.identifier = liquidityPool.lpToken.identifier(); - - return liquidityPool; - }).filter((pool: LiquidityPool | undefined) => pool !== undefined) as LiquidityPool[]; - }); - } - -} diff --git a/src/dex/api/splash-api.ts b/src/dex/api/splash-api.ts new file mode 100644 index 0000000..7a3d0d2 --- /dev/null +++ b/src/dex/api/splash-api.ts @@ -0,0 +1,77 @@ +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 } from '@app/utils'; +import { Splash } from '@dex/splash'; + +const MAX_INT: bigint = 9_223_372_036_854_775_807n; + +export class SplashApi extends BaseApi { + + protected readonly api: AxiosInstance; + protected readonly dex: Splash; + + constructor(dex: Splash, requestConfig: RequestConfig) { + super(); + + this.dex = dex; + + this.api = axios.create({ + timeout: requestConfig.timeout, + baseURL: `${appendSlash(requestConfig.proxyUrl)}https://api5.splash.trade/platform-api/v1/`, + withCredentials: false, + }); + } + + async liquidityPools(assetA: Token, assetB?: Token): Promise { + const assets: any = (await this.assets()).data['tokens']; + + return this.api.get('/pools/overview?verified=false&duplicated=false').then((response: any) => { + return response.data.map((pool: any) => this.liquidityPoolFromResponse(pool, assets)) as LiquidityPool[]; + }); + } + + private liquidityPoolFromResponse(poolData: any, assets: any): LiquidityPool { + poolData = poolData.pool; + + const tokenA: Token = poolData.x.asset === '.' + ? 'lovelace' + : new Asset(poolData.x.asset.split('.')[0], poolData.x.asset.split('.')[1]); + const tokenB = poolData.y.asset === '.' + ? 'lovelace' + : new Asset(poolData.y.asset.split('.')[0], poolData.y.asset.split('.')[1]); + + if (tokenA !== 'lovelace' && tokenA.identifier('.') in assets) { + tokenA.decimals = assets[tokenA.identifier('.')].decimals; + } + if (tokenB !== 'lovelace' && tokenB.identifier('.') in assets) { + tokenB.decimals = assets[tokenB.identifier('.')].decimals; + } + + const liquidityPool: LiquidityPool = new LiquidityPool( + Splash.identifier, + tokenA, + tokenB, + BigInt(poolData['x']['amount']) - BigInt(poolData['treasuryX']), + BigInt(poolData['y']['amount']) - BigInt(poolData['treasuryY']), + '', + '', + '', + ); + + const [lpTokenPolicyId, lpTokenAssetName] = poolData['lq']['asset'].split('.'); + + liquidityPool.lpToken = new Asset(lpTokenPolicyId, lpTokenAssetName); + liquidityPool.totalLpTokens = MAX_INT - BigInt(poolData['lq']['amount']); + liquidityPool.identifier = poolData['id']; + + return liquidityPool; + } + + private assets(): Promise { + return axios.get('https://spectrum.fi/cardano-token-list-v2.json'); + } + +} diff --git a/src/dex/api/teddyswap-api.ts b/src/dex/api/teddyswap-api.ts deleted file mode 100644 index 51fb920..0000000 --- a/src/dex/api/teddyswap-api.ts +++ /dev/null @@ -1,62 +0,0 @@ -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'; - -export class TeddyswapApi 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.teddyswap.org/v1`, - 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( - TeddySwap.identifier, - tokenA, - tokenB, - BigInt(poolResponse.lockedX.amount), - BigInt(poolResponse.lockedY.amount), - '', // Not supplied - this.dex.orderAddress, - 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)) * 100; - liquidityPool.identifier = liquidityPool.lpToken.identifier(); - - return liquidityPool; - }).filter((pool: LiquidityPool | undefined) => pool !== undefined) as LiquidityPool[]; - }); - } - -} diff --git a/src/dex/base-dex.ts b/src/dex/base-dex.ts index 955410d..d990f2c 100644 --- a/src/dex/base-dex.ts +++ b/src/dex/base-dex.ts @@ -47,7 +47,7 @@ export abstract class BaseDex { /** * Craft a swap order for this DEX. */ - abstract buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos?: SpendUTxO[]): Promise; + abstract buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos?: SpendUTxO[], dataProvider?: BaseDataProvider): Promise; /** * Craft a swap order cancellation for this DEX. diff --git a/src/dex/definitions/spectrum/order.ts b/src/dex/definitions/spectrum/order.ts deleted file mode 100644 index 2b25d08..0000000 --- a/src/dex/definitions/spectrum/order.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index e59b246..0000000 --- a/src/dex/definitions/spectrum/pool.ts +++ /dev/null @@ -1,62 +0,0 @@ -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/definitions/splash/order.ts b/src/dex/definitions/splash/order.ts new file mode 100644 index 0000000..c7bf6a8 --- /dev/null +++ b/src/dex/definitions/splash/order.ts @@ -0,0 +1,116 @@ +import { DatumParameterKey } from '@app/constants'; +import { DatumParameters, DefinitionField } from '@app/types'; + +export default { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.Action + }, + { + bytes: DatumParameterKey.Beacon + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SwapInTokenPolicyId + }, + { + bytes: DatumParameterKey.SwapInTokenAssetName + } + ] + }, + { + int: DatumParameterKey.SwapInAmount + }, + { + int: DatumParameterKey.BaseFee + }, + { + int: DatumParameterKey.MinReceive + }, + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SwapOutTokenPolicyId + }, + { + bytes: DatumParameterKey.SwapOutTokenAssetName + } + ] + }, + { + constructor: 0, + fields: [ + { + int: DatumParameterKey.LpFeeNumerator, + }, + { + int: DatumParameterKey.LpFeeDenominator, + } + ] + }, + { + int: DatumParameterKey.ExecutionFee + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + { + bytes: DatumParameterKey.SenderPubKeyHash + } + ] + }, + { + constructor: 0, + fields: [ + { + constructor: 0, + fields: [ + (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; + }, + ] + } + ] + } + ] + }, + { + bytes: DatumParameterKey.SenderPubKeyHash + }, + [ + { + bytes: DatumParameterKey.Batcher + } + ] + ] +} diff --git a/src/dex/definitions/teddyswap/pool.ts b/src/dex/definitions/splash/pool.ts similarity index 71% rename from src/dex/definitions/teddyswap/pool.ts rename to src/dex/definitions/splash/pool.ts index e59b246..5237f6e 100644 --- a/src/dex/definitions/teddyswap/pool.ts +++ b/src/dex/definitions/splash/pool.ts @@ -1,4 +1,5 @@ import { DatumParameterKey } from '@app/constants'; +import { DatumParameters, DefinitionField } from '@app/types'; export default { constructor: 0, @@ -48,15 +49,19 @@ export default { ] }, { - int: DatumParameterKey.LpFee + int: DatumParameterKey.Unknown }, - [ - { - bytes: DatumParameterKey.StakeAdminPolicy - } - ], { - int: DatumParameterKey.LqBound - } + int: DatumParameterKey.Unknown + }, + { + int: DatumParameterKey.PoolAssetATreasury + }, + { + int: DatumParameterKey.PoolAssetBTreasury + }, + (field: DefinitionField, parameters: DatumParameters, shouldExtract: boolean = true) => { + return; + }, ] } diff --git a/src/dex/definitions/teddyswap/order.ts b/src/dex/definitions/teddyswap/order.ts deleted file mode 100644 index 2b25d08..0000000 --- a/src/dex/definitions/teddyswap/order.ts +++ /dev/null @@ -1,85 +0,0 @@ -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/logo/spectrum.png b/src/dex/logo/spectrum.png deleted file mode 100644 index c63b757..0000000 Binary files a/src/dex/logo/spectrum.png and /dev/null differ diff --git a/src/dex/logo/splash.png b/src/dex/logo/splash.png new file mode 100644 index 0000000..ae883bb Binary files /dev/null and b/src/dex/logo/splash.png differ diff --git a/src/dex/logo/teddyswap.png b/src/dex/logo/teddyswap.png deleted file mode 100644 index 2b2118e..0000000 Binary files a/src/dex/logo/teddyswap.png and /dev/null differ diff --git a/src/dex/spectrum.ts b/src/dex/splash.ts similarity index 54% rename from src/dex/spectrum.ts rename to src/dex/splash.ts index 1ec3910..b54a665 100644 --- a/src/dex/spectrum.ts +++ b/src/dex/splash.ts @@ -15,39 +15,53 @@ import { 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'; -import { Script } from 'lucid-cardano'; +import pool from '@dex/definitions/splash/pool'; +import order from '@dex/definitions/splash/order'; +import { bytesToHex, correspondingReserves, hexToBytes, lucidUtils, tokensMatch } from '@app/utils'; +import { AddressDetails, Script } from 'lucid-cardano'; +import { Uint64BE } from 'int64-buffer'; +import blake2b from 'blake2b'; +import { SplashApi } from '@dex/api/splash-api'; const MAX_INT: bigint = 9_223_372_036_854_775_807n; +const EXECUTOR_FEE: bigint = 1100000n; +const WORST_ORDER_STEP_COST: bigint = 900000n; -export class Spectrum extends BaseDex { +export class Splash extends BaseDex { - public static readonly identifier: string = 'Spectrum'; - public readonly api: BaseApi; + public static readonly identifier: string = 'Splash'; + readonly api: BaseApi; /** * On-Chain constants. */ - public readonly orderAddress: string = 'addr1wynp362vmvr8jtc946d3a3utqgclfdl5y9d3kn849e359hsskr20n'; - public readonly cancelDatum: string = 'd8799f00000001ff'; + public readonly cancelDatum: string = 'd87980'; + public readonly orderScriptHash: string = '464eeee89f05aff787d40045af2a40a83fd96c513197d32fbc54ff02'; + public readonly batcherKey: string = '5cb2c968e5d1c7197a6ce7615967310a375545d9bc65063a964335b2'; public readonly orderScript: Script = { type: 'PlutusV2', - script: '5904f901000032323232323232323232323232323232323232323222253330143232323232323232323232323232323232323232323232323232323253330303370e90010010991919299981999b8753330333370e6eb4c0d0c0d403d2000148000520024800054cc090cdc399814805181a00e240042a66048664466ebcdd3981c0011ba730380013034004303400815330243370e666064444a666060002200426600666e00009200230380014800004d20041533024337126eb4c0d0c0d405800854cc0900044cccc8888cdc499b833370466e08cc0b403800c008004cdc019b823302d00e004483403ccdc100100099b8000648008c0d0078c0d0074dd6981a00b1bad303401b13322332303522533303200114a02a66607066ebcc0e400400c52889801181d0009ba90010023758606864606c606c606c606c606c002606a0286eb8c0d00614cc8cc0cc00452899191929981319b8f375c606c606e0046eb8c0d8c0dc0044cdc79bae3036002375c606c002606e04e606c002606603826666644444646466e24cdc099b81302900d375a00266e0ccdc10028020019814809a99981c191929981599b8f375c607660780046eb8c0ecc0f00044cdc79bae303b002375c60760026078058607600c26ea00144004dd424000606603a6eb4c0cc054004dd6981980c9bad303301853330313232325330253371e6eb8c0d4c0d8008dd7181a981b000899b8f375c606a0046eb8c0d4004c0d8098c0d4004c0c806c4cdc199b82001375a606402e66e04dd6981900b9bad30320181001337026604c01460620346604c00860620342c606600460540026ea8c0b8c0bc064dd599181718179818000981698170009817000998139bad302b00700a37566460566058605a002605460560026056002660486eb4c0a001401ccc88c8c8c94ccc0acc8c8c94ccc0b8cdc3a40040042646464a66606266e1d200000214a0266ebcdd38021ba70013034002302b001375400e2646464a66606266e1d200200214a0266ebcdd38021ba70013034002302b001375400e606200460500026ea8004400858c8c8c8c8c94ccc0bccdc3a4000004264646464a66606666e1d200000213232323253330373370e90010010b099ba548000004c0e8008c0c4004dd5000981a0008b181b00118168009baa001303000113374a9001015181900118148009baa001302c302d302e001302b302d0053756646464a66605866e1d2002002161533302c3371e6eb8c0b40040184c0b4c0b8c0bc01c58c0bc008c098004dd50009918151816000981498158019bae302700b302700a33022375a604c002008604c002604a002604a0206eb0c088008dd61810801181098108009810980f805180f800980f000980e800980e000980d800980d000980c800980c000980c002180b8008a4c2c4a66601600229000099980899baf300d3012001375200c6eb4c054c048dd5980a9809000a4000446660220040020062940cdd2a4000660026ea4008cc004dd48010042ba0489002232333004003375c601c0026eb8c038c03c004c03c004888cccc01000920002333300500248001d69bab00100323002375200244446601444a66600e002200a2a66601a66ebcc024c0380040184c010c044c0380044c008c03c00400555cfa5eb8155ce91299980299b8800248000584cc00c008004c0048894ccc014cdc3801240002600c00226600666e04009200230070012323002233002002001230022330020020015734ae855d1118011baa0015573c1', + script: '59042d01000033232323232323222323232232253330093232533300b0041323300100137566022602460246024602460246024601c6ea8008894ccc040004528099299980719baf00d300f301300214a226600600600260260022646464a66601c6014601e6ea80044c94ccc03cc030c040dd5000899191929998090038a99980900108008a5014a066ebcc020c04cdd5001180b180b980b980b980b980b980b980b980b980b98099baa00f3375e600860246ea8c010c048dd5180a98091baa00230043012375400260286eb0c050c054c054c044dd50028b1991191980080080191299980a8008a60103d87a80001323253330143375e6016602c6ea80080144cdd2a40006603000497ae0133004004001301900230170013758600a60206ea8010c04cc040dd50008b180098079baa0052301230130013322323300100100322533301200114a0264a66602066e3cdd7180a8010020a5113300300300130150013758602060226022602260226022602260226022601a6ea8004dd71808180898089808980898089808980898089808980898069baa0093001300c37540044601e00229309b2b19299980598050008a999804180218048008a51153330083005300900114a02c2c6ea8004c8c94ccc01cc010c020dd50028991919191919191919191919191919191919191919191919299981118128010991919191924c646600200200c44a6660500022930991980180198160011bae302a0015333022301f30233754010264646464a666052605800426464931929998141812800899192999816981800109924c64a666056605000226464a66606060660042649318140008b181880098169baa0021533302b3027001132323232323253330343037002149858dd6981a800981a8011bad30330013033002375a6062002605a6ea800858c0acdd50008b181700098151baa0031533302830240011533302b302a37540062930b0b18141baa002302100316302a001302a0023028001302437540102ca666042603c60446ea802c4c8c8c8c94ccc0a0c0ac00852616375a605200260520046eb4c09c004c08cdd50058b180d006180c8098b1bac30230013023002375c60420026042004603e002603e0046eb4c074004c074008c06c004c06c008c064004c064008dd6980b800980b8011bad30150013015002375a60260026026004602200260220046eb8c03c004c03c008dd7180680098049baa0051625333007300430083754002264646464a66601c60220042930b1bae300f001300f002375c601a00260126ea8004588c94ccc01cc0100044c8c94ccc030c03c00852616375c601a00260126ea800854ccc01cc00c0044c8c94ccc030c03c00852616375c601a00260126ea800858c01cdd50009b8748008dc3a4000ae6955ceaab9e5573eae815d0aba24c0126d8799fd87a9f581c96f5c1bee23481335ff4aece32fe1dfa1aa40a944a66d2d6edc9a9a5ffff0001', }; constructor(requestConfig: RequestConfig = {}) { super(); - this.api = new SpectrumApi(this, requestConfig); + this.api = new SplashApi(this, requestConfig); } public async liquidityPoolAddresses(provider: BaseDataProvider): Promise { return Promise.resolve([ 'addr1x94ec3t25egvhqy2n265xfhq882jxhkknurfe9ny4rl9k6dj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrst84slu', 'addr1x8nz307k3sr60gu0e47cmajssy4fmld7u493a4xztjrll0aj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswgxsta', + 'addr1x8xw6pmmy8jcnpss6sg7za9c5lk2v9nflq684vzxyn70unaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs4tm5z7', + 'addr1x8cq97k066w4rd37wprvd4qrfxctzlyd6a67us2uv6hnen9j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrsgzvahe', + 'addr1xxcdveqw6g88w6cvwkf705xw30gflshu79ljc3ysrmmluadj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrscak26z', + 'addr1x8mql508pa9emlqfeh0g6lmlzfmauf55eq49zmta8ny7q04j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs08z9dt', + 'addr1xxw7upjedpkr4wq839wf983jsnq3yg40l4cskzd7dy8eyndj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrsgddq74', + 'addr1x8zjsd5fagcwpysv2zklwu69kkqfcpwfvtxpz8s0r5kmakaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrszgx7ef', + 'addr1x92m92cttwgpllls5y4c889splwgujjyy0eccl424nlezm9j764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswdesh2', + 'addr1xxg94wrfjcdsjncmsxtj0r87zk69e0jfl28n934sznu95tdj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs2993lw', + 'addr1x9wnm7vle7al9q4aw63aw63wxz7aytnpc4h3gcjy0yufxwaj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrs84l0h4', ]); } @@ -80,7 +94,6 @@ export class Spectrum extends BaseDex { 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'); }); @@ -89,26 +102,27 @@ export class Spectrum extends BaseDex { 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 liquidityPool: LiquidityPool = new LiquidityPool( + Splash.identifier, + parameters.PoolAssetAPolicyId === '' + ? 'lovelace' + : new Asset(parameters.PoolAssetAPolicyId as string, parameters.PoolAssetAAssetName as string), + new Asset(parameters.PoolAssetBPolicyId as string, parameters.PoolAssetBAssetName as string), + relevantAssets[assetAIndex].quantity - BigInt(parameters.PoolAssetATreasury as number), + relevantAssets[assetBIndex].quantity - BigInt(parameters.PoolAssetBTreasury as number), + utxo.address, + '', + '', + ); + const [lpTokenPolicyId, lpTokenAssetName] = typeof parameters.LpTokenPolicyId === 'string' && typeof parameters.LpTokenAssetName === 'string' ? [parameters.LpTokenPolicyId, parameters.LpTokenAssetName] : [null, null]; @@ -131,27 +145,22 @@ export class Spectrum extends BaseDex { liquidityPool.identifier = liquidityPool.lpToken.identifier(); liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number' ? (1000 - parameters.LpFee) / 10 : 0.3; } catch (e) { - return liquidityPool; + return undefined; } - return liquidityPool; + return undefined; } 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; + return (reserveIn * reserveOut) / (reserveOut - swapOutAmount) - reserveIn; } 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); + return reserveOut - (reserveIn * reserveOut) / (reserveIn + swapInAmount); } priceImpactPercent(liquidityPool: LiquidityPool, swapInToken: Token, swapInAmount: bigint): number { @@ -162,7 +171,7 @@ export class Spectrum extends BaseDex { return (1 - (Number(reserveIn) / Number(reserveIn + swapInAmount))) * 100; } - public async buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos: SpendUTxO[] = []): Promise { + public async buildSwapOrder(liquidityPool: LiquidityPool, swapParameters: DatumParameters, spendUtxos: SpendUTxO[] = [], dataProvider?: BaseDataProvider): 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; @@ -170,10 +179,18 @@ export class Spectrum extends BaseDex { if (! batcherFee || ! deposit || ! minReceive) { return Promise.reject('Parameters for datum are not set.'); } - if (! liquidityPool.poolNft) { - return Promise.reject('Pool NFT is required.'); + if (! dataProvider) { + return Promise.reject('Data provider is required.'); } + const walletUtxos: UTxO[] = await dataProvider.utxos( + swapParameters[DatumParameterKey.Address] as string, + swapParameters[DatumParameterKey.SwapInTokenPolicyId] !== '' + ? new Asset(swapParameters.SwapInTokenPolicyId as string, swapParameters.SwapInTokenAssetName as string) + : undefined + ); + const firstUtxo: UTxO = walletUtxos[0]; + const decimalToFractionalImproved = (decimalValue: bigint | number): [bigint, bigint] => { const [whole, decimals = ''] = decimalValue.toString()?.split('.'); let truncatedDecimals = decimals.slice(0, 15); @@ -182,17 +199,24 @@ export class Spectrum extends BaseDex { return [numerator, denominator]; } - const batcherFeeForToken = Number(batcherFee.value) / Number(minReceive); - const [numerator, denominator] = decimalToFractionalImproved(batcherFeeForToken); - const lpfee: bigint = BigInt(1000 - Math.floor(liquidityPool.poolFeePercent * 10)); + const swapOutToken: Token = swapParameters.SwapOutTokenPolicyId === 'lovelace' + ? 'lovelace' + : new Asset(swapParameters.SwapOutTokenPolicyId as string, swapParameters.SwapOutTokenAssetName as string); + + const outDecimals: number = swapOutToken === 'lovelace' + ? 6 + : (tokensMatch(swapOutToken, liquidityPool.assetA)) ? (liquidityPool.assetA as Asset).decimals : (liquidityPool.assetB as Asset).decimals; + const [numerator, denominator] = decimalToFractionalImproved(Number(minReceive) / 10**outDecimals); swapParameters = { ...swapParameters, - [DatumParameterKey.TokenPolicyId]: liquidityPool.poolNft.policyId, - [DatumParameterKey.TokenAssetName]: liquidityPool.poolNft.nameHex, - [DatumParameterKey.LpFee]: lpfee, + [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: DefinitionBuilder = new DefinitionBuilder(); @@ -200,9 +224,26 @@ export class Spectrum extends BaseDex { builder.pushParameters(swapParameters); }); + const hash: string = blake2b(28).update(hexToBytes(datumBuilder.getCbor())).digest('hex'); + + swapParameters.Beacon = this.getBeacon(firstUtxo, hash); + + await datumBuilder.loadDefinition(order).then((builder: DefinitionBuilder) => { + builder.pushParameters(swapParameters); + }); + return [ this.buildSwapOrderPayment(swapParameters, { - address: this.orderAddress, + address: lucidUtils.credentialToAddress( + { + type: 'Script', + hash: this.orderScriptHash, + }, + { + type: 'Key', + hash: swapParameters.SenderStakingKeyHash as string, + }, + ), addressType: AddressType.Contract, assetBalances: [ { @@ -212,14 +253,16 @@ export class Spectrum extends BaseDex { ], datum: datumBuilder.getCbor(), isInlineDatum: true, - spendUtxos: spendUtxos, + spendUtxos: spendUtxos.concat({ utxo: firstUtxo }), }), ]; } public async buildCancelSwapOrder(txOutputs: UTxO[], returnAddress: string): Promise { const relevantUtxo: UTxO | undefined = txOutputs.find((utxo: UTxO) => { - return utxo.address === this.orderAddress; + const addressDetails: AddressDetails | undefined = lucidUtils.getAddressDetails(utxo.address); + + return (addressDetails.paymentCredential?.hash ?? '') === this.orderScriptHash; }); if (! relevantUtxo) { @@ -266,4 +309,15 @@ export class Spectrum extends BaseDex { }, ]; } + + private getBeacon(utxo: UTxO, datumHash: string) { + return blake2b(28).update( + Uint8Array.from([ + ...hexToBytes(utxo.txHash), + ...new Uint64BE(Number(utxo.outputIndex)).toArray(), + ...new Uint64BE(0).toArray(), + ...hexToBytes(datumHash), + ]) + ).digest('hex'); + } } diff --git a/src/dex/teddyswap.ts b/src/dex/teddyswap.ts deleted file mode 100644 index f818c8d..0000000 --- a/src/dex/teddyswap.ts +++ /dev/null @@ -1,280 +0,0 @@ -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, SpendUTxO, - 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/teddyswap/pool'; -import order from './definitions/teddyswap/order'; -import { correspondingReserves, tokensMatch } from '@app/utils'; -import { TeddyswapApi } from '@dex/api/teddyswap-api'; -import { Script } from 'lucid-cardano'; - -const MAX_INT: bigint = 9_223_372_036_854_775_807n; - -export class TeddySwap extends BaseDex { - - public static readonly identifier: string = 'TeddySwap'; - public readonly api: BaseApi; - - /** - * On-Chain constants. - */ - public readonly orderAddress: string = 'addr1z99tz7hungv6furtdl3zn72sree86wtghlcr4jc637r2eadkp2avt5gp297dnxhxcmy6kkptepsr5pa409qa7gf8stzs0706a3'; - public readonly cancelDatum: string = 'd8799f00000001ff'; - public readonly orderScript: Script = { - type: 'PlutusV2', - script: '5905090100003232323232323232323232323232323232323232323222253330153232323232323232323232323232323232323232323232323232323253330313370e90010010991919299812a99981a19b8753330343370e6eb4c0d4c0d803d2000148000520024800054cc094cdc399815005181a80e240042a6604a664466ebcdd3981c8011ba730390013035004303500815330253370e666066444a666062002200426600666e00009200230390014800004d20041533025337126eb4c0d4c0d805800854cc0940044cccc8888cdc499b833370466e08cc0b803800c008004cdc019b823302e00e004483403ccdc100100099b8000648008c0d4078c0d4074dd6981a80b1bad303501b13322332303622533303300114a02a66607266ebcc0e800400c52889801181d8009ba90010023758606a64606e606e606e606e606e002606c0286eb8c0d40604cdc3811811a9991981a0008a513232325330273371e6eb8c0dcc0e0008dd7181b981c000899b8f375c606e0046eb8c0dc004c0e00a0c0dc004c0d00704ccccc88888c8c8cdc499b8133702605401a6eb4004cdc199b82005004003302a0135333039323253302c3371e6eb8c0f0c0f4008dd7181e181e800899b8f375c60780046eb8c0f0004c0f40b4c0f00184dd400288009ba848000c0d0074dd6981a00a8009bad3034019375a6068030a666064646464a6604c66e3cdd7181b181b8011bae3036303700113371e6eb8c0d8008dd7181b000981b813981b000981980d899b83337040026eb4c0cc05ccdc09bad3033017375a6066030200266e04cc09c028c0c8068cc09c010c0c806858c0d0008c0ac004dd51817981800c9bab32302f30303031001302e302f001302f00133028375a605800e0146eacc8c0b0c0b4c0b8004c0acc0b0004c0b0004cc094dd6981480280399911919192999816191919299981799b87480080084c8c8c94ccc0c8cdc3a400000429404cdd79ba7004374e002606a00460580026ea801c4c8c8c94ccc0c8cdc3a400400429404cdd79ba7004374e002606a00460580026ea801cc0c8008c0a4004dd500088010b1919191919299981819b87480000084c8c8c8c94ccc0d0cdc3a4000004264646464a66607066e1d20020021613374a9000000981d80118190009baa0013035001163037002302e00137540026062002266e95200202b3033002302a0013754002605a605c605e0026058605c00a6eacc8c8c94ccc0b4cdc3a40040042c2a66605a66e3cdd7181700080309817181798180038b181800118138009baa00132302b302d001302a302c003375c60500166050014660466eb4c09c004010c09c004c098004c098040dd618118011bac3022002302230220013022302000a3020001301f001301e001301d001301c001301b001301a00130190013019004301800114985920aca597c9282533300b0011480004ccc044cdd7980698090009ba9006375a602a60246eacc054c04800520002233301100200100314a066e952000330013752004660026ea40080215d0245002232333004003375c601c0026eb8c038c03c004c03c004888cccc01000920002333300500248001d69bab00100323002375200244446601444a66600e002200a2a66601a66ebcc024c0380040184c010c044c0380044c008c03c00400555cfa5eb8155ce91299980299b8800248000584cc00c008004c0048894ccc014cdc3801240002600c00226600666e04009200230070012323002233002002001230022330020020015734ae855d1118011baa0015573c1', - }; - - constructor(requestConfig: RequestConfig = {}) { - super(); - - this.api = new TeddyswapApi(this, requestConfig); - } - - public async liquidityPoolAddresses(provider: BaseDataProvider): Promise { - return Promise.resolve([ - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zp5hu6t748dfdd6cxlxxssyqez4wqwcrq44crfgkltqh2cqcwcjyr', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zqgdzhkv23nm3v7tanurzu8v5vll365n7hq8f26937hatlqnv5cpz', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8z8rxrld450e6c360mu72ru7u8zz0602px3esxykcx87f9ns2tytsd', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zz6mve63ntrqp7yxgkk395rngtzdmzdjzzuzdkdks0afwqsmdsegq', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zr0vp2360e2j2gve54sxsheawjd6s6we2d25xl96a3r0jdqzvyqkl', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zzlsgmhduch9juwcjf6vjqeht0jv2g2mlz86wqh42h8akdqglnguu', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zxk96389hhwyhv0t07gh89wqnaqg9cqkwsz4esd9sm562rs55tl66', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8z9re630pc4dzmhtku8276tyq0glgn53h93vw5rl9e6w4g8su86xvk', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zpczswng09euafg44jclrg5tm7xg260qzyavu9dysz8g3js7pzqla', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8z92v2k4gz85r5rq035n2llzemqvcz70h7hdr3njur05y6nsmrsjpe', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zxn5qy8sn2d7wtdtvjcsv7v0h7u9zsleljxv3nschr5sj3sla73t7', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zzw03em2wpuy6t66rx4hqmggelr8r2whwru8uuptxzwdlfsss26rc', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zphr7r6v67asj5jc5w5uapfapv0u9433m3v9aag9w46spaqc60ygw', - 'addr1zy5th50h46anh3v7zdvh7ve6amac7k4h3mdfvt0p6czm8zrlxa5g3cwp6thfvzwhd9s4vcjjdwttsss65l09dum7g9rs0mr8px', - ]); - } - - 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('_lp'); - }); - - // 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( - TeddySwap.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; - });const nftToken: Asset | undefined = utxo.assetBalances.find((assetBalance) => { - return (assetBalance.asset as Asset).assetName?.toLowerCase()?.endsWith('_identity'); - })?.asset as Asset | undefined; - - 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(); - liquidityPool.poolFeePercent = typeof parameters.LpFee === 'number' ? (1000 - parameters.LpFee) / 10 : 0.3; - } 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: 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'); - const minReceive = swapParameters.MinReceive as bigint; - - 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: 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: bigint = BigInt(1000 - Math.floor(liquidityPool.poolFeePercent * 10)); - - swapParameters = { - ...swapParameters, - [DatumParameterKey.TokenPolicyId]: liquidityPool.poolNft.policyId, - [DatumParameterKey.TokenAssetName]: liquidityPool.poolNft.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(), - isInlineDatum: true, - spendUtxos: spendUtxos, - }), - ]; - } - - public async buildCancelSwapOrder(txOutputs: UTxO[], returnAddress: string): Promise { - const relevantUtxo: UTxO | undefined = txOutputs.find((utxo: 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, - isInlineDatum: false, - spendUtxos: [{ - utxo: relevantUtxo, - redeemer: this.cancelDatum, - validator: this.orderScript, - signer: returnAddress, - }], - } - ]; - } - - 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 69b6e6f..b1bcd42 100644 --- a/src/dexter.ts +++ b/src/dexter.ts @@ -15,12 +15,11 @@ import { FetchRequest } from '@requests/fetch-request'; 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'; import { SplitCancelSwapRequest } from '@requests/split-cancel-swap-request'; import { SundaeSwapV3 } from '@dex/sundaeswap-v3'; import { MinswapV2 } from '@dex/minswap-v2'; import { WingRidersV2 } from '@dex/wingriders-v2'; +import { Splash } from '@dex/splash'; export class Dexter { @@ -68,8 +67,7 @@ export class Dexter { [WingRiders.identifier]: new WingRiders(this.requestConfig), [WingRidersV2.identifier]: new WingRidersV2(this.requestConfig), [VyFinance.identifier]: new VyFinance(this.requestConfig), - [TeddySwap.identifier]: new TeddySwap(this.requestConfig), - [Spectrum.identifier]: new Spectrum(this.requestConfig), + [Splash.identifier]: new Splash(this.requestConfig), }; } diff --git a/src/index.ts b/src/index.ts index c3554fd..f75b32c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,5 +47,4 @@ export * from './dex/muesliswap'; export * from './dex/wingriders'; export * from './dex/wingriders-v2'; export * from './dex/vyfinance'; -export * from './dex/teddyswap'; -export * from './dex/spectrum'; +export * from './dex/splash'; diff --git a/src/requests/swap-request.ts b/src/requests/swap-request.ts index 087a9cb..0e38905 100644 --- a/src/requests/swap-request.ts +++ b/src/requests/swap-request.ts @@ -225,6 +225,7 @@ export class SwapRequest { // Standard parameters for a swap order const defaultSwapParameters: DatumParameters = { + [DatumParameterKey.Address]: this._dexter.walletProvider.address(), [DatumParameterKey.SenderPubKeyHash]: this._dexter.walletProvider.publicKeyHash(), [DatumParameterKey.SenderStakingKeyHash]: this._dexter.walletProvider.stakingKeyHash(), [DatumParameterKey.ReceiverPubKeyHash]: this._dexter.walletProvider.publicKeyHash(), @@ -246,7 +247,8 @@ export class SwapRequest { return { utxo, } - }) as SpendUTxO[] + }) as SpendUTxO[], + this._dexter.dataProvider ); } diff --git a/src/utils.ts b/src/utils.ts index af1979f..a80b97f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import { Token } from '@dex/models/asset'; import { LiquidityPool } from '@dex/models/liquidity-pool'; import { C, Datum, fromHex, Lucid, toHex, Utils } from 'lucid-cardano'; import { DatumJson } from '@app/types'; +import { encoder } from 'js-encoding-utils'; export const lucidUtils: Utils = new Utils(new Lucid()); @@ -62,4 +63,7 @@ export function datumJsonToCbor(json: DatumJson): Datum { }; return toHex(convert(json).to_bytes()); -} \ No newline at end of file +} + +export const bytesToHex = (bytes: Uint8Array): string => encoder.arrayBufferToHexString(bytes); +export const hexToBytes = (hex: string): Uint8Array => encoder.hexStringToArrayBuffer(hex); \ No newline at end of file