@@ -9,11 +9,13 @@ use solana_sdk::compute_budget::ComputeBudgetInstruction;
99use solana_sdk:: instruction:: Instruction ;
1010use solana_sdk:: message:: v0:: Message ;
1111use solana_sdk:: message:: VersionedMessage ;
12+ use solana_sdk:: program_pack:: Pack ;
1213use solana_sdk:: pubkey:: Pubkey ;
1314use solana_sdk:: transaction:: VersionedTransaction ;
1415
1516use spl_associated_token_account:: get_associated_token_address_with_program_id;
1617use spl_token:: instruction:: transfer;
18+ use spl_token:: state:: { Account as TokenAccount , Mint } ;
1719use squads_multisig:: anchor_lang:: { AnchorSerialize , InstructionData } ;
1820use squads_multisig:: client:: get_multisig;
1921use 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