Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Classic Liquidity Pool #177

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/stellar-plus/error/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from 'stellar-plus/error/helpers/soroban-rpc'
import { TransactionData, TransactionInvocationMeta } from 'stellar-plus/error/helpers/transaction'
import { DefaultHorizonHandlerErrorCodes } from 'stellar-plus/horizon/errors'
import { ClassicLiquidityPoolHandlerErrorCodes } from 'stellar-plus/markets/classic-liquidity-pool/errors'
import { DefaultRpcHandlerErrorCodes } from 'stellar-plus/rpc/default-handler/errors'
import { ValidationCloudRpcHandlerErrorCodes } from 'stellar-plus/rpc/validation-cloud-handler/errors'

Expand Down Expand Up @@ -52,6 +53,7 @@ export type ErrorCodes =
| ErrorCodesPipelineSimulateTransaction
| ErrorCodesPipelineSorobanGetTransaction
| ErrorCodesPipelineClassicSignRequirements
| ClassicLiquidityPoolHandlerErrorCodes

export enum GeneralErrorCodes {
ER000 = 'ER000',
Expand Down
1 change: 1 addition & 0 deletions src/stellar-plus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as Regex from 'stellar-plus/utils/regex'

export * as Account from 'stellar-plus/account/index'
export * as Asset from 'stellar-plus/asset/index'
export * as Markets from 'stellar-plus/markets/index'
export * as Network from 'stellar-plus/network'
export { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/index'
export * from 'stellar-plus/utils/unit-conversion'
Expand Down
66 changes: 66 additions & 0 deletions src/stellar-plus/markets/classic-liquidity-pool/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { StellarPlusError } from 'stellar-plus/error'

export enum ClassicLiquidityPoolHandlerErrorCodes {
CLPH001 = 'CLPH001',
CLPH002 = 'CLPH002',
CLPH003 = 'CLPH003',
CLPH004 = 'CLPH004',
CLPH005 = 'CLPH005',
}

const liquidityPoolIdNotDefined = (): StellarPlusError => {
return new StellarPlusError({
code: ClassicLiquidityPoolHandlerErrorCodes.CLPH001,
message: 'Liquidity Pool ID not defined!',
source: 'ClassicLiquidityPoolHandler',
details:
'The liquidity pool ID is required for this operation. Ensure that the liquidity pool ID is defined and provided correctly.',
})
}

const trustlineAlreadyExists = (): StellarPlusError => {
return new StellarPlusError({
code: ClassicLiquidityPoolHandlerErrorCodes.CLPH002,
message: 'Trustline already exists!',
source: 'ClassicLiquidityPoolHandler',
details:
'A trustline for this liquidity pool already exists for the specified account. Ensure that the trustline is not already set up before attempting to create a new one.',
})
}

const liquidityPoolNotFound = (): StellarPlusError => {
return new StellarPlusError({
code: ClassicLiquidityPoolHandlerErrorCodes.CLPH003,
message: 'Liquidity pool not found!',
source: 'ClassicLiquidityPoolHandler',
details: 'The specified liquidity pool could not be found. Please check the liquidity pool ID and try again.',
})
}

const liquidityPoolRequiredAssets = (): StellarPlusError => {
return new StellarPlusError({
code: ClassicLiquidityPoolHandlerErrorCodes.CLPH004,
message: 'Liquidity pool missing required assets!',
source: 'ClassicLiquidityPoolHandler',
details:
'The liquidity pool does not have the two required assets. Ensure the liquidity pool includes both assets.',
})
}

const failedToCreateHandlerFromLiquidityPoolId = (): StellarPlusError => {
return new StellarPlusError({
code: ClassicLiquidityPoolHandlerErrorCodes.CLPH005,
message: 'Failed to create handler from liquidity pool ID!',
source: 'ClassicLiquidityPoolHandler',
details:
'The handler could not be created from the provided liquidity pool ID. Verify the liquidity pool ID and the assets involved.',
})
}

export const CLPHError = {
liquidityPoolIdNotDefined,
trustlineAlreadyExists,
liquidityPoolNotFound,
liquidityPoolRequiredAssets,
failedToCreateHandlerFromLiquidityPoolId,
}
262 changes: 262 additions & 0 deletions src/stellar-plus/markets/classic-liquidity-pool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { LiquidityPoolAsset, Operation, Asset as StellarAsset, getLiquidityPoolId } from '@stellar/stellar-sdk'

import { ClassicAssetHandler } from 'stellar-plus/asset'
import { ClassicTransactionPipeline } from 'stellar-plus/core/pipelines/classic-transaction'
import { ClassicTransactionPipelineOutput } from 'stellar-plus/core/pipelines/classic-transaction/types'
import { TransactionInvocation } from 'stellar-plus/core/types'
import { HorizonHandlerClient } from 'stellar-plus/horizon'
import {
BaseInvocation,
ClassicLiquidityPoolHandlerConstructorArgs,
ClassicLiquidityPoolHandlerInstanceArgs,
ClassicLiquidityPoolHandler as IClassicLiquidityPoolHandler,
} from 'stellar-plus/markets/classic-liquidity-pool/types'

import { CLPHError } from './errors'

/**
* Constants
*/
const LIQUIDITY_POOL_SHARE_TYPE = 'liquidity_pool_shares'
const LIQUIDITY_POOL_MODEL = 'constant_product'

export class ClassicLiquidityPoolHandler implements IClassicLiquidityPoolHandler {
public assetA: ClassicAssetHandler
public assetB: ClassicAssetHandler
public liquidityPoolId?: string

private classicTransactionPipeline: ClassicTransactionPipeline
private horizonHandler: HorizonHandlerClient

/**
* @class ClassicLiquidityPoolHandler
* @implements {IClassicLiquidityPoolHandler}
*
* @param {ClassicAssetHandler} assetA - The first asset in the liquidity pool.
* @param {ClassicAssetHandler} assetB - The second asset in the liquidity pool.
* @param {NetworkConfig} networkConfig - The network configuration to use for Stellar.
* @param {ClassicTransactionPipelineOptions} [options] - Optional configurations.
* @param {ClassicTransactionPipelineOptions} [options.classicTransactionPipeline] - Custom configurations for the transaction pipeline.
*
* @description A handler class for managing a classic liquidity pool on the Stellar network.
*/
constructor(args: ClassicLiquidityPoolHandlerConstructorArgs) {
this.assetA = args.assetA
this.assetB = args.assetB
this.horizonHandler = new HorizonHandlerClient(args.networkConfig)

this.classicTransactionPipeline = new ClassicTransactionPipeline(
args.networkConfig,
args.options?.classicTransactionPipeline
)
}

/**
* @static
* @param {string} liquidityPoolId - The ID of the liquidity pool on the Stellar network.
* @param {NetworkConfig} networkConfig - The network configuration to use.
* @param {Object} [options] - Optional configurations.
*
* @returns {Promise<ClassicLiquidityPoolHandler>} - A promise that resolves to a new instance of the ClassicLiquidityPoolHandler.
*
* @description - Creates an instance from a liquidity pool ID.
*/
public static async fromLiquidityPoolId(
args: ClassicLiquidityPoolHandlerInstanceArgs
): Promise<ClassicLiquidityPoolHandler> {
const { liquidityPoolId, networkConfig, options } = args
const horizonHandler = new HorizonHandlerClient(networkConfig)

try {
const liquidityPool = await horizonHandler.server.liquidityPools().liquidityPoolId(liquidityPoolId).call()

// Check if the liquidity pool exists
if (!liquidityPool) {
throw CLPHError.liquidityPoolNotFound()
}

// Extract assets from the liquidity pool
const [reserveA, reserveB] = liquidityPool.reserves
if (!reserveA || !reserveB) {
throw CLPHError.liquidityPoolRequiredAssets()
}

const [assetCodeA, assetIssuerA] = reserveA.asset.split(':')
const [assetCodeB, assetIssuerB] = reserveB.asset.split(':')

// Creates instances of ClassicAssetHandler for each asset
const assetA = new ClassicAssetHandler({
code: assetCodeA,
issuerAccount: assetIssuerA,
networkConfig: networkConfig,
})

const assetB = new ClassicAssetHandler({
code: assetCodeB,
issuerAccount: assetIssuerB,
networkConfig: networkConfig,
})

// Create an instance using the extracted assets
const handler = new ClassicLiquidityPoolHandler({
assetA,
assetB,
networkConfig: networkConfig,
options: options,
})

handler.liquidityPoolId = liquidityPoolId

return handler
} catch (error) {
throw CLPHError.failedToCreateHandlerFromLiquidityPoolId()
}
}

/**
* @param {string} to - The account to which the trustline should be added.
* @param {number} [fee=30] - The fee for the trustline transaction.
* @param {TransactionInvocation} txInvocation - The transaction invocation object
*
* @returns {Promise<ClassicTransactionPipelineOutput>} - The result of the add trustline transaction.
*
* @description - Adds a trustline for the liquidity pool asset to the specified account.
*/
public async addTrustline(
args: { to: string; fee?: number } & BaseInvocation
): Promise<ClassicTransactionPipelineOutput> {
const { to, fee = 30 } = args
const txInvocation = args as TransactionInvocation

const assetA = this.createStellarAsset(this.assetA)
const assetB = this.createStellarAsset(this.assetB)
const liquidityPoolAsset = new LiquidityPoolAsset(assetA, assetB, fee)

this.liquidityPoolId = getLiquidityPoolId(LIQUIDITY_POOL_MODEL, liquidityPoolAsset).toString('hex')

// Check if the trustline already exists.
const trustlineExists = await this.checkLiquidityPoolTrustlineExists(to)
if (trustlineExists) {
throw CLPHError.trustlineAlreadyExists()
}

const addTrustlineOperation = Operation.changeTrust({ source: to, asset: liquidityPoolAsset })

const result = await this.classicTransactionPipeline.execute({
txInvocation,
operations: [addTrustlineOperation],
options: { ...args.options },
})

return result
}

/**
* @private
* @param {string} accountId - The ID of the account to check for an existing trustline.
* @returns {Promise<boolean>} - Whether the trustline for the liquidity pool exists.
*
* @description - Checks if the trustline for the liquidity pool exists.
*/
private async checkLiquidityPoolTrustlineExists(accountId: string): Promise<boolean> {
try {
const account = await this.horizonHandler.loadAccount(accountId)
return account.balances.some(
(balance) =>
balance.asset_type === LIQUIDITY_POOL_SHARE_TYPE && balance.liquidity_pool_id === this.liquidityPoolId
)
} catch (error) {
return false
}
}

/**
* @private
* @param {ClassicAssetHandler} assetHandler - The asset handler for creating a Stellar asset.
* @returns {StellarAsset} - The created Stellar asset.
*
* @description - Helper to create Stellar Asset from ClassicAssetHandler.
*/
private createStellarAsset(assetHandler: ClassicAssetHandler): StellarAsset {
return new StellarAsset(assetHandler.code, assetHandler.issuerPublicKey)
}

/**
* @param {string} amountA - The amount of asset A to deposit.
* @param {string} amountB - The amount of asset B to deposit.
* @param {number|string|object} [minPrice={n:1,d:1}] - Minimum price for the deposit transaction.
* @param {number|string|object} [maxPrice={n:1,d:1}] - Maximum price for the deposit transaction.
* @param {TransactionInvocation} txInvocation - The transaction invocation object
*
* @returns {Promise<ClassicTransactionPipelineOutput>} - The result of the deposit transaction.
*
* @description - Deposits assets into the liquidity pool.
*/
public async deposit(
args: {
amountA: string
amountB: string
minPrice?: number | string | object
maxPrice?: number | string | object
} & BaseInvocation
): Promise<ClassicTransactionPipelineOutput> {
const { amountA, amountB, minPrice = { n: 1, d: 1 }, maxPrice = { n: 1, d: 1 } } = args
const txInvocation = args as TransactionInvocation

if (!this.liquidityPoolId) {
throw CLPHError.liquidityPoolIdNotDefined()
}

const depositOperation = Operation.liquidityPoolDeposit({
liquidityPoolId: this.liquidityPoolId,
maxAmountA: amountA,
maxAmountB: amountB,
minPrice,
maxPrice,
})

const result = await this.classicTransactionPipeline.execute({
txInvocation,
operations: [depositOperation],
options: { ...args.options },
})

return result
}

/**
* @param {string} amount - The amount of liquidity pool shares to withdraw.
* @param {string} [minAmountA='0'] - Minimum amount of asset A to receive.
* @param {string} [minAmountB='0'] - Minimum amount of asset B to receive.
* @param {TransactionInvocation} txInvocation - The transaction invocation object
*
* @returns {Promise<ClassicTransactionPipelineOutput>} - The result of the withdrawal transaction.
*
* @description - Withdraws assets from the liquidity pool.
*/
public async withdraw(
args: { amount: string; minAmountA?: string; minAmountB?: string } & BaseInvocation
): Promise<ClassicTransactionPipelineOutput> {
const { amount, minAmountA = '0', minAmountB = '0' } = args
const txInvocation = args as TransactionInvocation

if (!this.liquidityPoolId) {
throw CLPHError.liquidityPoolIdNotDefined()
}

const withdrawOperation = Operation.liquidityPoolWithdraw({
liquidityPoolId: this.liquidityPoolId,
amount,
minAmountA,
minAmountB,
})

const result = await this.classicTransactionPipeline.execute({
txInvocation,
operations: [withdrawOperation],
options: { ...args.options },
})

return result
}
}
Loading
Loading