-
Notifications
You must be signed in to change notification settings - Fork 7
Sync to token-2022 CLI helper #278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
|
||
/// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Typically the I'd go with something like |
||
} | ||
|
||
#[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)) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
metadata_account
is specified, use that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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?