Skip to content

Commit 05f2372

Browse files
Merge pull request #170 from cavemanloverboy/cli-token-account-dest
Cli token account dest
2 parents d08bc69 + 94e81b7 commit 05f2372

File tree

1 file changed

+174
-8
lines changed

1 file changed

+174
-8
lines changed

cli/src/command/initiate_transfer.rs

Lines changed: 174 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ use solana_sdk::compute_budget::ComputeBudgetInstruction;
99
use solana_sdk::instruction::Instruction;
1010
use solana_sdk::message::v0::Message;
1111
use solana_sdk::message::VersionedMessage;
12+
use solana_sdk::program_pack::Pack;
1213
use solana_sdk::pubkey::Pubkey;
1314
use solana_sdk::transaction::VersionedTransaction;
1415

1516
use spl_associated_token_account::get_associated_token_address_with_program_id;
1617
use spl_token::instruction::transfer;
18+
use spl_token::state::{Account as TokenAccount, Mint};
1719
use squads_multisig::anchor_lang::{AnchorSerialize, InstructionData};
1820
use squads_multisig::client::get_multisig;
1921
use squads_multisig::pda::{get_proposal_pda, get_transaction_pda, get_vault_pda};
@@ -51,7 +53,9 @@ pub struct InitiateTransfer {
5153
#[arg(long)]
5254
token_amount_u64: u64,
5355

54-
/// The recipient of the Token(s)
56+
/// The recipient wallet address or token account address. If a wallet address is provided,
57+
/// the associated token account (ATA) will be derived automatically. If a token account address
58+
/// is provided, it will be validated to ensure it's for the correct mint.
5559
#[arg(long)]
5660
recipient: String,
5761

@@ -106,7 +110,8 @@ impl InitiateTransfer {
106110

107111
let transaction_creator_keypair = create_signer_from_path(keypair).unwrap();
108112
let transaction_creator = transaction_creator_keypair.pubkey();
109-
let fee_payer_keypair = fee_payer_keypair.map(|path| create_signer_from_path(path).unwrap());
113+
let fee_payer_keypair =
114+
fee_payer_keypair.map(|path| create_signer_from_path(path).unwrap());
110115
let fee_payer = fee_payer_keypair.as_ref().map(|kp| kp.pubkey());
111116

112117
let rpc_url = rpc_url.unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string());
@@ -119,6 +124,33 @@ impl InitiateTransfer {
119124

120125
let token_mint = Pubkey::from_str(&token_mint_address).expect("Invalid Token Mint Address");
121126

127+
// Fetch mint account to get decimals
128+
let mint_account = rpc_client
129+
.get_account(&token_mint)
130+
.await
131+
.map_err(|e| eyre::eyre!("Failed to fetch mint account {}: {}", token_mint, e))?;
132+
133+
if mint_account.owner != token_program_id {
134+
return Err(eyre::eyre!(
135+
"Mint account {} is not owned by token program {}",
136+
token_mint,
137+
token_program_id
138+
));
139+
}
140+
141+
let mint = Mint::unpack(&mint_account.data)
142+
.map_err(|e| eyre::eyre!("Failed to deserialize mint account {}: {}", token_mint, e))?;
143+
let decimals = mint.decimals;
144+
145+
let resolved_recipient = resolve_recipient_token_account(
146+
rpc_client,
147+
&recipient_pubkey,
148+
&token_mint,
149+
&token_program_id,
150+
)
151+
.await?;
152+
let recipient_ata = resolved_recipient.token_account;
153+
122154
let multisig_data = get_multisig(rpc_client, &multisig).await?;
123155

124156
let transaction_index = multisig_data.transaction_index + 1;
@@ -141,6 +173,13 @@ impl InitiateTransfer {
141173
println!("Vault Index: {}", vault_index);
142174
println!();
143175

176+
println!("Recipient: {}", resolved_recipient.authority);
177+
println!("Recipient Token Account: {}", recipient_ata);
178+
println!(
179+
"Transfer Amount: {}",
180+
format_token_amount(token_amount_u64, decimals)
181+
);
182+
144183
let proceed = Confirm::new()
145184
.with_prompt("Do you want to proceed?")
146185
.default(false)
@@ -167,12 +206,6 @@ impl InitiateTransfer {
167206
&token_program_id,
168207
);
169208

170-
let recipient_ata = get_associated_token_address_with_program_id(
171-
&recipient_pubkey,
172-
&token_mint,
173-
&token_program_id,
174-
);
175-
176209
let transfer_message = TransactionMessage::try_compile(
177210
&vault_pda.0,
178211
&[transfer(
@@ -256,3 +289,136 @@ impl InitiateTransfer {
256289
Ok(())
257290
}
258291
}
292+
293+
/// Formats a token amount with decimals without using floating-point arithmetic.
294+
///
295+
/// This function converts a raw token amount (as u64) to a human-readable string
296+
/// with the appropriate decimal point placement based on the token's decimals.
297+
///
298+
/// # Arguments
299+
///
300+
/// * `amount` - The raw token amount as u64 (e.g., 1000000 for 1 token with 6 decimals)
301+
/// * `decimals` - The number of decimal places for the token (typically 6, 8, or 9)
302+
///
303+
/// # Returns
304+
///
305+
/// Returns a formatted string with the decimal point correctly placed.
306+
/// Examples:
307+
/// - format_token_amount(1000000, 6) -> "1.000000"
308+
/// - format_token_amount(123456789, 9) -> "0.123456789"
309+
/// - format_token_amount(100, 6) -> "0.000100"
310+
fn format_token_amount(amount: u64, decimals: u8) -> String {
311+
let amount_str = amount.to_string();
312+
let amount_len = amount_str.len();
313+
let decimals_usize = decimals as usize;
314+
315+
if amount_len <= decimals_usize {
316+
// Amount is smaller than one unit, pad with zeros
317+
let padded = format!("{:0>width$}", amount_str, width = decimals_usize);
318+
format!("0.{}", padded)
319+
} else {
320+
// Amount is >= 1 unit, insert decimal point
321+
let integer_part = &amount_str[..amount_len - decimals_usize];
322+
let fractional_part = &amount_str[amount_len - decimals_usize..];
323+
// Trim trailing zeros from fractional part
324+
let trimmed_fractional = fractional_part.trim_end_matches('0');
325+
if trimmed_fractional.is_empty() {
326+
integer_part.to_string()
327+
} else {
328+
format!("{}.{}", integer_part, trimmed_fractional)
329+
}
330+
}
331+
}
332+
333+
/// Resolved recipient information for a token transfer.
334+
struct ResolvedRecipient {
335+
/// The token account address to receive the transfer
336+
token_account: Pubkey,
337+
/// The wallet address that owns/controls the token account
338+
authority: Pubkey,
339+
}
340+
341+
/// Resolves the recipient token account address for a token transfer.
342+
///
343+
/// This function determines the appropriate token account to use as the recipient:
344+
/// - If the provided `recipient_pubkey` is already a valid token account for the specified `token_mint`,
345+
/// it returns that address directly.
346+
/// - If `recipient_pubkey` is a token account for a different mint, it returns an error.
347+
/// - If `recipient_pubkey` is not owned by the token program it derives and returns the associated
348+
/// token address (ATA) for the recipient and mint.
349+
///
350+
/// # Arguments
351+
///
352+
/// * `rpc_client` - The RPC client used to query account information
353+
/// * `recipient_pubkey` - The recipient's wallet address (may be a token account or wallet address)
354+
/// * `token_mint` - The token mint address for the transfer
355+
/// * `token_program_id` - The token program ID (e.g., SPL Token or Token-2022)
356+
///
357+
/// # Returns
358+
///
359+
/// Returns `Ok(ResolvedRecipient)` containing the recipient token account address and authority, or an error if:
360+
/// - The recipient account is a token account for a different mint
361+
/// - The recipient account is owned by the token program but fails to deserialize as a token account
362+
async fn resolve_recipient_token_account(
363+
rpc_client: &RpcClient,
364+
recipient_pubkey: &Pubkey,
365+
token_mint: &Pubkey,
366+
token_program_id: &Pubkey,
367+
) -> eyre::Result<ResolvedRecipient> {
368+
match rpc_client.get_account(recipient_pubkey).await {
369+
Ok(account_info) => {
370+
// Check if the account is owned by the token program
371+
if account_info.owner == *token_program_id {
372+
// Try to deserialize as a token account to validate it's for the correct mint
373+
match TokenAccount::unpack(&account_info.data) {
374+
Ok(token_account) => {
375+
// Verify the token account is for the correct mint
376+
if token_account.mint == *token_mint {
377+
// It's a valid token account for the correct mint, use it directly
378+
Ok(ResolvedRecipient {
379+
token_account: *recipient_pubkey,
380+
authority: token_account.owner,
381+
})
382+
} else {
383+
// Token account exists but for a different mint, return error
384+
Err(eyre::eyre!(
385+
"Recipient token account {} is for mint {}, but transfer is for mint {}",
386+
recipient_pubkey,
387+
token_account.mint,
388+
token_mint
389+
))
390+
}
391+
}
392+
Err(_) => {
393+
// Failed to deserialize as token account, return error
394+
Err(eyre::eyre!(
395+
"Recipient account {} is owned by token program but failed to deserialize as a token account",
396+
recipient_pubkey
397+
))
398+
}
399+
}
400+
} else {
401+
// Not a token account, derive the ATA
402+
Ok(ResolvedRecipient {
403+
token_account: get_associated_token_address_with_program_id(
404+
recipient_pubkey,
405+
token_mint,
406+
token_program_id,
407+
),
408+
authority: *recipient_pubkey,
409+
})
410+
}
411+
}
412+
Err(_) => {
413+
// Account doesn't exist, derive the ATA
414+
Ok(ResolvedRecipient {
415+
token_account: get_associated_token_address_with_program_id(
416+
recipient_pubkey,
417+
token_mint,
418+
token_program_id,
419+
),
420+
authority: *recipient_pubkey,
421+
})
422+
}
423+
}
424+
}

0 commit comments

Comments
 (0)