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

Commit 200b2ac

Browse files
committed
token-cli: final fixes for client conversion
1 parent 2c6fbf7 commit 200b2ac

File tree

10 files changed

+125
-114
lines changed

10 files changed

+125
-114
lines changed

token/cli/src/main.rs

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ async fn command_transfer(
783783
let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?;
784784

785785
// if the user got the decimals wrong, they may well have calculated the transfer amount wrong
786+
// we only check in online mode, because in offline, mint_info.decimals is always 9
786787
if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) {
787788
return Err(format!(
788789
"Decimals {} was provided, but actual value is {}",
@@ -793,6 +794,10 @@ async fn command_transfer(
793794
}
794795

795796
// decimals determines whether transfer_checked is used or not
797+
// in online mode, mint_decimals may be None but mint_info.decimals is always correct
798+
// in offline mode, mint_info.decimals may be wrong, but mint_decimals is always provided
799+
// and in online mode, when mint_decimals is provided, it is verified correct
800+
// hence the fallthrough logic here
796801
let decimals = if use_unchecked_instruction {
797802
None
798803
} else if mint_decimals.is_some() {
@@ -808,21 +813,13 @@ async fn command_transfer(
808813
token.get_associated_token_address(&sender_owner)
809814
};
810815

811-
// in any sign_only block, we can safely unwrap this
812-
let maybe_sender_state = match token.get_account_info(&sender).await {
813-
Ok(a) => Some(a),
814-
Err(_) if config.sign_only => None,
815-
Err(e) => {
816-
return Err(format!("Error: failed to fetch sender account {}: {}", sender, e).into())
817-
}
818-
};
819-
816+
// the amount the user wants to tranfer, as a f64
820817
let maybe_transfer_balance =
821818
ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals));
822819

823820
// the amount we will transfer, as a u64
824821
let transfer_balance = if !config.sign_only {
825-
let sender_balance = maybe_sender_state.unwrap().base.amount;
822+
let sender_balance = token.get_account_info(&sender).await?.base.amount;
826823
let transfer_balance = maybe_transfer_balance.unwrap_or(sender_balance);
827824

828825
println_display(
@@ -959,17 +956,30 @@ async fn command_transfer(
959956
}
960957

961958
// ...and, finally, the transfer
962-
let res = token
963-
.transfer(
964-
&sender,
965-
&recipient_token_account,
966-
&sender_owner,
967-
transfer_balance,
968-
decimals,
969-
fundable_owner,
970-
&bulk_signers,
971-
)
972-
.await?;
959+
let res = if let Some(recipient_owner) = fundable_owner {
960+
token
961+
.create_recipient_associated_account_and_transfer(
962+
&sender,
963+
&recipient_token_account,
964+
&recipient_owner,
965+
&sender_owner,
966+
transfer_balance,
967+
decimals,
968+
&bulk_signers,
969+
)
970+
.await?
971+
} else {
972+
token
973+
.transfer(
974+
&sender,
975+
&recipient_token_account,
976+
&sender_owner,
977+
transfer_balance,
978+
decimals,
979+
&bulk_signers,
980+
)
981+
.await?
982+
};
973983

974984
let tx_return = finish_tx(config, &res, no_wait).await?;
975985
Ok(match tx_return {
@@ -1367,21 +1377,28 @@ async fn command_close(
13671377
recipient: Pubkey,
13681378
bulk_signers: BulkSigners,
13691379
) -> CommandResult {
1370-
let source_account = config.get_account_checked(&account).await?;
1380+
let mint_pubkey = if !config.sign_only {
1381+
let source_account = config.get_account_checked(&account).await?;
1382+
1383+
let source_state = StateWithExtensionsOwned::<Account>::unpack(source_account.data)
1384+
.map_err(|_| format!("Could not deserialize token account {}", account))?;
1385+
let source_amount = source_state.base.amount;
13711386

1372-
let source_state = StateWithExtensionsOwned::<Account>::unpack(source_account.data)
1373-
.map_err(|_| format!("Could not deserialize token account {}", account))?;
1374-
let source_amount = source_state.base.amount;
1387+
if !source_state.base.is_native() && source_amount > 0 {
1388+
return Err(format!(
1389+
"Account {} still has {} tokens; empty the account in order to close it.",
1390+
account, source_amount,
1391+
)
1392+
.into());
1393+
}
13751394

1376-
if !source_state.base.is_native() && source_amount > 0 {
1377-
return Err(format!(
1378-
"Account {} still has {} tokens; empty the account in order to close it.",
1379-
account, source_amount,
1380-
)
1381-
.into());
1382-
}
1395+
source_state.base.mint
1396+
} else {
1397+
// default is safe here because close doesnt use it
1398+
Pubkey::default()
1399+
};
13831400

1384-
let token = token_client_from_config(config, &source_state.base.mint);
1401+
let token = token_client_from_config(config, &mint_pubkey);
13851402
let res = token
13861403
.close_account(&account, &recipient, &close_authority, &bulk_signers)
13871404
.await?;
@@ -1655,9 +1672,13 @@ async fn command_gc(
16551672
println_display(config, format!("Processing token: {}", token_pubkey));
16561673

16571674
let token = token_client_from_config(config, &token_pubkey);
1658-
let associated_token_account = token.get_associated_token_address(&owner);
16591675
let total_balance: u64 = accounts.values().map(|account| account.0).sum();
16601676

1677+
let associated_token_account = token.get_associated_token_address(&owner);
1678+
if !accounts.contains_key(&associated_token_account) && total_balance > 0 {
1679+
token.create_associated_token_account(&owner).await?;
1680+
}
1681+
16611682
for (address, (amount, decimals, frozen, close_authority)) in accounts {
16621683
let is_associated = address == associated_token_account;
16631684

@@ -1676,6 +1697,10 @@ async fn command_gc(
16761697
continue;
16771698
}
16781699

1700+
if is_associated {
1701+
println!("Closing associated account {}", address);
1702+
}
1703+
16791704
// this logic is quite fiendish, but its more readable this way than if/else
16801705
let maybe_res = match (close_authority == owner, is_associated, amount == 0) {
16811706
// owner authority, associated or auxiliary, empty -> close
@@ -1687,9 +1712,10 @@ async fn command_gc(
16871712
// owner authority, auxiliary, nonempty -> empty and close
16881713
(true, false, false) => Some(
16891714
token
1690-
.empty_and_close_auxiliary_account(
1715+
.empty_and_close_account(
16911716
&address,
16921717
&owner,
1718+
&associated_token_account,
16931719
&owner,
16941720
decimals,
16951721
&bulk_signers,
@@ -1705,7 +1731,6 @@ async fn command_gc(
17051731
&owner,
17061732
amount,
17071733
Some(decimals),
1708-
Some(owner),
17091734
&bulk_signers,
17101735
)
17111736
.await,

token/client/src/token.rs

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ pub enum TokenError {
5252
AccountInvalidOwner,
5353
#[error("invalid account mint")]
5454
AccountInvalidMint,
55-
#[error("invalid account")]
56-
AccountInvalidAccount,
55+
#[error("invalid associated account address")]
56+
AccountInvalidAssociatedAddress,
57+
#[error("invalid auxiliary account address")]
58+
AccountInvalidAuxiliaryAddress,
5759
#[error("proof error: {0}")]
5860
Proof(ProofError),
5961
#[error("maximum deposit transfer amount exceeded")]
@@ -77,7 +79,8 @@ impl PartialEq for TokenError {
7779
(Self::AccountNotFound, Self::AccountNotFound) => true,
7880
(Self::AccountInvalidOwner, Self::AccountInvalidOwner) => true,
7981
(Self::AccountInvalidMint, Self::AccountInvalidMint) => true,
80-
(Self::AccountInvalidAccount, Self::AccountInvalidAccount) => true,
82+
(Self::AccountInvalidAssociatedAddress, Self::AccountInvalidAssociatedAddress) => true,
83+
(Self::AccountInvalidAuxiliaryAddress, Self::AccountInvalidAuxiliaryAddress) => true,
8184
(
8285
Self::MaximumDepositTransferAmountExceeded,
8386
Self::MaximumDepositTransferAmountExceeded,
@@ -408,7 +411,6 @@ where
408411
Ok(transaction)
409412
}
410413

411-
// XXX TODO remove process_ixs from program-2022-test/tests/close_account.rs and private this function
412414
pub async fn process_ixs<S: Signers>(
413415
&self,
414416
token_instructions: &[Instruction],
@@ -712,26 +714,64 @@ where
712714
authority: &Pubkey,
713715
amount: u64,
714716
decimals: Option<u8>,
715-
fundable_owner: Option<Pubkey>,
716717
signing_keypairs: &S,
717718
) -> TokenResult<T::Output> {
718719
let signing_pubkeys = signing_keypairs.pubkeys();
719720
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
720721

721-
let mut instructions = vec![];
722+
let instructions = if let Some(decimals) = decimals {
723+
[instruction::transfer_checked(
724+
&self.program_id,
725+
source,
726+
&self.pubkey,
727+
destination,
728+
authority,
729+
&multisig_signers,
730+
amount,
731+
decimals,
732+
)?]
733+
} else {
734+
#[allow(deprecated)]
735+
[instruction::transfer(
736+
&self.program_id,
737+
source,
738+
destination,
739+
authority,
740+
&multisig_signers,
741+
amount,
742+
)?]
743+
};
722744

723-
if let Some(recipient) = fundable_owner {
724-
if *destination != self.get_associated_token_address(&recipient) {
725-
return Err(TokenError::AccountInvalidAccount);
726-
}
745+
self.process_ixs(&instructions, signing_keypairs).await
746+
}
727747

728-
instructions.push(create_associated_token_account_idempotent(
748+
/// Transfer tokens to an associated account, creating it if it does not exist
749+
#[allow(clippy::too_many_arguments)]
750+
pub async fn create_recipient_associated_account_and_transfer<S: Signers>(
751+
&self,
752+
source: &Pubkey,
753+
destination: &Pubkey,
754+
destination_owner: &Pubkey,
755+
authority: &Pubkey,
756+
amount: u64,
757+
decimals: Option<u8>,
758+
signing_keypairs: &S,
759+
) -> TokenResult<T::Output> {
760+
let signing_pubkeys = signing_keypairs.pubkeys();
761+
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
762+
763+
if *destination != self.get_associated_token_address(destination_owner) {
764+
return Err(TokenError::AccountInvalidAssociatedAddress);
765+
}
766+
767+
let mut instructions = vec![
768+
(create_associated_token_account_idempotent(
729769
&self.payer.pubkey(),
730-
&recipient,
770+
destination_owner,
731771
&self.pubkey,
732772
&self.program_id,
733-
));
734-
};
773+
)),
774+
];
735775

736776
if let Some(decimals) = decimals {
737777
instructions.push(instruction::transfer_checked(
@@ -923,11 +963,12 @@ where
923963
self.process_ixs(&instructions, signing_keypairs).await
924964
}
925965

926-
/// Close an auxiliary account, reclaiming its lamports and consolidating its tokens
927-
pub async fn empty_and_close_auxiliary_account<S: Signers>(
966+
/// Close an account, reclaiming its lamports and tokens
967+
pub async fn empty_and_close_account<S: Signers>(
928968
&self,
929-
auxiliary_account: &Pubkey,
969+
account_to_close: &Pubkey,
930970
lamports_destination: &Pubkey,
971+
tokens_destination: &Pubkey,
931972
authority: &Pubkey,
932973
decimals: u8,
933974
signing_keypairs: &S,
@@ -936,39 +977,17 @@ where
936977
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
937978

938979
// this implicitly validates that the mint on self is correct
939-
let account_state = self.get_account_info(auxiliary_account).await?;
940-
let account_owner = &account_state.base.owner;
941-
942-
// we move tokens to the owner of the closed account, NOT the authority that authorizes its closure
943-
let associated_account = self.get_associated_token_address(account_owner);
944-
if *auxiliary_account == associated_account {
945-
return Err(TokenError::AccountInvalidAccount);
946-
}
980+
let account_state = self.get_account_info(account_to_close).await?;
947981

948982
let mut instructions = vec![];
949983

950984
if !self.is_native() && account_state.base.amount > 0 {
951-
match self.get_account(&associated_account).await {
952-
Err(TokenError::AccountNotFound) => {
953-
instructions.push(create_associated_token_account(
954-
&self.payer.pubkey(),
955-
account_owner,
956-
&self.pubkey,
957-
&self.program_id,
958-
))
959-
}
960-
Err(e) => return Err(e),
961-
_ => {}
962-
};
963-
964-
// we sign the transfer with authority, rather than owner
965-
// which means that if a separate close authority is being used, it must be a delegate also
966-
// this is to avoid overcomplicating the interface for a twice-niche usecase (closing nonempty nonowned)
985+
// if a separate close authority is being used, it must be a delegate also
967986
instructions.push(instruction::transfer_checked(
968987
&self.program_id,
969-
auxiliary_account,
988+
account_to_close,
970989
&self.pubkey,
971-
&associated_account,
990+
tokens_destination,
972991
authority,
973992
&multisig_signers,
974993
account_state.base.amount,
@@ -978,7 +997,7 @@ where
978997

979998
instructions.push(instruction::close_account(
980999
&self.program_id,
981-
auxiliary_account,
1000+
account_to_close,
9821001
lamports_destination,
9831002
authority,
9841003
&multisig_signers,

token/client/tests/program-test.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,6 @@ async fn transfer() {
310310
&alice.pubkey(),
311311
transfer_amount,
312312
Some(decimals),
313-
None,
314313
&vec![&alice],
315314
)
316315
.await

token/program-2022-test/tests/burn.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ async fn run_burn_and_close_system_or_incinerator(context: TestContext, non_owne
236236
&alice.pubkey(),
237237
1,
238238
Some(decimals),
239-
None,
240239
&vec![&alice],
241240
)
242241
.await

token/program-2022-test/tests/confidential_transfer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl ConfidentialTokenAccountMeta {
154154
.unwrap();
155155

156156
token
157-
.enable_required_transfer_memos(&token_account, &owner.pubkey(), &vec![owner])
157+
.enable_required_transfer_memos(&token_account, &owner.pubkey(), &[owner])
158158
.await
159159
.unwrap();
160160

0 commit comments

Comments
 (0)