Skip to content

Commit 0ed80b6

Browse files
authored
Sync to token-2022 CLI helper (#278)
* Sync to token-2022 CLI helper * change CLI api
1 parent e03b90b commit 0ed80b6

File tree

4 files changed

+308
-0
lines changed

4 files changed

+308
-0
lines changed

clients/cli/src/cli.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use {
99
sync_metadata_to_spl_token::{
1010
command_sync_metadata_to_spl_token, SyncMetadataToSplTokenArgs,
1111
},
12+
sync_metadata_to_token2022::{
13+
command_sync_metadata_to_token2022, SyncMetadataToToken2022Args,
14+
},
1215
unwrap::{command_unwrap, UnwrapArgs},
1316
wrap::{command_wrap, WrapArgs},
1417
CommandResult,
@@ -107,6 +110,8 @@ pub enum Command {
107110
/// Sync metadata from unwrapped mint to wrapped SPL Token mint's `Metaplex`
108111
/// metadata account
109112
SyncMetadataToSplToken(SyncMetadataToSplTokenArgs),
113+
/// Sync metadata from unwrapped mint to wrapped Token-2022 mint
114+
SyncMetadataToToken2022(SyncMetadataToToken2022Args),
110115
}
111116

112117
impl Command {
@@ -126,6 +131,9 @@ impl Command {
126131
Command::SyncMetadataToSplToken(args) => {
127132
command_sync_metadata_to_spl_token(config, args, matches, wallet_manager).await
128133
}
134+
Command::SyncMetadataToToken2022(args) => {
135+
command_sync_metadata_to_token2022(config, args, matches, wallet_manager).await
136+
}
129137
}
130138
}
131139
}

clients/cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod create_mint;
77
mod find_pdas;
88
mod output;
99
mod sync_metadata_to_spl_token;
10+
mod sync_metadata_to_token2022;
1011
mod unwrap;
1112
mod wrap;
1213

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use {
2+
crate::{
3+
common::{parse_pubkey, process_transaction},
4+
config::Config,
5+
output::{format_output, println_display},
6+
CommandResult,
7+
},
8+
clap::{ArgMatches, Args},
9+
mpl_token_metadata::accounts::Metadata as MetaplexMetadata,
10+
serde_derive::{Deserialize, Serialize},
11+
serde_with::{serde_as, DisplayFromStr},
12+
solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay},
13+
solana_pubkey::Pubkey,
14+
solana_remote_wallet::remote_wallet::RemoteWalletManager,
15+
solana_signature::Signature,
16+
solana_signer::Signer,
17+
solana_transaction::Transaction,
18+
spl_token_wrap::{
19+
get_wrapped_mint_address, get_wrapped_mint_authority,
20+
instruction::sync_metadata_to_token_2022,
21+
},
22+
std::{
23+
fmt::{Display, Formatter},
24+
rc::Rc,
25+
},
26+
};
27+
28+
#[derive(Clone, Debug, Args)]
29+
pub struct SyncMetadataToToken2022Args {
30+
/// The address of the unwrapped mint whose metadata will be synced from
31+
#[clap(value_parser = parse_pubkey)]
32+
pub unwrapped_mint: Pubkey,
33+
34+
/// Specify that the source metadata is from a `Metaplex` Token Metadata
35+
/// account. The CLI will derive the PDA automatically.
36+
#[clap(long)]
37+
pub metaplex: bool,
38+
39+
/// Optional source metadata account when the unwrapped mint's metadata
40+
/// pointer points to an external account or third-party program
41+
#[clap(long, value_parser = parse_pubkey, conflicts_with = "metaplex", requires = "program-id")]
42+
pub metadata_account: Option<Pubkey>,
43+
44+
/// Optional owner program for the source metadata account, when owned by a
45+
/// third-party program
46+
#[clap(long, value_parser = parse_pubkey)]
47+
pub program_id: Option<Pubkey>,
48+
}
49+
50+
#[serde_as]
51+
#[derive(Debug, Serialize, Deserialize)]
52+
#[serde(rename_all = "camelCase")]
53+
pub struct SyncMetadataToToken2022Output {
54+
#[serde_as(as = "DisplayFromStr")]
55+
pub unwrapped_mint: Pubkey,
56+
57+
#[serde_as(as = "DisplayFromStr")]
58+
pub wrapped_mint: Pubkey,
59+
60+
#[serde_as(as = "DisplayFromStr")]
61+
pub wrapped_mint_authority: Pubkey,
62+
63+
#[serde_as(as = "Option<DisplayFromStr>")]
64+
pub source_metadata: Option<Pubkey>,
65+
66+
#[serde_as(as = "Option<DisplayFromStr>")]
67+
pub owner_program: Option<Pubkey>,
68+
69+
pub signatures: Vec<Signature>,
70+
}
71+
72+
impl Display for SyncMetadataToToken2022Output {
73+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74+
writeln_name_value(f, "Unwrapped mint:", &self.unwrapped_mint.to_string())?;
75+
writeln_name_value(f, "Wrapped mint:", &self.wrapped_mint.to_string())?;
76+
writeln_name_value(
77+
f,
78+
"Wrapped mint authority:",
79+
&self.wrapped_mint_authority.to_string(),
80+
)?;
81+
if let Some(src) = self.source_metadata {
82+
writeln_name_value(f, "Source metadata:", &src.to_string())?;
83+
}
84+
if let Some(owner) = self.owner_program {
85+
writeln_name_value(f, "Owner program:", &owner.to_string())?;
86+
}
87+
88+
writeln!(f, "Signers:")?;
89+
for signature in &self.signatures {
90+
writeln!(f, " {signature}")?;
91+
}
92+
93+
Ok(())
94+
}
95+
}
96+
97+
impl QuietDisplay for SyncMetadataToToken2022Output {
98+
fn write_str(&self, _: &mut dyn std::fmt::Write) -> std::fmt::Result {
99+
Ok(())
100+
}
101+
}
102+
impl VerboseDisplay for SyncMetadataToToken2022Output {}
103+
104+
pub async fn command_sync_metadata_to_token2022(
105+
config: &Config,
106+
args: SyncMetadataToToken2022Args,
107+
_matches: &ArgMatches,
108+
_wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
109+
) -> CommandResult {
110+
let payer = config.fee_payer()?;
111+
112+
let wrapped_mint = get_wrapped_mint_address(&args.unwrapped_mint, &spl_token_2022::id());
113+
let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint);
114+
115+
let source_metadata = if args.metaplex {
116+
let (metaplex_pda, _) = MetaplexMetadata::find_pda(&args.unwrapped_mint);
117+
Some(metaplex_pda)
118+
} else {
119+
args.metadata_account
120+
};
121+
122+
println_display(
123+
config,
124+
format!(
125+
"Syncing metadata to Token-2022 mint {} from {}",
126+
wrapped_mint, args.unwrapped_mint
127+
),
128+
);
129+
130+
let instruction = sync_metadata_to_token_2022(
131+
&spl_token_wrap::id(),
132+
&wrapped_mint,
133+
&wrapped_mint_authority,
134+
&args.unwrapped_mint,
135+
source_metadata.as_ref(),
136+
args.program_id.as_ref(),
137+
);
138+
139+
let blockhash = config.rpc_client.get_latest_blockhash().await?;
140+
141+
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
142+
transaction.partial_sign(&[payer.clone()], blockhash);
143+
144+
process_transaction(config, transaction.clone()).await?;
145+
146+
let output = SyncMetadataToToken2022Output {
147+
unwrapped_mint: args.unwrapped_mint,
148+
wrapped_mint,
149+
wrapped_mint_authority,
150+
source_metadata,
151+
owner_program: args.program_id,
152+
signatures: transaction.signatures,
153+
};
154+
155+
Ok(format_output(config, output))
156+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use {
2+
crate::helpers::{
3+
create_unwrapped_mint, execute_create_mint, setup_test_env, TOKEN_WRAP_CLI_BIN,
4+
},
5+
mpl_token_metadata::{
6+
accounts::Metadata as MetaplexMetadata,
7+
instructions::{CreateMetadataAccountV3, CreateMetadataAccountV3InstructionArgs},
8+
types::DataV2,
9+
utils::clean,
10+
},
11+
serde_json::Value,
12+
serial_test::serial,
13+
solana_sdk_ids::system_program,
14+
solana_signer::Signer,
15+
solana_system_interface::instruction::transfer,
16+
solana_transaction::Transaction,
17+
spl_token_2022::{
18+
extension::{BaseStateWithExtensions, PodStateWithExtensions},
19+
pod::PodMint,
20+
},
21+
spl_token_metadata_interface::state::TokenMetadata,
22+
spl_token_wrap::get_wrapped_mint_address,
23+
std::process::Command,
24+
};
25+
26+
mod helpers;
27+
28+
#[tokio::test(flavor = "multi_thread")]
29+
#[serial]
30+
async fn test_sync_metadata_from_spl_token_to_token2022() {
31+
let env = setup_test_env().await;
32+
33+
// 1. Create a standard SPL Token mint
34+
let unwrapped_mint = create_unwrapped_mint(&env, &spl_token::id()).await;
35+
36+
// 2. Create Metaplex metadata for the SPL Token mint
37+
let (metaplex_pda, _) = MetaplexMetadata::find_pda(&unwrapped_mint);
38+
let name = "Test Token".to_string();
39+
let symbol = "TEST".to_string();
40+
let uri = "http://test.com".to_string();
41+
42+
let create_meta_ix = CreateMetadataAccountV3 {
43+
metadata: metaplex_pda,
44+
mint: unwrapped_mint,
45+
mint_authority: env.payer.pubkey(),
46+
payer: env.payer.pubkey(),
47+
update_authority: (env.payer.pubkey(), true),
48+
system_program: system_program::id(),
49+
rent: None,
50+
}
51+
.instruction(CreateMetadataAccountV3InstructionArgs {
52+
data: DataV2 {
53+
name: name.clone(),
54+
symbol: symbol.clone(),
55+
uri: uri.clone(),
56+
seller_fee_basis_points: 0,
57+
creators: None,
58+
collection: None,
59+
uses: None,
60+
},
61+
is_mutable: true,
62+
collection_details: None,
63+
});
64+
65+
let meta_tx = Transaction::new_signed_with_payer(
66+
&[create_meta_ix],
67+
Some(&env.payer.pubkey()),
68+
&[&env.payer],
69+
env.rpc_client.get_latest_blockhash().await.unwrap(),
70+
);
71+
env.rpc_client
72+
.send_and_confirm_transaction(&meta_tx)
73+
.await
74+
.unwrap();
75+
76+
// 3. Create the corresponding wrapped Token-2022 mint for the SPL Token mint
77+
execute_create_mint(&env, &unwrapped_mint, &spl_token_2022::id()).await;
78+
79+
// 4. Fund the wrapped mint account for the extra space needed for the metadata
80+
// extension
81+
let wrapped_mint_address = get_wrapped_mint_address(&unwrapped_mint, &spl_token_2022::id());
82+
let fund_tx = Transaction::new_signed_with_payer(
83+
&[transfer(
84+
&env.payer.pubkey(),
85+
&wrapped_mint_address,
86+
1_000_000_000,
87+
)],
88+
Some(&env.payer.pubkey()),
89+
&[&env.payer],
90+
env.rpc_client.get_latest_blockhash().await.unwrap(),
91+
);
92+
env.rpc_client
93+
.send_and_confirm_transaction(&fund_tx)
94+
.await
95+
.unwrap();
96+
97+
// 5. Execute the sync-metadata-to-token2022 command
98+
let output = Command::new(TOKEN_WRAP_CLI_BIN)
99+
.args([
100+
"sync-metadata-to-token2022",
101+
"-C",
102+
&env.config_file_path,
103+
&unwrapped_mint.to_string(),
104+
"--metaplex",
105+
"--output",
106+
"json",
107+
])
108+
.output()
109+
.unwrap();
110+
111+
if !output.status.success() {
112+
let stderr = String::from_utf8_lossy(&output.stderr);
113+
let stdout = String::from_utf8_lossy(&output.stdout);
114+
panic!(
115+
"sync-metadata-to-token2022 command failed:\nSTDOUT:\n{}\nSTDERR:\n{}",
116+
stdout, stderr
117+
);
118+
}
119+
assert!(output.status.success());
120+
121+
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
122+
assert_eq!(
123+
json["wrappedMint"].as_str().unwrap(),
124+
&wrapped_mint_address.to_string()
125+
);
126+
127+
// 6. Verify the metadata was written correctly to the wrapped mint
128+
let wrapped_mint_account_after = env
129+
.rpc_client
130+
.get_account(&wrapped_mint_address)
131+
.await
132+
.unwrap();
133+
let wrapped_mint_state =
134+
PodStateWithExtensions::<PodMint>::unpack(&wrapped_mint_account_after.data).unwrap();
135+
let token_metadata = wrapped_mint_state
136+
.get_variable_len_extension::<TokenMetadata>()
137+
.unwrap();
138+
139+
assert_eq!(clean(token_metadata.name), name);
140+
assert_eq!(clean(token_metadata.symbol), symbol);
141+
assert_eq!(clean(token_metadata.uri), uri);
142+
assert_eq!(token_metadata.mint, wrapped_mint_address);
143+
}

0 commit comments

Comments
 (0)