diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c099559..a3d426e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,5 +30,7 @@ jobs: env: BLOCKFROST_PROJECT_ID_MAINNET: ${{ secrets.BLOCKFROST_PROJECT_ID_MAINNET }} BLOCKFROST_PROJECT_ID_TESTNET: ${{ secrets.BLOCKFROST_PROJECT_ID_TESTNET }} + MAESTRO_API_KEY_MAINNET: ${{ secrets.MAESTRO_API_KEY_MAINNET }} + MAESTRO_API_KEY_TESTNET: ${{ secrets.MAESTRO_API_KEY_TESTNET }} - name: Check format & lint run: pnpm run check-format diff --git a/.gitignore b/.gitignore index 2de0fae..d62e772 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ build build-tsc coverage .idea + +package-lock.json \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 378eac2..c795b05 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1 @@ -build +build \ No newline at end of file diff --git a/README.md b/README.md index a256453..6fae989 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ The Minswap open-source providing a comprehensive suite of off-chain tools price - [x] Create orders and submit with Lucid - [x] Syncer to sync minswap's liquidity pool data -We provide two adapter `BlockfrostAdapter` and `MinswapAdapter` to get the price and liquidity pool information. +We provide multiple adapters to get the price and liquidity pool information. + - `BlockfrostAdapter`: use [Blockfrost](https://blockfrost.dev) to query the data. - `MinswapAdapter`: use Syncer to query the data. If you want to use `MinswapAdapter` you need to run syncer by yourself. +- `MaestroAdapter`: use [Maestro](https://www.gomaestro.org/) to query the data. ## Install @@ -29,6 +31,7 @@ This package depends on `lucid-cardano`, which is an ESM package, so it's also a Create an adapter using either `BlockfrostAdapter` or `MinswapAdapter`: ### BlockfrostAdapter: + ```ts import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; import { BlockfrostAdapter, NetworkId } from "@minswap/sdk"; @@ -36,15 +39,16 @@ import { BlockfrostAdapter, NetworkId } from "@minswap/sdk"; const blockFrostApi = new BlockFrostAPI({ projectId: "", network: "mainnet", -}) +}); const blockfrostAdapter = new BlockfrostAdapter( NetworkId.MAINNET, blockFrostApi -) +); ``` ### MinswapAdapter: + - [Install docker compose](https://docs.docker.com/compose/install). - Update the `.env` file to specify the exact network you want to sync. - Run the command: `docker compose -f docker-compose.yaml up --build -d` to build. @@ -52,31 +56,57 @@ const blockfrostAdapter = new BlockfrostAdapter( ```ts import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; -import { BlockfrostAdapter, MinswapAdapter, NetworkEnvironment, NetworkId, newPrismaClient, PostgresRepositoryReader } from "@minswap/sdk"; +import { + BlockfrostAdapter, + MinswapAdapter, + NetworkEnvironment, + NetworkId, + newPrismaClient, + PostgresRepositoryReader, +} from "@minswap/sdk"; const blockFrostApi = new BlockFrostAPI({ projectId: "", network: "mainnet", -}) +}); -const prismaClient = await newPrismaClient("postgresql://postgres:minswap@postgres:5432/syncer?schema=public&connection_limit=5") +const prismaClient = await newPrismaClient( + "postgresql://postgres:minswap@postgres:5432/syncer?schema=public&connection_limit=5" +); const repositoryReader = new PostgresRepositoryReader( NetworkEnvironment.MAINNET, prismaClient -) +); const minswapAdapter = new MinswapAdapter({ networkId: NetworkId.MAINNET, networkEnv: NetworkEnvironment.MAINNET, blockFrostApi: blockFrostApi, - repository: repositoryReader -}) + repository: repositoryReader, +}); +``` + +### Maestro Adapter: + +```ts +import { MaestroAdapter } from "@minswap/sdk"; +import { MaestroClient, Configuration } from "@maestro-org/typescript-sdk"; + +const maestroClient = new MaestroClient( + new Configuration({ + apiKey: maestroApiKey, + network: cardanoNetwork, + }) +); + +const maestroAdapter = new MaestroAdapter(NetworkId.TESTNET, maestroClient); ``` ### Example 1: Get current price of MIN/ADA pool #### MIN/ADA pool v1: + ```ts for (let i = 1; ; i++) { const pools = await adapter.getV1Pools({ @@ -90,7 +120,7 @@ for (let i = 1; ; i++) { (p) => p.assetA === "lovelace" && p.assetB === - "29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e" + "29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e" ); if (minAdaPool) { const [price0, price1] = await adapter.getV1PoolPrice({ pool: minAdaPool }); @@ -105,23 +135,25 @@ for (let i = 1; ; i++) { ``` #### MIN/ADA pool v2: + ```ts const minAdaPool = await adapter.getV2PoolByPair( Asset.fromString("lovelace"), - Asset.fromString("29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e") -) + Asset.fromString( + "29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e" + ) +); if (minAdaPool) { const [a, b] = await adapter.getV2PoolPrice({ pool: minAdaPool }); - console.log( - `ADA/MIN price: ${a.toString()}; MIN/ADA price: ${b.toString()}` - ); + console.log(`ADA/MIN price: ${a.toString()}; MIN/ADA price: ${b.toString()}`); } ``` ### Example 2: Get historical prices of MIN/ADA pool #### MIN/ADA pool v1: + ```ts const MIN_ADA_POOL_ID = "6aa2153e1ae896a95539c9d62f76cedcdabdcdf144e564b8955f609d660cf6a2"; @@ -138,11 +170,14 @@ for (const historyPoint of history) { ``` #### MIN/ADA pool v2: + ```ts for (let i = 1; ; i++) { const pools = await adapter.getV2PoolHistory({ assetA: Asset.fromString("lovelace"), - assetB: Asset.fromString("29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e"), + assetB: Asset.fromString( + "29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c64d494e" + ), page: i, }); if (pools.length === 0) { diff --git a/examples/example.ts b/examples/blockfrost-adapter-example.ts similarity index 99% rename from examples/example.ts rename to examples/blockfrost-adapter-example.ts index e1d4f5c..607a071 100644 --- a/examples/example.ts +++ b/examples/blockfrost-adapter-example.ts @@ -16,7 +16,6 @@ import BigNumber from "bignumber.js"; import { ADA, Asset, - BlockfrostAdapter, calculateDeposit, calculateSwapExactIn, calculateSwapExactOut, @@ -33,6 +32,8 @@ import { StableswapCalculation, StableswapConstant, } from "../src"; + +import { BlockfrostAdapter } from "../src/adapters/blockfrost"; import { LbeV2 } from "../src/lbe-v2/lbe-v2"; import { Stableswap } from "../src/stableswap"; import { LbeV2Types } from "../src/types/lbe-v2"; diff --git a/examples/expired-order-monitor-example.ts b/examples/expired-order-monitor-example.ts index 3b85155..137e900 100644 --- a/examples/expired-order-monitor-example.ts +++ b/examples/expired-order-monitor-example.ts @@ -1,7 +1,8 @@ import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; import { Network } from "@minswap/lucid-cardano"; -import { BlockfrostAdapter, NetworkId } from "../src"; +import { NetworkId } from "../src"; +import { BlockfrostAdapter } from "../src/adapters/blockfrost"; import { ExpiredOrderMonitor } from "../src/expired-order-monitor"; import { getBackendLucidInstance } from "../src/utils/lucid"; diff --git a/examples/lbe-v2-worker-example.ts b/examples/lbe-v2-worker-example.ts index 5681f17..f0bb52e 100644 --- a/examples/lbe-v2-worker-example.ts +++ b/examples/lbe-v2-worker-example.ts @@ -1,7 +1,8 @@ import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; import { Network } from "@minswap/lucid-cardano"; -import { BlockfrostAdapter, NetworkId } from "../src"; +import { NetworkId } from "../src"; +import { BlockfrostAdapter } from "../src/adapters/blockfrost"; import { LbeV2Worker } from "../src/lbe-v2-worker/worker"; import { NetworkEnvironment } from "../src/types/network"; import { getBackendLucidInstance } from "../src/utils/lucid"; diff --git a/examples/maestro-adapter-example.ts b/examples/maestro-adapter-example.ts new file mode 100644 index 0000000..61a411d --- /dev/null +++ b/examples/maestro-adapter-example.ts @@ -0,0 +1,100 @@ +import { Address, Lucid, Network, TxComplete } from "@minswap/lucid-cardano"; +import { Asset, NetworkId, PoolV2 } from "../src"; +import { getBackendMaestroLucidInstance } from "../src/utils/lucid"; +import { LbeV2 } from "../src/lbe-v2/lbe-v2"; +import { MaestroAdapter } from "../src/adapters/maestro"; +import { MaestroClient, Configuration } from "@maestro-org/typescript-sdk"; +import invariant from "@minswap/tiny-invariant"; + +async function main(): Promise { + const cardanoNetwork: Network = "Preprod"; + const maestroApiKey = ""; + + const address = + "addr_test1qqf2dhk96l2kq4xh2fkhwksv0h49vy9exw383eshppn863jereuqgh2zwxsedytve5gp9any9jwc5hz98sd47rwfv40stc26fr"; + + const lucid = await getBackendMaestroLucidInstance( + cardanoNetwork, + maestroApiKey, + address + ); + + const maestroClient = new MaestroClient( + new Configuration({ + apiKey: maestroApiKey, + network: cardanoNetwork, + }) + ); + + const maestroAdapter = new MaestroAdapter(NetworkId.TESTNET, maestroClient); + + const txComplete = await _lbeV2DepositOrderExample( + lucid, + address, + maestroAdapter + ); + const signedTx = await txComplete + .signWithPrivateKey("") + .complete(); + + const txId = await signedTx.submit(); + console.info(`Transaction submitted successfully: ${txId}`); +} + +// Example Tx: 7af5ea80b6a4a587e2c6cfce383367829f0cb68c90b65656c8198a72afc3f419 +async function _lbeV2DepositOrderExample( + lucid: Lucid, + address: Address, + maestroAdapter: MaestroAdapter +): Promise { + const baseAsset = Asset.fromString( + "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed7243414b45" + ); + const raiseAsset = Asset.fromString("lovelace"); + + const lbeId = PoolV2.computeLPAssetName(baseAsset, raiseAsset); + const treasury = await maestroAdapter.getLbeV2TreasuryByLbeId(lbeId); + invariant(treasury !== null, `Can not find treasury by lbeId ${lbeId}`); + const treasuryUtxos = await lucid.utxosByOutRef([ + { txHash: treasury.txIn.txHash, outputIndex: treasury.txIn.index }, + ]); + invariant(treasuryUtxos.length === 1, "Can not find treasury Utxo"); + + const seller = await maestroAdapter.getLbeV2SellerByLbeId(lbeId); + invariant(seller !== null, `Can not find seller by lbeId ${lbeId}`); + const sellerUtxos = await lucid.utxosByOutRef([ + { txHash: seller.txIn.txHash, outputIndex: seller.txIn.index }, + ]); + invariant(sellerUtxos.length === 1, "Can not find seller Utxo"); + + const orders = await maestroAdapter.getLbeV2OrdersByLbeIdAndOwner( + lbeId, + address + ); + const orderUtxos = + orders.length > 0 + ? await lucid.utxosByOutRef( + orders.map((o) => ({ + txHash: o.txIn.txHash, + outputIndex: o.txIn.index, + })) + ) + : []; + + invariant( + orderUtxos.length === orders.length, + "Can not find enough order Utxos" + ); + + const currentSlot = await maestroAdapter.currentSlot(); + return new LbeV2(lucid).depositOrWithdrawOrder({ + currentSlot: currentSlot, + existingOrderUtxos: orderUtxos, + treasuryUtxo: treasuryUtxos[0], + sellerUtxo: sellerUtxos[0], + owner: address, + action: { type: "deposit", additionalAmount: 1_000_000n }, + }); +} + +void main(); diff --git a/package.json b/package.json index 3384f3a..60388d6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "prisma:dbpull": "prisma db pull", "prisma:generate": "prisma generate", "postinstall": "prisma generate", - "syncer:start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/syncer/main.ts" + "syncer:start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/syncer/main.ts", + "type-check": "tsc --noEmit" }, "keywords": [ "minswap", @@ -51,6 +52,7 @@ "@blockfrost/blockfrost-js": "^5.5.0", "@cardano-ogmios/client": "^6.5.0", "@cardano-ogmios/schema": "^6.5.0", + "@maestro-org/typescript-sdk": "^1.6.3", "@minswap/lucid-cardano": "0.10.10-minswap.4", "@minswap/tiny-invariant": "^1.2.0", "big.js": "^6.2.1", @@ -64,7 +66,7 @@ "@eslint/compat": "^1.1.1", "@eslint/js": "^9.9.1", "@jest/globals": "^29.7.0", - "@prisma/client": "^5.19.1", + "@prisma/client": "^6.0.1", "@types/big.js": "^6.2.2", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", @@ -78,7 +80,7 @@ "jest": "^29.7.0", "json-bigint": "^1.0.0", "prettier": "3.3.3", - "prisma": "^5.19.1", + "prisma": "^6.0.1", "rimraf": "^6.0.1", "rollup": "^4.21.2", "rollup-plugin-dts": "^6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2306cf..160e77b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@cardano-ogmios/schema': specifier: ^6.5.0 version: 6.9.0 + '@maestro-org/typescript-sdk': + specifier: ^1.6.3 + version: 1.6.3 '@minswap/lucid-cardano': specifier: 0.10.10-minswap.4 version: 0.10.10-minswap.4 @@ -56,8 +59,8 @@ importers: specifier: ^29.7.0 version: 29.7.0 '@prisma/client': - specifier: ^5.19.1 - version: 5.22.0(prisma@5.22.0) + specifier: ^6.0.1 + version: 6.1.0(prisma@6.1.0) '@types/big.js': specifier: ^6.2.2 version: 6.2.2 @@ -98,8 +101,8 @@ importers: specifier: 3.3.3 version: 3.3.3 prisma: - specifier: ^5.19.1 - version: 5.22.0 + specifier: ^6.0.1 + version: 6.1.0 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -633,6 +636,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@maestro-org/typescript-sdk@1.6.3': + resolution: {integrity: sha512-iN0eb4ccyRj16XybNm0BRRPQbi7MMUkLA1zeUKqad5ynNnDAFLt6+3/wRe12Te9fhKgsT5Xsgk7JlTnjlUZLHA==} + '@minswap/lucid-cardano@0.10.10-minswap.4': resolution: {integrity: sha512-LFBm2F4ZuLC4z5ktXwQQPeyi9l2P2ypBu/5ZZr+b/ievYw6lDnwhYk6ju0zxjXYk5FG3wlQjW4Qpwqsy0ZlsKA==} engines: {node: '>=14'} @@ -663,29 +669,29 @@ packages: resolution: {integrity: sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==} engines: {node: '>=10.12.0'} - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} + '@prisma/client@6.1.0': + resolution: {integrity: sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==} + engines: {node: '>=18.18'} peerDependencies: prisma: '*' peerDependenciesMeta: prisma: optional: true - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} + '@prisma/debug@6.1.0': + resolution: {integrity: sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==} - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} + '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': + resolution: {integrity: sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==} - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} + '@prisma/engines@6.1.0': + resolution: {integrity: sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==} - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} + '@prisma/fetch-engine@6.1.0': + resolution: {integrity: sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==} - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} + '@prisma/get-platform@6.1.0': + resolution: {integrity: sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==} '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} @@ -1021,6 +1027,9 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1449,6 +1458,15 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -2115,15 +2133,18 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} + prisma@6.1.0: + resolution: {integrity: sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==} + engines: {node: '>=18.18'} hasBin: true prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -3162,6 +3183,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@maestro-org/typescript-sdk@1.6.3': + dependencies: + axios: 1.7.9 + transitivePeerDependencies: + - debug + '@minswap/lucid-cardano@0.10.10-minswap.4': dependencies: '@peculiar/webcrypto': 1.5.0 @@ -3203,30 +3230,30 @@ snapshots: tslib: 2.8.1 webcrypto-core: 1.8.1 - '@prisma/client@5.22.0(prisma@5.22.0)': + '@prisma/client@6.1.0(prisma@6.1.0)': optionalDependencies: - prisma: 5.22.0 + prisma: 6.1.0 - '@prisma/debug@5.22.0': {} + '@prisma/debug@6.1.0': {} - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} + '@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': {} - '@prisma/engines@5.22.0': + '@prisma/engines@6.1.0': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 + '@prisma/debug': 6.1.0 + '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959 + '@prisma/fetch-engine': 6.1.0 + '@prisma/get-platform': 6.1.0 - '@prisma/fetch-engine@5.22.0': + '@prisma/fetch-engine@6.1.0': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 + '@prisma/debug': 6.1.0 + '@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959 + '@prisma/get-platform': 6.1.0 - '@prisma/get-platform@5.22.0': + '@prisma/get-platform@6.1.0': dependencies: - '@prisma/debug': 5.22.0 + '@prisma/debug': 6.1.0 '@rollup/pluginutils@5.1.3(rollup@4.27.4)': dependencies: @@ -3550,6 +3577,14 @@ snapshots: asynckit@0.4.0: {} + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-jest@29.7.0(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -4036,6 +4071,8 @@ snapshots: flatted@3.3.2: {} + follow-redirects@1.15.9: {} + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -4826,9 +4863,9 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@5.22.0: + prisma@6.1.0: dependencies: - '@prisma/engines': 5.22.0 + '@prisma/engines': 6.1.0 optionalDependencies: fsevents: 2.3.3 @@ -4837,6 +4874,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-from-env@1.1.0: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 diff --git a/src/adapters/adapter.ts b/src/adapters/adapter.ts new file mode 100644 index 0000000..b3d8dbd --- /dev/null +++ b/src/adapters/adapter.ts @@ -0,0 +1,206 @@ +import { Address } from "@minswap/lucid-cardano"; +import Big from "big.js"; + +import { Asset } from "../types/asset"; +import { FactoryV2 } from "../types/factory"; +import { LbeV2Types } from "../types/lbe-v2"; +import { PoolV1, PoolV2, StablePool } from "../types/pool"; +import { TxHistory } from "../types/tx.internal"; + +export type GetPoolByIdParams = { + id: string; +}; + +export type GetPoolInTxParams = { + txHash: string; +}; + +export type GetPoolPriceParams = { + pool: PoolV1.State; + decimalsA?: number; + decimalsB?: number; +}; + +export type GetPoolsParams = { + page?: number; + cursor?: string; +}; + +export type GetStablePoolHistoryParams = { + lpAsset: Asset; +}; + +export type GetStablePoolPriceParams = { + pool: StablePool.State; + assetAIndex: number; + assetBIndex: number; +}; + +export type GetV1PoolHistoryParams = { + id: string; +}; + +export type GetV2PoolHistoryParams = + | { + assetA: Asset; + assetB: Asset; + } + | { + lpAsset: Asset; + }; + +export type GetV2PoolPriceParams = { + pool: PoolV2.State; + decimalsA?: number; + decimalsB?: number; +}; + +export interface Adapter { + getAssetDecimals(asset: string): Promise; + + getDatumByDatumHash(datumHash: string): Promise; + + currentSlot(): Promise; + + /** + * Get pool state in a transaction. + * @param {Object} params - The parameters. + * @param {string} params.txHash - The transaction hash containing pool output. One of the way to acquire is by calling getPoolHistory. + * @returns {PoolV1.State} - Returns the pool state or null if the transaction doesn't contain pool. + */ + getV1PoolInTx({ txHash }: GetPoolInTxParams): Promise; + + /** + * Get a specific pool by its ID. + * @param {Object} params - The parameters. + * @param {string} params.pool - The pool ID. This is the asset name of a pool's NFT and LP tokens. It can also be acquired by calling pool.id. + * @returns {PoolV1.State | null} - Returns the pool or null if not found. + */ + getV1PoolById({ id }: GetPoolByIdParams): Promise; + + /** + * @returns The latest pools or empty array if current page is after last page + */ + getV1Pools(params: GetPoolsParams): Promise; + + getV1PoolHistory(params: GetV1PoolHistoryParams): Promise; + + /** + * Get pool price. + * @param {Object} params - The parameters to calculate pool price. + * @param {string} params.pool - The pool we want to get price. + * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from the adapter. + * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from the adapter. + * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. + */ + getV1PoolPrice(params: GetPoolPriceParams): Promise<[Big, Big]>; + + getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; + + getV2Pools( + params: GetPoolsParams + ): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; + + getV2PoolByPair(assetA: Asset, assetB: Asset): Promise; + + getV2PoolByLp(lpAsset: Asset): Promise; + + getV2PoolHistory(params: GetV2PoolHistoryParams): Promise; + + /** + * Get pool price. + * @param {Object} params - The parameters to calculate pool price. + * @param {string} params.pool - The pool we want to get price. + * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from the adapter. + * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from the adapter. + * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. + */ + getV2PoolPrice(params: GetV2PoolPriceParams): Promise<[Big, Big]>; + + getAllFactoriesV2(): Promise<{ + factories: FactoryV2.State[]; + errors: unknown[]; + }>; + + getFactoryV2ByPair( + assetA: Asset, + assetB: Asset + ): Promise; + + getAllStablePools(): Promise<{ + pools: StablePool.State[]; + errors: unknown[]; + }>; + + getStablePoolByLpAsset(lpAsset: Asset): Promise; + + getStablePoolByNFT(nft: Asset): Promise; + + getStablePoolHistory( + params: GetStablePoolHistoryParams + ): Promise; + + /** + * Get stable pool price. + * + * A Stable Pool can contain more than two assets, so we need to specify which assets we want to retrieve the price against by using assetAIndex and assetBIndex. + * @param {Object} params - The parameters to calculate pool price. + * @param {string} params.pool - The pool we want to get price. + * @param {number} params.assetAIndex + * @param {number} params.assetBIndex + * @returns {[string, string]} - Returns price of @assetA agains @assetB + */ + getStablePoolPrice(params: GetStablePoolPriceParams): Big; + + getAllLbeV2Factories(): Promise<{ + factories: LbeV2Types.FactoryState[]; + errors: unknown[]; + }>; + + getLbeV2Factory( + baseAsset: Asset, + raiseAsset: Asset + ): Promise; + + getLbeV2HeadAndTailFactory(lbeId: string): Promise<{ + head: LbeV2Types.FactoryState; + tail: LbeV2Types.FactoryState; + } | null>; + + getAllLbeV2Treasuries(): Promise<{ + treasuries: LbeV2Types.TreasuryState[]; + errors: unknown[]; + }>; + + getLbeV2TreasuryByLbeId( + lbeId: string + ): Promise; + + getAllLbeV2Managers(): Promise<{ + managers: LbeV2Types.ManagerState[]; + errors: unknown[]; + }>; + + getLbeV2ManagerByLbeId( + lbeId: string + ): Promise; + + getAllLbeV2Sellers(): Promise<{ + sellers: LbeV2Types.SellerState[]; + errors: unknown[]; + }>; + + getLbeV2SellerByLbeId(lbeId: string): Promise; + + getAllLbeV2Orders(): Promise<{ + orders: LbeV2Types.OrderState[]; + errors: unknown[]; + }>; + + getLbeV2OrdersByLbeId(lbeId: string): Promise; + + getLbeV2OrdersByLbeIdAndOwner( + lbeId: string, + owner: Address + ): Promise; +} diff --git a/src/adapter.ts b/src/adapters/blockfrost.ts similarity index 82% rename from src/adapter.ts rename to src/adapters/blockfrost.ts index 9fabb77..676395c 100644 --- a/src/adapter.ts +++ b/src/adapters/blockfrost.ts @@ -16,37 +16,37 @@ import * as Prisma from "@prisma/client"; import Big from "big.js"; import JSONBig from "json-bigint"; -import { StableswapCalculation } from "./calculate"; -import { PostgresRepositoryReader } from "./syncer/repository/postgres-repository"; -import { Asset } from "./types/asset"; +import { StableswapCalculation } from "../calculate"; +import { PostgresRepositoryReader } from "../syncer/repository/postgres-repository"; +import { Asset } from "../types/asset"; import { DexV1Constant, DexV2Constant, LbeV2Constant, StableswapConstant, -} from "./types/constants"; -import { FactoryV2 } from "./types/factory"; -import { LbeV2Types } from "./types/lbe-v2"; -import { NetworkEnvironment, NetworkId } from "./types/network"; -import { OrderV2 } from "./types/order"; -import { PoolV1, PoolV2, StablePool } from "./types/pool"; +} from "../types/constants"; +import { FactoryV2 } from "../types/factory"; +import { LbeV2Types } from "../types/lbe-v2"; +import { NetworkEnvironment, NetworkId } from "../types/network"; +import { OrderV2 } from "../types/order"; +import { PoolV1, PoolV2, StablePool } from "../types/pool"; import { checkValidPoolOutput, isValidPoolOutput, normalizeAssets, -} from "./types/pool.internal"; -import { StringUtils } from "./types/string"; -import { TxHistory, TxIn, Value } from "./types/tx.internal"; -import { getScriptHashFromAddress } from "./utils/address-utils.internal"; -import { networkEnvToLucidNetwork } from "./utils/network.internal"; - -export type GetPoolInTxParams = { - txHash: string; -}; - -export type GetPoolByIdParams = { - id: string; -}; +} from "../types/pool.internal"; +import { StringUtils } from "../types/string"; +import { TxHistory, TxIn, Value } from "../types/tx.internal"; +import { getScriptHashFromAddress } from "../utils/address-utils.internal"; +import { networkEnvToLucidNetwork } from "../utils/network.internal"; +import { + Adapter, + GetPoolByIdParams, + GetPoolInTxParams, + GetPoolPriceParams, + GetStablePoolPriceParams, + GetV2PoolPriceParams, +} from "./adapter"; export type GetPoolsParams = Omit & { page: number; @@ -67,178 +67,10 @@ export type GetV2PoolHistoryParams = PaginationOptions & } ); -export type GetPoolPriceParams = { - pool: PoolV1.State; - decimalsA?: number; - decimalsB?: number; -}; - -export type GetV2PoolPriceParams = { - pool: PoolV2.State; - decimalsA?: number; - decimalsB?: number; -}; - export type GetStablePoolHistoryParams = PaginationOptions & { lpAsset: Asset; }; -export type GetStablePoolPriceParams = { - pool: StablePool.State; - assetAIndex: number; - assetBIndex: number; -}; - -interface Adapter { - getAssetDecimals(asset: string): Promise; - - getDatumByDatumHash(datumHash: string): Promise; - - currentSlot(): Promise; - - /** - * Get pool state in a transaction. - * @param {Object} params - The parameters. - * @param {string} params.txHash - The transaction hash containing pool output. One of the way to acquire is by calling getPoolHistory. - * @returns {PoolV1.State} - Returns the pool state or null if the transaction doesn't contain pool. - */ - getV1PoolInTx({ txHash }: GetPoolInTxParams): Promise; - - /** - * Get a specific pool by its ID. - * @param {Object} params - The parameters. - * @param {string} params.pool - The pool ID. This is the asset name of a pool's NFT and LP tokens. It can also be acquired by calling pool.id. - * @returns {PoolV1.State | null} - Returns the pool or null if not found. - */ - getV1PoolById({ id }: GetPoolByIdParams): Promise; - - /** - * @returns The latest pools or empty array if current page is after last page - */ - getV1Pools(params: GetPoolsParams): Promise; - - getV1PoolHistory(params: GetV1PoolHistoryParams): Promise; - - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ - getV1PoolPrice(params: GetPoolPriceParams): Promise<[Big, Big]>; - - getAllV2Pools(): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; - - getV2Pools( - params: GetPoolsParams - ): Promise<{ pools: PoolV2.State[]; errors: unknown[] }>; - - getV2PoolByPair(assetA: Asset, assetB: Asset): Promise; - - getV2PoolByLp(lpAsset: Asset): Promise; - - getV2PoolHistory(params: GetV2PoolHistoryParams): Promise; - - /** - * Get pool price. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {string} [params.decimalsA] - The decimals of assetA in pool, if undefined then query from Blockfrost. - * @param {string} [params.decimalsB] - The decimals of assetB in pool, if undefined then query from Blockfrost. - * @returns {[string, string]} - Returns a pair of asset A/B price and B/A price, adjusted to decimals. - */ - getV2PoolPrice(params: GetV2PoolPriceParams): Promise<[Big, Big]>; - - getAllFactoriesV2(): Promise<{ - factories: FactoryV2.State[]; - errors: unknown[]; - }>; - - getFactoryV2ByPair( - assetA: Asset, - assetB: Asset - ): Promise; - - getAllStablePools(): Promise<{ - pools: StablePool.State[]; - errors: unknown[]; - }>; - - getStablePoolByLpAsset(lpAsset: Asset): Promise; - - getStablePoolByNFT(nft: Asset): Promise; - - getStablePoolHistory( - params: GetStablePoolHistoryParams - ): Promise; - - /** - * Get stable pool price. - * - * A Stable Pool can contain more than two assets, so we need to specify which assets we want to retrieve the price against by using assetAIndex and assetBIndex. - * @param {Object} params - The parameters to calculate pool price. - * @param {string} params.pool - The pool we want to get price. - * @param {number} params.assetAIndex - * @param {number} params.assetBIndex - * @returns {[string, string]} - Returns price of @assetA agains @assetB - */ - getStablePoolPrice(params: GetStablePoolPriceParams): Big; - - getAllLbeV2Factories(): Promise<{ - factories: LbeV2Types.FactoryState[]; - errors: unknown[]; - }>; - - getLbeV2Factory( - baseAsset: Asset, - raiseAsset: Asset - ): Promise; - - getLbeV2HeadAndTailFactory(lbeId: string): Promise<{ - head: LbeV2Types.FactoryState; - tail: LbeV2Types.FactoryState; - } | null>; - - getAllLbeV2Treasuries(): Promise<{ - treasuries: LbeV2Types.TreasuryState[]; - errors: unknown[]; - }>; - - getLbeV2TreasuryByLbeId( - lbeId: string - ): Promise; - - getAllLbeV2Managers(): Promise<{ - managers: LbeV2Types.ManagerState[]; - errors: unknown[]; - }>; - - getLbeV2ManagerByLbeId( - lbeId: string - ): Promise; - - getAllLbeV2Sellers(): Promise<{ - sellers: LbeV2Types.SellerState[]; - errors: unknown[]; - }>; - - getLbeV2SellerByLbeId(lbeId: string): Promise; - - getAllLbeV2Orders(): Promise<{ - orders: LbeV2Types.OrderState[]; - errors: unknown[]; - }>; - - getLbeV2OrdersByLbeId(lbeId: string): Promise; - - getLbeV2OrdersByLbeIdAndOwner( - lbeId: string, - owner: Address - ): Promise; -} - export class BlockfrostAdapter implements Adapter { protected readonly networkId: NetworkId; private readonly blockFrostApi: BlockFrostAPI; @@ -334,7 +166,10 @@ export class BlockfrostAdapter implements Adapter { utxo.data_hash, `expect pool to have datum hash, got ${utxo.data_hash}` ); - const txIn: TxIn = { txHash: utxo.tx_hash, index: utxo.output_index }; + const txIn: TxIn = { + txHash: utxo.tx_hash, + index: utxo.output_index, + }; return new PoolV1.State( utxo.address, txIn, diff --git a/src/adapters/index.ts b/src/adapters/index.ts new file mode 100644 index 0000000..631bd0f --- /dev/null +++ b/src/adapters/index.ts @@ -0,0 +1 @@ +export * from "./adapter"; diff --git a/src/adapters/maestro.ts b/src/adapters/maestro.ts new file mode 100644 index 0000000..8ddc1d6 --- /dev/null +++ b/src/adapters/maestro.ts @@ -0,0 +1,914 @@ +import { + Asset as MaestroUtxoAsset, + MaestroClient, + UtxoWithSlot, +} from "@maestro-org/typescript-sdk"; +import { Address } from "@minswap/lucid-cardano"; +import invariant from "@minswap/tiny-invariant"; +import Big from "big.js"; + +import { PoolV1, PoolV2, StablePool } from ".."; +import { StableswapCalculation } from "../calculate"; +import { Asset } from "../types/asset"; +import { + DexV1Constant, + DexV2Constant, + LbeV2Constant, + StableswapConstant, +} from "../types/constants"; +import { FactoryV2 } from "../types/factory"; +import { LbeV2Types } from "../types/lbe-v2"; +import { NetworkId } from "../types/network"; +import { OrderV2 } from "../types/order"; +import { + checkValidPoolOutput, + isValidPoolOutput, + normalizeAssets, +} from "../types/pool.internal"; +import { StringUtils } from "../types/string"; +import { TxHistory, TxIn, Value } from "../types/tx.internal"; +import { getScriptHashFromAddress } from "../utils/address-utils.internal"; +import { + Adapter, + GetPoolByIdParams, + GetPoolInTxParams, + GetPoolPriceParams, + GetStablePoolHistoryParams, + GetStablePoolPriceParams, + GetV2PoolHistoryParams, + GetV2PoolPriceParams, +} from "./adapter"; + +export declare class MaestroServerError { + code: number; + error: string; + message: string; +} + +export type MaestroPaginationOptions = { + count?: number; + cursor?: string; + order?: "asc" | "desc"; +}; + +export type GetPoolsParams = Omit & { + cursor: string; +}; + +export type GetV1PoolHistoryParams = MaestroPaginationOptions & { + id: string; +}; + +export class MaestroAdapter implements Adapter { + protected readonly networkId: NetworkId; + private readonly maestroClient: MaestroClient; + + constructor(networkId: NetworkId, maestroClient: MaestroClient) { + this.networkId = networkId; + this.maestroClient = maestroClient; + } + + private mapMaestroAssetToValue(assets: MaestroUtxoAsset[]): Value { + return assets.map((asset) => ({ + unit: asset.unit.toString(), + quantity: asset.amount.toString(), + })); + } + + public async getAssetDecimals(asset: string): Promise { + if (asset === "lovelace") { + return 6; + } + try { + const assetAInfo = await this.maestroClient.assets.assetInfo(asset); + return assetAInfo.data.token_registry_metadata?.decimals ?? 0; + } catch { + return 0; + } + } + + public async getDatumByDatumHash(datumHash: string): Promise { + const scriptsDatum = await this.maestroClient.datum.lookupDatum(datumHash); + return scriptsDatum.data.bytes; + } + + public async currentSlot(): Promise { + const latestBlock = (await this.maestroClient.blocks.blockLatest()).data + .absolute_slot; + return latestBlock ?? 0; + } + + public async getV1PoolInTx({ + txHash, + }: GetPoolInTxParams): Promise { + const poolTx = await this.maestroClient.transactions.txInfo(txHash); + const poolUtxo = poolTx.data.outputs.find( + (o: (typeof poolTx.data.outputs)[number]) => + getScriptHashFromAddress(o.address) === DexV1Constant.POOL_SCRIPT_HASH + ); + if (!poolUtxo) { + return null; + } + + const poolUtxoAmount = this.mapMaestroAssetToValue(poolUtxo.assets); + const poolUtxoDatumHash = poolUtxo.datum?.hash ?? ""; + + checkValidPoolOutput(poolUtxo.address, poolUtxoAmount, poolUtxoDatumHash); + invariant( + poolUtxoDatumHash, + `expect pool to have datum hash, got ${poolUtxoDatumHash}` + ); + + const txIn: TxIn = { txHash: txHash, index: poolUtxo.index }; + return new PoolV1.State( + poolUtxo.address, + txIn, + poolUtxoAmount, + poolUtxoDatumHash + ); + } + + public async getV1PoolById({ + id, + }: GetPoolByIdParams): Promise { + const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; + const nftTxs = await this.maestroClient.assets.assetTxs(nft, { + count: 1, + order: "desc", + }); + if (nftTxs.data.length === 0) { + return null; + } + return this.getV1PoolInTx({ txHash: nftTxs.data[0].tx_hash }); + } + + public async getV1Pools({ + cursor, + count = 100, + order = "asc", + }: GetPoolsParams): Promise { + const utxosResponse = await this.maestroClient.addresses.utxosByPaymentCred( + DexV1Constant.POOL_SCRIPT_HASH, + { cursor, count, order } + ); + const utxos = utxosResponse.data; + return utxos + .filter((utxo: (typeof utxos)[number]) => + isValidPoolOutput( + utxo.address, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum?.hash ?? "" + ) + ) + .map((utxo: (typeof utxos)[number]) => { + invariant( + utxo.datum?.hash, + `expect pool to have datum hash, got ${utxo.datum?.hash}` + ); + const txIn: TxIn = { txHash: utxo.tx_hash, index: utxo.index }; + return new PoolV1.State( + utxo.address, + txIn, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum?.hash + ); + }); + } + + public async getV1PoolHistory({ + id, + count = 100, + order = "desc", + }: GetV1PoolHistoryParams): Promise { + const nft = `${DexV1Constant.POOL_NFT_POLICY_ID}${id}`; + const nftTxs = await this.maestroClient.assets.assetTxs(nft, { + count, + order, + }); + const nftTxsData = nftTxs.data; + return nftTxsData.map( + (tx: (typeof nftTxsData)[number]): TxHistory => ({ + txHash: tx.tx_hash, + txIndex: 0, // TBD if this works: Maestro Asset Txs doesn't return index + blockHeight: tx.slot, + time: new Date(tx.timestamp), + }) + ); + } + + public async getV1PoolPrice({ + pool, + decimalsA, + decimalsB, + }: GetPoolPriceParams): Promise<[Big, Big]> { + if (decimalsA === undefined) { + decimalsA = await this.getAssetDecimals(pool.assetA); + } + if (decimalsB === undefined) { + decimalsB = await this.getAssetDecimals(pool.assetB); + } + const adjustedReserveA = Big(pool.reserveA.toString()).div( + Big(10).pow(decimalsA) + ); + const adjustedReserveB = Big(pool.reserveB.toString()).div( + Big(10).pow(decimalsB) + ); + const priceAB = adjustedReserveA.div(adjustedReserveB); + const priceBA = adjustedReserveB.div(adjustedReserveA); + return [priceAB, priceBA]; + } + + public async getAllV2Pools(): Promise<{ + pools: PoolV2.State[]; + errors: unknown[]; + }> { + const v2Config = DexV2Constant.CONFIG[this.networkId]; + const utxos = await this.maestroClient.addresses.utxosByPaymentCred( + v2Config.poolScriptHashBech32, + { + asset: v2Config.poolAuthenAsset, + } + ); + + let utxosData: UtxoWithSlot[] = utxos.data; + let nextCursor: string | null = utxos.next_cursor ?? null; + + while (nextCursor) { + const utxosResponse = + await this.maestroClient.addresses.utxosByPaymentCred( + v2Config.poolScriptHashBech32, + { + asset: v2Config.poolAuthenAsset, + cursor: nextCursor, + } + ); + utxosData = utxosData.concat(utxosResponse.data); + nextCursor = utxosResponse.next_cursor ?? null; + } + + const pools: PoolV2.State[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (!utxo.datum?.type) { + throw new Error(`Cannot find datum of Pool V2, tx: ${utxo.tx_hash}`); + } + const pool = new PoolV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum?.bytes ?? "" + ); + pools.push(pool); + } catch (err) { + errors.push(err); + } + } + return { + pools: pools, + errors: errors, + }; + } + + public async getV2Pools({ + cursor, + count = 100, + order = "asc", + }: GetPoolsParams): Promise<{ pools: PoolV2.State[]; errors: unknown[] }> { + const v2Config = DexV2Constant.CONFIG[this.networkId]; + const utxos = await this.maestroClient.addresses.utxosByPaymentCred( + v2Config.poolScriptHashBech32, + { + asset: v2Config.poolAuthenAsset, + cursor, + count, + order, + } + ); + const utxosData = utxos.data; + + const pools: PoolV2.State[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error(`Cannot find datum of Pool V2, tx: ${utxo.tx_hash}`); + } + const pool = new PoolV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + pools.push(pool); + } catch (err) { + errors.push(err); + } + } + return { + pools: pools, + errors: errors, + }; + } + + public async getV2PoolByPair( + assetA: Asset, + assetB: Asset + ): Promise { + const [normalizedAssetA, normalizedAssetB] = normalizeAssets( + Asset.toString(assetA), + Asset.toString(assetB) + ); + const { pools: allPools } = await this.getAllV2Pools(); + return ( + allPools.find( + (pool) => + pool.assetA === normalizedAssetA && pool.assetB === normalizedAssetB + ) ?? null + ); + } + + public async getV2PoolByLp(lpAsset: Asset): Promise { + const { pools: allPools } = await this.getAllV2Pools(); + return ( + allPools.find((pool) => Asset.compare(pool.lpAsset, lpAsset) === 0) ?? + null + ); + } + + public async getV2PoolHistory( + _params: GetV2PoolHistoryParams + ): Promise { + throw Error("Not supported yet. Please use MinswapAdapter"); + } + + public async getV2PoolPrice({ + pool, + decimalsA, + decimalsB, + }: GetV2PoolPriceParams): Promise<[Big, Big]> { + if (decimalsA === undefined) { + decimalsA = await this.getAssetDecimals(pool.assetA); + } + if (decimalsB === undefined) { + decimalsB = await this.getAssetDecimals(pool.assetB); + } + const adjustedReserveA = Big(pool.reserveA.toString()).div( + Big(10).pow(decimalsA) + ); + const adjustedReserveB = Big(pool.reserveB.toString()).div( + Big(10).pow(decimalsB) + ); + const priceAB = adjustedReserveA.div(adjustedReserveB); + const priceBA = adjustedReserveB.div(adjustedReserveA); + return [priceAB, priceBA]; + } + + public async getAllFactoriesV2(): Promise<{ + factories: FactoryV2.State[]; + errors: unknown[]; + }> { + const v2Config = DexV2Constant.CONFIG[this.networkId]; + const utxos = await this.maestroClient.addresses.utxosByAddress( + v2Config.factoryScriptHashBech32, + { + asset: v2Config.factoryAsset, + } + ); + const utxosData = utxos.data; + + const factories: FactoryV2.State[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of Factory V2, tx: ${utxo.tx_hash}` + ); + } + const factory = new FactoryV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + factories.push(factory); + } catch (err) { + errors.push(err); + } + } + return { + factories: factories, + errors: errors, + }; + } + + public async getFactoryV2ByPair( + assetA: Asset, + assetB: Asset + ): Promise { + const factoryIdent = PoolV2.computeLPAssetName(assetA, assetB); + const { factories: allFactories } = await this.getAllFactoriesV2(); + for (const factory of allFactories) { + if ( + StringUtils.compare(factory.head, factoryIdent) < 0 && + StringUtils.compare(factoryIdent, factory.tail) < 0 + ) { + return factory; + } + } + + return null; + } + + public async getAllV2Orders(): Promise<{ + orders: OrderV2.State[]; + errors: unknown[]; + }> { + const v2Config = DexV2Constant.CONFIG[this.networkId]; + const utxos = await this.maestroClient.addresses.utxosByAddress( + v2Config.orderScriptHashBech32 + ); + + const utxosData = utxos.data; + const orders: OrderV2.State[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + let order: OrderV2.State | undefined = undefined; + if (utxo.datum?.type === "inline") { + order = new OrderV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + } else if (utxo.datum?.hash !== null) { + const orderDatumHash = utxo.datum?.hash ?? ""; + const orderDatum = + await this.maestroClient.datum.lookupDatum(orderDatumHash); + order = new OrderV2.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + orderDatum.data.bytes + ); + } + + if (order === undefined) { + throw new Error(`Cannot find datum of Order V2, tx: ${utxo.tx_hash}`); + } + + orders.push(order); + } catch (err) { + errors.push(err); + } + } + return { + orders: orders, + errors: errors, + }; + } + + private async parseStablePoolState( + utxo: UtxoWithSlot + ): Promise { + let datum: string; + if (utxo.datum?.type) { + datum = utxo.datum.bytes ?? ""; + } else if (utxo.datum?.hash) { + datum = await this.getDatumByDatumHash(utxo.datum.hash); + } else { + throw new Error("Cannot find datum of Stable Pool"); + } + const pool = new StablePool.State( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + datum + ); + return pool; + } + + public async getAllStablePools(): Promise<{ + pools: StablePool.State[]; + errors: unknown[]; + }> { + const poolAddresses = StableswapConstant.CONFIG[this.networkId].map( + (cfg) => cfg.poolAddress + ); + const pools: StablePool.State[] = []; + const errors: unknown[] = []; + for (const poolAddr of poolAddresses) { + const utxos = await this.maestroClient.addresses.utxosByAddress(poolAddr); + const utxosData = utxos.data; + try { + for (const utxo of utxosData) { + const pool = await this.parseStablePoolState(utxo); + pools.push(pool); + } + } catch (err) { + errors.push(err); + } + } + + return { + pools: pools, + errors: errors, + }; + } + + public async getStablePoolByLpAsset( + lpAsset: Asset + ): Promise { + const config = StableswapConstant.CONFIG[this.networkId].find( + (cfg) => cfg.lpAsset === Asset.toString(lpAsset) + ); + invariant( + config, + `getStablePoolByLpAsset: Can not find stableswap config by LP Asset ${Asset.toString( + lpAsset + )}` + ); + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.poolAddress, + { + asset: config.nftAsset, + } + ); + const utxosData = utxos.data; + + if (utxosData.length === 1) { + const poolUtxo = utxosData[0]; + return await this.parseStablePoolState(poolUtxo); + } + return null; + } + + public async getStablePoolByNFT( + nft: Asset + ): Promise { + const poolAddress = StableswapConstant.CONFIG[this.networkId].find( + (cfg) => cfg.nftAsset === Asset.toString(nft) + )?.poolAddress; + if (!poolAddress) { + throw new Error( + `Cannot find Stable Pool having NFT ${Asset.toString(nft)}` + ); + } + + const utxos = await this.maestroClient.addresses.utxosByAddress( + poolAddress, + { + asset: Asset.toString(nft), + } + ); + const utxosData = utxos.data; + + if (utxosData.length === 1) { + const poolUtxo = utxosData[0]; + return await this.parseStablePoolState(poolUtxo); + } + return null; + } + + getStablePoolHistory( + _params: GetStablePoolHistoryParams + ): Promise { + throw Error("Not supported yet. Please use MinswapAdapter"); + } + + public getStablePoolPrice({ + pool, + assetAIndex, + assetBIndex, + }: GetStablePoolPriceParams): Big { + const config = pool.config; + const [priceNum, priceDen] = StableswapCalculation.getPrice( + pool.datum.balances, + config.multiples, + pool.amp, + assetAIndex, + assetBIndex + ); + + return Big(priceNum.toString()).div(priceDen.toString()); + } + + public async getAllLbeV2Factories(): Promise<{ + factories: LbeV2Types.FactoryState[]; + errors: unknown[]; + }> { + const config = LbeV2Constant.CONFIG[this.networkId]; + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.factoryHashBech32, + { + asset: config.factoryAsset, + } + ); + const utxosData = utxos.data; + + const factories: LbeV2Types.FactoryState[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of LBE V2 Factory, tx: ${utxo.tx_hash}` + ); + } + + const factory = new LbeV2Types.FactoryState( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + factories.push(factory); + } catch (err) { + errors.push(err); + } + } + return { + factories: factories, + errors: errors, + }; + } + + public async getLbeV2Factory( + baseAsset: Asset, + raiseAsset: Asset + ): Promise { + const factoryIdent = PoolV2.computeLPAssetName(baseAsset, raiseAsset); + const { factories: allFactories } = await this.getAllLbeV2Factories(); + for (const factory of allFactories) { + if ( + StringUtils.compare(factory.head, factoryIdent) < 0 && + StringUtils.compare(factoryIdent, factory.tail) < 0 + ) { + return factory; + } + } + + return null; + } + + public async getLbeV2HeadAndTailFactory(lbeId: string): Promise<{ + head: LbeV2Types.FactoryState; + tail: LbeV2Types.FactoryState; + } | null> { + const { factories: allFactories } = await this.getAllLbeV2Factories(); + let head: LbeV2Types.FactoryState | undefined = undefined; + let tail: LbeV2Types.FactoryState | undefined = undefined; + for (const factory of allFactories) { + if (factory.head === lbeId) { + tail = factory; + } + if (factory.tail === lbeId) { + head = factory; + } + } + if (head === undefined || tail === undefined) { + return null; + } + return { head, tail }; + } + + public async getAllLbeV2Treasuries(): Promise<{ + treasuries: LbeV2Types.TreasuryState[]; + errors: unknown[]; + }> { + const config = LbeV2Constant.CONFIG[this.networkId]; + + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.treasuryHashBech32, + { + asset: config.treasuryAsset, + } + ); + const utxosData = utxos.data; + + const treasuries: LbeV2Types.TreasuryState[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of LBE V2 Treasury, tx: ${utxo.tx_hash}` + ); + } + + const treasury = new LbeV2Types.TreasuryState( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + treasuries.push(treasury); + } catch (err) { + errors.push(err); + } + } + return { + treasuries: treasuries, + errors: errors, + }; + } + + public async getLbeV2TreasuryByLbeId( + lbeId: string + ): Promise { + const { treasuries: allTreasuries } = await this.getAllLbeV2Treasuries(); + for (const treasury of allTreasuries) { + if (treasury.lbeId === lbeId) { + return treasury; + } + } + return null; + } + + public async getAllLbeV2Managers(): Promise<{ + managers: LbeV2Types.ManagerState[]; + errors: unknown[]; + }> { + const config = LbeV2Constant.CONFIG[this.networkId]; + + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.managerHashBech32, + { + asset: config.managerAsset, + } + ); + const utxosData = utxos.data; + + const managers: LbeV2Types.ManagerState[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of Lbe V2 Manager, tx: ${utxo.tx_hash}` + ); + } + + const manager = new LbeV2Types.ManagerState( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + managers.push(manager); + } catch (err) { + errors.push(err); + } + } + return { + managers: managers, + errors: errors, + }; + } + + public async getLbeV2ManagerByLbeId( + lbeId: string + ): Promise { + const { managers } = await this.getAllLbeV2Managers(); + for (const manager of managers) { + if (manager.lbeId === lbeId) { + return manager; + } + } + return null; + } + + public async getAllLbeV2Sellers(): Promise<{ + sellers: LbeV2Types.SellerState[]; + errors: unknown[]; + }> { + const config = LbeV2Constant.CONFIG[this.networkId]; + + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.sellerHashBech32, + { + asset: config.sellerAsset, + } + ); + const utxosData = utxos.data; + + const sellers: LbeV2Types.SellerState[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of Lbe V2 Seller, tx: ${utxo.tx_hash}` + ); + } + + const seller = new LbeV2Types.SellerState( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + sellers.push(seller); + } catch (err) { + errors.push(err); + } + } + return { + sellers: sellers, + errors: errors, + }; + } + + public async getLbeV2SellerByLbeId( + lbeId: string + ): Promise { + const { sellers } = await this.getAllLbeV2Sellers(); + for (const seller of sellers) { + if (seller.lbeId === lbeId) { + return seller; + } + } + return null; + } + + public async getAllLbeV2Orders(): Promise<{ + orders: LbeV2Types.OrderState[]; + errors: unknown[]; + }> { + const config = LbeV2Constant.CONFIG[this.networkId]; + + const utxos = await this.maestroClient.addresses.utxosByAddress( + config.orderHashBech32, + { + asset: config.orderAsset, + } + ); + const utxosData = utxos.data; + + const orders: LbeV2Types.OrderState[] = []; + const errors: unknown[] = []; + for (const utxo of utxosData) { + try { + if (utxo.datum?.type != "inline") { + throw new Error( + `Cannot find datum of Lbe V2 Order, tx: ${utxo.tx_hash}` + ); + } + + const order = new LbeV2Types.OrderState( + this.networkId, + utxo.address, + { txHash: utxo.tx_hash, index: utxo.index }, + this.mapMaestroAssetToValue(utxo.assets), + utxo.datum.hash + ); + orders.push(order); + } catch (err) { + errors.push(err); + } + } + return { + orders: orders, + errors: errors, + }; + } + + public async getLbeV2OrdersByLbeId( + lbeId: string + ): Promise { + const { orders: allOrders } = await this.getAllLbeV2Orders(); + const orders: LbeV2Types.OrderState[] = []; + for (const order of allOrders) { + if (order.lbeId === lbeId) { + orders.push(order); + } + } + return orders; + } + + public async getLbeV2OrdersByLbeIdAndOwner( + lbeId: string, + owner: Address + ): Promise { + const { orders: allOrders } = await this.getAllLbeV2Orders(); + const orders: LbeV2Types.OrderState[] = []; + for (const order of allOrders) { + if (order.lbeId === lbeId && order.owner === owner) { + orders.push(order); + } + } + return orders; + } +} diff --git a/src/dex-v2.ts b/src/dex-v2.ts index a94f05c..9b4efee 100644 --- a/src/dex-v2.ts +++ b/src/dex-v2.ts @@ -13,21 +13,20 @@ import { } from "@minswap/lucid-cardano"; import invariant from "@minswap/tiny-invariant"; +import { BlockfrostAdapter } from "./adapters/blockfrost"; +import { BatcherFee } from "./batcher-fee-reduction/calculate"; +import { DexVersion } from "./batcher-fee-reduction/configs.internal"; +import { compareUtxo, DexV2Calculation } from "./calculate"; +import { Asset } from "./types/asset"; import { - Asset, - BlockfrostAdapter, - compareUtxo, - DexV2Calculation, DexV2Constant, FIXED_DEPOSIT_ADA, MetadataMessage, - OrderV2, - PoolV2, -} from "."; -import { BatcherFee } from "./batcher-fee-reduction/calculate"; -import { DexVersion } from "./batcher-fee-reduction/configs.internal"; +} from "./types/constants"; import { FactoryV2 } from "./types/factory"; import { NetworkEnvironment, NetworkId } from "./types/network"; +import { OrderV2 } from "./types/order"; +import { PoolV2 } from "./types/pool"; import { lucidToNetworkEnv } from "./utils/network.internal"; import { buildUtxoToStoreDatum } from "./utils/tx.internal"; diff --git a/src/expired-order-monitor.ts b/src/expired-order-monitor.ts index 98ec277..2d3360d 100644 --- a/src/expired-order-monitor.ts +++ b/src/expired-order-monitor.ts @@ -1,6 +1,7 @@ import { Lucid } from "@minswap/lucid-cardano"; -import { BlockfrostAdapter, DexV2, DexV2Constant, OrderV2 } from "."; +import { DexV2, DexV2Constant, OrderV2 } from "."; +import { BlockfrostAdapter } from "./adapters/blockfrost"; import { runRecurringJob } from "./utils/job"; type DexV2WorkerConstructor = { diff --git a/src/index.ts b/src/index.ts index f975a77..47aa7c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from "./adapter"; +export * from "./adapters"; export * from "./calculate"; export * from "./dex"; export * from "./dex-v2"; diff --git a/src/lbe-v2-worker/worker.ts b/src/lbe-v2-worker/worker.ts index 3624edc..fa80c77 100644 --- a/src/lbe-v2-worker/worker.ts +++ b/src/lbe-v2-worker/worker.ts @@ -1,7 +1,8 @@ import { Data, Lucid, UnixTime, UTxO } from "@minswap/lucid-cardano"; import invariant from "@minswap/tiny-invariant"; -import { BlockfrostAdapter, LbeV2Constant, PoolV2 } from ".."; +import { LbeV2Constant, PoolV2 } from ".."; +import { BlockfrostAdapter } from "../adapters/blockfrost"; import { LbeV2 } from "../lbe-v2/lbe-v2"; import { LbeV2Types } from "../types/lbe-v2"; import { NetworkEnvironment, NetworkId } from "../types/network"; diff --git a/src/types/constants.ts b/src/types/constants.ts index 7c73380..f5eab20 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -1,7 +1,7 @@ import { Address, OutRef, Script } from "@minswap/lucid-cardano"; import invariant from "@minswap/tiny-invariant"; -import { Asset } from ".."; +import { Asset } from "./asset"; import { NetworkEnvironment, NetworkId } from "./network"; export namespace DexV1Constant { diff --git a/src/utils/lucid.ts b/src/utils/lucid.ts index 18d5471..d06f3ff 100644 --- a/src/utils/lucid.ts +++ b/src/utils/lucid.ts @@ -1,4 +1,11 @@ -import { Address, Blockfrost, Lucid, Network } from "@minswap/lucid-cardano"; +import { + Address, + Blockfrost, + Lucid, + Maestro, + MaestroSupportedNetworks, + Network, +} from "@minswap/lucid-cardano"; /** * Initialize Lucid Instance for Backend Environment @@ -12,7 +19,7 @@ export async function getBackendLucidInstance( network: Network, projectId: string, blockfrostUrl: string, - address: Address + address: Address, ): Promise { const provider = new Blockfrost(blockfrostUrl, projectId); const lucid = await Lucid.new(provider, network); @@ -21,3 +28,26 @@ export async function getBackendLucidInstance( }); return lucid; } + +/** + * Initialize Lucid Maestro Instance for Backend Environment + * @param network Network you're working on + * @param apiKey Maestro API KEY + * @param address Your own address + * @returns + */ +export async function getBackendMaestroLucidInstance( + network: MaestroSupportedNetworks, + apiKey: string, + address: Address, +): Promise { + const provider = new Maestro({ + network: network, + apiKey: apiKey, + }); + const lucid = await Lucid.new(provider, network); + lucid.selectWalletFrom({ + address: address, + }); + return lucid; +} diff --git a/test/adapter.test.ts b/test/adapter.test.ts index 7d06918..40c8d03 100644 --- a/test/adapter.test.ts +++ b/test/adapter.test.ts @@ -1,12 +1,13 @@ import { BlockFrostAPI } from "@blockfrost/blockfrost-js"; +import { Configuration, MaestroClient } from "@maestro-org/typescript-sdk"; +import { Network } from "@minswap/lucid-cardano"; -import { - ADA, - Asset, - BlockfrostAdapter, - NetworkId, - StableswapConstant, -} from "../src"; +import { Adapter } from "../src/adapters"; +import { BlockfrostAdapter } from "../src/adapters/blockfrost"; +import { MaestroAdapter } from "../src/adapters/maestro"; +import { ADA, Asset } from "../src/types/asset"; +import { StableswapConstant } from "../src/types/constants"; +import { NetworkId } from "../src/types/network"; function mustGetEnv(key: string): string { const val = process.env[key]; @@ -25,164 +26,184 @@ const MIN_ADA_POOL_V1_ID_TESTNET = const MIN_ADA_POOL_V1_ID_MAINNET = "6aa2153e1ae896a95539c9d62f76cedcdabdcdf144e564b8955f609d660cf6a2"; -let adapterTestnet: BlockfrostAdapter; -let adapterMainnet: BlockfrostAdapter; - -beforeAll(() => { - adapterTestnet = new BlockfrostAdapter( +function getBlockfrostAdapters(): [Adapter, Adapter] { + const blockfrostAdapterTestnet = new BlockfrostAdapter( NetworkId.TESTNET, new BlockFrostAPI({ projectId: mustGetEnv("BLOCKFROST_PROJECT_ID_TESTNET"), network: "preprod", }) ); - adapterMainnet = new BlockfrostAdapter( + const blockfrostAdapterMainnet = new BlockfrostAdapter( NetworkId.MAINNET, new BlockFrostAPI({ projectId: mustGetEnv("BLOCKFROST_PROJECT_ID_MAINNET"), network: "mainnet", }) ); -}); - -test("getAssetDecimals", async () => { - expect(await adapterTestnet.getAssetDecimals("lovelace")).toBe(6); - expect(await adapterTestnet.getAssetDecimals(MIN_TESTNET)).toBe(0); - expect(await adapterMainnet.getAssetDecimals("lovelace")).toBe(6); - expect(await adapterMainnet.getAssetDecimals(MIN_MAINNET)).toBe(6); -}); - -async function testPoolPrice(adapter: BlockfrostAdapter): Promise { - const pools = await adapter.getV1Pools({ - page: 1, - }); - expect(pools.length).toBeGreaterThan(0); - // check random 5 pools - for (let i = 0; i < 5; i++) { - const idx = Math.floor(Math.random() * pools.length); - const pool = pools[idx]; - const [priceAB, priceBA] = await adapter.getV1PoolPrice({ pool }); - // product of 2 prices must be approximately equal to 1 - // abs(priceAB * priceBA - 1) <= epsilon - expect(priceAB.mul(priceBA).sub(1).abs().toNumber()).toBeLessThanOrEqual( - 1e-6 - ); - } + return [blockfrostAdapterTestnet, blockfrostAdapterMainnet]; } -test("getPoolPrice", async () => { - await testPoolPrice(adapterTestnet); - await testPoolPrice(adapterMainnet); -}, 10000); - -test("getV1PoolById", async () => { - const adaMINTestnet = await adapterTestnet.getV1PoolById({ - id: MIN_ADA_POOL_V1_ID_TESTNET, - }); - expect(adaMINTestnet).not.toBeNull(); - expect(adaMINTestnet?.assetA).toEqual("lovelace"); - expect(adaMINTestnet?.assetB).toEqual(MIN_TESTNET); +function getMaestroAdapters(): [Adapter, Adapter] { + const cardanoNetworkPreprod: Network = "Preprod"; + const maestroAdapterTestnet = new MaestroAdapter( + NetworkId.TESTNET, + new MaestroClient( + new Configuration({ + apiKey: mustGetEnv("MAESTRO_API_KEY_TESTNET"), + network: cardanoNetworkPreprod, + }) + ) + ); + const cardanoNetworkMainnet: Network = "Mainnet"; + const maestroAdapterMainnet = new MaestroAdapter( + NetworkId.MAINNET, + new MaestroClient( + new Configuration({ + apiKey: mustGetEnv("MAESTRO_API_KEY_MAINNET"), + network: cardanoNetworkMainnet, + }) + ) + ); + return [maestroAdapterTestnet, maestroAdapterMainnet]; +} - const adaMINMainnet = await adapterMainnet.getV1PoolById({ - id: MIN_ADA_POOL_V1_ID_MAINNET, +describe.each([ + ["Blockfrost", ...getBlockfrostAdapters()], + ["Maestro", ...getMaestroAdapters()], +])("Run test with %s adapter", (_name, adapterTestnet, adapterMainnet) => { + test("getAssetDecimals", async () => { + expect(await adapterTestnet.getAssetDecimals("lovelace")).toBe(6); + expect(await adapterTestnet.getAssetDecimals(MIN_TESTNET)).toBe(0); + expect(await adapterMainnet.getAssetDecimals("lovelace")).toBe(6); + expect(await adapterMainnet.getAssetDecimals(MIN_MAINNET)).toBe(6); }); - expect(adaMINMainnet).not.toBeNull(); - expect(adaMINMainnet?.assetA).toEqual("lovelace"); - expect(adaMINMainnet?.assetB).toEqual(MIN_MAINNET); -}); -async function testPriceHistory( - adapter: BlockfrostAdapter, - id: string -): Promise { - const history = await adapter.getV1PoolHistory({ id: id }); - for (let i = 0; i < Math.min(5, history.length); i++) { - const pool = await adapter.getV1PoolInTx({ txHash: history[i].txHash }); - expect(pool?.txIn.txHash).toEqual(history[i].txHash); + async function testPoolPrice(adapter: Adapter): Promise { + const pools = await adapter.getV1Pools({}); + expect(pools.length).toBeGreaterThan(0); + // check random 5 pools + for (let i = 0; i < 5; i++) { + const idx = Math.floor(Math.random() * pools.length); + const pool = pools[idx]; + const [priceAB, priceBA] = await adapter.getV1PoolPrice({ pool }); + // product of 2 prices must be approximately equal to 1 + // abs(priceAB * priceBA - 1) <= epsilon + expect(priceAB.mul(priceBA).sub(1).abs().toNumber()).toBeLessThanOrEqual( + 1e-6 + ); + } } -} - -test("get prices of last 5 states of MIN/ADA pool", async () => { - await testPriceHistory(adapterTestnet, MIN_ADA_POOL_V1_ID_TESTNET); - await testPriceHistory(adapterMainnet, MIN_ADA_POOL_V1_ID_MAINNET); -}); -test("getV2PoolByPair", async () => { - const pool = await adapterTestnet.getV2PoolByPair(ADA, { - policyId: "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72", - tokenName: "4d494e", + test("getPoolPrice", async () => { + await testPoolPrice(adapterTestnet); + await testPoolPrice(adapterMainnet); + }, 10000); + + test("getV1PoolById", async () => { + const adaMINTestnet = await adapterTestnet.getV1PoolById({ + id: MIN_ADA_POOL_V1_ID_TESTNET, + }); + expect(adaMINTestnet).not.toBeNull(); + expect(adaMINTestnet?.assetA).toEqual("lovelace"); + expect(adaMINTestnet?.assetB).toEqual(MIN_TESTNET); + + const adaMINMainnet = await adapterMainnet.getV1PoolById({ + id: MIN_ADA_POOL_V1_ID_MAINNET, + }); + expect(adaMINMainnet).not.toBeNull(); + expect(adaMINMainnet?.assetA).toEqual("lovelace"); + expect(adaMINMainnet?.assetB).toEqual(MIN_MAINNET); }); - expect(pool).not.toBeNull(); - expect(pool?.assetA).toEqual("lovelace"); - expect(pool?.assetB).toEqual(MIN_TESTNET); -}); -test("getAllV2Pools", async () => { - const { pools } = await adapterTestnet.getAllV2Pools(); - expect(pools.length > 0); -}); + async function testPriceHistory(adapter: any, id: string): Promise { + const history = await adapter.getV1PoolHistory({ id }); + for (let i = 0; i < Math.min(5, history.length); i++) { + const pool = await adapter.getV1PoolInTx({ txHash: history[i].txHash }); + expect(pool?.txIn.txHash).toEqual(history[i].txHash); + } + } -test("getV2Pools", async () => { - const { pools } = await adapterTestnet.getV2Pools({ - page: 1, + test("get prices of last 5 states of MIN/ADA pool", async () => { + await testPriceHistory(adapterTestnet, MIN_ADA_POOL_V1_ID_TESTNET); + await testPriceHistory(adapterMainnet, MIN_ADA_POOL_V1_ID_MAINNET); }); - expect(pools.length > 0); -}); - -test("getAllStablePools", async () => { - const numberOfStablePoolsTestnet = - StableswapConstant.CONFIG[NetworkId.TESTNET].length; - const numberOfStablePoolsMainnet = - StableswapConstant.CONFIG[NetworkId.MAINNET].length; - const { pools: testnetPools } = await adapterTestnet.getAllStablePools(); - expect(testnetPools.length === numberOfStablePoolsTestnet); - const { pools: mainnetPools } = await adapterMainnet.getAllStablePools(); - expect(mainnetPools.length === numberOfStablePoolsMainnet); -}); + test("getV2PoolByPair", async () => { + const pool = await adapterTestnet.getV2PoolByPair(ADA, { + policyId: "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72", + tokenName: "4d494e", + }); + expect(pool).not.toBeNull(); + expect(pool?.assetA).toEqual("lovelace"); + expect(pool?.assetB).toEqual(MIN_TESTNET); + }); -test("getStablePoolByLPAsset", async () => { - const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; - const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET]; + test("getAllV2Pools", async () => { + const { pools } = await adapterTestnet.getAllV2Pools(); + expect(pools.length > 0); + }); - for (const cfg of testnetCfgs) { - const pool = await adapterTestnet.getStablePoolByLpAsset( - Asset.fromString(cfg.lpAsset) - ); - expect(pool).not.toBeNull(); - expect(pool?.nft).toEqual(cfg.nftAsset); - expect(pool?.assets).toEqual(cfg.assets); - } + test("getV2Pools", async () => { + const { pools } = await adapterTestnet.getV2Pools({ page: 1 }); + expect(pools.length > 0); + }); - for (const cfg of mainnetCfgs) { - const pool = await adapterMainnet.getStablePoolByLpAsset( - Asset.fromString(cfg.lpAsset) - ); - expect(pool).not.toBeNull(); - expect(pool?.nft).toEqual(cfg.nftAsset); - expect(pool?.assets).toEqual(cfg.assets); - } -}); + test("getAllStablePools", async () => { + const numberOfStablePoolsTestnet = + StableswapConstant.CONFIG[NetworkId.TESTNET].length; + const numberOfStablePoolsMainnet = + StableswapConstant.CONFIG[NetworkId.MAINNET].length; + const { pools: testnetPools } = await adapterTestnet.getAllStablePools(); + expect(testnetPools.length === numberOfStablePoolsTestnet); -test("getStablePoolByNFT", async () => { - const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; - const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET]; + const { pools: mainnetPools } = await adapterMainnet.getAllStablePools(); + expect(mainnetPools.length === numberOfStablePoolsMainnet); + }); - for (const cfg of testnetCfgs) { - const pool = await adapterTestnet.getStablePoolByNFT( - Asset.fromString(cfg.nftAsset) - ); - expect(pool).not.toBeNull(); - expect(pool?.nft).toEqual(cfg.nftAsset); - expect(pool?.assets).toEqual(cfg.assets); - } + test("getStablePoolByLPAsset", async () => { + const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; + const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET]; + + for (const cfg of testnetCfgs) { + const pool = await adapterTestnet.getStablePoolByLpAsset( + Asset.fromString(cfg.lpAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } + + for (const cfg of mainnetCfgs) { + const pool = await adapterMainnet.getStablePoolByLpAsset( + Asset.fromString(cfg.lpAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } + }); - for (const cfg of mainnetCfgs) { - const pool = await adapterMainnet.getStablePoolByNFT( - Asset.fromString(cfg.nftAsset) - ); - expect(pool).not.toBeNull(); - expect(pool?.nft).toEqual(cfg.nftAsset); - expect(pool?.assets).toEqual(cfg.assets); - } + test("getStablePoolByNFT", async () => { + const testnetCfgs = StableswapConstant.CONFIG[NetworkId.TESTNET]; + const mainnetCfgs = StableswapConstant.CONFIG[NetworkId.MAINNET]; + + for (const cfg of testnetCfgs) { + const pool = await adapterTestnet.getStablePoolByNFT( + Asset.fromString(cfg.nftAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } + + for (const cfg of mainnetCfgs) { + const pool = await adapterMainnet.getStablePoolByNFT( + Asset.fromString(cfg.nftAsset) + ); + expect(pool).not.toBeNull(); + expect(pool?.nft).toEqual(cfg.nftAsset); + expect(pool?.assets).toEqual(cfg.assets); + } + }); });