Skip to content

Commit 743a50c

Browse files
authored
Sync metadata from spl-token to token-2022 (#229)
* Sync metadata from spl-token to token-2022 * review updates
1 parent a94bcb0 commit 743a50c

File tree

10 files changed

+1094
-662
lines changed

10 files changed

+1094
-662
lines changed

Cargo.lock

Lines changed: 494 additions & 444 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ edition = "2021"
3131

3232
[workspace.dependencies]
3333
anyhow = "1.0.98"
34+
borsh = "0.10.4"
3435
bytemuck = { version = "1.23.2", features = ["derive"] }
3536
clap = { version = "3.2.25", features = ["derive"] }
3637
mollusk-svm = "0.4.2"
3738
mollusk-svm-programs-token = "0.4.1"
39+
mpl-token-metadata = "5.1.0"
3840
num-derive = "0.4.2"
3941
num-traits = "0.2.19"
4042
serde = "1.0.219"

program/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ test-sbf = []
1313

1414
[dependencies]
1515
bytemuck = { workspace = true }
16+
mpl-token-metadata = { workspace = true, features = ["serde"] }
1617
num-derive = { workspace = true }
1718
num-traits = { workspace = true }
19+
serde_json = { workspace = true }
1820
solana-account-info = { workspace = true }
1921
solana-cpi = { workspace = true }
2022
solana-instruction = { workspace = true }
@@ -38,6 +40,7 @@ thiserror = { workspace = true }
3840
spl-token-2022 = { workspace = true }
3941

4042
[dev-dependencies]
43+
borsh = { workspace = true }
4144
mollusk-svm = { workspace = true }
4245
mollusk-svm-programs-token = { workspace = true }
4346
solana-account = { workspace = true }

program/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ pub enum TokenWrapError {
4545
/// Unwrapped mint does not have the `TokenMetadata` extension
4646
#[error("Unwrapped mint does not have the TokenMetadata extension")]
4747
UnwrappedMintHasNoMetadata,
48+
49+
// 10
50+
/// `Metaplex` metadata account address does not match expected PDA
51+
#[error("Metaplex metadata account address does not match expected PDA")]
52+
MetaplexMetadataMismatch,
4853
}
4954

5055
impl From<TokenWrapError> for ProgramError {
@@ -77,6 +82,7 @@ impl ToStr for TokenWrapError {
7782
TokenWrapError::EscrowMismatch => "Error: EscrowMismatch",
7883
TokenWrapError::EscrowInGoodState => "Error: EscrowInGoodState",
7984
TokenWrapError::UnwrappedMintHasNoMetadata => "Error: UnwrappedMintHasNoMetadata",
85+
TokenWrapError::MetaplexMetadataMismatch => "Error: MetaplexMetadataMismatch",
8086
}
8187
}
8288
}

program/src/instruction.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ pub enum TokenWrapInstruction {
114114
///
115115
/// Supports (unwrapped to wrapped):
116116
/// - Token-2022 to Token-2022
117-
/// - SPL-token to Token-2022 (still `TODO`)
117+
/// - SPL-token to Token-2022
118118
///
119119
/// If the `TokenMetadata` extension on the wrapped mint if not present, it
120120
/// will initialize it. The client is responsible for funding the wrapped
@@ -125,9 +125,11 @@ pub enum TokenWrapInstruction {
125125
/// Accounts expected by this instruction:
126126
///
127127
/// 0. `[w]` Wrapped mint
128-
/// 1. `[]` Wrapped mint authority (PDA)
128+
/// 1. `[]` Wrapped mint authority PDA
129129
/// 2. `[]` Unwrapped mint
130130
/// 3. `[]` Token-2022 program
131+
/// 4. `[]` (Optional) `Metaplex` Metadata PDA. Required if the unwrapped
132+
/// mint is an `spl-token` mint.
131133
SyncMetadataToToken2022,
132134
}
133135

@@ -310,13 +312,19 @@ pub fn sync_metadata_to_token_2022(
310312
wrapped_mint: &Pubkey,
311313
wrapped_mint_authority: &Pubkey,
312314
unwrapped_mint: &Pubkey,
315+
metaplex_metadata: Option<&Pubkey>,
313316
) -> Instruction {
314-
let accounts = vec![
317+
let mut accounts = vec![
315318
AccountMeta::new(*wrapped_mint, false),
316319
AccountMeta::new_readonly(*wrapped_mint_authority, false),
317320
AccountMeta::new_readonly(*unwrapped_mint, false),
318321
AccountMeta::new_readonly(spl_token_2022::id(), false),
319322
];
323+
324+
if let Some(pubkey) = metaplex_metadata {
325+
accounts.push(AccountMeta::new_readonly(*pubkey, false));
326+
}
327+
320328
let data = TokenWrapInstruction::SyncMetadataToToken2022.pack();
321329
Instruction::new_with_bytes(*program_id, &data, accounts)
322330
}

program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod entrypoint;
66
pub mod error;
77
pub mod instruction;
8+
pub mod metaplex;
89
pub mod mint_customizer;
910
pub mod processor;
1011
pub mod state;

program/src/metaplex.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! `Metaplex` related helpers
2+
3+
use {
4+
mpl_token_metadata::accounts::Metadata as MetaplexMetadata, solana_account_info::AccountInfo,
5+
solana_program_error::ProgramError, spl_pod::optional_keys::OptionalNonZeroPubkey,
6+
spl_token_metadata_interface::state::TokenMetadata,
7+
};
8+
9+
fn extract_additional_metadata(
10+
metaplex_metadata: &MetaplexMetadata,
11+
) -> Result<Vec<(String, String)>, ProgramError> {
12+
let mut additional_metadata = vec![
13+
(
14+
"key".to_string(),
15+
serde_json::to_string(&metaplex_metadata.key)
16+
.map_err(|_| ProgramError::InvalidAccountData)?,
17+
),
18+
(
19+
"seller_fee_basis_points".to_string(),
20+
metaplex_metadata.seller_fee_basis_points.to_string(),
21+
),
22+
(
23+
"primary_sale_happened".to_string(),
24+
metaplex_metadata.primary_sale_happened.to_string(),
25+
),
26+
(
27+
"is_mutable".to_string(),
28+
metaplex_metadata.is_mutable.to_string(),
29+
),
30+
];
31+
32+
if let Some(creators) = &metaplex_metadata.creators {
33+
if !creators.is_empty() {
34+
additional_metadata.push((
35+
"creators".to_string(),
36+
serde_json::to_string(creators).map_err(|_| ProgramError::InvalidAccountData)?,
37+
));
38+
}
39+
}
40+
if let Some(edition_nonce) = metaplex_metadata.edition_nonce {
41+
additional_metadata.push(("edition_nonce".to_string(), edition_nonce.to_string()));
42+
}
43+
if let Some(token_standard) = &metaplex_metadata.token_standard {
44+
additional_metadata.push((
45+
"token_standard".to_string(),
46+
serde_json::to_string(token_standard).map_err(|_| ProgramError::InvalidAccountData)?,
47+
));
48+
}
49+
if let Some(collection) = &metaplex_metadata.collection {
50+
additional_metadata.push((
51+
"collection".to_string(),
52+
serde_json::to_string(collection).map_err(|_| ProgramError::InvalidAccountData)?,
53+
));
54+
}
55+
if let Some(uses) = &metaplex_metadata.uses {
56+
additional_metadata.push((
57+
"uses".to_string(),
58+
serde_json::to_string(uses).map_err(|_| ProgramError::InvalidAccountData)?,
59+
));
60+
}
61+
if let Some(collection_details) = &metaplex_metadata.collection_details {
62+
additional_metadata.push((
63+
"collection_details".to_string(),
64+
serde_json::to_string(collection_details)
65+
.map_err(|_| ProgramError::InvalidAccountData)?,
66+
));
67+
}
68+
if let Some(programmable_config) = &metaplex_metadata.programmable_config {
69+
additional_metadata.push((
70+
"programmable_config".to_string(),
71+
serde_json::to_string(programmable_config)
72+
.map_err(|_| ProgramError::InvalidAccountData)?,
73+
));
74+
}
75+
76+
Ok(additional_metadata)
77+
}
78+
79+
/// Converts `Metaplex` metadata to the Token-2022 `TokenMetadata` format.
80+
pub fn metaplex_to_token_2022_metadata(
81+
unwrapped_mint_info: &AccountInfo,
82+
metaplex_metadata_info: &AccountInfo,
83+
) -> Result<TokenMetadata, ProgramError> {
84+
let metaplex_data = metaplex_metadata_info.try_borrow_data()?;
85+
let metaplex_metadata = MetaplexMetadata::safe_deserialize(&metaplex_data)
86+
.map_err(|_| ProgramError::InvalidAccountData)?;
87+
88+
let additional_metadata = extract_additional_metadata(&metaplex_metadata)?;
89+
90+
Ok(TokenMetadata {
91+
update_authority: OptionalNonZeroPubkey(metaplex_metadata.update_authority),
92+
mint: *unwrapped_mint_info.key,
93+
name: metaplex_metadata.name,
94+
symbol: metaplex_metadata.symbol,
95+
uri: metaplex_metadata.uri,
96+
additional_metadata,
97+
})
98+
}

program/src/processor.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ use {
88
get_wrapped_mint_backpointer_address_signer_seeds,
99
get_wrapped_mint_backpointer_address_with_seed, get_wrapped_mint_signer_seeds,
1010
instruction::TokenWrapInstruction,
11+
metaplex::metaplex_to_token_2022_metadata,
1112
mint_customizer::{
1213
default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer,
1314
},
1415
state::Backpointer,
1516
},
17+
mpl_token_metadata::accounts::Metadata as MetaplexMetadata,
1618
solana_account_info::{next_account_info, AccountInfo},
1719
solana_cpi::{invoke, invoke_signed},
1820
solana_msg::msg,
@@ -536,11 +538,6 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
536538
return Err(ProgramError::IncorrectProgramId);
537539
}
538540

539-
// TODO: Temp until spl-token branch is added
540-
if *unwrapped_mint_info.owner != spl_token_2022::id() {
541-
return Err(ProgramError::IncorrectProgramId);
542-
}
543-
544541
if *wrapped_mint_info.owner != spl_token_2022::id() {
545542
return Err(ProgramError::IncorrectProgramId);
546543
}
@@ -556,12 +553,27 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
556553
return Err(TokenWrapError::MintAuthorityMismatch.into());
557554
}
558555

559-
// Get metadata from the token-2022 unwrapped mint
560-
let unwrapped_mint_data = unwrapped_mint_info.try_borrow_data()?;
561-
let unwrapped_mint_state = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?;
562-
let unwrapped_metadata = unwrapped_mint_state
563-
.get_variable_len_extension::<TokenMetadata>()
564-
.map_err(|_| TokenWrapError::UnwrappedMintHasNoMetadata)?;
556+
let unwrapped_metadata = if *unwrapped_mint_info.owner == spl_token_2022::id() {
557+
// Source is Token-2022: read from extension
558+
let unwrapped_mint_data = unwrapped_mint_info.try_borrow_data()?;
559+
let unwrapped_mint_state = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?;
560+
unwrapped_mint_state
561+
.get_variable_len_extension::<TokenMetadata>()
562+
.map_err(|_| TokenWrapError::UnwrappedMintHasNoMetadata)?
563+
} else if *unwrapped_mint_info.owner == spl_token::id() {
564+
// Source is spl-token: read from Metaplex PDA
565+
let metaplex_metadata_info = next_account_info(account_info_iter)?;
566+
let (expected_metaplex_pda, _) = MetaplexMetadata::find_pda(unwrapped_mint_info.key);
567+
if *metaplex_metadata_info.owner != mpl_token_metadata::ID {
568+
return Err(ProgramError::InvalidAccountOwner);
569+
}
570+
if *metaplex_metadata_info.key != expected_metaplex_pda {
571+
return Err(TokenWrapError::MetaplexMetadataMismatch.into());
572+
}
573+
metaplex_to_token_2022_metadata(unwrapped_mint_info, metaplex_metadata_info)?
574+
} else {
575+
return Err(ProgramError::IncorrectProgramId);
576+
};
565577

566578
let authority_bump_seed = [authority_bump];
567579
let authority_signer_seeds =

program/tests/helpers/sync_metadata_builder.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ use {
44
extensions::MintExtension,
55
mint_builder::MintBuilder,
66
},
7+
borsh::BorshSerialize,
78
mollusk_svm::{result::Check, Mollusk},
9+
mpl_token_metadata::{accounts::Metadata as MetaplexMetadata, types::Key},
810
solana_account::Account,
911
solana_pubkey::Pubkey,
1012
spl_token_wrap::{
11-
get_wrapped_mint_address, get_wrapped_mint_authority,
13+
get_wrapped_mint_address, get_wrapped_mint_authority, id,
1214
instruction::sync_metadata_to_token_2022,
1315
},
1416
};
1517

1618
pub struct SyncMetadataResult {
17-
pub unwrapped_mint: KeyedAccount,
1819
pub wrapped_mint: KeyedAccount,
1920
pub wrapped_mint_authority: KeyedAccount,
2021
}
@@ -25,6 +26,7 @@ pub struct SyncMetadataBuilder<'a> {
2526
unwrapped_mint: Option<KeyedAccount>,
2627
wrapped_mint: Option<KeyedAccount>,
2728
wrapped_mint_authority: Option<Pubkey>,
29+
metaplex_metadata: Option<KeyedAccount>,
2830
}
2931

3032
impl Default for SyncMetadataBuilder<'_> {
@@ -35,6 +37,7 @@ impl Default for SyncMetadataBuilder<'_> {
3537
unwrapped_mint: None,
3638
wrapped_mint: None,
3739
wrapped_mint_authority: None,
40+
metaplex_metadata: None,
3841
}
3942
}
4043
}
@@ -59,6 +62,11 @@ impl<'a> SyncMetadataBuilder<'a> {
5962
self
6063
}
6164

65+
pub fn metaplex_metadata(mut self, account: KeyedAccount) -> Self {
66+
self.metaplex_metadata = Some(account);
67+
self
68+
}
69+
6270
pub fn check(mut self, check: Check<'a>) -> Self {
6371
self.checks.push(check);
6472
self
@@ -93,33 +101,68 @@ impl<'a> SyncMetadataBuilder<'a> {
93101
.build()
94102
});
95103

104+
let metaplex_metadata: Option<KeyedAccount> = self.metaplex_metadata.or_else(|| {
105+
if unwrapped_mint.account.owner == spl_token::id() {
106+
let metadata = MetaplexMetadata {
107+
key: Key::MetadataV1,
108+
update_authority: Default::default(),
109+
mint: unwrapped_mint.key,
110+
name: "x".to_string(),
111+
symbol: "y".to_string(),
112+
uri: "z".to_string(),
113+
seller_fee_basis_points: 0,
114+
creators: None,
115+
primary_sale_happened: false,
116+
is_mutable: false,
117+
edition_nonce: None,
118+
token_standard: None,
119+
collection: None,
120+
uses: None,
121+
collection_details: None,
122+
programmable_config: None,
123+
};
124+
Some(KeyedAccount {
125+
key: MetaplexMetadata::find_pda(&unwrapped_mint.key).0,
126+
account: Account {
127+
lamports: 1_000_000_000,
128+
data: metadata.try_to_vec().unwrap(),
129+
owner: mpl_token_metadata::ID,
130+
..Default::default()
131+
},
132+
})
133+
} else {
134+
None
135+
}
136+
});
137+
96138
let instruction = sync_metadata_to_token_2022(
97-
&spl_token_wrap::id(),
139+
&id(),
98140
&wrapped_mint.key,
99141
&wrapped_mint_authority,
100142
&unwrapped_mint.key,
143+
metaplex_metadata.as_ref().map(|ka| &ka.key),
101144
);
102145

103-
let accounts = &[
146+
let mut accounts = vec![
104147
wrapped_mint.pair(),
105148
(wrapped_mint_authority, Account::default()),
106149
unwrapped_mint.pair(),
107150
TokenProgram::SplToken2022.keyed_account(),
108151
];
109152

153+
if let Some(metadata) = metaplex_metadata {
154+
accounts.push(metadata.pair());
155+
}
156+
110157
if self.checks.is_empty() {
111158
self.checks.push(Check::success());
112159
}
113160

114161
let result =
115162
self.mollusk
116-
.process_and_validate_instruction(&instruction, accounts, &self.checks);
163+
.process_and_validate_instruction(&instruction, &accounts, &self.checks);
117164

118165
SyncMetadataResult {
119-
unwrapped_mint: KeyedAccount {
120-
key: unwrapped_mint.key,
121-
account: result.get_account(&unwrapped_mint.key).unwrap().clone(),
122-
},
123166
wrapped_mint: KeyedAccount {
124167
key: wrapped_mint.key,
125168
account: result.get_account(&wrapped_mint.key).unwrap().clone(),

0 commit comments

Comments
 (0)