use arch_program::compute_budget::ComputeBudgetInstruction;
use arch_program::program_pack::Pack;
use arch_program::{
account::AccountMeta, instruction::Instruction, pubkey::Pubkey, system_instruction,
};
use apl_token;
use arch_token_metadata as program;
use program::state::{
DESCRIPTION_MAX_LEN, IMAGE_MAX_LEN, MAX_ATTRIBUTES, MAX_KEY_LENGTH, MAX_VALUE_LENGTH,
NAME_MAX_LEN, SYMBOL_MAX_LEN,
};
use anyhow::Context as _;
use program::state::{TokenMetadata, TokenMetadataAttributes};
pub struct TokenMetadataClient {
pub program_id: Pubkey,
}
impl TokenMetadataClient {
pub fn new(program_id: Pubkey) -> Self {
Self { program_id }
}
pub fn metadata_pda(&self, mint: &Pubkey) -> Pubkey {
let (pda, _bump) = program::find_metadata_pda_with_program(&self.program_id, mint);
pda
}
pub fn metadata_pda_and_bump(&self, mint: &Pubkey) -> (Pubkey, u8) {
program::find_metadata_pda_with_program(&self.program_id, mint)
}
pub fn attributes_pda(&self, mint: &Pubkey) -> Pubkey {
let (pda, _bump) = program::find_attributes_pda_with_program(&self.program_id, mint);
pda
}
pub fn attributes_pda_and_bump(&self, mint: &Pubkey) -> (Pubkey, u8) {
program::find_attributes_pda_with_program(&self.program_id, mint)
}
pub fn create_metadata_ix(&self, params: CreateMetadataParams) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
self.validate_metadata_fields(
¶ms.name,
¶ms.symbol,
¶ms.image,
¶ms.description,
)?;
let data = program::instruction::MetadataInstruction::CreateMetadata {
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
immutable: params.immutable,
}
.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(params.payer, true),
AccountMeta::new_readonly(Pubkey::system_program(), false),
AccountMeta::new_readonly(params.mint, false),
AccountMeta::new(metadata_pda, false),
AccountMeta::new_readonly(params.mint_or_freeze_authority, true),
],
data,
})
}
pub fn update_metadata_ix(&self, params: UpdateMetadataParams) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
self.validate_optional_metadata_fields(
params.name.as_ref(),
params.symbol.as_ref(),
params.image.as_ref(),
params.description.as_ref(),
)?;
let data = program::instruction::MetadataInstruction::UpdateMetadata {
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
}
.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(metadata_pda, false),
AccountMeta::new_readonly(params.update_authority, true),
],
data,
})
}
pub fn create_attributes_ix(
&self,
params: CreateAttributesParams,
) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
let attributes_pda = self.attributes_pda(¶ms.mint);
self.validate_attributes(¶ms.data)?;
let data =
program::instruction::MetadataInstruction::CreateAttributes { data: params.data }
.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(params.payer, true),
AccountMeta::new_readonly(Pubkey::system_program(), false),
AccountMeta::new_readonly(params.mint, false),
AccountMeta::new(attributes_pda, false),
AccountMeta::new_readonly(params.update_authority, true),
AccountMeta::new_readonly(metadata_pda, false),
],
data,
})
}
pub fn replace_attributes_ix(
&self,
params: ReplaceAttributesParams,
) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
let attributes_pda = self.attributes_pda(¶ms.mint);
self.validate_attributes(¶ms.data)?;
let data =
program::instruction::MetadataInstruction::ReplaceAttributes { data: params.data }
.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(attributes_pda, false),
AccountMeta::new_readonly(params.update_authority, true),
AccountMeta::new_readonly(metadata_pda, false),
],
data,
})
}
pub fn transfer_authority_ix(
&self,
params: TransferAuthorityParams,
) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
let data = program::instruction::MetadataInstruction::TransferAuthority {
new_authority: params.new_authority,
}
.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(metadata_pda, false),
AccountMeta::new_readonly(params.current_update_authority, true),
],
data,
})
}
pub fn make_immutable_ix(&self, params: MakeImmutableParams) -> anyhow::Result<Instruction> {
let metadata_pda = self.metadata_pda(¶ms.mint);
let data = program::instruction::MetadataInstruction::MakeImmutable.pack();
Ok(Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(metadata_pda, false),
AccountMeta::new_readonly(params.current_update_authority, true),
],
data,
})
}
pub fn create_mint_account_ix(&self, payer: Pubkey, mint: Pubkey) -> Instruction {
use arch_program::account::MIN_ACCOUNT_LAMPORTS;
system_instruction::create_account(
&payer,
&mint,
MIN_ACCOUNT_LAMPORTS,
apl_token::state::Mint::LEN as u64,
&apl_token::id(),
)
}
pub fn initialize_mint2_ix(
&self,
mint: Pubkey,
mint_authority: Pubkey,
freeze_authority: Option<Pubkey>,
decimals: u8,
) -> anyhow::Result<Instruction> {
let ix = apl_token::instruction::initialize_mint2(
&apl_token::id(),
&mint,
&mint_authority,
freeze_authority.as_ref(),
decimals,
)?;
Ok(ix)
}
pub fn set_mint_authority_ix(
&self,
mint: Pubkey,
new_authority: Option<Pubkey>,
current_authority: Pubkey,
) -> anyhow::Result<Instruction> {
let ix = apl_token::instruction::set_authority(
&apl_token::id(),
&mint,
new_authority.as_ref(),
apl_token::instruction::AuthorityType::MintTokens,
¤t_authority,
&[],
)?;
Ok(ix)
}
pub fn create_token_with_metadata_tx(
&self,
params: TxCreateTokenWithMetadataParams,
) -> anyhow::Result<Vec<Instruction>> {
let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
let init_mint_ix = self.initialize_mint2_ix(
params.mint,
params.mint_authority,
params.freeze_authority,
params.decimals,
)?;
let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
payer: params.payer,
mint: params.mint,
mint_or_freeze_authority: params.mint_authority,
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
immutable: params.immutable,
})?;
Ok(vec![create_mint_ix, init_mint_ix, create_md_ix])
}
pub fn create_token_with_metadata_tx_with_budget(
&self,
params: TxCreateTokenWithMetadataParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.create_token_with_metadata_tx(params)?);
Ok(out)
}
pub fn create_token_with_metadata_and_attributes_tx(
&self,
params: TxCreateTokenWithMetadataAndAttributesParams,
) -> anyhow::Result<Vec<Instruction>> {
let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
let init_mint_ix = self.initialize_mint2_ix(
params.mint,
params.mint_authority,
params.freeze_authority,
params.decimals,
)?;
let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
payer: params.payer,
mint: params.mint,
mint_or_freeze_authority: params.mint_authority,
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
immutable: params.immutable,
})?;
let create_attrs_ix = self.create_attributes_ix(CreateAttributesParams {
payer: params.payer,
mint: params.mint,
update_authority: params.mint_authority,
data: params.attributes,
})?;
Ok(vec![
create_mint_ix,
init_mint_ix,
create_md_ix,
create_attrs_ix,
])
}
pub fn create_token_with_metadata_and_attributes_tx_with_budget(
&self,
params: TxCreateTokenWithMetadataAndAttributesParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.create_token_with_metadata_and_attributes_tx(params)?);
Ok(out)
}
pub fn create_token_with_freeze_auth_metadata_tx(
&self,
params: TxCreateTokenWithFreezeAuthMetadataParams,
) -> anyhow::Result<Vec<Instruction>> {
let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
let init_mint_ix = self.initialize_mint2_ix(
params.mint,
params.initial_mint_authority,
Some(params.freeze_authority),
params.decimals,
)?;
let clear_mint_auth_ix =
self.set_mint_authority_ix(params.mint, None, params.initial_mint_authority)?;
let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
payer: params.payer,
mint: params.mint,
mint_or_freeze_authority: params.freeze_authority,
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
immutable: params.immutable,
})?;
Ok(vec![
create_mint_ix,
init_mint_ix,
clear_mint_auth_ix,
create_md_ix,
])
}
pub fn create_token_with_freeze_auth_metadata_tx_with_budget(
&self,
params: TxCreateTokenWithFreezeAuthMetadataParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.create_token_with_freeze_auth_metadata_tx(params)?);
Ok(out)
}
pub fn create_attributes_tx(
&self,
params: CreateAttributesParams,
) -> anyhow::Result<Vec<Instruction>> {
Ok(vec![self.create_attributes_ix(params)?])
}
pub fn create_attributes_tx_with_budget(
&self,
params: CreateAttributesParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.create_attributes_tx(params)?);
Ok(out)
}
pub fn replace_attributes_tx(
&self,
params: ReplaceAttributesParams,
) -> anyhow::Result<Vec<Instruction>> {
Ok(vec![self.replace_attributes_ix(params)?])
}
pub fn replace_attributes_tx_with_budget(
&self,
params: ReplaceAttributesParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.replace_attributes_tx(params)?);
Ok(out)
}
pub fn transfer_authority_then_update_tx(
&self,
params: TxTransferAuthorityThenUpdateParams,
) -> anyhow::Result<Vec<Instruction>> {
let transfer_ix = self.transfer_authority_ix(TransferAuthorityParams {
mint: params.mint,
current_update_authority: params.current_update_authority,
new_authority: params.new_authority,
})?;
let update_ix = self.update_metadata_ix(UpdateMetadataParams {
mint: params.mint,
update_authority: params.new_authority,
name: params.name,
symbol: params.symbol,
image: params.image,
description: params.description,
})?;
Ok(vec![transfer_ix, update_ix])
}
pub fn transfer_authority_then_update_tx_with_budget(
&self,
params: TxTransferAuthorityThenUpdateParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.transfer_authority_then_update_tx(params)?);
Ok(out)
}
pub fn make_immutable_tx(
&self,
params: MakeImmutableParams,
) -> anyhow::Result<Vec<Instruction>> {
Ok(vec![self.make_immutable_ix(params)?])
}
pub fn make_immutable_tx_with_budget(
&self,
params: MakeImmutableParams,
budget: ComputeBudgetOptions,
) -> anyhow::Result<Vec<Instruction>> {
let mut out = self.compute_budget_ixs(&budget);
out.extend(self.make_immutable_tx(params)?);
Ok(out)
}
pub fn set_compute_unit_limit_ix(&self, units: u32) -> Instruction {
ComputeBudgetInstruction::set_compute_unit_limit(units)
}
pub fn request_heap_frame_ix(&self, bytes: u32) -> Instruction {
ComputeBudgetInstruction::request_heap_frame(bytes)
}
fn compute_budget_ixs(&self, opts: &ComputeBudgetOptions) -> Vec<Instruction> {
let mut out: Vec<Instruction> = Vec::new();
if let Some(units) = opts.units {
out.push(self.set_compute_unit_limit_ix(units));
}
if let Some(bytes) = opts.heap_bytes {
out.push(self.request_heap_frame_ix(bytes));
}
out
}
fn validate_metadata_fields(
&self,
name: &str,
symbol: &str,
image: &str,
description: &str,
) -> anyhow::Result<()> {
anyhow::ensure!(name.len() <= NAME_MAX_LEN, "name too long");
anyhow::ensure!(symbol.len() <= SYMBOL_MAX_LEN, "symbol too long");
anyhow::ensure!(image.len() <= IMAGE_MAX_LEN, "image too long");
anyhow::ensure!(
description.len() <= DESCRIPTION_MAX_LEN,
"description too long"
);
Ok(())
}
fn validate_optional_metadata_fields(
&self,
name: Option<&String>,
symbol: Option<&String>,
image: Option<&String>,
description: Option<&String>,
) -> anyhow::Result<()> {
if let Some(v) = name {
anyhow::ensure!(v.len() <= NAME_MAX_LEN, "name too long");
}
if let Some(v) = symbol {
anyhow::ensure!(v.len() <= SYMBOL_MAX_LEN, "symbol too long");
}
if let Some(v) = image {
anyhow::ensure!(v.len() <= IMAGE_MAX_LEN, "image too long");
}
if let Some(v) = description {
anyhow::ensure!(v.len() <= DESCRIPTION_MAX_LEN, "description too long");
}
Ok(())
}
fn validate_attributes(&self, data: &[(String, String)]) -> anyhow::Result<()> {
anyhow::ensure!(data.len() <= MAX_ATTRIBUTES, "too many attributes");
for (k, v) in data.iter() {
anyhow::ensure!(
!k.is_empty() && !v.is_empty(),
"attribute key and value must be non-empty"
);
anyhow::ensure!(k.len() <= MAX_KEY_LENGTH, "attribute key too long");
anyhow::ensure!(v.len() <= MAX_VALUE_LENGTH, "attribute value too long");
}
Ok(())
}
}
impl Default for TokenMetadataClient {
fn default() -> Self {
Self {
program_id: program::id(),
}
}
}
pub fn default_program_id() -> Pubkey {
program::id()
}
pub mod well_known_attributes {
pub const TWITTER: &str = "twitter";
pub const TELEGRAM: &str = "telegram";
pub const WEBSITE: &str = "website";
pub const DISCORD: &str = "discord";
pub const COINGECKO: &str = "coingecko";
pub const WHITEPAPER: &str = "whitepaper";
pub const AUDIT: &str = "audit";
pub const CATEGORY: &str = "category";
pub const TAGS: &str = "tags";
}
pub struct AccountDataLite {
pub data: Vec<u8>,
pub owner: Pubkey,
}
#[async_trait::async_trait]
pub trait AsyncAccountReader: Send + Sync {
async fn get_multiple_accounts(
&self,
pubkeys: &[Pubkey],
) -> anyhow::Result<Vec<Option<AccountDataLite>>>;
}
pub struct TokenMetadataReader<R: AsyncAccountReader> {
program_id: Pubkey,
rpc: R,
}
impl<R: AsyncAccountReader> TokenMetadataReader<R> {
pub fn new(program_id: Pubkey, rpc: R) -> Self {
Self { program_id, rpc }
}
fn metadata_pda(&self, mint: &Pubkey) -> Pubkey {
let (pda, _bump) = program::find_metadata_pda_with_program(&self.program_id, mint);
pda
}
fn attributes_pda(&self, mint: &Pubkey) -> Pubkey {
let (pda, _bump) = program::find_attributes_pda_with_program(&self.program_id, mint);
pda
}
fn is_owner_ok(&self, owner: &Pubkey) -> bool {
owner == &self.program_id
}
pub async fn get_token_metadata(&self, mint: Pubkey) -> anyhow::Result<Option<TokenMetadata>> {
let pda = self.metadata_pda(&mint);
let v = self.rpc.get_multiple_accounts(&[pda]).await?.pop().unwrap();
let Some(acc) = v else { return Ok(None) };
if !self.is_owner_ok(&acc.owner) {
return Ok(None);
}
let md = TokenMetadata::unpack_from_slice(&acc.data).context("unpack TokenMetadata")?;
Ok(Some(md))
}
pub async fn get_token_metadata_attributes(
&self,
mint: Pubkey,
) -> anyhow::Result<Option<TokenMetadataAttributes>> {
let pda = self.attributes_pda(&mint);
let v = self.rpc.get_multiple_accounts(&[pda]).await?.pop().unwrap();
let Some(acc) = v else { return Ok(None) };
if !self.is_owner_ok(&acc.owner) {
return Ok(None);
}
let attrs = TokenMetadataAttributes::unpack_from_slice(&acc.data)
.context("unpack TokenMetadataAttributes")?;
Ok(Some(attrs))
}
pub async fn get_token_details(
&self,
mint: Pubkey,
) -> anyhow::Result<(Option<TokenMetadata>, Option<TokenMetadataAttributes>)> {
let md_pda = self.metadata_pda(&mint);
let at_pda = self.attributes_pda(&mint);
let res = self.rpc.get_multiple_accounts(&[md_pda, at_pda]).await?;
let md_opt = match &res[0] {
Some(acc) if self.is_owner_ok(&acc.owner) => {
Some(TokenMetadata::unpack_from_slice(&acc.data).context("unpack TokenMetadata")?)
}
_ => None,
};
let at_opt = match &res[1] {
Some(acc) if self.is_owner_ok(&acc.owner) => Some(
TokenMetadataAttributes::unpack_from_slice(&acc.data)
.context("unpack TokenMetadataAttributes")?,
),
_ => None,
};
Ok((md_opt, at_opt))
}
pub async fn get_token_metadata_batch(
&self,
mints: &[Pubkey],
) -> anyhow::Result<Vec<Option<TokenMetadata>>> {
let pdas: Vec<Pubkey> = mints.iter().map(|m| self.metadata_pda(m)).collect();
let res = self.rpc.get_multiple_accounts(&pdas).await?;
let mut out = Vec::with_capacity(res.len());
for maybe in res {
if let Some(acc) = maybe {
if self.is_owner_ok(&acc.owner) {
let md = TokenMetadata::unpack_from_slice(&acc.data)
.context("unpack TokenMetadata")?;
out.push(Some(md));
continue;
}
}
out.push(None);
}
Ok(out)
}
pub async fn get_token_metadata_attributes_batch(
&self,
mints: &[Pubkey],
) -> anyhow::Result<Vec<Option<TokenMetadataAttributes>>> {
let pdas: Vec<Pubkey> = mints.iter().map(|m| self.attributes_pda(m)).collect();
let res = self.rpc.get_multiple_accounts(&pdas).await?;
let mut out = Vec::with_capacity(res.len());
for maybe in res {
if let Some(acc) = maybe {
if self.is_owner_ok(&acc.owner) {
let attrs = TokenMetadataAttributes::unpack_from_slice(&acc.data)
.context("unpack TokenMetadataAttributes")?;
out.push(Some(attrs));
continue;
}
}
out.push(None);
}
Ok(out)
}
}
#[async_trait::async_trait]
impl AsyncAccountReader for arch_sdk::AsyncArchRpcClient {
async fn get_multiple_accounts(
&self,
pubkeys: &[Pubkey],
) -> anyhow::Result<Vec<Option<AccountDataLite>>> {
let mut out: Vec<Option<AccountDataLite>> = Vec::with_capacity(pubkeys.len());
for pk in pubkeys.iter() {
match self.read_account_info(*pk).await {
Ok(info) => {
out.push(Some(AccountDataLite {
data: info.data,
owner: info.owner,
}));
}
Err(_e) => {
out.push(None);
}
}
}
Ok(out)
}
}
#[derive(Clone, Debug)]
pub struct CreateMetadataParams {
pub payer: Pubkey,
pub mint: Pubkey,
pub mint_or_freeze_authority: Pubkey,
pub name: String,
pub symbol: String,
pub image: String,
pub description: String,
pub immutable: bool,
}
#[derive(Clone, Debug)]
pub struct UpdateMetadataParams {
pub mint: Pubkey,
pub update_authority: Pubkey,
pub name: Option<String>,
pub symbol: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
}
#[derive(Clone, Debug)]
pub struct CreateAttributesParams {
pub payer: Pubkey,
pub mint: Pubkey,
pub update_authority: Pubkey,
pub data: Vec<(String, String)>,
}
#[derive(Clone, Debug)]
pub struct ReplaceAttributesParams {
pub mint: Pubkey,
pub update_authority: Pubkey,
pub data: Vec<(String, String)>,
}
#[derive(Clone, Debug)]
pub struct TransferAuthorityParams {
pub mint: Pubkey,
pub current_update_authority: Pubkey,
pub new_authority: Pubkey,
}
#[derive(Clone, Debug)]
pub struct MakeImmutableParams {
pub mint: Pubkey,
pub current_update_authority: Pubkey,
}
#[derive(Clone, Debug)]
pub struct TxCreateTokenWithMetadataParams {
pub payer: Pubkey,
pub mint: Pubkey,
pub mint_authority: Pubkey,
pub freeze_authority: Option<Pubkey>,
pub decimals: u8,
pub name: String,
pub symbol: String,
pub image: String,
pub description: String,
pub immutable: bool,
}
#[derive(Clone, Debug)]
pub struct TxCreateTokenWithMetadataAndAttributesParams {
pub payer: Pubkey,
pub mint: Pubkey,
pub mint_authority: Pubkey,
pub freeze_authority: Option<Pubkey>,
pub decimals: u8,
pub name: String,
pub symbol: String,
pub image: String,
pub description: String,
pub immutable: bool,
pub attributes: Vec<(String, String)>,
}
#[derive(Clone, Debug)]
pub struct TxCreateTokenWithFreezeAuthMetadataParams {
pub payer: Pubkey,
pub mint: Pubkey,
pub initial_mint_authority: Pubkey,
pub freeze_authority: Pubkey,
pub decimals: u8,
pub name: String,
pub symbol: String,
pub image: String,
pub description: String,
pub immutable: bool,
}
#[derive(Clone, Debug)]
pub struct TxTransferAuthorityThenUpdateParams {
pub mint: Pubkey,
pub current_update_authority: Pubkey,
pub new_authority: Pubkey,
pub name: Option<String>,
pub symbol: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ComputeBudgetOptions {
pub units: Option<u32>,
pub heap_bytes: Option<u32>,
}