Repository: solana-labs/solana-program-library
Pull request #6135 (2024-01-16 02:40:02)

[token-CLI] Add command_initialize_group to Token Cli
I have added the necessary clap commands and functions to drive `token_group_initialize_with_rent_transfer()` from the Token CLI.  I am running into error 0xc `InvalidInstruction` and cannot find what is causing this. Is there anything obvious that I cannot see? #5937

Thank you!

Edit: To recreate this error, run
```
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --enable-group
```
then I initialize the group using the mint address created
```
spl-token initialize-group A6iv673L7PEZDxYDwQbMZsgmS5bGzExavADQSAcKA3Tt 3   
```




tonton-sol: @buffalojoec please let me know what you think!
buffalojoec: > I am running into error 0xc InvalidInstruction and cannot find what is causing this. Is there anything obvious that I cannot see?

Are you running this test with Token2022's feature flag enabled for `token-group`?
tonton-sol: Yes I believe it is enabled. And the error comes before the token-group processor checks if it is enabled.
buffalojoec: > Yes I believe it is enabled. And the error comes before the token-group processor checks if it is enabled.

And you're using the CLI from the repo? `target/debug/spl-token`?
tonton-sol: Yes, and I can see the new clap subcommand I added. 
buffalojoec: The subcommand is just on the CLI side. We'll have to configure the tests to use Token2022's program `.so` file with the features enabled.

To test it manually, you can do:

```
cargo build-sbf --manifest-path token/program-2022/Cargo.toml --features token-group zk-ops
solana-test-validator --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb target/deploy/spl_token_2022.so
```

And then we'll just have to configure that same BPF program override in the test validator config of the tests. This might be outside the scope of this PR though.

If you manage to crack it, great! If not, we can wait on this and rebase after I add that support.
tonton-sol: I had a fundamental misunderstanding lol. The deployed devnet token-22 program doesn't have the token-group enabled. I followed the steps for testing the cli and enabled it correctly. I also wrote a test that simply creates the mint and initializes it. The test passes. What else should I add to the test? Maybe check the max_size? Since the `GroupPointer` struct does not have a max_size field, would I need to read it directly from the raw data?
tonton-sol: Actually I think this can be done with the `spl-token-group-interface`! Would it make sense to roll `command_update_max_size` into this pr? For example it seems the metadata test includes multiple commands.
buffalojoec: > Actually I think this can be done with the `spl-token-group-interface`! Would it make sense to roll `command_update_max_size` into this pr? For example it seems the metadata test includes multiple commands.

Maybe just try fetching the mint and unpacking `TokenGroup` and checking the values for things like update authority/mint?
tonton-sol: Okay I believe this is ready to go!

Patch:
diff --git a/token/cli/Cargo.toml b/token/cli/Cargo.toml
index 0e29f36f7..20a0fc552 100644
--- a/token/cli/Cargo.toml
+++ b/token/cli/Cargo.toml
@@ -36,6 +36,7 @@ spl-token-2022 = { version = "1.0", path = "../program-2022", features = [
 ] }
 spl-token-client = { version = "0.8", path = "../client" }
 spl-token-metadata-interface = { version = "0.2", path = "../../token-metadata/interface" }
+spl-token-group-interface = { version = "0.1", path = "../../token-group/interface" }
 spl-associated-token-account = { version = "2.0", path = "../../associated-token-account/program", features = [
   "no-entrypoint",
 ] }
diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs
index 59f090a11..57f9fff98 100644
--- a/token/cli/src/clap_app.rs
+++ b/token/cli/src/clap_app.rs
@@ -107,6 +107,7 @@ pub enum CommandName {
     SetTransferHook,
     InitializeMetadata,
     UpdateMetadata,
+    InitializeGroup,
     UpdateConfidentialTransferSettings,
     ConfigureConfidentialTransferAccount,
     EnableConfidentialCredits,
@@ -960,6 +961,52 @@ pub fn app<'a, 'b>(
                 .arg(transfer_lamports_arg())
                 .offline_args_config(&SignOnlyNeedsTransferLamports{}),
         )
+        .subcommand(
+            SubCommand::with_name(CommandName::InitializeGroup.into())
+                .about("Initialize group 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 group account."),
+                )
+                .arg(
+                        Arg::with_name("max_size")
+                        .validator(is_amount)
+                        .value_name("MAX_SIZE")
+                        .takes_value(true)
+                        .required(true)
+                        .index(2)
+                        .help("The number of members in the group."),
+                    )
+                .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("update_authority")
+                        .long("update-authority")
+                        .value_name("ADDRESS")
+                        .validator(is_valid_pubkey)
+                        .takes_value(true)
+                        .help(
+                            "Specify the update authority address. \
+                             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 2f3072403..db72e329c 100644
--- a/token/cli/src/command.rs
+++ b/token/cli/src/command.rs
@@ -596,6 +596,37 @@ async fn command_update_metadata(
     })
 }
 
+#[allow(clippy::too_many_arguments)]
+async fn command_initialize_group(
+    config: &Config<'_>,
+    token_pubkey: Pubkey,
+    mint_authority: Pubkey,
+    update_authority: Pubkey,
+    max_size: u32,
+    bulk_signers: Vec<Arc<dyn Signer>>,
+) -> CommandResult {
+    let token = token_client_from_config(config, &token_pubkey, None)?;
+
+    let res = token
+        .token_group_initialize_with_rent_transfer(
+            &config.fee_payer()?.pubkey(),
+            &mint_authority,
+            &update_authority,
+            max_size,
+            &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,
@@ -3455,6 +3486,27 @@ pub async fn process_command<'a>(
             )
             .await
         }
+        (CommandName::InitializeGroup, arg_matches) => {
+            let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
+                .unwrap()
+                .unwrap();
+            let max_size = value_t_or_exit!(arg_matches, "max_size", u32);
+            let (mint_authority_signer, mint_authority) =
+                config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager);
+            let update_authority =
+                config.pubkey_or_default(arg_matches, "update_authority", &mut wallet_manager)?;
+            let bulk_signers = vec![mint_authority_signer];
+
+            command_initialize_group(
+                config,
+                token_pubkey,
+                mint_authority,
+                update_authority,
+                max_size,
+                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 d78b3c4fe..20111a9ce 100644
--- a/token/cli/tests/command.rs
+++ b/token/cli/tests/command.rs
@@ -46,6 +46,7 @@ use {
         },
         token::Token,
     },
+    spl_token_group_interface::state::TokenGroup,
     spl_token_metadata_interface::state::TokenMetadata,
     std::{ffi::OsString, path::PathBuf, str::FromStr, sync::Arc},
     tempfile::NamedTempFile,
@@ -131,6 +132,7 @@ async fn main() {
         async_trial!(group_member_pointer, test_validator, payer),
         async_trial!(transfer_hook, test_validator, payer),
         async_trial!(metadata, test_validator, payer),
+        async_trial!(group, test_validator, payer),
         async_trial!(confidential_transfer_with_fee, test_validator, payer),
         // GC messes with every other test, so have it on its own test validator
         async_trial!(gc, gc_test_validator, gc_payer),
@@ -3788,3 +3790,56 @@ async fn metadata(test_validator: &TestValidator, payer: &Keypair) {
         Some(mint).try_into().unwrap()
     );
 }
+
+async fn group(test_validator: &TestValidator, payer: &Keypair) {
+    let program_id = spl_token_2022::id();
+    let config = test_config_with_default_signer(test_validator, payer, &program_id);
+    let max_size = 10;
+
+    // Create token
+    let result = process_test_command(
+        &config,
+        payer,
+        &[
+            "spl-token",
+            CommandName::CreateToken.into(),
+            "--program-id",
+            &program_id.to_string(),
+            "--enable-group",
+        ],
+    )
+    .await;
+
+    let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
+    let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap();
+
+    // Initialize the group
+    process_test_command(
+        &config,
+        payer,
+        &[
+            "spl-token",
+            CommandName::InitializeGroup.into(),
+            &mint.to_string(),
+            &max_size.to_string(),
+        ],
+    )
+    .await
+    .unwrap();
+
+    let account = config.rpc_client.get_account(&mint).await.unwrap();
+    let mint_state = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
+
+    let extension = mint_state.get_extension::<TokenGroup>().unwrap();
+    assert_eq!(
+        extension.update_authority,
+        Some(payer.pubkey()).try_into().unwrap()
+    );
+    assert_eq!(extension.max_size, max_size.into());
+
+    let extension_pointer = mint_state.get_extension::<GroupPointer>().unwrap();
+    assert_eq!(
+        extension_pointer.group_address,
+        Some(mint).try_into().unwrap()
+    );
+}