arch_token_metadata_sdk/
lib.rs

1//! Arch Token Metadata – Rust SDK (client-side helpers)
2//!
3//! This crate provides:
4//! - PDA helpers for core metadata and attributes accounts
5//! - Instruction builders with correct account ordering and client-side validation
6//! - Transaction builders for common flows (compose Vec<Instruction>)
7//!
8//! The builders mirror the on-chain program invariants documented in `docs/SECURITY.md`.
9//! Signers, recent blockhashes, and submission are left to the caller.
10
11use arch_program::compute_budget::ComputeBudgetInstruction;
12use arch_program::program_pack::Pack;
13use arch_program::{
14    account::AccountMeta, instruction::Instruction, pubkey::Pubkey, system_instruction,
15};
16
17use apl_token;
18
19use arch_token_metadata as program;
20use program::state::{
21    DESCRIPTION_MAX_LEN, IMAGE_MAX_LEN, MAX_ATTRIBUTES, MAX_KEY_LENGTH, MAX_VALUE_LENGTH,
22    NAME_MAX_LEN, SYMBOL_MAX_LEN,
23};
24
25// Reader support
26use anyhow::Context as _;
27use program::state::{TokenMetadata, TokenMetadataAttributes};
28
29/// Thin client for building PDAs and instructions for the Arch Token Metadata program.
30///
31/// The `program_id` must be the deployed Arch Token Metadata program id.
32pub struct TokenMetadataClient {
33    pub program_id: Pubkey,
34}
35
36impl TokenMetadataClient {
37    pub fn new(program_id: Pubkey) -> Self {
38        Self { program_id }
39    }
40
41    /// Derive the metadata PDA for a given mint.
42    pub fn metadata_pda(&self, mint: &Pubkey) -> Pubkey {
43        let (pda, _bump) = program::find_metadata_pda_with_program(&self.program_id, mint);
44        pda
45    }
46
47    /// Derive the metadata PDA for a given mint, with the bump.
48    pub fn metadata_pda_and_bump(&self, mint: &Pubkey) -> (Pubkey, u8) {
49        program::find_metadata_pda_with_program(&self.program_id, mint)
50    }
51
52    /// Derive the attributes PDA for a given mint.
53    pub fn attributes_pda(&self, mint: &Pubkey) -> Pubkey {
54        let (pda, _bump) = program::find_attributes_pda_with_program(&self.program_id, mint);
55        pda
56    }
57
58    /// Derive the attributes PDA for a given mint, with the bump.
59    pub fn attributes_pda_and_bump(&self, mint: &Pubkey) -> (Pubkey, u8) {
60        program::find_attributes_pda_with_program(&self.program_id, mint)
61    }
62
63    /// Build a CreateMetadata instruction.
64    ///
65    /// Accounts (strict order):
66    /// - payer (writable, signer)
67    /// - system_program (readonly)
68    /// - mint (readonly)
69    /// - metadata_pda (writable)
70    /// - mint_or_freeze_authority (readonly, signer)
71    pub fn create_metadata_ix(&self, params: CreateMetadataParams) -> anyhow::Result<Instruction> {
72        let metadata_pda = self.metadata_pda(&params.mint);
73        self.validate_metadata_fields(
74            &params.name,
75            &params.symbol,
76            &params.image,
77            &params.description,
78        )?;
79
80        let data = program::instruction::MetadataInstruction::CreateMetadata {
81            name: params.name,
82            symbol: params.symbol,
83            image: params.image,
84            description: params.description,
85            immutable: params.immutable,
86        }
87        .pack();
88
89        Ok(Instruction {
90            program_id: self.program_id,
91            accounts: vec![
92                AccountMeta::new(params.payer, true),
93                AccountMeta::new_readonly(Pubkey::system_program(), false),
94                AccountMeta::new_readonly(params.mint, false),
95                AccountMeta::new(metadata_pda, false),
96                AccountMeta::new_readonly(params.mint_or_freeze_authority, true),
97            ],
98            data,
99        })
100    }
101
102    /// Build an UpdateMetadata instruction.
103    ///
104    /// Accounts (strict order):
105    /// - metadata_pda (writable)
106    /// - update_authority (readonly, signer)
107    pub fn update_metadata_ix(&self, params: UpdateMetadataParams) -> anyhow::Result<Instruction> {
108        let metadata_pda = self.metadata_pda(&params.mint);
109        self.validate_optional_metadata_fields(
110            params.name.as_ref(),
111            params.symbol.as_ref(),
112            params.image.as_ref(),
113            params.description.as_ref(),
114        )?;
115        let data = program::instruction::MetadataInstruction::UpdateMetadata {
116            name: params.name,
117            symbol: params.symbol,
118            image: params.image,
119            description: params.description,
120        }
121        .pack();
122
123        Ok(Instruction {
124            program_id: self.program_id,
125            accounts: vec![
126                AccountMeta::new(metadata_pda, false),
127                AccountMeta::new_readonly(params.update_authority, true),
128            ],
129            data,
130        })
131    }
132
133    /// Build a CreateAttributes instruction.
134    ///
135    /// Accounts (strict order):
136    /// - payer (writable, signer)
137    /// - system_program (readonly)
138    /// - mint (readonly)
139    /// - attributes_pda (writable)
140    /// - update_authority (readonly, signer)
141    /// - metadata_pda (readonly)
142    pub fn create_attributes_ix(
143        &self,
144        params: CreateAttributesParams,
145    ) -> anyhow::Result<Instruction> {
146        let metadata_pda = self.metadata_pda(&params.mint);
147        let attributes_pda = self.attributes_pda(&params.mint);
148        self.validate_attributes(&params.data)?;
149
150        let data =
151            program::instruction::MetadataInstruction::CreateAttributes { data: params.data }
152                .pack();
153
154        Ok(Instruction {
155            program_id: self.program_id,
156            accounts: vec![
157                AccountMeta::new(params.payer, true),
158                AccountMeta::new_readonly(Pubkey::system_program(), false),
159                AccountMeta::new_readonly(params.mint, false),
160                AccountMeta::new(attributes_pda, false),
161                AccountMeta::new_readonly(params.update_authority, true),
162                AccountMeta::new_readonly(metadata_pda, false),
163            ],
164            data,
165        })
166    }
167
168    /// Build a ReplaceAttributes instruction.
169    ///
170    /// Accounts (strict order):
171    /// - attributes_pda (writable)
172    /// - update_authority (readonly, signer)
173    /// - metadata_pda (readonly)
174    pub fn replace_attributes_ix(
175        &self,
176        params: ReplaceAttributesParams,
177    ) -> anyhow::Result<Instruction> {
178        let metadata_pda = self.metadata_pda(&params.mint);
179        let attributes_pda = self.attributes_pda(&params.mint);
180        self.validate_attributes(&params.data)?;
181
182        let data =
183            program::instruction::MetadataInstruction::ReplaceAttributes { data: params.data }
184                .pack();
185
186        Ok(Instruction {
187            program_id: self.program_id,
188            accounts: vec![
189                AccountMeta::new(attributes_pda, false),
190                AccountMeta::new_readonly(params.update_authority, true),
191                AccountMeta::new_readonly(metadata_pda, false),
192            ],
193            data,
194        })
195    }
196
197    /// Build a TransferAuthority instruction.
198    ///
199    /// Accounts (strict order):
200    /// - metadata_pda (writable)
201    /// - current_update_authority (readonly, signer)
202    pub fn transfer_authority_ix(
203        &self,
204        params: TransferAuthorityParams,
205    ) -> anyhow::Result<Instruction> {
206        let metadata_pda = self.metadata_pda(&params.mint);
207        let data = program::instruction::MetadataInstruction::TransferAuthority {
208            new_authority: params.new_authority,
209        }
210        .pack();
211
212        Ok(Instruction {
213            program_id: self.program_id,
214            accounts: vec![
215                AccountMeta::new(metadata_pda, false),
216                AccountMeta::new_readonly(params.current_update_authority, true),
217            ],
218            data,
219        })
220    }
221
222    /// Build a MakeImmutable instruction.
223    ///
224    /// Accounts (strict order):
225    /// - metadata_pda (writable)
226    /// - current_update_authority (readonly, signer)
227    pub fn make_immutable_ix(&self, params: MakeImmutableParams) -> anyhow::Result<Instruction> {
228        let metadata_pda = self.metadata_pda(&params.mint);
229        let data = program::instruction::MetadataInstruction::MakeImmutable.pack();
230
231        Ok(Instruction {
232            program_id: self.program_id,
233            accounts: vec![
234                AccountMeta::new(metadata_pda, false),
235                AccountMeta::new_readonly(params.current_update_authority, true),
236            ],
237            data,
238        })
239    }
240
241    // Upstream APL Token program helpers
242    /// Build a SystemProgram create_account to allocate an APL Token mint account.
243    pub fn create_mint_account_ix(&self, payer: Pubkey, mint: Pubkey) -> Instruction {
244        use arch_program::account::MIN_ACCOUNT_LAMPORTS;
245        system_instruction::create_account(
246            &payer,
247            &mint,
248            MIN_ACCOUNT_LAMPORTS,
249            apl_token::state::Mint::LEN as u64,
250            &apl_token::id(),
251        )
252    }
253
254    /// Build an APL Token initialize_mint2 instruction.
255    pub fn initialize_mint2_ix(
256        &self,
257        mint: Pubkey,
258        mint_authority: Pubkey,
259        freeze_authority: Option<Pubkey>,
260        decimals: u8,
261    ) -> anyhow::Result<Instruction> {
262        let ix = apl_token::instruction::initialize_mint2(
263            &apl_token::id(),
264            &mint,
265            &mint_authority,
266            freeze_authority.as_ref(),
267            decimals,
268        )?;
269        Ok(ix)
270    }
271
272    /// Build an APL Token set_authority(MintTokens) instruction.
273    pub fn set_mint_authority_ix(
274        &self,
275        mint: Pubkey,
276        new_authority: Option<Pubkey>,
277        current_authority: Pubkey,
278    ) -> anyhow::Result<Instruction> {
279        let ix = apl_token::instruction::set_authority(
280            &apl_token::id(),
281            &mint,
282            new_authority.as_ref(),
283            apl_token::instruction::AuthorityType::MintTokens,
284            &current_authority,
285            &[],
286        )?;
287        Ok(ix)
288    }
289
290    // Transaction patterns (compose instructions; signing and submission left to caller)
291    /// Create an APL Token mint and Arch Token Metadata in one sequence.
292    ///
293    /// Returns a Vec<Instruction> with: [create_mint, initialize_mint2, create_metadata].
294    pub fn create_token_with_metadata_tx(
295        &self,
296        params: TxCreateTokenWithMetadataParams,
297    ) -> anyhow::Result<Vec<Instruction>> {
298        let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
299
300        let init_mint_ix = self.initialize_mint2_ix(
301            params.mint,
302            params.mint_authority,
303            params.freeze_authority,
304            params.decimals,
305        )?;
306
307        let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
308            payer: params.payer,
309            mint: params.mint,
310            mint_or_freeze_authority: params.mint_authority,
311            name: params.name,
312            symbol: params.symbol,
313            image: params.image,
314            description: params.description,
315            immutable: params.immutable,
316        })?;
317
318        Ok(vec![create_mint_ix, init_mint_ix, create_md_ix])
319    }
320
321    /// Same as `create_token_with_metadata_tx`, but allows prepending compute-budget instructions.
322    pub fn create_token_with_metadata_tx_with_budget(
323        &self,
324        params: TxCreateTokenWithMetadataParams,
325        budget: ComputeBudgetOptions,
326    ) -> anyhow::Result<Vec<Instruction>> {
327        let mut out = self.compute_budget_ixs(&budget);
328        out.extend(self.create_token_with_metadata_tx(params)?);
329        Ok(out)
330    }
331
332    /// Create mint, initialize, create metadata, and create attributes in one sequence.
333    /// Returns: [create_mint, initialize_mint2, create_metadata, create_attributes].
334    pub fn create_token_with_metadata_and_attributes_tx(
335        &self,
336        params: TxCreateTokenWithMetadataAndAttributesParams,
337    ) -> anyhow::Result<Vec<Instruction>> {
338        let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
339        let init_mint_ix = self.initialize_mint2_ix(
340            params.mint,
341            params.mint_authority,
342            params.freeze_authority,
343            params.decimals,
344        )?;
345
346        let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
347            payer: params.payer,
348            mint: params.mint,
349            mint_or_freeze_authority: params.mint_authority,
350            name: params.name,
351            symbol: params.symbol,
352            image: params.image,
353            description: params.description,
354            immutable: params.immutable,
355        })?;
356
357        let create_attrs_ix = self.create_attributes_ix(CreateAttributesParams {
358            payer: params.payer,
359            mint: params.mint,
360            update_authority: params.mint_authority,
361            data: params.attributes,
362        })?;
363
364        Ok(vec![
365            create_mint_ix,
366            init_mint_ix,
367            create_md_ix,
368            create_attrs_ix,
369        ])
370    }
371
372    /// Same as `create_token_with_metadata_and_attributes_tx`, with compute-budget instructions.
373    pub fn create_token_with_metadata_and_attributes_tx_with_budget(
374        &self,
375        params: TxCreateTokenWithMetadataAndAttributesParams,
376        budget: ComputeBudgetOptions,
377    ) -> anyhow::Result<Vec<Instruction>> {
378        let mut out = self.compute_budget_ixs(&budget);
379        out.extend(self.create_token_with_metadata_and_attributes_tx(params)?);
380        Ok(out)
381    }
382
383    /// Create metadata using freeze authority by clearing mint authority beforehand.
384    /// Returns: [create_mint, initialize_mint2(with freeze), set_authority(MintTokens -> None), create_metadata]
385    pub fn create_token_with_freeze_auth_metadata_tx(
386        &self,
387        params: TxCreateTokenWithFreezeAuthMetadataParams,
388    ) -> anyhow::Result<Vec<Instruction>> {
389        let create_mint_ix = self.create_mint_account_ix(params.payer, params.mint);
390        let init_mint_ix = self.initialize_mint2_ix(
391            params.mint,
392            params.initial_mint_authority,
393            Some(params.freeze_authority),
394            params.decimals,
395        )?;
396
397        let clear_mint_auth_ix =
398            self.set_mint_authority_ix(params.mint, None, params.initial_mint_authority)?;
399
400        // create metadata signed by freeze authority
401        let create_md_ix = self.create_metadata_ix(CreateMetadataParams {
402            payer: params.payer,
403            mint: params.mint,
404            mint_or_freeze_authority: params.freeze_authority,
405            name: params.name,
406            symbol: params.symbol,
407            image: params.image,
408            description: params.description,
409            immutable: params.immutable,
410        })?;
411
412        Ok(vec![
413            create_mint_ix,
414            init_mint_ix,
415            clear_mint_auth_ix,
416            create_md_ix,
417        ])
418    }
419
420    /// Same as `create_token_with_freeze_auth_metadata_tx`, with compute-budget instructions.
421    pub fn create_token_with_freeze_auth_metadata_tx_with_budget(
422        &self,
423        params: TxCreateTokenWithFreezeAuthMetadataParams,
424        budget: ComputeBudgetOptions,
425    ) -> anyhow::Result<Vec<Instruction>> {
426        let mut out = self.compute_budget_ixs(&budget);
427        out.extend(self.create_token_with_freeze_auth_metadata_tx(params)?);
428        Ok(out)
429    }
430
431    /// Convenience wrapper returning one-instruction Vec for create_attributes.
432    pub fn create_attributes_tx(
433        &self,
434        params: CreateAttributesParams,
435    ) -> anyhow::Result<Vec<Instruction>> {
436        Ok(vec![self.create_attributes_ix(params)?])
437    }
438
439    /// `create_attributes_tx` with compute-budget instructions prepended.
440    pub fn create_attributes_tx_with_budget(
441        &self,
442        params: CreateAttributesParams,
443        budget: ComputeBudgetOptions,
444    ) -> anyhow::Result<Vec<Instruction>> {
445        let mut out = self.compute_budget_ixs(&budget);
446        out.extend(self.create_attributes_tx(params)?);
447        Ok(out)
448    }
449
450    /// Convenience wrapper returning one-instruction Vec for replace_attributes.
451    pub fn replace_attributes_tx(
452        &self,
453        params: ReplaceAttributesParams,
454    ) -> anyhow::Result<Vec<Instruction>> {
455        Ok(vec![self.replace_attributes_ix(params)?])
456    }
457
458    /// `replace_attributes_tx` with compute-budget instructions prepended.
459    pub fn replace_attributes_tx_with_budget(
460        &self,
461        params: ReplaceAttributesParams,
462        budget: ComputeBudgetOptions,
463    ) -> anyhow::Result<Vec<Instruction>> {
464        let mut out = self.compute_budget_ixs(&budget);
465        out.extend(self.replace_attributes_tx(params)?);
466        Ok(out)
467    }
468
469    /// Transfer authority then immediately update metadata. Requires both current and new authorities to sign.
470    /// Returns: [transfer_authority, update_metadata]
471    pub fn transfer_authority_then_update_tx(
472        &self,
473        params: TxTransferAuthorityThenUpdateParams,
474    ) -> anyhow::Result<Vec<Instruction>> {
475        let transfer_ix = self.transfer_authority_ix(TransferAuthorityParams {
476            mint: params.mint,
477            current_update_authority: params.current_update_authority,
478            new_authority: params.new_authority,
479        })?;
480
481        let update_ix = self.update_metadata_ix(UpdateMetadataParams {
482            mint: params.mint,
483            update_authority: params.new_authority,
484            name: params.name,
485            symbol: params.symbol,
486            image: params.image,
487            description: params.description,
488        })?;
489
490        Ok(vec![transfer_ix, update_ix])
491    }
492
493    /// `transfer_authority_then_update_tx` with compute-budget instructions prepended.
494    pub fn transfer_authority_then_update_tx_with_budget(
495        &self,
496        params: TxTransferAuthorityThenUpdateParams,
497        budget: ComputeBudgetOptions,
498    ) -> anyhow::Result<Vec<Instruction>> {
499        let mut out = self.compute_budget_ixs(&budget);
500        out.extend(self.transfer_authority_then_update_tx(params)?);
501        Ok(out)
502    }
503
504    /// Convenience wrapper returning one-instruction Vec for make_immutable.
505    pub fn make_immutable_tx(
506        &self,
507        params: MakeImmutableParams,
508    ) -> anyhow::Result<Vec<Instruction>> {
509        Ok(vec![self.make_immutable_ix(params)?])
510    }
511
512    /// `make_immutable_tx` with compute-budget instructions prepended.
513    pub fn make_immutable_tx_with_budget(
514        &self,
515        params: MakeImmutableParams,
516        budget: ComputeBudgetOptions,
517    ) -> anyhow::Result<Vec<Instruction>> {
518        let mut out = self.compute_budget_ixs(&budget);
519        out.extend(self.make_immutable_tx(params)?);
520        Ok(out)
521    }
522
523    /// Compute budget: set a per-transaction compute unit limit.
524    pub fn set_compute_unit_limit_ix(&self, units: u32) -> Instruction {
525        ComputeBudgetInstruction::set_compute_unit_limit(units)
526    }
527
528    /// Compute budget: request a specific heap frame size in bytes (multiple of 1024).
529    pub fn request_heap_frame_ix(&self, bytes: u32) -> Instruction {
530        ComputeBudgetInstruction::request_heap_frame(bytes)
531    }
532
533    fn compute_budget_ixs(&self, opts: &ComputeBudgetOptions) -> Vec<Instruction> {
534        let mut out: Vec<Instruction> = Vec::new();
535        if let Some(units) = opts.units {
536            out.push(self.set_compute_unit_limit_ix(units));
537        }
538        if let Some(bytes) = opts.heap_bytes {
539            out.push(self.request_heap_frame_ix(bytes));
540        }
541        out
542    }
543
544    fn validate_metadata_fields(
545        &self,
546        name: &str,
547        symbol: &str,
548        image: &str,
549        description: &str,
550    ) -> anyhow::Result<()> {
551        anyhow::ensure!(name.len() <= NAME_MAX_LEN, "name too long");
552        anyhow::ensure!(symbol.len() <= SYMBOL_MAX_LEN, "symbol too long");
553        anyhow::ensure!(image.len() <= IMAGE_MAX_LEN, "image too long");
554        anyhow::ensure!(
555            description.len() <= DESCRIPTION_MAX_LEN,
556            "description too long"
557        );
558        Ok(())
559    }
560
561    fn validate_optional_metadata_fields(
562        &self,
563        name: Option<&String>,
564        symbol: Option<&String>,
565        image: Option<&String>,
566        description: Option<&String>,
567    ) -> anyhow::Result<()> {
568        if let Some(v) = name {
569            anyhow::ensure!(v.len() <= NAME_MAX_LEN, "name too long");
570        }
571        if let Some(v) = symbol {
572            anyhow::ensure!(v.len() <= SYMBOL_MAX_LEN, "symbol too long");
573        }
574        if let Some(v) = image {
575            anyhow::ensure!(v.len() <= IMAGE_MAX_LEN, "image too long");
576        }
577        if let Some(v) = description {
578            anyhow::ensure!(v.len() <= DESCRIPTION_MAX_LEN, "description too long");
579        }
580        Ok(())
581    }
582
583    fn validate_attributes(&self, data: &[(String, String)]) -> anyhow::Result<()> {
584        anyhow::ensure!(data.len() <= MAX_ATTRIBUTES, "too many attributes");
585        for (k, v) in data.iter() {
586            anyhow::ensure!(
587                !k.is_empty() && !v.is_empty(),
588                "attribute key and value must be non-empty"
589            );
590            anyhow::ensure!(k.len() <= MAX_KEY_LENGTH, "attribute key too long");
591            anyhow::ensure!(v.len() <= MAX_VALUE_LENGTH, "attribute value too long");
592        }
593        Ok(())
594    }
595}
596
597impl Default for TokenMetadataClient {
598    fn default() -> Self {
599        Self {
600            program_id: program::id(),
601        }
602    }
603}
604
605/// Return the canonical default program id for Arch Token Metadata.
606pub fn default_program_id() -> Pubkey {
607    program::id()
608}
609
610// Well-known attribute keys
611pub mod well_known_attributes {
612    pub const TWITTER: &str = "twitter";
613    pub const TELEGRAM: &str = "telegram";
614    pub const WEBSITE: &str = "website";
615    pub const DISCORD: &str = "discord";
616    pub const COINGECKO: &str = "coingecko";
617    pub const WHITEPAPER: &str = "whitepaper";
618    pub const AUDIT: &str = "audit";
619    pub const CATEGORY: &str = "category";
620    pub const TAGS: &str = "tags";
621}
622
623// === Reader (async RPC-based helpers) ===
624
625/// Minimal account data used by the reader utilities.
626pub struct AccountDataLite {
627    pub data: Vec<u8>,
628    pub owner: Pubkey,
629}
630
631/// Minimal async RPC trait required by reader utilities. Implemented for arch_sdk client via an adapter.
632#[async_trait::async_trait]
633pub trait AsyncAccountReader: Send + Sync {
634    async fn get_multiple_accounts(
635        &self,
636        pubkeys: &[Pubkey],
637    ) -> anyhow::Result<Vec<Option<AccountDataLite>>>;
638}
639
640/// Reader for fetching and decoding token metadata accounts using an injected async RPC.
641pub struct TokenMetadataReader<R: AsyncAccountReader> {
642    program_id: Pubkey,
643    rpc: R,
644}
645
646impl<R: AsyncAccountReader> TokenMetadataReader<R> {
647    pub fn new(program_id: Pubkey, rpc: R) -> Self {
648        Self { program_id, rpc }
649    }
650
651    fn metadata_pda(&self, mint: &Pubkey) -> Pubkey {
652        let (pda, _bump) = program::find_metadata_pda_with_program(&self.program_id, mint);
653        pda
654    }
655    fn attributes_pda(&self, mint: &Pubkey) -> Pubkey {
656        let (pda, _bump) = program::find_attributes_pda_with_program(&self.program_id, mint);
657        pda
658    }
659
660    fn is_owner_ok(&self, owner: &Pubkey) -> bool {
661        owner == &self.program_id
662    }
663
664    pub async fn get_token_metadata(&self, mint: Pubkey) -> anyhow::Result<Option<TokenMetadata>> {
665        let pda = self.metadata_pda(&mint);
666        let v = self.rpc.get_multiple_accounts(&[pda]).await?.pop().unwrap();
667        let Some(acc) = v else { return Ok(None) };
668        if !self.is_owner_ok(&acc.owner) {
669            return Ok(None);
670        }
671        let md = TokenMetadata::unpack_from_slice(&acc.data).context("unpack TokenMetadata")?;
672        Ok(Some(md))
673    }
674
675    pub async fn get_token_metadata_attributes(
676        &self,
677        mint: Pubkey,
678    ) -> anyhow::Result<Option<TokenMetadataAttributes>> {
679        let pda = self.attributes_pda(&mint);
680        let v = self.rpc.get_multiple_accounts(&[pda]).await?.pop().unwrap();
681        let Some(acc) = v else { return Ok(None) };
682        if !self.is_owner_ok(&acc.owner) {
683            return Ok(None);
684        }
685        let attrs = TokenMetadataAttributes::unpack_from_slice(&acc.data)
686            .context("unpack TokenMetadataAttributes")?;
687        Ok(Some(attrs))
688    }
689
690    pub async fn get_token_details(
691        &self,
692        mint: Pubkey,
693    ) -> anyhow::Result<(Option<TokenMetadata>, Option<TokenMetadataAttributes>)> {
694        let md_pda = self.metadata_pda(&mint);
695        let at_pda = self.attributes_pda(&mint);
696        let res = self.rpc.get_multiple_accounts(&[md_pda, at_pda]).await?;
697        let md_opt = match &res[0] {
698            Some(acc) if self.is_owner_ok(&acc.owner) => {
699                Some(TokenMetadata::unpack_from_slice(&acc.data).context("unpack TokenMetadata")?)
700            }
701            _ => None,
702        };
703        let at_opt = match &res[1] {
704            Some(acc) if self.is_owner_ok(&acc.owner) => Some(
705                TokenMetadataAttributes::unpack_from_slice(&acc.data)
706                    .context("unpack TokenMetadataAttributes")?,
707            ),
708            _ => None,
709        };
710        Ok((md_opt, at_opt))
711    }
712
713    pub async fn get_token_metadata_batch(
714        &self,
715        mints: &[Pubkey],
716    ) -> anyhow::Result<Vec<Option<TokenMetadata>>> {
717        let pdas: Vec<Pubkey> = mints.iter().map(|m| self.metadata_pda(m)).collect();
718        let res = self.rpc.get_multiple_accounts(&pdas).await?;
719        let mut out = Vec::with_capacity(res.len());
720        for maybe in res {
721            if let Some(acc) = maybe {
722                if self.is_owner_ok(&acc.owner) {
723                    let md = TokenMetadata::unpack_from_slice(&acc.data)
724                        .context("unpack TokenMetadata")?;
725                    out.push(Some(md));
726                    continue;
727                }
728            }
729            out.push(None);
730        }
731        Ok(out)
732    }
733
734    pub async fn get_token_metadata_attributes_batch(
735        &self,
736        mints: &[Pubkey],
737    ) -> anyhow::Result<Vec<Option<TokenMetadataAttributes>>> {
738        let pdas: Vec<Pubkey> = mints.iter().map(|m| self.attributes_pda(m)).collect();
739        let res = self.rpc.get_multiple_accounts(&pdas).await?;
740        let mut out = Vec::with_capacity(res.len());
741        for maybe in res {
742            if let Some(acc) = maybe {
743                if self.is_owner_ok(&acc.owner) {
744                    let attrs = TokenMetadataAttributes::unpack_from_slice(&acc.data)
745                        .context("unpack TokenMetadataAttributes")?;
746                    out.push(Some(attrs));
747                    continue;
748                }
749            }
750            out.push(None);
751        }
752        Ok(out)
753    }
754}
755
756// Concrete adapter for arch_sdk::AsyncArchRpcClient
757#[async_trait::async_trait]
758impl AsyncAccountReader for arch_sdk::AsyncArchRpcClient {
759    async fn get_multiple_accounts(
760        &self,
761        pubkeys: &[Pubkey],
762    ) -> anyhow::Result<Vec<Option<AccountDataLite>>> {
763        let mut out: Vec<Option<AccountDataLite>> = Vec::with_capacity(pubkeys.len());
764        for pk in pubkeys.iter() {
765            match self.read_account_info(*pk).await {
766                Ok(info) => {
767                    out.push(Some(AccountDataLite {
768                        data: info.data,
769                        owner: info.owner,
770                    }));
771                }
772                Err(_e) => {
773                    // If account missing or RPC error, return None for this entry
774                    out.push(None);
775                }
776            }
777        }
778        Ok(out)
779    }
780}
781
782// === Params ===
783/// Parameters for CreateMetadata instruction.
784#[derive(Clone, Debug)]
785pub struct CreateMetadataParams {
786    /// Account that pays for the metadata PDA creation
787    pub payer: Pubkey,
788    /// Token mint the metadata is associated with
789    pub mint: Pubkey,
790    /// Signer that must match the mint authority, or the freeze authority if mint authority is None
791    pub mint_or_freeze_authority: Pubkey,
792    /// Token name (<= NAME_MAX_LEN)
793    pub name: String,
794    /// Token symbol (<= SYMBOL_MAX_LEN)
795    pub symbol: String,
796    /// Image URI (<= IMAGE_MAX_LEN)
797    pub image: String,
798    /// Description text (<= DESCRIPTION_MAX_LEN)
799    pub description: String,
800    /// If true, metadata is immutable (no update authority retained)
801    pub immutable: bool,
802}
803
804/// Parameters for UpdateMetadata instruction.
805#[derive(Clone, Debug)]
806pub struct UpdateMetadataParams {
807    /// Token mint whose metadata is being updated
808    pub mint: Pubkey,
809    /// Current update authority (must sign)
810    pub update_authority: Pubkey,
811    /// Optional new name (<= NAME_MAX_LEN)
812    pub name: Option<String>,
813    /// Optional new symbol (<= SYMBOL_MAX_LEN)
814    pub symbol: Option<String>,
815    /// Optional new image URI (<= IMAGE_MAX_LEN)
816    pub image: Option<String>,
817    /// Optional new description (<= DESCRIPTION_MAX_LEN)
818    pub description: Option<String>,
819}
820
821/// Parameters for CreateAttributes instruction.
822#[derive(Clone, Debug)]
823pub struct CreateAttributesParams {
824    /// Payer for attributes PDA creation
825    pub payer: Pubkey,
826    /// Token mint the attributes are associated with
827    pub mint: Pubkey,
828    /// Current update authority (must sign)
829    pub update_authority: Pubkey,
830    /// Attribute key-value pairs; length <= MAX_ATTRIBUTES; each key/value length constrained
831    pub data: Vec<(String, String)>,
832}
833
834/// Parameters for ReplaceAttributes instruction.
835#[derive(Clone, Debug)]
836pub struct ReplaceAttributesParams {
837    /// Token mint whose attributes are being replaced
838    pub mint: Pubkey,
839    /// Current update authority (must sign)
840    pub update_authority: Pubkey,
841    /// New full attributes vector to replace the existing one
842    pub data: Vec<(String, String)>,
843}
844
845/// Parameters for TransferAuthority instruction.
846#[derive(Clone, Debug)]
847pub struct TransferAuthorityParams {
848    /// Token mint whose metadata authority is being transferred
849    pub mint: Pubkey,
850    /// Current update authority (must sign)
851    pub current_update_authority: Pubkey,
852    /// New authority to set
853    pub new_authority: Pubkey,
854}
855
856/// Parameters for MakeImmutable instruction.
857#[derive(Clone, Debug)]
858pub struct MakeImmutableParams {
859    /// Token mint whose metadata is being made immutable
860    pub mint: Pubkey,
861    /// Current update authority (must sign)
862    pub current_update_authority: Pubkey,
863}
864
865/// Parameters for tx_create_token_with_metadata transaction pattern.
866#[derive(Clone, Debug)]
867pub struct TxCreateTokenWithMetadataParams {
868    /// Payer that funds the mint account and metadata PDA
869    pub payer: Pubkey,
870    /// Mint account public key
871    pub mint: Pubkey,
872    /// Initial mint authority
873    pub mint_authority: Pubkey,
874    /// Optional freeze authority for the mint
875    pub freeze_authority: Option<Pubkey>,
876    /// Number of decimals for the mint
877    pub decimals: u8,
878    /// Metadata name
879    pub name: String,
880    /// Metadata symbol
881    pub symbol: String,
882    /// Metadata image URI
883    pub image: String,
884    /// Metadata description
885    pub description: String,
886    /// Whether metadata should be immutable at creation
887    pub immutable: bool,
888}
889
890/// Parameters for tx_create_token_with_metadata_and_attributes transaction pattern.
891#[derive(Clone, Debug)]
892pub struct TxCreateTokenWithMetadataAndAttributesParams {
893    /// Payer that funds the mint account and PDAs
894    pub payer: Pubkey,
895    /// Mint account public key
896    pub mint: Pubkey,
897    /// Initial mint authority (and update authority for metadata/attributes)
898    pub mint_authority: Pubkey,
899    /// Optional freeze authority for the mint
900    pub freeze_authority: Option<Pubkey>,
901    /// Number of decimals for the mint
902    pub decimals: u8,
903    /// Metadata fields
904    pub name: String,
905    pub symbol: String,
906    pub image: String,
907    pub description: String,
908    pub immutable: bool,
909    /// Initial attributes to set
910    pub attributes: Vec<(String, String)>,
911}
912
913/// Parameters for freeze-authority metadata creation flow.
914#[derive(Clone, Debug)]
915pub struct TxCreateTokenWithFreezeAuthMetadataParams {
916    /// Payer that funds the mint account and metadata PDA
917    pub payer: Pubkey,
918    /// Mint account public key
919    pub mint: Pubkey,
920    /// Initial mint authority that will be cleared to None before create_metadata
921    pub initial_mint_authority: Pubkey,
922    /// Freeze authority who will sign create_metadata
923    pub freeze_authority: Pubkey,
924    /// Number of decimals for the mint
925    pub decimals: u8,
926    /// Metadata fields
927    pub name: String,
928    pub symbol: String,
929    pub image: String,
930    pub description: String,
931    pub immutable: bool,
932}
933
934/// Parameters for transfer_authority followed by update in one transaction.
935#[derive(Clone, Debug)]
936pub struct TxTransferAuthorityThenUpdateParams {
937    /// Token mint
938    pub mint: Pubkey,
939    /// Current update authority (signs transfer)
940    pub current_update_authority: Pubkey,
941    /// New authority to transfer to (signs update)
942    pub new_authority: Pubkey,
943    /// Update fields applied by the new authority
944    pub name: Option<String>,
945    pub symbol: Option<String>,
946    pub image: Option<String>,
947    pub description: Option<String>,
948}
949
950/// Optional compute budget options to prepend to transaction builders.
951#[derive(Clone, Copy, Debug, Default)]
952pub struct ComputeBudgetOptions {
953    /// Optional per-transaction compute unit limit.
954    pub units: Option<u32>,
955    /// Optional requested heap frame size (multiple of 1024).
956    pub heap_bytes: Option<u32>,
957}