Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 7847f35

Browse files
committed
token-cli: convert transfer to client
1 parent e6739b5 commit 7847f35

File tree

9 files changed

+163
-136
lines changed

9 files changed

+163
-136
lines changed

token/cli/src/main.rs

Lines changed: 107 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,12 @@ use solana_sdk::{
3232
message::Message,
3333
native_token::*,
3434
program_option::COption,
35-
program_pack::Pack,
3635
pubkey::Pubkey,
3736
signature::{Keypair, Signer},
3837
system_program,
3938
transaction::Transaction,
4039
};
41-
use spl_associated_token_account::{
42-
get_associated_token_address_with_program_id, instruction::create_associated_token_account,
43-
};
40+
use spl_associated_token_account::get_associated_token_address_with_program_id;
4441
use spl_token_2022::{
4542
extension::{
4643
interest_bearing_mint::InterestBearingConfig, memo_transfer::MemoTransfer,
@@ -789,7 +786,7 @@ async fn validate_mint(config: &Config<'_>, token: Pubkey) -> Result<Pubkey, Err
789786
#[allow(clippy::too_many_arguments)]
790787
async fn command_transfer(
791788
config: &Config<'_>,
792-
token: Pubkey,
789+
token_pubkey: Pubkey,
793790
ui_amount: Option<f64>,
794791
recipient: Pubkey,
795792
sender: Option<Pubkey>,
@@ -804,33 +801,45 @@ async fn command_transfer(
804801
no_wait: bool,
805802
allow_non_system_account_recipient: bool,
806803
) -> CommandResult {
807-
let mint_info = config.get_mint_info(&token, mint_decimals).await?;
804+
let token = token_client_from_config(config, &token_pubkey);
805+
let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?;
806+
807+
// decimals arg, which determines whether transfer or transfer_checked instruction is used
808+
// the logic here is tricky because "do what we are told" and "do the right thing" are at odds
809+
let decimals = if use_unchecked_instruction {
810+
None
811+
} else if !config.sign_only {
812+
Some(mint_info.decimals)
813+
} else if mint_decimals.is_some() {
814+
mint_decimals
815+
} else {
816+
return Err("Decimals must be specified with --mint-decimals in offline mode".into());
817+
};
818+
819+
// pubkey of the actual account we are sending from
808820
let sender = if let Some(sender) = sender {
809821
sender
810822
} else {
811-
get_associated_token_address_with_program_id(&sender_owner, &token, &mint_info.program_id)
823+
token.get_associated_token_address(&sender_owner)
812824
};
813-
config.check_account(&sender, Some(token)).await?;
825+
826+
// in any sign_only block, we can safely unwrap this
827+
let maybe_sender_state = match token.get_account_info(&sender).await {
828+
Ok(a) => Some(a),
829+
Err(_) if config.sign_only => None,
830+
Err(e) => {
831+
return Err(format!("Error: failed to fetch sender account {}: {}", sender, e).into())
832+
}
833+
};
834+
814835
let maybe_transfer_balance =
815836
ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals));
837+
838+
// the amount we will transfer, as a u64
816839
let transfer_balance = if !config.sign_only {
817-
let sender_token_amount = config
818-
.rpc_client
819-
.get_token_account_balance(&sender)
820-
.await
821-
.map_err(|err| {
822-
format!(
823-
"Error: Failed to get token balance of sender address {}: {}",
824-
sender, err
825-
)
826-
})?;
827-
let sender_balance = sender_token_amount.amount.parse::<u64>().map_err(|err| {
828-
format!(
829-
"Token account {} balance could not be parsed: {}",
830-
sender, err
831-
)
832-
})?;
840+
let sender_balance = maybe_sender_state.unwrap().base.amount;
833841
let transfer_balance = maybe_transfer_balance.unwrap_or(sender_balance);
842+
834843
println_display(
835844
config,
836845
format!(
@@ -851,59 +860,58 @@ async fn command_transfer(
851860
)
852861
.into());
853862
}
863+
854864
transfer_balance
855865
} else {
856866
maybe_transfer_balance.unwrap()
857867
};
858868

859-
let mut instructions = vec![];
869+
// determine whether recipient is a token account or an expected owner of one
870+
let recipient_is_token_account = if !config.sign_only {
871+
// in online mode we can fetch it and see
872+
let maybe_recipient_account_data = config.program_client.get_account(recipient).await?;
860873

861-
let mut recipient_token_account = recipient;
862-
let mut minimum_balance_for_rent_exemption = 0;
874+
// if the account exists...
875+
if let Some(recipient_account_data) = maybe_recipient_account_data {
876+
// ...and its a token or system account, we are happy
877+
// but if its something else, we gate transfer with a flag
878+
let recipient_is_token_account = recipient_account_data.owner == config.program_id
879+
&& Account::valid_account_data(&recipient_account_data.data);
880+
let recipient_is_system_account = recipient_account_data.owner == system_program::id();
863881

864-
let recipient_is_token_account = if !config.sign_only {
865-
let recipient_account_info = config
866-
.rpc_client
867-
.get_account_with_commitment(&recipient, config.rpc_client.commitment())
868-
.await?
869-
.value
870-
.map(|account| {
871-
(
872-
account.owner == mint_info.program_id
873-
&& Account::valid_account_data(account.data.as_slice()),
874-
account.owner == system_program::id(),
875-
)
876-
});
877-
if let Some((recipient_is_token_account, recipient_is_system_account)) =
878-
recipient_account_info
879-
{
880882
if !recipient_is_token_account
881883
&& !recipient_is_system_account
882884
&& !allow_non_system_account_recipient
883885
{
884886
return Err("Error: The recipient address is not owned by the System Program. \
885-
Add `--allow-non-system-account-recipient` to complete the transfer. \
886-
".into());
887+
Add `--allow-non-system-account-recipient` to complete the transfer.".into());
887888
}
888-
} else if recipient_account_info.is_none() && !allow_unfunded_recipient {
889+
890+
recipient_is_token_account
891+
}
892+
// if it doesnt exist, it definitely isnt a token account!
893+
// we gate transfer with a different flag
894+
else if maybe_recipient_account_data.is_none() && allow_unfunded_recipient {
895+
false
896+
} else {
889897
return Err("Error: The recipient address is not funded. \
890-
Add `--allow-unfunded-recipient` to complete the transfer. \
891-
"
892-
.into());
898+
Add `--allow-unfunded-recipient` to complete the transfer."
899+
.into());
893900
}
894-
recipient_account_info
895-
.map(|(recipient_is_token_account, _)| recipient_is_token_account)
896-
.unwrap_or(false)
897901
} else {
902+
// in offline mode we gotta trust them
898903
!recipient_is_ata_owner
899904
};
900905

901-
if !recipient_is_token_account {
902-
recipient_token_account = get_associated_token_address_with_program_id(
903-
&recipient,
904-
&mint_info.address,
905-
&mint_info.program_id,
906-
);
906+
// now if its a token account, life is ez
907+
let (recipient_token_account, fundable_owner) = if recipient_is_token_account {
908+
(recipient, None)
909+
}
910+
// but if not, we need to determine if we can or should create an ata for recipient
911+
else {
912+
// first, get the ata address
913+
let recipient_token_account = token.get_associated_token_address(&recipient);
914+
907915
println_display(
908916
config,
909917
format!(
@@ -912,19 +920,16 @@ async fn command_transfer(
912920
),
913921
);
914922

923+
// if we can fetch it to determine if it exists, do so
915924
let needs_funding = if !config.sign_only {
916925
if let Some(recipient_token_account_data) = config
917-
.rpc_client
918-
.get_account_with_commitment(
919-
&recipient_token_account,
920-
config.rpc_client.commitment(),
921-
)
926+
.program_client
927+
.get_account(recipient_token_account)
922928
.await?
923-
.value
924929
{
925930
if recipient_token_account_data.owner == system_program::id() {
926931
true
927-
} else if recipient_token_account_data.owner == mint_info.program_id {
932+
} else if recipient_token_account_data.owner == config.program_id {
928933
false
929934
} else {
930935
return Err(
@@ -934,80 +939,54 @@ async fn command_transfer(
934939
} else {
935940
true
936941
}
937-
} else {
942+
}
943+
// otherwise trust the cli flag
944+
else {
938945
fund_recipient
939946
};
940947

941-
if needs_funding {
948+
// and now we determine if we will actually fund it, based on its need and our willingness
949+
let fundable_owner = if needs_funding {
942950
if fund_recipient {
943-
if !config.sign_only {
944-
minimum_balance_for_rent_exemption += config
945-
.program_client
946-
.get_minimum_balance_for_rent_exemption(Account::LEN)
947-
.await?;
948-
println_display(
949-
config,
950-
format!(
951-
" Funding recipient: {} ({} SOL)",
952-
recipient_token_account,
953-
lamports_to_sol(minimum_balance_for_rent_exemption)
954-
),
955-
);
956-
}
957-
instructions.push(create_associated_token_account(
958-
&config.fee_payer.pubkey(),
959-
&recipient,
960-
&mint_info.address,
961-
&mint_info.program_id,
962-
));
951+
println_display(
952+
config,
953+
format!(" Funding recipient: {}", recipient_token_account,),
954+
);
955+
956+
Some(recipient)
963957
} else {
964958
return Err(
965959
"Error: Recipient's associated token account does not exist. \
966960
Add `--fund-recipient` to fund their account"
967961
.into(),
968962
);
969963
}
970-
}
964+
} else {
965+
None
966+
};
967+
968+
(recipient_token_account, fundable_owner)
969+
};
970+
971+
// set up memo if provided...
972+
if let Some(text) = memo {
973+
token.with_memo(text, vec![config.default_signer.pubkey()]);
971974
}
972975

973-
if use_unchecked_instruction {
974-
#[allow(deprecated)]
975-
instructions.push(transfer(
976-
&mint_info.program_id,
977-
&sender,
978-
&recipient_token_account,
979-
&sender_owner,
980-
&config.multisigner_pubkeys,
981-
transfer_balance,
982-
)?);
983-
} else {
984-
instructions.push(transfer_checked(
985-
&mint_info.program_id,
976+
// ...and, finally, the transfer
977+
let res = token
978+
.transfer(
986979
&sender,
987-
&mint_info.address,
988980
&recipient_token_account,
989981
&sender_owner,
990-
&config.multisigner_pubkeys,
991982
transfer_balance,
992-
mint_info.decimals,
993-
)?);
994-
}
995-
if let Some(text) = memo {
996-
instructions.push(spl_memo::build_memo(
997-
text.as_bytes(),
998-
&[&config.fee_payer.pubkey()],
999-
));
1000-
}
1001-
let tx_return = handle_tx(
1002-
&CliSignerInfo {
1003-
signers: bulk_signers,
1004-
},
1005-
config,
1006-
no_wait,
1007-
minimum_balance_for_rent_exemption,
1008-
instructions,
1009-
)
1010-
.await?;
983+
decimals,
984+
fundable_owner,
985+
&bulk_signers,
986+
)
987+
.await?;
988+
989+
let tx_return = finish_tx(config, &res, no_wait).await?;
1011990
Ok(match tx_return {
1012991
TransactionReturnData::CliSignature(signature) => {
1013992
config.output_format.formatted_string(&signature)
@@ -3541,6 +3520,7 @@ mod tests {
35413520
solana_sdk::{
35423521
bpf_loader,
35433522
signature::{write_keypair_file, Keypair, Signer},
3523+
transaction::Transaction,
35443524
},
35453525
solana_test_validator::{ProgramInfo, TestValidator, TestValidatorGenesis},
35463526
spl_token_client::client::{
@@ -3830,8 +3810,7 @@ mod tests {
38303810
let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
38313811
let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap();
38323812
let account = config.rpc_client.get_account(&mint).await.unwrap();
3833-
let mint_account =
3834-
StateWithExtensionsOwned::<spl_token_2022::state::Mint>::unpack(account.data).unwrap();
3813+
let mint_account = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
38353814
let extension = mint_account
38363815
.get_extension::<InterestBearingConfig>()
38373816
.unwrap();
@@ -3852,8 +3831,7 @@ mod tests {
38523831
let new_rate: i16 = 300;
38533832
let token = create_interest_bearing_token(&config, &payer, initial_rate).await;
38543833
let account = config.rpc_client.get_account(&token).await.unwrap();
3855-
let mint_account =
3856-
StateWithExtensionsOwned::<spl_token_2022::state::Mint>::unpack(account.data).unwrap();
3834+
let mint_account = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
38573835
let extension = mint_account
38583836
.get_extension::<InterestBearingConfig>()
38593837
.unwrap();
@@ -3873,8 +3851,7 @@ mod tests {
38733851
.await;
38743852
let _value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
38753853
let account = config.rpc_client.get_account(&token).await.unwrap();
3876-
let mint_account =
3877-
StateWithExtensionsOwned::<spl_token_2022::state::Mint>::unpack(account.data).unwrap();
3854+
let mint_account = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
38783855
let extension = mint_account
38793856
.get_extension::<InterestBearingConfig>()
38803857
.unwrap();
@@ -4327,8 +4304,8 @@ mod tests {
43274304
result.unwrap();
43284305

43294306
let account = config.rpc_client.get_account(&token).await.unwrap();
4330-
let mint = Mint::unpack(&account.data).unwrap();
4331-
assert_eq!(mint.mint_authority, COption::None);
4307+
let mint = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
4308+
assert_eq!(mint.base.mint_authority, COption::None);
43324309
}
43334310
}
43344311

0 commit comments

Comments
 (0)