wp-solana-pool-traits 0.1.1

Traits and utilities for Solana liquidity pool operations: PoolViewer, PoolInfuser, PositionViewer
Documentation
use anyhow::Result;
use async_trait::async_trait;
use solana_sdk::pubkey::Pubkey;
use wp_solana_rpc::RpcContext;

use crate::types::{CurrencyAmount, Price};

/// Position metadata containing basic position information
#[derive(Debug, Clone)]
pub struct PositionMetadata {
    /// Position mint address (NFT mint)
    pub position_mint: Pubkey,
    /// Position address (PDA derived from position mint)
    pub position_address: Pubkey,
    /// Pool address this position belongs to
    pub pool_address: Pubkey,
    /// Lower tick index of the position
    pub tick_lower: i32,
    /// Upper tick index of the position
    pub tick_upper: i32,
    /// Liquidity amount in the position
    pub liquidity: u128,
}

/// Position values containing amounts, fees, and rewards
#[derive(Debug, Clone)]
pub struct PositionValues {
    /// Position mint address
    pub position_mint: Pubkey,
    /// Amount of token A in the position
    pub amount_a: CurrencyAmount,
    /// Amount of token B in the position
    pub amount_b: CurrencyAmount,
    /// Current price of token B in terms of token A
    pub price: Price,
    /// Fees available to collect in token A
    pub fee_owed_a: CurrencyAmount,
    /// Fees available to collect in token B
    pub fee_owed_b: CurrencyAmount,
    /// Rewards available to collect
    pub rewards: Vec<RewardValue>,
}

/// Reward value information
#[derive(Debug, Clone)]
pub struct RewardValue {
    /// Reward mint address
    pub mint: Pubkey,
    /// Amount of reward available to collect
    pub amount: u64,
}

/// Trait for viewing a single position
#[async_trait]
pub trait PositionViewer: Send + Sync {
    /// Get the position metadata (position_mint, liquidity, tick range)
    fn position_metadata(&self) -> PositionMetadata;

    /// Get the position values including amounts, fees, and rewards
    async fn position_values(&self, ctx: &RpcContext) -> Result<PositionValues>;

    /// Get the token A and token B amounts in the position
    async fn amounts(&self, ctx: &RpcContext) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let values = self.position_values(ctx).await?;
        Ok((values.amount_a, values.amount_b))
    }

    /// Get uncollected amounts owed for token A and token B (fees)
    async fn amounts_owed(&self, ctx: &RpcContext) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let values = self.position_values(ctx).await?;
        Ok((values.fee_owed_a, values.fee_owed_b))
    }

    /// Get the total value of the position in token A and token B
    /// (includes token amounts and amounts owed)
    async fn values(&self, ctx: &RpcContext) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let values = self.position_values(ctx).await?;
        let total_a = crate::common::price_utils::calculate_position_value(
            values.amount_a.clone(),
            values.amount_b.clone(),
            Some(values.fee_owed_a.clone()),
            Some(values.fee_owed_b.clone()),
            values.price.clone(),
            true,
        )?;
        let total_b = crate::common::price_utils::calculate_position_value(
            values.amount_a,
            values.amount_b,
            Some(values.fee_owed_a),
            Some(values.fee_owed_b),
            values.price,
            false,
        )?;
        Ok((total_a, total_b))
    }

    /// Get the total value of the position in token A and token B
    /// (excludes amounts owed, only token amounts)
    async fn values_without_amounts_owed(
        &self,
        ctx: &RpcContext,
    ) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let values = self.position_values(ctx).await?;
        let value_a = crate::common::price_utils::calculate_position_value(
            values.amount_a.clone(),
            values.amount_b.clone(),
            None,
            None,
            values.price.clone(),
            true,
        )?;
        let value_b = crate::common::price_utils::calculate_position_value(
            values.amount_a,
            values.amount_b,
            None,
            None,
            values.price,
            false,
        )?;
        Ok((value_a, value_b))
    }
}

/// Trait for viewing multiple positions
#[async_trait]
pub trait MultiPositionViewer: Send + Sync {
    /// Get the position metadata for a specific position
    fn position_metadata(&self, position_mint: Pubkey) -> Option<PositionMetadata>;

    /// Get the position metadata for all positions
    fn position_metadatas(&self) -> Vec<PositionMetadata>;

    /// Add a position to the viewer
    fn add_position(&mut self, metadata: PositionMetadata);

    /// Add a position to the viewer by fetching it from the blockchain
    async fn add_position_with_mint(
        &mut self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<()>;

    /// Get the position values for all positions
    async fn position_values(&self, ctx: &RpcContext) -> Result<Vec<PositionValues>>;

    /// Get the token A and token B amounts for all positions
    async fn amounts(&self, ctx: &RpcContext) -> Result<Vec<(CurrencyAmount, CurrencyAmount)>> {
        let values = self.position_values(ctx).await?;
        Ok(values.into_iter().map(|v| (v.amount_a, v.amount_b)).collect())
    }

    /// Get uncollected amounts owed for token A and token B for all positions
    async fn amounts_owed(
        &self,
        ctx: &RpcContext,
    ) -> Result<Vec<(CurrencyAmount, CurrencyAmount)>> {
        let values = self.position_values(ctx).await?;
        Ok(values.into_iter().map(|v| (v.fee_owed_a, v.fee_owed_b)).collect())
    }

    /// Get the total value of all positions in token A and token B
    /// (includes token amounts and amounts owed)
    async fn values(&self, ctx: &RpcContext) -> Result<Vec<(CurrencyAmount, CurrencyAmount)>> {
        let values = self.position_values(ctx).await?;
        let mut all_values = Vec::new();
        for v in values {
            let total_a = crate::common::price_utils::calculate_position_value(
                v.amount_a.clone(),
                v.amount_b.clone(),
                Some(v.fee_owed_a.clone()),
                Some(v.fee_owed_b.clone()),
                v.price.clone(),
                true,
            )?;
            let total_b = crate::common::price_utils::calculate_position_value(
                v.amount_a,
                v.amount_b,
                Some(v.fee_owed_a),
                Some(v.fee_owed_b),
                v.price,
                false,
            )?;
            all_values.push((total_a, total_b));
        }
        Ok(all_values)
    }

    /// Get the total value of all positions in token A and token B
    /// (excludes amounts owed, only token amounts)
    async fn values_without_amounts_owed(
        &self,
        ctx: &RpcContext,
    ) -> Result<Vec<(CurrencyAmount, CurrencyAmount)>> {
        let values = self.position_values(ctx).await?;
        let mut all_values = Vec::new();
        for v in values {
            let value_a = crate::common::price_utils::calculate_position_value(
                v.amount_a.clone(),
                v.amount_b.clone(),
                None,
                None,
                v.price.clone(),
                true,
            )?;
            let value_b = crate::common::price_utils::calculate_position_value(
                v.amount_a, v.amount_b, None, None, v.price, false,
            )?;
            all_values.push((value_a, value_b));
        }
        Ok(all_values)
    }

    /// Get the token A and token B amounts for a specific position
    async fn amounts_for_position(
        &self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let v = self.position_value_for(ctx, position_mint).await?;
        Ok((v.amount_a, v.amount_b))
    }

    /// Get uncollected amounts owed for token A and token B for a specific
    /// position
    async fn amounts_owed_for_position(
        &self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let v = self.position_value_for(ctx, position_mint).await?;
        Ok((v.fee_owed_a, v.fee_owed_b))
    }

    /// Get the total value of a specific position in token A and token B
    /// (includes token amounts and amounts owed)
    async fn values_for_position(
        &self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let v = self.position_value_for(ctx, position_mint).await?;
        let total_a = crate::common::price_utils::calculate_position_value(
            v.amount_a.clone(),
            v.amount_b.clone(),
            Some(v.fee_owed_a.clone()),
            Some(v.fee_owed_b.clone()),
            v.price.clone(),
            true,
        )?;
        let total_b = crate::common::price_utils::calculate_position_value(
            v.amount_a,
            v.amount_b,
            Some(v.fee_owed_a),
            Some(v.fee_owed_b),
            v.price,
            false,
        )?;
        Ok((total_a, total_b))
    }

    /// Get the total value of a specific position in token A and token B
    /// (excludes amounts owed, only token amounts)
    async fn values_without_amounts_owed_for_position(
        &self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<(CurrencyAmount, CurrencyAmount)> {
        let v = self.position_value_for(ctx, position_mint).await?;
        let value_a = crate::common::price_utils::calculate_position_value(
            v.amount_a.clone(),
            v.amount_b.clone(),
            None,
            None,
            v.price.clone(),
            true,
        )?;
        let value_b = crate::common::price_utils::calculate_position_value(
            v.amount_a, v.amount_b, None, None, v.price, false,
        )?;
        Ok((value_a, value_b))
    }

    /// Fetch all position values and return the one matching `position_mint`.
    /// Internal default helper shared by the `*_for_position` defaults.
    async fn position_value_for(
        &self,
        ctx: &RpcContext,
        position_mint: Pubkey,
    ) -> Result<PositionValues> {
        let values = self.position_values(ctx).await?;
        values
            .into_iter()
            .find(|v| v.position_mint == position_mint)
            .ok_or_else(|| anyhow::anyhow!("Position {} not found", position_mint))
    }
}