Repository: solana-labs/solana-program-library
Pull request #6163 (2024-01-22 22:43:15)

token-cli: Add initialize-member command
#### Problem

Token-2022 and token-client support initializing token group members, but we can't do it in the CLI.

#### Solution

Following the excellent work in #6135, pretty much do the same thing, but for initializing members


Patch:
diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs
index 5cb136fc2..d32ae7416 100644
--- a/token/cli/src/clap_app.rs
+++ b/token/cli/src/clap_app.rs
@@ -109,6 +109,7 @@ pub enum CommandName {
     UpdateMetadata,
     InitializeGroup,
     UpdateGroupMaxSize,
+    InitializeMember,
     UpdateConfidentialTransferSettings,
     ConfigureConfidentialTransferAccount,
     EnableConfidentialCredits,
@@ -1041,6 +1042,53 @@ pub fn app<'a, 'b>(
                         ),
                 )
         )
+        .subcommand(
+            SubCommand::with_name(CommandName::InitializeMember.into())
+                .about("Initialize group member extension on a token mint")
+                .arg(
+                    Arg::with_name("token")
+                        .validator(is_valid_pubkey)
+                        .value_name("TOKEN_MINT_ADDRESS")
+                        .takes_value(true)
+                        .required(true)
+                        .index(1)
+                        .help("The token address of the member account."),
+                )
+                .arg(
+                    Arg::with_name("group_token")
+                        .validator(is_valid_pubkey)
+                        .value_name("TOKEN_MINT_ADDRESS")
+                        .takes_value(true)
+                        .required(true)
+                        .index(2)
+                        .help("The token address of the group account that the token will join."),
+                )
+                .arg(
+                    Arg::with_name("mint_authority")
+                        .long("mint-authority")
+                        .alias("owner")
+                        .value_name("KEYPAIR")
+                        .validator(is_valid_signer)
+                        .takes_value(true)
+                        .help(
+                            "Specify the mint authority keypair. \
+                             This may be a keypair file or the ASK keyword. \
+                             Defaults to the client keypair."
+                        ),
+                )
+                .arg(
+                    Arg::with_name("group_update_authority")
+                        .long("group-update-authority")
+                        .value_name("KEYPAIR")
+                        .validator(is_valid_signer)
+                        .takes_value(true)
+                        .help(
+                            "Specify the update authority keypair. \
+                             This may be a keypair file or the ASK keyword. \
+                             Defaults to the client keypair address."
+                        ),
+                )
+        )
         .subcommand(
             SubCommand::with_name(CommandName::CreateAccount.into())
                 .about("Create a new token account")
diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs
index 3b1b8667a..fc91007d8 100644
--- a/token/cli/src/command.rs
+++ b/token/cli/src/command.rs
@@ -653,6 +653,37 @@ async fn command_update_group_max_size(
     })
 }
 
+async fn command_initialize_member(
+    config: &Config<'_>,
+    member_token_pubkey: Pubkey,
+    mint_authority: Pubkey,
+    group_token_pubkey: Pubkey,
+    group_update_authority: Pubkey,
+    bulk_signers: Vec<Arc<dyn Signer>>,
+) -> CommandResult {
+    let token = token_client_from_config(config, &member_token_pubkey, None)?;
+
+    let res = token
+        .token_group_initialize_member_with_rent_transfer(
+            &config.fee_payer()?.pubkey(),
+            &mint_authority,
+            &group_token_pubkey,
+            &group_update_authority,
+            &bulk_signers,
+        )
+        .await?;
+
+    let tx_return = finish_tx(config, &res, false).await?;
+    Ok(match tx_return {
+        TransactionReturnData::CliSignature(signature) => {
+            config.output_format.formatted_string(&signature)
+        }
+        TransactionReturnData::CliSignOnlyData(sign_only_data) => {
+            config.output_format.formatted_string(&sign_only_data)
+        }
+    })
+}
+
 async fn command_set_transfer_fee(
     config: &Config<'_>,
     token_pubkey: Pubkey,
@@ -3551,6 +3582,34 @@ pub async fn process_command<'a>(
             )
             .await
         }
+        (CommandName::InitializeMember, arg_matches) => {
+            let member_token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
+                .unwrap()
+                .unwrap();
+            let group_token_pubkey =
+                pubkey_of_signer(arg_matches, "group_token", &mut wallet_manager)
+                    .unwrap()
+                    .unwrap();
+            let (mint_authority_signer, mint_authority) =
+                config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager);
+            let (group_update_authority_signer, group_update_authority) = config.signer_or_default(
+                arg_matches,
+                "group_update_authority",
+                &mut wallet_manager,
+            );
+            let mut bulk_signers = vec![mint_authority_signer];
+            push_signer_with_dedup(group_update_authority_signer, &mut bulk_signers);
+
+            command_initialize_member(
+                config,
+                member_token_pubkey,
+                mint_authority,
+                group_token_pubkey,
+                group_update_authority,
+                bulk_signers,
+            )
+            .await
+        }
         (CommandName::CreateAccount, arg_matches) => {
             let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
                 .unwrap()


Test patch:
diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs
index 3b91cedd8..ae349d6a7 100644
--- a/token/cli/tests/command.rs
+++ b/token/cli/tests/command.rs
@@ -46,7 +46,7 @@ use {
         },
         token::Token,
     },
-    spl_token_group_interface::state::TokenGroup,
+    spl_token_group_interface::state::{TokenGroup, TokenGroupMember},
     spl_token_metadata_interface::state::TokenMetadata,
     std::{ffi::OsString, path::PathBuf, str::FromStr, sync::Arc},
     tempfile::NamedTempFile,
@@ -3865,4 +3865,51 @@ async fn group(test_validator: &TestValidator, payer: &Keypair) {
 
     let updated_extension = updated_mint_state.get_extension::<TokenGroup>().unwrap();
     assert_eq!(updated_extension.max_size, new_max_size.into());
+
+    // Create member token
+    let result = process_test_command(
+        &config,
+        payer,
+        &[
+            "spl-token",
+            CommandName::CreateToken.into(),
+            "--program-id",
+            &program_id.to_string(),
+            "--enable-member",
+        ],
+    )
+    .await
+    .unwrap();
+
+    let value: serde_json::Value = serde_json::from_str(&result).unwrap();
+    let member_mint =
+        Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap();
+
+    // Initialize it as a member of the group
+    process_test_command(
+        &config,
+        payer,
+        &[
+            "spl-token",
+            CommandName::InitializeMember.into(),
+            &member_mint.to_string(),
+            &mint.to_string(),
+        ],
+    )
+    .await
+    .unwrap();
+
+    let account = config.rpc_client.get_account(&mint).await.unwrap();
+    let group_mint_state = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
+    let extension = group_mint_state.get_extension::<TokenGroup>().unwrap();
+    assert_eq!(u32::from(extension.size), 1);
+
+    let account = config.rpc_client.get_account(&member_mint).await.unwrap();
+    let member_mint_state = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
+    let extension = member_mint_state
+        .get_extension::<TokenGroupMember>()
+        .unwrap();
+    assert_eq!(extension.group, mint);
+    assert_eq!(extension.mint, member_mint);
+    assert_eq!(u32::from(extension.member_number), 1);
 }