diff --git a/docs/database-indexer.md b/docs/database-indexer.md new file mode 100644 index 0000000..ad6f9f8 --- /dev/null +++ b/docs/database-indexer.md @@ -0,0 +1,138 @@ +# Minswap Database Indexer Documentation + +## Overview + +The Minswap Database Indexer listens to events from the Cardano Blockchain and stores relevant data related to Minswap Liquidity Pools in a PostgreSQL database. This allows for efficient querying and retrieval of historical liquidity pool data. The indexer uses **Ogmios** as a WebSocket to capture blockchain events in real-time. + +### How Minswap Indexer Works + +- **Ogmios WebSocket Integration**: The Minswap Indexer uses **Ogmios** as a WebSocket to listen to blockchain events emitted by the Cardano Blockchain. These events are processed in real-time, ensuring the data is always up-to-date. + +- **Data Normalization and Storage**: Whenever the indexer receives an event related to the Minswap Liquidity Pool, it normalizes the data and stores it in a PostgreSQL database. The normalized data allows for efficient querying and tracking of liquidity pool events. + +- **Database Schema**: + - The database schema, which defines how the liquidity pool data is structured and stored, can be found under [Database](../src/syncer/postgres/prisma/schema.prisma). + +- **Indexer Logic**: + - The indexer’s core logic for handling blockchain events and processing liquidity pool data is located in [Indexer](../src/syncer/syncer.ts). + +--- + +### Retrieving Historical Data of Liquidity Pools + +To retrieve the historical data of liquidity pools from the Minswap Indexer, follow these steps: + +#### **Required Tools**: + +- **Docker**: You can install Docker by following this [installation guide](https://docs.docker.com/get-started/get-docker/). +- **Docker Compose**: You can install Docker Compose by following this [installation guide](https://docs.docker.com/compose/install/) + +#### **Step-by-Step Guide**: + +1. **Run the Minswap Indexer using Docker**: + - First, we need to start the indexer and listen for events from the Cardano Blockchain. To do this, use the following Docker Compose command: + + - 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. + - Run the command: `docker compose -f docker-compose.yaml logs -f` to view log. + - This command will initiate the Minswap Indexer, which will start listening to real-time blockchain events via Ogmios. + +2. **Understanding the block Table** + - The `block` table in the PostgreSQL database represents the progress of the indexer. Each entry in this table corresponds to a specific block processed by the indexer. To verify if the indexer is up-to-date, check if the latest block recorded in this table matches the latest block of the Cardano blockchain. + + ```sql + SELECT * FROM block ORDER BY id DESC LIMIT 1; + ``` + + - If the block number from this query matches the latest block from the Cardano network, the data is fully synchronized. + +3. **Retrieve Liquidity Pool Information** + - Once the data is up-to-date, you can query liquidity pool information. One convenient way to do this is by using the **MinswapAdapter**. + + Here's an example of retrieving liquidity pool data using the MinswapAdapter: + + ```typescript + const blockfrostProjectId = ""; + + const prismaClient = await newPrismaClient("postgresql://postgres:minswap@postgres:5432/syncer?schema=public&connection_limit=5") + const repository = new PostgresRepositoryReader( + NetworkEnvironment.TESTNET_PREPROD, + prismaClient + ) + + const adapter = new MinswapAdapter({ + networkId: NetworkId.TESTNET, + networkEnv: NetworkEnvironment.TESTNET_PREPROD, + blockFrostApi: new BlockFrostAPI({ + projectId: blockfrostProjectId, + network: "preprod", + }), + repository: repository + }) + + // Example LP Asset of a Stable Pool + const stablePoolLPAsset = Asset.fromString("8db03e0cc042a5f82434123a0509f590210996f1c7410c94f913ac48757364632d757364742d76312e342d6c70") + + // Example LP Asset of a V2 Pool + const v2PoolLPAsset = Asset.fromString("d6aae2059baee188f74917493cf7637e679cd219bdfbbf4dcbeb1d0b6c3ea488e6ff940bb6fb1b18fd605b5931d9fefde6440117015ba484cf321200") + + // Retrieve the latest information of a specific Stable Pool + const latestStablePool = await adapter.getStablePoolByLpAsset(stablePoolLPAsset) + invariant(latestStablePool) + + // Retrieve the latest price of asset index 0 agains asset index 1 + const latestStablePoolPrice = adapter.getStablePoolPrice({ + pool: latestStablePool, + assetAIndex: 0, + assetBIndex: 1 + }) + // Retrieve the historical information of a specific Stable Pool + const historialStablePools = await adapter.getStablePoolHistory({ + lpAsset: stablePoolLPAsset, + }) + // Retrieve the historical prices of asset index 0 agains asset index 1 + const historicalStablePoolPrices = historialStablePools.map((pool) => + adapter.getStablePoolPrice({ + pool: pool, + assetAIndex: 0, + assetBIndex: 1 + }) + ) + + // Retrieve the latest information of a specific V2 Pool + const v2Pool = await adapter.getV2PoolByLp(v2PoolLPAsset) + invariant(v2Pool) + + // Retrieve the latest price of asset A agains asset B and vice versa + const latestV2PoolPrice = await adapter.getV2PoolPrice({ + pool: v2Pool + }) + + // Retrieve the historical information of a specific V2 Pool + const historialV2Pools = await adapter.getV2PoolHistory({ + lpAsset: v2PoolLPAsset, + }) + + // Retrieve the historical prices of asset A agains asset B and vice versa + const historicalV2PoolPrices = await Promise.all(historialV2Pools.map((pool) => + adapter.getV2PoolPrice({ + pool: pool, + }) + )) + ``` + +### Minswap Indexer Extensibility +Currently, the Minswap Indexer is designed specifically to listen to and process transactions related to Minswap's Liquidity Pools. However, if your business logic requires more complex data retrieval or processing beyond liquidity pool transactions, the Minswap Indexer can be extended to suit your needs. + +To achieve this, you can modify the [handleBlock](../src/syncer/syncer.ts#L100) function. This function is responsible for processing each block and extracting the relevant transactions. By extending this function, you can listen to and handle other types of transactions or blockchain events that are important to your application. + +**Extending the handleBlock Function** + +The handleBlock function is at the core of the indexer's event handling process. It currently focuses on transactions involving Minswap's Liquidity Pools, but you can modify it to: +- Process additional types of smart contract interactions. +- Extract and store data from non-liquidity pool transactions. +- Retrieve custom blockchain events that align with your specific use case. + + +## Conclusion +The Minswap Database Indexer is an essential tool for tracking and retrieving real-time liquidity pool data from the Cardano Blockchain. By using Ogmios to listen to blockchain events and storing the normalized data in a PostgreSQL database, the indexer ensures accurate and up-to-date information. \ No newline at end of file diff --git a/docs/transaction.md b/docs/transaction.md new file mode 100644 index 0000000..94d52c6 --- /dev/null +++ b/docs/transaction.md @@ -0,0 +1,180 @@ +# Minswap AMM V2 & Stableswap Classes Documentation + +## Overview + +This documentation provides details on how to interact with the **Stableswap** and **AMM V2** classes in the Minswap platform. These classes allow users to create stableswap orders, manage liquidity pools, and interact with decentralized exchanges (DEXs) using Minswap's platform, while benefiting from Minswap Batcher fee discounts. + +### Transaction Builder Functions + +- **Stableswap class**: Located in `src/stableswap.ts`. +- **AMM V2 class**: Located in `src/dex-v2.ts`. +- **Example file**: Demonstrates usage of both classes, located in `examples/example.ts`. + +### Utility Functions + +- All utility functions are located in the [Calculate](../src/calculate.ts) file. These functions provide the necessary calculations for operations such as trades, deposits, and withdrawals related to the DEX V2 and Stable Liquidity Pool. +- You can combine these utility functions with the [Slippage](../src/utils/slippage.internal.ts) file to manage volatile liquidity pools efficiently. + +### Batcher Fee Discount + +Currently, everyone who swaps on the Minswap DEX pays a 2 $ADA fee to execute the DEX order. To increase the utility of the $MIN token within the platform, $MIN holders are entitled to a discount on this 2 $ADA Batcher Fee. More details about this can be found in the [Minswap Official Docs](https://docs.minswap.org/min-token/usdmin-tokenomics/trading-fee-discount). + +Technically, the Batcher Fee Discount is calculated based on the ADA-MIN LP Tokens and MIN tokens that users are holding. This calculation is handled by the [calculateBatcherFee](../src/batcher-fee-reduction/calculate.ts) function. + +If you are transacting through the `Stableswap` or `DexV2` classes, the transaction is automatically constructed with the Batcher Fee Discount if you are eligible for it. + +--- + +## Example Usage + +### 1. Make a Trade on a Stable Pool + +```typescript +const network: Network = "Preprod"; +const blockfrostProjectId = ""; +const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; + +const address = ""; + +const lucid = await getBackendLucidInstance( + network, + blockfrostProjectId, + blockfrostUrl, + address +); + +const blockfrostAdapter = new BlockfrostAdapter( + NetworkId.TESTNET, + new BlockFrostAPI({ + projectId: blockfrostProjectId, + network: "preprod", + }) +); + +const utxos = await lucid.utxosAt(address); + +const lpAsset = Asset.fromString(""); +const config = StableswapConstant.getConfigByLpAsset(lpAsset, NetworkId.TESTNET); + +const pool = await blockfrostAdapter.getStablePoolByLpAsset(lpAsset); + +invariant(pool, `Can not find pool by lp asset ${Asset.toString(lpAsset)}`); + +const swapAmount = 1_000n; + +// This pool has 2 assets in its config: [tDJED, tiUSD]. +// Index-0 Asset is tDJED, and Index-1 Asset is tiUSD. +// This order swaps 1_000n tDJED to tiUSD. +const amountOut = StableswapCalculation.calculateSwapAmount({ + inIndex: 0, + outIndex: 1, + amountIn: swapAmount, + amp: pool.amp, + multiples: config.multiples, + datumBalances: pool.datum.balances, + fee: config.fee, + adminFee: config.adminFee, + feeDenominator: config.feeDenominator, +}); + +const txComplete = await new Stableswap(lucid).createBulkOrdersTx({ + sender: address, + availableUtxos: utxos, + options: [ + { + lpAsset: lpAsset, + type: StableOrder.StepType.SWAP, + assetInAmount: swapAmount, + assetInIndex: 0n, + assetOutIndex: 1n, + minimumAssetOut: amountOut, + }, + ], +}); + +const signedTx = await txComplete.signWithPrivateKey("").complete(); +const txId = await signedTx.submit(); +console.info(`Transaction submitted successfully: ${txId}`); +``` + +### 2. Make a Trade on a DEX V2 Pool + +```typescript +const network: Network = "Preprod"; +const blockfrostProjectId = ""; +const blockfrostUrl = "https://cardano-preprod.blockfrost.io/api/v0"; + +const address = ""; + +const lucid = await getBackendLucidInstance( + network, + blockfrostProjectId, + blockfrostUrl, + address +); + +const blockfrostAdapter = new BlockfrostAdapter( + NetworkId.TESTNET, + new BlockFrostAPI({ + projectId: blockfrostProjectId, + network: "preprod", + }) +); + +const utxos = await lucid.utxosAt(address); + +const assetA = ADA; +const assetB = MIN; + +const pool = await blockfrostAdapter.getV2PoolByPair(assetA, assetB); +invariant(pool, "could not find pool"); + +const swapAmount = 5_000_000n; +const amountOut = DexV2Calculation.calculateAmountOut({ + reserveIn: pool.reserveA, + reserveOut: pool.reserveB, + amountIn: swapAmount, + tradingFeeNumerator: pool.feeA[0], +}); +// 20% slippage tolerance +const slippageTolerance = new BigNumber(20).div(100); +const acceptedAmountOut = Slippage.apply({ + slippage: slippageTolerance, + amount: amountOut, + type: "down", +}); + +const txComplete = await new DexV2(lucid, blockfrostAdapter).createBulkOrdersTx({ + sender: address, + availableUtxos: utxos, + orderOptions: [ + { + type: OrderV2.StepType.SWAP_EXACT_IN, + amountIn: swapAmount, + assetIn: assetA, + direction: OrderV2.Direction.A_TO_B, + minimumAmountOut: acceptedAmountOut, + lpAsset: pool.lpAsset, + isLimitOrder: false, + killOnFailed: false, + }, + ], +}); + +const signedTx = await txComplete.signWithPrivateKey("").complete(); +const txId = await signedTx.submit(); +console.info(`Transaction submitted successfully: ${txId}`); + +``` + +## Additional Examples + +You can explore more examples in the [Examples](../examples/example.ts) folder to learn how to integrate the Stableswap and DexV2 classes in more complex scenarios. + +## Conclusion +The Stableswap and AMM V2 classes offer powerful tools for interacting with Minswap’s decentralized exchange. They allow users to easily manage liquidity pools and make swaps, with built-in support for Minswap Batcher Fee discounts. By utilizing these classes, users can create efficient transactions and leverage the utility of $MIN to reduce costs. + +For more details, you can refer to the specific class files: + +- [Stableswap class](../src/stableswap.ts) +- [AMM V2 class](../src/dex-v2.ts) \ No newline at end of file diff --git a/src/adapter.ts b/src/adapter.ts index 329faf8..58954bb 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -15,6 +15,7 @@ import { slotToBeginUnixTime, } from "lucid-cardano"; +import { StableswapCalculation } from "./calculate"; import { PostgresRepositoryReader } from "./syncer/repository/postgres-repository"; import { Asset } from "./types/asset"; import { @@ -51,10 +52,16 @@ export type GetV1PoolHistoryParams = PaginationOptions & { id: string; }; -export type GetV2PoolHistoryParams = PaginationOptions & { - assetA: Asset; - assetB: Asset; -}; +export type GetV2PoolHistoryParams = PaginationOptions & + ( + | { + assetA: Asset; + assetB: Asset; + } + | { + lpAsset: Asset; + } + ); export type GetPoolPriceParams = { pool: PoolV1.State; @@ -68,6 +75,16 @@ export type GetV2PoolPriceParams = { decimalsB?: number; }; +export type GetStablePoolHistoryParams = PaginationOptions & { + lpAsset: Asset; +}; + +export type GetStablePoolPriceParams = { + pool: StablePool.State; + assetAIndex: number; + assetBIndex: number; +}; + interface Adapter { getAssetDecimals(asset: string): Promise; @@ -146,6 +163,22 @@ interface Adapter { 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; } export class BlockfrostAdapter implements Adapter { @@ -566,6 +599,29 @@ export class BlockfrostAdapter implements Adapter { } 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()); + } } export type MinswapAdapterConstructor = { @@ -743,14 +799,16 @@ export class MinswapAdapter extends BlockfrostAdapter { return this.prismaPoolV2ToPoolV2State(prismaPool); } - override async getV2PoolHistory({ - assetA, - assetB, - page = 1, - count = 100, - order = "desc", - }: GetV2PoolHistoryParams): Promise { - const lpAsset = PoolV2.computeLPAssetName(assetA, assetB); + override async getV2PoolHistory( + options: GetV2PoolHistoryParams + ): Promise { + const { page = 1, count = 100, order = "desc" } = options; + let lpAsset: string; + if ("lpAsset" in options) { + lpAsset = Asset.toString(options.lpAsset); + } else { + lpAsset = PoolV2.computeLPAssetName(options.assetA, options.assetB); + } const prismaPools = await this.repository.getHistoricalPoolV2ByLpAsset( lpAsset, page - 1, @@ -838,4 +896,25 @@ export class MinswapAdapter extends BlockfrostAdapter { } return this.prismaStablePoolToStablePoolState(prismaStablePool); } + + override async getStablePoolHistory({ + lpAsset, + page = 1, + count = 100, + order = "desc", + }: GetStablePoolHistoryParams): Promise { + const prismaPools = await this.repository.getHistoricalStablePoolsByLpAsset( + Asset.toString(lpAsset), + page - 1, + count, + order + ); + if (prismaPools.length === 0) { + return []; + } + + return prismaPools.map((pool) => + this.prismaStablePoolToStablePoolState(pool) + ); + } } diff --git a/src/calculate.ts b/src/calculate.ts index 69f82d5..56fe0e5 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -1011,4 +1011,74 @@ export namespace StableswapCalculation { ((amountOutWithoutFee - amountOut) * adminFee) / feeDenominator; return amountOut; } + + export function getPrice( + balances: bigint[], + multiples: bigint[], + amp: bigint, + assetAIndex: number, + assetBIndex: number + ): [bigint, bigint] { + const mulBalances = zipWith(balances, multiples, (a, b) => a * b); + const length = BigInt(mulBalances.length); + const ann = amp * length; + const d = getD(mulBalances, amp); + + // Dr = D / (N_COINS ** N_COINS) + // for i in range(N_COINS): + // Dr = Dr * D / xp[i] + + //drNumerator = D^(n+1) + //drDenominator = n^n*xp[0]*xp[1]*...*xp[n-1] + let drNumerator = d; + let drDenominator = 1n; + for (let i = 0n; i < length; ++i) { + drNumerator = drNumerator * d; + drDenominator = drDenominator * mulBalances[Number(i)] * length; + } + + // (ANN * xp[assetAIndex] + Dr * xp[assetAIndex] / xp[assetBIndex]) / (ANN * xp[assetAIndex] + Dr) + // = (drDenominator * ANN * xp[assetAIndex] * xp[assetBIndex] + drNumerator*xp[assetAIndex]) + // / xp[assetBIndex] * (drDenominator * Ann * xp[assetAIndex] + drNumerator) + // this is price of asset[assetBIndex] / multiples[assetBIndex] base on asset[assetAIndex] / multiples[assetAIndex] + // => price = priceWithMultiple / multiples[assetAIndex] * multiples[assetBIndex] + return shortenFraction([ + (drDenominator * + ann * + mulBalances[assetAIndex] * + mulBalances[assetBIndex] + + drNumerator * mulBalances[assetAIndex]) * + multiples[assetBIndex], + mulBalances[assetBIndex] * + (drDenominator * ann * mulBalances[assetAIndex] + drNumerator) * + multiples[assetAIndex], + ]); + } +} + +function shortenFraction([numerator, denominator]: [bigint, bigint]): [ + bigint, + bigint, +] { + const gcd = gcdFunction(numerator, denominator); + if (gcd === 0n) { + return [1n, 1n]; + } else { + return [numerator / gcd, denominator / gcd]; + } +} + +function gcdFunction(a: bigint, b: bigint): bigint { + if (a > b) { + if (b === 0n) { + return a; + } + return gcdFunction(a % b, b); + } else if (a < b) { + if (a === 0n) { + return b; + } + return gcdFunction(a, b % a); + } + return a; } diff --git a/src/syncer/repository/postgres-repository.ts b/src/syncer/repository/postgres-repository.ts index a95c7b2..48212a4 100644 --- a/src/syncer/repository/postgres-repository.ts +++ b/src/syncer/repository/postgres-repository.ts @@ -178,6 +178,19 @@ export class PostgresRepositoryReader { }, }) } + + async getHistoricalStablePoolsByLpAsset(lpAsset: string, offset: number, limit: number, orderBy: "asc" | "desc"): Promise { + return await this.prismaClientInTx.stablePool.findMany({ + where: { + lp_asset: lpAsset + }, + skip: offset * limit, + take: limit, + orderBy: { + id: orderBy + } + }) + } } export class PostgresRepositoryWriterInTransaction extends PostgresRepositoryReader {