Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
938 changes: 494 additions & 444 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ edition = "2021"

[workspace.dependencies]
anyhow = "1.0.98"
borsh = "0.10.4"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version of borsh synced to the version used in mpl-token-metadata due to compatibility reasons

bytemuck = { version = "1.23.2", features = ["derive"] }
clap = { version = "3.2.25", features = ["derive"] }
mollusk-svm = "0.4.2"
mollusk-svm-programs-token = "0.4.1"
mpl-token-metadata = "5.1.0"
num-derive = "0.4.2"
num-traits = "0.2.19"
serde = "1.0.219"
Expand Down
3 changes: 3 additions & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ test-sbf = []

[dependencies]
bytemuck = { workspace = true }
mpl-token-metadata = { workspace = true, features = ["serde"] }
num-derive = { workspace = true }
num-traits = { workspace = true }
serde_json = { workspace = true }
solana-account-info = { workspace = true }
solana-cpi = { workspace = true }
solana-instruction = { workspace = true }
Expand All @@ -38,6 +40,7 @@ thiserror = { workspace = true }
spl-token-2022 = { workspace = true }

[dev-dependencies]
borsh = { workspace = true }
mollusk-svm = { workspace = true }
mollusk-svm-programs-token = { workspace = true }
solana-account = { workspace = true }
Expand Down
6 changes: 6 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ pub enum TokenWrapError {
/// Unwrapped mint does not have the `TokenMetadata` extension
#[error("Unwrapped mint does not have the TokenMetadata extension")]
UnwrappedMintHasNoMetadata,

// 10
/// `Metaplex` metadata account address does not match expected PDA
#[error("Metaplex metadata account address does not match expected PDA")]
MetaplexMetadataMismatch,
}

impl From<TokenWrapError> for ProgramError {
Expand Down Expand Up @@ -77,6 +82,7 @@ impl ToStr for TokenWrapError {
TokenWrapError::EscrowMismatch => "Error: EscrowMismatch",
TokenWrapError::EscrowInGoodState => "Error: EscrowInGoodState",
TokenWrapError::UnwrappedMintHasNoMetadata => "Error: UnwrappedMintHasNoMetadata",
TokenWrapError::MetaplexMetadataMismatch => "Error: MetaplexMetadataMismatch",
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pub enum TokenWrapInstruction {
///
/// Supports (unwrapped to wrapped):
/// - Token-2022 to Token-2022
/// - SPL-token to Token-2022 (still `TODO`)
/// - SPL-token to Token-2022
///
/// If the `TokenMetadata` extension on the wrapped mint if not present, it
/// will initialize it. The client is responsible for funding the wrapped
Expand All @@ -125,9 +125,11 @@ pub enum TokenWrapInstruction {
/// Accounts expected by this instruction:
///
/// 0. `[w]` Wrapped mint
/// 1. `[]` Wrapped mint authority (PDA)
/// 1. `[]` Wrapped mint authority PDA
/// 2. `[]` Unwrapped mint
/// 3. `[]` Token-2022 program
/// 4. `[]` (Optional) `Metaplex` Metadata PDA. Required if the unwrapped
/// mint is an `spl-token` mint.
Comment on lines +131 to +132

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoot, this actually reminds me that TokenMetadata can actually exist in a separate account for Token2022 mints as well. The MetadataPointer can point to another account.

As followup work, maybe here in this optional account slot, we can just say "Token Metadata account, if separate" or something like that, and also mention the Metaplex PDA?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Totally forgot about that case. That feels like independent conditional branch of work. Cool if it goes in the next PR?

SyncMetadataToToken2022,
}

Expand Down Expand Up @@ -310,13 +312,19 @@ pub fn sync_metadata_to_token_2022(
wrapped_mint: &Pubkey,
wrapped_mint_authority: &Pubkey,
unwrapped_mint: &Pubkey,
metaplex_metadata: Option<&Pubkey>,
) -> Instruction {
let accounts = vec![
let mut accounts = vec![
AccountMeta::new(*wrapped_mint, false),
AccountMeta::new_readonly(*wrapped_mint_authority, false),
AccountMeta::new_readonly(*unwrapped_mint, false),
AccountMeta::new_readonly(spl_token_2022::id(), false),
];

if let Some(pubkey) = metaplex_metadata {
accounts.push(AccountMeta::new_readonly(*pubkey, false));
}

let data = TokenWrapInstruction::SyncMetadataToToken2022.pack();
Instruction::new_with_bytes(*program_id, &data, accounts)
}
1 change: 1 addition & 0 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod entrypoint;
pub mod error;
pub mod instruction;
pub mod metaplex;
pub mod mint_customizer;
pub mod processor;
pub mod state;
Expand Down
98 changes: 98 additions & 0 deletions program/src/metaplex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! `Metaplex` related helpers

use {
mpl_token_metadata::accounts::Metadata as MetaplexMetadata, solana_account_info::AccountInfo,
solana_program_error::ProgramError, spl_pod::optional_keys::OptionalNonZeroPubkey,
spl_token_metadata_interface::state::TokenMetadata,
};

fn extract_additional_metadata(
metaplex_metadata: &MetaplexMetadata,
) -> Result<Vec<(String, String)>, ProgramError> {
Comment on lines +9 to +11
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this is to convert the remaining metaplex metadata fields to token-2022-metadata-ext-compatible. And for that it has to be added to the additional_metadata: Vec<(String, String)> field. Using serde to serialize the value. This may be overkill 🤷 . Worth discussing.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this works for me! Nice!

let mut additional_metadata = vec![
(
"key".to_string(),
serde_json::to_string(&metaplex_metadata.key)
.map_err(|_| ProgramError::InvalidAccountData)?,
),
(
"seller_fee_basis_points".to_string(),
metaplex_metadata.seller_fee_basis_points.to_string(),
),
(
"primary_sale_happened".to_string(),
metaplex_metadata.primary_sale_happened.to_string(),
),
(
"is_mutable".to_string(),
metaplex_metadata.is_mutable.to_string(),
),
];

if let Some(creators) = &metaplex_metadata.creators {
if !creators.is_empty() {
additional_metadata.push((
"creators".to_string(),
serde_json::to_string(creators).map_err(|_| ProgramError::InvalidAccountData)?,
));
}
}
if let Some(edition_nonce) = metaplex_metadata.edition_nonce {
additional_metadata.push(("edition_nonce".to_string(), edition_nonce.to_string()));
}
if let Some(token_standard) = &metaplex_metadata.token_standard {
additional_metadata.push((
"token_standard".to_string(),
serde_json::to_string(token_standard).map_err(|_| ProgramError::InvalidAccountData)?,
));
}
if let Some(collection) = &metaplex_metadata.collection {
additional_metadata.push((
"collection".to_string(),
serde_json::to_string(collection).map_err(|_| ProgramError::InvalidAccountData)?,
));
}
if let Some(uses) = &metaplex_metadata.uses {
additional_metadata.push((
"uses".to_string(),
serde_json::to_string(uses).map_err(|_| ProgramError::InvalidAccountData)?,
));
}
if let Some(collection_details) = &metaplex_metadata.collection_details {
additional_metadata.push((
"collection_details".to_string(),
serde_json::to_string(collection_details)
.map_err(|_| ProgramError::InvalidAccountData)?,
));
}
if let Some(programmable_config) = &metaplex_metadata.programmable_config {
additional_metadata.push((
"programmable_config".to_string(),
serde_json::to_string(programmable_config)
.map_err(|_| ProgramError::InvalidAccountData)?,
));
}

Ok(additional_metadata)
}

/// Converts `Metaplex` metadata to the Token-2022 `TokenMetadata` format.
pub fn metaplex_to_token_2022_metadata(
unwrapped_mint_info: &AccountInfo,
metaplex_metadata_info: &AccountInfo,
) -> Result<TokenMetadata, ProgramError> {
let metaplex_data = metaplex_metadata_info.try_borrow_data()?;
let metaplex_metadata = MetaplexMetadata::safe_deserialize(&metaplex_data)
.map_err(|_| ProgramError::InvalidAccountData)?;

let additional_metadata = extract_additional_metadata(&metaplex_metadata)?;

Ok(TokenMetadata {
update_authority: OptionalNonZeroPubkey(metaplex_metadata.update_authority),
mint: *unwrapped_mint_info.key,
name: metaplex_metadata.name,
symbol: metaplex_metadata.symbol,
uri: metaplex_metadata.uri,
additional_metadata,
})
}
34 changes: 23 additions & 11 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use {
get_wrapped_mint_backpointer_address_signer_seeds,
get_wrapped_mint_backpointer_address_with_seed, get_wrapped_mint_signer_seeds,
instruction::TokenWrapInstruction,
metaplex::metaplex_to_token_2022_metadata,
mint_customizer::{
default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer,
},
state::Backpointer,
},
mpl_token_metadata::accounts::Metadata as MetaplexMetadata,
solana_account_info::{next_account_info, AccountInfo},
solana_cpi::{invoke, invoke_signed},
solana_msg::msg,
Expand Down Expand Up @@ -536,11 +538,6 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
return Err(ProgramError::IncorrectProgramId);
}

// TODO: Temp until spl-token branch is added
if *unwrapped_mint_info.owner != spl_token_2022::id() {
return Err(ProgramError::IncorrectProgramId);
}

if *wrapped_mint_info.owner != spl_token_2022::id() {
return Err(ProgramError::IncorrectProgramId);
}
Expand All @@ -556,12 +553,27 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
return Err(TokenWrapError::MintAuthorityMismatch.into());
}

// Get metadata from the token-2022 unwrapped mint
let unwrapped_mint_data = unwrapped_mint_info.try_borrow_data()?;
let unwrapped_mint_state = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?;
let unwrapped_metadata = unwrapped_mint_state
.get_variable_len_extension::<TokenMetadata>()
.map_err(|_| TokenWrapError::UnwrappedMintHasNoMetadata)?;
let unwrapped_metadata = if *unwrapped_mint_info.owner == spl_token_2022::id() {
// Source is Token-2022: read from extension
let unwrapped_mint_data = unwrapped_mint_info.try_borrow_data()?;
let unwrapped_mint_state = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?;
unwrapped_mint_state
.get_variable_len_extension::<TokenMetadata>()
.map_err(|_| TokenWrapError::UnwrappedMintHasNoMetadata)?
Comment on lines +557 to +562

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is where you'd have a check to see if the address in the MetadataPointer is the mint, or if it's another account, which should have been provided last.

} else if *unwrapped_mint_info.owner == spl_token::id() {
// Source is spl-token: read from Metaplex PDA
let metaplex_metadata_info = next_account_info(account_info_iter)?;
let (expected_metaplex_pda, _) = MetaplexMetadata::find_pda(unwrapped_mint_info.key);
if *metaplex_metadata_info.owner != mpl_token_metadata::ID {
return Err(ProgramError::InvalidAccountOwner);
}
if *metaplex_metadata_info.key != expected_metaplex_pda {
return Err(TokenWrapError::MetaplexMetadataMismatch.into());
}
metaplex_to_token_2022_metadata(unwrapped_mint_info, metaplex_metadata_info)?
} else {
return Err(ProgramError::IncorrectProgramId);
};

let authority_bump_seed = [authority_bump];
let authority_signer_seeds =
Expand Down
61 changes: 52 additions & 9 deletions program/tests/helpers/sync_metadata_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ use {
extensions::MintExtension,
mint_builder::MintBuilder,
},
borsh::BorshSerialize,
mollusk_svm::{result::Check, Mollusk},
mpl_token_metadata::{accounts::Metadata as MetaplexMetadata, types::Key},
solana_account::Account,
solana_pubkey::Pubkey,
spl_token_wrap::{
get_wrapped_mint_address, get_wrapped_mint_authority,
get_wrapped_mint_address, get_wrapped_mint_authority, id,
instruction::sync_metadata_to_token_2022,
},
};

pub struct SyncMetadataResult {
pub unwrapped_mint: KeyedAccount,
pub wrapped_mint: KeyedAccount,
pub wrapped_mint_authority: KeyedAccount,
}
Expand All @@ -25,6 +26,7 @@ pub struct SyncMetadataBuilder<'a> {
unwrapped_mint: Option<KeyedAccount>,
wrapped_mint: Option<KeyedAccount>,
wrapped_mint_authority: Option<Pubkey>,
metaplex_metadata: Option<KeyedAccount>,
}

impl Default for SyncMetadataBuilder<'_> {
Expand All @@ -35,6 +37,7 @@ impl Default for SyncMetadataBuilder<'_> {
unwrapped_mint: None,
wrapped_mint: None,
wrapped_mint_authority: None,
metaplex_metadata: None,
}
}
}
Expand All @@ -59,6 +62,11 @@ impl<'a> SyncMetadataBuilder<'a> {
self
}

pub fn metaplex_metadata(mut self, account: KeyedAccount) -> Self {
self.metaplex_metadata = Some(account);
self
}

pub fn check(mut self, check: Check<'a>) -> Self {
self.checks.push(check);
self
Expand Down Expand Up @@ -93,33 +101,68 @@ impl<'a> SyncMetadataBuilder<'a> {
.build()
});

let metaplex_metadata: Option<KeyedAccount> = self.metaplex_metadata.or_else(|| {
if unwrapped_mint.account.owner == spl_token::id() {
let metadata = MetaplexMetadata {
key: Key::MetadataV1,
update_authority: Default::default(),
mint: unwrapped_mint.key,
name: "x".to_string(),
symbol: "y".to_string(),
uri: "z".to_string(),
seller_fee_basis_points: 0,
creators: None,
primary_sale_happened: false,
is_mutable: false,
edition_nonce: None,
token_standard: None,
collection: None,
uses: None,
collection_details: None,
programmable_config: None,
};
Some(KeyedAccount {
key: MetaplexMetadata::find_pda(&unwrapped_mint.key).0,
account: Account {
lamports: 1_000_000_000,
data: metadata.try_to_vec().unwrap(),
owner: mpl_token_metadata::ID,
..Default::default()
},
})
} else {
None
}
});

let instruction = sync_metadata_to_token_2022(
&spl_token_wrap::id(),
&id(),
&wrapped_mint.key,
&wrapped_mint_authority,
&unwrapped_mint.key,
metaplex_metadata.as_ref().map(|ka| &ka.key),
);

let accounts = &[
let mut accounts = vec![
wrapped_mint.pair(),
(wrapped_mint_authority, Account::default()),
unwrapped_mint.pair(),
TokenProgram::SplToken2022.keyed_account(),
];

if let Some(metadata) = metaplex_metadata {
accounts.push(metadata.pair());
}

if self.checks.is_empty() {
self.checks.push(Check::success());
}

let result =
self.mollusk
.process_and_validate_instruction(&instruction, accounts, &self.checks);
.process_and_validate_instruction(&instruction, &accounts, &self.checks);

SyncMetadataResult {
unwrapped_mint: KeyedAccount {
key: unwrapped_mint.key,
account: result.get_account(&unwrapped_mint.key).unwrap().clone(),
},
wrapped_mint: KeyedAccount {
key: wrapped_mint.key,
account: result.get_account(&wrapped_mint.key).unwrap().clone(),
Expand Down
Loading