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
8 changes: 8 additions & 0 deletions clients/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use {
sync_metadata_to_spl_token::{
command_sync_metadata_to_spl_token, SyncMetadataToSplTokenArgs,
},
sync_metadata_to_token2022::{
command_sync_metadata_to_token2022, SyncMetadataToToken2022Args,
},
unwrap::{command_unwrap, UnwrapArgs},
wrap::{command_wrap, WrapArgs},
CommandResult,
Expand Down Expand Up @@ -107,6 +110,8 @@ pub enum Command {
/// Sync metadata from unwrapped mint to wrapped SPL Token mint's `Metaplex`
/// metadata account
SyncMetadataToSplToken(SyncMetadataToSplTokenArgs),
/// Sync metadata from unwrapped mint to wrapped Token-2022 mint
SyncMetadataToToken2022(SyncMetadataToToken2022Args),
}

impl Command {
Expand All @@ -126,6 +131,9 @@ impl Command {
Command::SyncMetadataToSplToken(args) => {
command_sync_metadata_to_spl_token(config, args, matches, wallet_manager).await
}
Command::SyncMetadataToToken2022(args) => {
command_sync_metadata_to_token2022(config, args, matches, wallet_manager).await
}
}
}
}
1 change: 1 addition & 0 deletions clients/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod create_mint;
mod find_pdas;
mod output;
mod sync_metadata_to_spl_token;
mod sync_metadata_to_token2022;
mod unwrap;
mod wrap;

Expand Down
156 changes: 156 additions & 0 deletions clients/cli/src/sync_metadata_to_token2022.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use {
crate::{
common::{parse_pubkey, process_transaction},
config::Config,
output::{format_output, println_display},
CommandResult,
},
clap::{ArgMatches, Args},
mpl_token_metadata::accounts::Metadata as MetaplexMetadata,
serde_derive::{Deserialize, Serialize},
serde_with::{serde_as, DisplayFromStr},
solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay},
solana_pubkey::Pubkey,
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_signature::Signature,
solana_signer::Signer,
solana_transaction::Transaction,
spl_token_wrap::{
get_wrapped_mint_address, get_wrapped_mint_authority,
instruction::sync_metadata_to_token_2022,
},
std::{
fmt::{Display, Formatter},
rc::Rc,
},
};

#[derive(Clone, Debug, Args)]
pub struct SyncMetadataToToken2022Args {
/// The address of the unwrapped mint whose metadata will be synced from
#[clap(value_parser = parse_pubkey)]
pub unwrapped_mint: Pubkey,

/// Specify that the source metadata is from a `Metaplex` Token Metadata
/// account. The CLI will derive the PDA automatically.
#[clap(long)]
pub metaplex: bool,
Comment on lines +34 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than forcing people to specify this, it would be better to figure it out dynamically.

  • If metadata_account is specified, use that.
  • If the unwrapped mint is SPL-Token, then use metaplex.
  • If the unwrapped mint is SPL-Token-2022, then check for the metadata pointer, and fall back to metaplex.

Copy link
Member Author

Choose a reason for hiding this comment

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

If the unwrapped mint is SPL-Token-2022, then check for the metadata pointer, and fall back to metaplex.

Oh, wait a sec. Are you saying that having a Metaplex PDA for a token-2022 is authoritative if it doesn't have a metadata pointer? If so the program also needs to be updated to handle this case. At the moment, it's treating the pointer as the only authoritative source.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's up to us to decide! It's probably the best fallback -- if someone didn't specify anything during their token-2022 mint creation, and later created metaplex metadata, it would be nice to use that.

I don't know how common this is though -- it's possible that no mints like this exist. What do you think?


/// Optional source metadata account when the unwrapped mint's metadata
/// pointer points to an external account or third-party program
#[clap(long, value_parser = parse_pubkey, conflicts_with = "metaplex", requires = "program-id")]
pub metadata_account: Option<Pubkey>,

/// Optional owner program for the source metadata account, when owned by a
/// third-party program
#[clap(long, value_parser = parse_pubkey)]
pub program_id: Option<Pubkey>,
Comment on lines +44 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Typically the --program-id arg is to specify the address of the program to target, ie the token-wrap program in this case.

I'd go with something like --metadata-program-id so that we can eventually specify --program-id globally in the CLI

}

#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncMetadataToToken2022Output {
#[serde_as(as = "DisplayFromStr")]
pub unwrapped_mint: Pubkey,

#[serde_as(as = "DisplayFromStr")]
pub wrapped_mint: Pubkey,

#[serde_as(as = "DisplayFromStr")]
pub wrapped_mint_authority: Pubkey,

#[serde_as(as = "Option<DisplayFromStr>")]
pub source_metadata: Option<Pubkey>,

#[serde_as(as = "Option<DisplayFromStr>")]
pub owner_program: Option<Pubkey>,

pub signatures: Vec<Signature>,
}

impl Display for SyncMetadataToToken2022Output {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln_name_value(f, "Unwrapped mint:", &self.unwrapped_mint.to_string())?;
writeln_name_value(f, "Wrapped mint:", &self.wrapped_mint.to_string())?;
writeln_name_value(
f,
"Wrapped mint authority:",
&self.wrapped_mint_authority.to_string(),
)?;
if let Some(src) = self.source_metadata {
writeln_name_value(f, "Source metadata:", &src.to_string())?;
}
if let Some(owner) = self.owner_program {
writeln_name_value(f, "Owner program:", &owner.to_string())?;
}

writeln!(f, "Signers:")?;
for signature in &self.signatures {
writeln!(f, " {signature}")?;
}

Ok(())
}
}

impl QuietDisplay for SyncMetadataToToken2022Output {
fn write_str(&self, _: &mut dyn std::fmt::Write) -> std::fmt::Result {
Ok(())
}
}
impl VerboseDisplay for SyncMetadataToToken2022Output {}

pub async fn command_sync_metadata_to_token2022(
config: &Config,
args: SyncMetadataToToken2022Args,
_matches: &ArgMatches,
_wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
) -> CommandResult {
let payer = config.fee_payer()?;

let wrapped_mint = get_wrapped_mint_address(&args.unwrapped_mint, &spl_token_2022::id());
let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint);

let source_metadata = if args.metaplex {
let (metaplex_pda, _) = MetaplexMetadata::find_pda(&args.unwrapped_mint);
Some(metaplex_pda)
} else {
args.metadata_account
};

println_display(
config,
format!(
"Syncing metadata to Token-2022 mint {} from {}",
wrapped_mint, args.unwrapped_mint
),
);

let instruction = sync_metadata_to_token_2022(
&spl_token_wrap::id(),
&wrapped_mint,
&wrapped_mint_authority,
&args.unwrapped_mint,
source_metadata.as_ref(),
args.program_id.as_ref(),
);

let blockhash = config.rpc_client.get_latest_blockhash().await?;

let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.partial_sign(&[payer.clone()], blockhash);

process_transaction(config, transaction.clone()).await?;

let output = SyncMetadataToToken2022Output {
unwrapped_mint: args.unwrapped_mint,
wrapped_mint,
wrapped_mint_authority,
source_metadata,
owner_program: args.program_id,
signatures: transaction.signatures,
};

Ok(format_output(config, output))
}
143 changes: 143 additions & 0 deletions clients/cli/tests/test_sync_metadata_to_token2022.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use {
crate::helpers::{
create_unwrapped_mint, execute_create_mint, setup_test_env, TOKEN_WRAP_CLI_BIN,
},
mpl_token_metadata::{
accounts::Metadata as MetaplexMetadata,
instructions::{CreateMetadataAccountV3, CreateMetadataAccountV3InstructionArgs},
types::DataV2,
utils::clean,
},
serde_json::Value,
serial_test::serial,
solana_sdk_ids::system_program,
solana_signer::Signer,
solana_system_interface::instruction::transfer,
solana_transaction::Transaction,
spl_token_2022::{
extension::{BaseStateWithExtensions, PodStateWithExtensions},
pod::PodMint,
},
spl_token_metadata_interface::state::TokenMetadata,
spl_token_wrap::get_wrapped_mint_address,
std::process::Command,
};

mod helpers;

#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn test_sync_metadata_from_spl_token_to_token2022() {
let env = setup_test_env().await;

// 1. Create a standard SPL Token mint
let unwrapped_mint = create_unwrapped_mint(&env, &spl_token::id()).await;

// 2. Create Metaplex metadata for the SPL Token mint
let (metaplex_pda, _) = MetaplexMetadata::find_pda(&unwrapped_mint);
let name = "Test Token".to_string();
let symbol = "TEST".to_string();
let uri = "http://test.com".to_string();

let create_meta_ix = CreateMetadataAccountV3 {
metadata: metaplex_pda,
mint: unwrapped_mint,
mint_authority: env.payer.pubkey(),
payer: env.payer.pubkey(),
update_authority: (env.payer.pubkey(), true),
system_program: system_program::id(),
rent: None,
}
.instruction(CreateMetadataAccountV3InstructionArgs {
data: DataV2 {
name: name.clone(),
symbol: symbol.clone(),
uri: uri.clone(),
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
});

let meta_tx = Transaction::new_signed_with_payer(
&[create_meta_ix],
Some(&env.payer.pubkey()),
&[&env.payer],
env.rpc_client.get_latest_blockhash().await.unwrap(),
);
env.rpc_client
.send_and_confirm_transaction(&meta_tx)
.await
.unwrap();

// 3. Create the corresponding wrapped Token-2022 mint for the SPL Token mint
execute_create_mint(&env, &unwrapped_mint, &spl_token_2022::id()).await;

// 4. Fund the wrapped mint account for the extra space needed for the metadata
// extension
let wrapped_mint_address = get_wrapped_mint_address(&unwrapped_mint, &spl_token_2022::id());
let fund_tx = Transaction::new_signed_with_payer(
&[transfer(
&env.payer.pubkey(),
&wrapped_mint_address,
1_000_000_000,
)],
Some(&env.payer.pubkey()),
&[&env.payer],
env.rpc_client.get_latest_blockhash().await.unwrap(),
);
env.rpc_client
.send_and_confirm_transaction(&fund_tx)
.await
.unwrap();

// 5. Execute the sync-metadata-to-token2022 command
let output = Command::new(TOKEN_WRAP_CLI_BIN)
.args([
"sync-metadata-to-token2022",
"-C",
&env.config_file_path,
&unwrapped_mint.to_string(),
"--metaplex",
"--output",
"json",
])
.output()
.unwrap();

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
panic!(
"sync-metadata-to-token2022 command failed:\nSTDOUT:\n{}\nSTDERR:\n{}",
stdout, stderr
);
}
assert!(output.status.success());

let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
json["wrappedMint"].as_str().unwrap(),
&wrapped_mint_address.to_string()
);

// 6. Verify the metadata was written correctly to the wrapped mint
let wrapped_mint_account_after = env
.rpc_client
.get_account(&wrapped_mint_address)
.await
.unwrap();
let wrapped_mint_state =
PodStateWithExtensions::<PodMint>::unpack(&wrapped_mint_account_after.data).unwrap();
let token_metadata = wrapped_mint_state
.get_variable_len_extension::<TokenMetadata>()
.unwrap();

assert_eq!(clean(token_metadata.name), name);
assert_eq!(clean(token_metadata.symbol), symbol);
assert_eq!(clean(token_metadata.uri), uri);
assert_eq!(token_metadata.mint, wrapped_mint_address);
}