Skip to content

Commit

Permalink
feat: return complete breakdown from the Portfolio tool
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrostr committed Jan 23, 2025
1 parent 06eb13a commit d7e4fa7
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 3 deletions.
125 changes: 125 additions & 0 deletions listen-kit/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use anyhow::Result;
use futures::future::join_all;
use reqwest::Client;
use serde::{Deserialize, Serialize};

use crate::balance::Holding;
use crate::dexscreener::{search_ticker, PairInfo};

pub async fn fetch_pair_info(mint_or_symbol: String) -> Result<PairInfo> {
Expand All @@ -26,3 +30,124 @@ pub async fn fetch_pair_info(mint_or_symbol: String) -> Result<PairInfo> {
anyhow::anyhow!("No matching pairs found for {}", mint_or_symbol)
})
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenMetadata {
address: String,
name: String,
symbol: String,
decimals: u8,
#[serde(rename = "logoURI")]
logo_uri: String,
#[serde(rename = "daily_volume", default)]
volume_24h: Option<f64>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PriceData {
id: String,
#[serde(rename = "type")]
price_type: String,
price: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PriceResponse {
data: std::collections::HashMap<String, PriceData>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PortfolioItem {
address: String,
name: String,
symbol: String,
decimals: u8,
#[serde(rename = "logoURI")]
logo_uri: String,
price: f64,
amount: f64,
daily_volume: f64,
}

pub async fn holdings_to_portfolio(
holdings: Vec<Holding>,
) -> Result<Vec<PortfolioItem>> {
let client = Client::new();

// Fetch metadata for all tokens
let metadata_futures: Vec<_> = holdings
.iter()
.map(|holding| {
let client = &client;
async move {
let url =
format!("https://tokens.jup.ag/token/{}", holding.mint);
client.get(&url).send().await?.json::<TokenMetadata>().await
}
})
.collect();

let metadata_results = join_all(metadata_futures).await;
let token_metadata: Vec<TokenMetadata> = metadata_results
.into_iter()
.collect::<Result<Vec<_>, _>>()?;

// Fetch prices for all tokens
let mints: Vec<_> = holdings.iter().map(|h| h.mint.as_str()).collect();
let prices_url =
format!("https://api.jup.ag/price/v2?ids={}", mints.join(","));
let price_response: PriceResponse =
client.get(&prices_url).send().await?.json().await?;

// Combine all data into portfolio items
let portfolio: Vec<PortfolioItem> = holdings
.iter()
.zip(token_metadata.iter())
.map(|(holding, metadata)| {
let price = price_response
.data
.get(&holding.mint)
.map(|p| p.price.parse::<f64>().unwrap_or(0.0))
.unwrap_or(0.0);

let amount =
holding.amount as f64 / (10f64.powi(metadata.decimals as i32));

PortfolioItem {
address: metadata.address.clone(),
name: metadata.name.clone(),
symbol: metadata.symbol.clone(),
decimals: metadata.decimals,
logo_uri: metadata.logo_uri.clone(),
price,
amount,
daily_volume: metadata.volume_24h.unwrap_or(0.0),
}
})
.collect();

Ok(portfolio)
}

#[cfg(test)]
mod tests {
use solana_sdk::signer::Signer;

use super::*;
use crate::balance::get_holdings;
use crate::util::{load_keypair_for_tests, make_rpc_client};

#[tokio::test]
async fn test_holdings_to_portfolio() {
let holdings = get_holdings(
&make_rpc_client(),
&load_keypair_for_tests().pubkey(),
)
.await
.unwrap();

let portfolio = holdings_to_portfolio(holdings).await.unwrap();

assert!(!portfolio.is_empty());
}
}
6 changes: 3 additions & 3 deletions listen-kit/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;

use crate::balance::Holding;
use crate::data::PortfolioItem;
use crate::dexscreener::PairInfo;
use crate::util::wrap_unsafe;

Expand Down Expand Up @@ -206,7 +206,7 @@ pub async fn sell_pump_token(
}

#[tool]
pub async fn portfolio() -> Result<Vec<Holding>> {
pub async fn portfolio() -> Result<Vec<PortfolioItem>> {
let keypair = KEYPAIR.read().await;
let holdings = wrap_unsafe(move || async move {
crate::balance::get_holdings(&create_rpc(), &keypair.pubkey())
Expand All @@ -216,7 +216,7 @@ pub async fn portfolio() -> Result<Vec<Holding>> {
.await
.map_err(|e| anyhow!("{:#?}", e))?;

Ok(holdings)
crate::data::holdings_to_portfolio(holdings).await
}

#[tool]
Expand Down

0 comments on commit d7e4fa7

Please sign in to comment.