use std::str::FromStr;
use std::sync::Arc;
use mpl_token_metadata::accounts::{Edition, MasterEdition, Metadata};
use mpl_token_metadata::types::Key;
use solana_program::pubkey::Pubkey;
use crate::cache::CacheStore;
use crate::das::{AccountDecoder, DasAsset, MasterEditionRecord, PrintEditionRecord};
use crate::upstream::{AccountData, UpstreamClient};
const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
const TOKEN_2022_PROGRAM_ID: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
const SPL_TOKEN_MIN_MINT_SIZE: usize = 82;
const MPL_TOKEN_METADATA_PROGRAM: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
#[derive(Debug, thiserror::Error)]
pub enum FetchError {
#[error(transparent)]
Upstream(#[from] crate::upstream::UpstreamError),
#[error(transparent)]
Decoder(#[from] crate::das::DecoderError),
#[error(transparent)]
Cache(#[from] crate::cache::CacheError),
}
pub type FetchResult<T> = Result<T, FetchError>;
pub async fn fetch_and_cache_asset<U, C>(
upstream: &U,
cache: &C,
decoders: &[Arc<dyn AccountDecoder>],
address: &str,
) -> FetchResult<Option<DasAsset>>
where
U: UpstreamClient + ?Sized,
C: CacheStore + ?Sized,
{
let Some(mut account) = upstream.get_account(address).await? else {
return Ok(None);
};
let owner_str = bs58::encode(account.owner).into_string();
if (owner_str == SPL_TOKEN_PROGRAM_ID || owner_str == TOKEN_2022_PROGRAM_ID)
&& account.data.len() >= SPL_TOKEN_MIN_MINT_SIZE
{
let metadata_pda = derive_metadata_pda(address);
let Some(md_account) = upstream.get_account(&metadata_pda).await? else {
return Ok(None);
};
account = md_account;
}
let owner_str = bs58::encode(account.owner).into_string();
for decoder in decoders {
if decoder.program_id() == owner_str {
if let Some(mut asset) = decoder.decode(address, &account.data)? {
if asset.ownership.owner.is_empty() {
if let Some(owner) = resolve_token_metadata_owner(upstream, &account).await {
asset.ownership.owner = owner;
}
}
cache.put_asset(asset.clone()).await?;
if matches!(
asset.interface.as_str(),
"V1_NFT" | "ProgrammableNFT" | "LegacyNFT"
) {
index_edition_pda(upstream, cache, address).await;
}
return Ok(Some(asset));
}
}
}
Ok(None)
}
async fn index_edition_pda<U, C>(upstream: &U, cache: &C, mint_b58: &str)
where
U: UpstreamClient + ?Sized,
C: CacheStore + ?Sized,
{
let edition_pda = derive_edition_pda(mint_b58);
if edition_pda.is_empty() {
return;
}
let Ok(Some(account)) = upstream.get_account(&edition_pda).await else {
return;
};
if account.data.is_empty() {
return;
}
match account.data[0] {
k if k == Key::MasterEditionV2 as u8 || k == Key::MasterEditionV1 as u8 => {
if let Ok(master) = MasterEdition::from_bytes(&account.data) {
let _ = cache
.put_master_edition(MasterEditionRecord {
master_mint: mint_b58.to_string(),
master_edition_pda: edition_pda,
supply: master.supply,
max_supply: master.max_supply,
})
.await;
}
}
k if k == Key::EditionV1 as u8 => {
if let Ok(edition) = Edition::from_bytes(&account.data) {
let _ = cache
.put_print_edition(PrintEditionRecord {
print_mint: mint_b58.to_string(),
print_edition_pda: edition_pda,
parent_master_edition_pda: edition.parent.to_string(),
edition_num: edition.edition,
})
.await;
}
}
_ => {}
}
}
fn derive_edition_pda(mint_b58: &str) -> String {
let Ok(mint) = Pubkey::from_str(mint_b58) else {
return String::new();
};
let Ok(program) = Pubkey::from_str(MPL_TOKEN_METADATA_PROGRAM) else {
return String::new();
};
let (pda, _bump) = Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), b"edition"],
&program,
);
pda.to_string()
}
async fn resolve_token_metadata_owner<U>(
upstream: &U,
metadata_account: &AccountData,
) -> Option<String>
where
U: UpstreamClient + ?Sized,
{
let metadata = Metadata::from_bytes(&metadata_account.data).ok()?;
resolve_owner_for_mint(upstream, &metadata.mint.to_string()).await
}
pub async fn resolve_owner_for_mint<U>(upstream: &U, mint: &str) -> Option<String>
where
U: UpstreamClient + ?Sized,
{
let raw = upstream
.rpc_call("getTokenLargestAccounts", serde_json::json!([mint]))
.await
.ok()?;
let parsed: serde_json::Value = serde_json::from_slice(&raw).ok()?;
let token_account_addr = parsed
.get("value")
.and_then(serde_json::Value::as_array)
.and_then(|arr| {
arr.iter()
.find(|entry| entry.get("amount").and_then(serde_json::Value::as_str) != Some("0"))
})
.and_then(|entry| entry.get("address"))
.and_then(serde_json::Value::as_str)?;
let token_account = upstream.get_account(token_account_addr).await.ok()??;
if token_account.data.len() < 64 {
return None;
}
let mut owner_bytes = [0u8; 32];
owner_bytes.copy_from_slice(&token_account.data[32..64]);
Some(bs58::encode(owner_bytes).into_string())
}
pub async fn decode_and_cache<C>(
cache: &C,
decoders: &[Arc<dyn AccountDecoder>],
address: &str,
account: &AccountData,
) -> FetchResult<Option<DasAsset>>
where
C: CacheStore + ?Sized,
{
let owner_str = bs58::encode(account.owner).into_string();
for decoder in decoders {
if decoder.program_id() == owner_str {
if let Some(asset) = decoder.decode(address, &account.data)? {
cache.put_asset(asset.clone()).await?;
return Ok(Some(asset));
}
}
}
Ok(None)
}
fn derive_metadata_pda(mint_b58: &str) -> String {
let Ok(mint) = Pubkey::from_str(mint_b58) else {
return String::new();
};
let Ok(program) = Pubkey::from_str(MPL_TOKEN_METADATA_PROGRAM) else {
return String::new();
};
let (pda, _bump) =
Pubkey::find_program_address(&[b"metadata", program.as_ref(), mint.as_ref()], &program);
pda.to_string()
}