Parcourir la source

Add token Amount/UiAmount conversion Instructions (#2928)

* Add Amount/UiAmount conversion Instructions to spl-token

* Use invalid-mint error in spl_token get_account_data_size

* Add Amount/UiAmount conversion Instructions to spl-token-2022
Tyera Eulberg il y a 3 ans
Parent
commit
9d202dcc56
4 fichiers modifiés avec 372 ajouts et 5 suppressions
  1. 1 0
      program/Cargo.toml
  2. 96 3
      program/src/instruction.rs
  3. 49 0
      program/src/lib.rs
  4. 226 2
      program/src/processor.rs

+ 1 - 0
program/Cargo.toml

@@ -22,6 +22,7 @@ thiserror = "1.0"
 
 [dev-dependencies]
 lazy_static = "1.4.0"
+serial_test = "0.5.1"
 solana-program-test = "1.9.5"
 solana-sdk = "1.9.5"
 

+ 96 - 3
program/src/instruction.rs

@@ -19,7 +19,7 @@ pub const MAX_SIGNERS: usize = 11;
 /// Instructions supported by the token program.
 #[repr(C)]
 #[derive(Clone, Debug, PartialEq)]
-pub enum TokenInstruction {
+pub enum TokenInstruction<'a> {
     /// Initializes a new mint and optionally deposits all the newly minted
     /// tokens in an account.
     ///
@@ -434,10 +434,38 @@ pub enum TokenInstruction {
     /// Data expected by this instruction:
     ///   None
     InitializeImmutableOwner,
+    /// Convert an Amount of tokens to a UiAmount `string`, using the given mint.
+    /// In this version of the program, the mint can only specify the number of decimals.
+    ///
+    /// Fails on an invalid mint.
+    ///
+    /// Return data can be fetched using `sol_get_return_data` and deserialized with
+    /// `String::from_utf8`.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   0. `[]` The mint to calculate for
+    AmountToUiAmount {
+        /// The amount of tokens to reformat.
+        amount: u64,
+    },
+    /// Convert a UiAmount of tokens to a little-endian `u64` raw Amount, using the given mint.
+    /// In this version of the program, the mint can only specify the number of decimals.
+    ///
+    /// Return data can be fetched using `sol_get_return_data` and deserializing
+    /// the return data as a little-endian `u64`.
+    ///
+    /// Accounts expected by this instruction:
+    ///
+    ///   0. `[]` The mint to calculate for
+    UiAmountToAmount {
+        /// The ui_amount of tokens to reformat.
+        ui_amount: &'a str,
+    },
 }
-impl TokenInstruction {
+impl<'a> TokenInstruction<'a> {
     /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
-    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
+    pub fn unpack(input: &'a [u8]) -> Result<Self, ProgramError> {
         use TokenError::InvalidInstruction;
 
         let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
@@ -556,6 +584,19 @@ impl TokenInstruction {
             }
             21 => Self::GetAccountDataSize,
             22 => Self::InitializeImmutableOwner,
+            23 => {
+                let (amount, _rest) = rest.split_at(8);
+                let amount = amount
+                    .try_into()
+                    .ok()
+                    .map(u64::from_le_bytes)
+                    .ok_or(InvalidInstruction)?;
+                Self::AmountToUiAmount { amount }
+            }
+            24 => {
+                let ui_amount = std::str::from_utf8(rest).map_err(|_| InvalidInstruction)?;
+                Self::UiAmountToAmount { ui_amount }
+            }
             _ => return Err(TokenError::InvalidInstruction.into()),
         })
     }
@@ -658,6 +699,14 @@ impl TokenInstruction {
             &Self::InitializeImmutableOwner => {
                 buf.push(22);
             }
+            &Self::AmountToUiAmount { amount } => {
+                buf.push(23);
+                buf.extend_from_slice(&amount.to_le_bytes());
+            }
+            Self::UiAmountToAmount { ui_amount } => {
+                buf.push(24);
+                buf.extend_from_slice(ui_amount.as_bytes());
+            }
         };
         buf
     }
@@ -1358,6 +1407,36 @@ pub fn initialize_immutable_owner(
     })
 }
 
+/// Creates an `AmountToUiAmount` instruction
+pub fn amount_to_ui_amount(
+    token_program_id: &Pubkey,
+    mint_pubkey: &Pubkey,
+    amount: u64,
+) -> Result<Instruction, ProgramError> {
+    check_program_account(token_program_id)?;
+
+    Ok(Instruction {
+        program_id: *token_program_id,
+        accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)],
+        data: TokenInstruction::AmountToUiAmount { amount }.pack(),
+    })
+}
+
+/// Creates a `UiAmountToAmount` instruction
+pub fn ui_amount_to_amount(
+    token_program_id: &Pubkey,
+    mint_pubkey: &Pubkey,
+    ui_amount: &str,
+) -> Result<Instruction, ProgramError> {
+    check_program_account(token_program_id)?;
+
+    Ok(Instruction {
+        program_id: *token_program_id,
+        accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)],
+        data: TokenInstruction::UiAmountToAmount { ui_amount }.pack(),
+    })
+}
+
 /// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS
 pub fn is_valid_signer_index(index: usize) -> bool {
     (MIN_SIGNERS..=MAX_SIGNERS).contains(&index)
@@ -1592,5 +1671,19 @@ mod test {
         assert_eq!(packed, expect);
         let unpacked = TokenInstruction::unpack(&expect).unwrap();
         assert_eq!(unpacked, check);
+
+        let check = TokenInstruction::AmountToUiAmount { amount: 42 };
+        let packed = check.pack();
+        let expect = vec![23u8, 42, 0, 0, 0, 0, 0, 0, 0];
+        assert_eq!(packed, expect);
+        let unpacked = TokenInstruction::unpack(&expect).unwrap();
+        assert_eq!(unpacked, check);
+
+        let check = TokenInstruction::UiAmountToAmount { ui_amount: "0.42" };
+        let packed = check.pack();
+        let expect = vec![24u8, 48, 46, 52, 50];
+        assert_eq!(packed, expect);
+        let unpacked = TokenInstruction::unpack(&expect).unwrap();
+        assert_eq!(unpacked, check);
     }
 }

+ 49 - 0
program/src/lib.rs

@@ -27,6 +27,55 @@ pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 {
     amount as f64 / 10_usize.pow(decimals as u32) as f64
 }
 
+/// Convert a raw amount to its UI representation (using the decimals field defined in its mint)
+pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String {
+    let decimals = decimals as usize;
+    if decimals > 0 {
+        // Left-pad zeros to decimals + 1, so we at least have an integer zero
+        let mut s = format!("{:01$}", amount, decimals + 1);
+        // Add the decimal point (Sorry, "," locales!)
+        s.insert(s.len() - decimals, '.');
+        s
+    } else {
+        amount.to_string()
+    }
+}
+
+/// Convert a raw amount to its UI representation using the given decimals field
+/// Excess zeroes or unneeded decimal point are trimmed.
+pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String {
+    let mut s = amount_to_ui_amount_string(amount, decimals);
+    if decimals > 0 {
+        let zeros_trimmed = s.trim_end_matches('0');
+        s = zeros_trimmed.trim_end_matches('.').to_string();
+    }
+    s
+}
+
+/// Try to convert a UI represenation of a token amount to its raw amount using the given decimals
+/// field
+pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result<u64, ProgramError> {
+    let decimals = decimals as usize;
+    let mut parts = ui_amount.split('.');
+    let mut amount_str = parts.next().unwrap().to_string(); // splitting a string, even an empty one, will always yield an iterator of at least len == 1
+    let after_decimal = parts.next().unwrap_or("");
+    let after_decimal = after_decimal.trim_end_matches('0');
+    if (amount_str.is_empty() && after_decimal.is_empty())
+        || parts.next().is_some()
+        || after_decimal.len() > decimals
+    {
+        return Err(ProgramError::InvalidArgument);
+    }
+
+    amount_str.push_str(after_decimal);
+    for _ in 0..decimals.saturating_sub(after_decimal.len()) {
+        amount_str.push('0');
+    }
+    amount_str
+        .parse::<u64>()
+        .map_err(|_| ProgramError::InvalidArgument)
+}
+
 solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
 
 /// Checks that the supplied program ID is the correct one for SPL-token

+ 226 - 2
program/src/processor.rs

@@ -1,9 +1,11 @@
 //! Program state processor
 
 use crate::{
+    amount_to_ui_amount_string_trimmed,
     error::TokenError,
     instruction::{is_valid_signer_index, AuthorityType, TokenInstruction, MAX_SIGNERS},
     state::{Account, AccountState, Mint, Multisig},
+    try_ui_amount_into_amount,
 };
 use num_traits::FromPrimitive;
 use solana_program::{
@@ -768,7 +770,8 @@ impl Processor {
         // make sure the mint is valid
         let mint_info = next_account_info(account_info_iter)?;
         Self::check_account_owner(program_id, mint_info)?;
-        let _ = Mint::unpack(&mint_info.data.borrow())?;
+        let _ = Mint::unpack(&mint_info.data.borrow())
+            .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
         set_return_data(&Account::LEN.to_le_bytes());
         Ok(())
     }
@@ -785,6 +788,42 @@ impl Processor {
         Ok(())
     }
 
+    /// Processes an [AmountToUiAmount](enum.TokenInstruction.html) instruction
+    pub fn process_amount_to_ui_amount(
+        program_id: &Pubkey,
+        accounts: &[AccountInfo],
+        amount: u64,
+    ) -> ProgramResult {
+        let account_info_iter = &mut accounts.iter();
+        let mint_info = next_account_info(account_info_iter)?;
+        Self::check_account_owner(program_id, mint_info)?;
+
+        let mint = Mint::unpack(&mint_info.data.borrow_mut())
+            .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
+        let ui_amount = amount_to_ui_amount_string_trimmed(amount, mint.decimals);
+
+        set_return_data(&ui_amount.into_bytes());
+        Ok(())
+    }
+
+    /// Processes an [AmountToUiAmount](enum.TokenInstruction.html) instruction
+    pub fn process_ui_amount_to_amount(
+        program_id: &Pubkey,
+        accounts: &[AccountInfo],
+        ui_amount: &str,
+    ) -> ProgramResult {
+        let account_info_iter = &mut accounts.iter();
+        let mint_info = next_account_info(account_info_iter)?;
+        Self::check_account_owner(program_id, mint_info)?;
+
+        let mint = Mint::unpack(&mint_info.data.borrow_mut())
+            .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
+        let amount = try_ui_amount_into_amount(ui_amount.to_string(), mint.decimals)?;
+
+        set_return_data(&amount.to_le_bytes());
+        Ok(())
+    }
+
     /// Processes an [Instruction](enum.Instruction.html).
     pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
         let instruction = TokenInstruction::unpack(input)?;
@@ -893,6 +932,14 @@ impl Processor {
                 msg!("Instruction: InitializeImmutableOwner");
                 Self::process_initialize_immutable_owner(accounts)
             }
+            TokenInstruction::AmountToUiAmount { amount } => {
+                msg!("Instruction: AmountToUiAmount");
+                Self::process_amount_to_ui_amount(program_id, accounts, amount)
+            }
+            TokenInstruction::UiAmountToAmount { ui_amount } => {
+                msg!("Instruction: UiAmountToAmount");
+                Self::process_ui_amount_to_amount(program_id, accounts, ui_amount)
+            }
         }
     }
 
@@ -997,6 +1044,7 @@ impl PrintProgramError for TokenError {
 mod tests {
     use super::*;
     use crate::instruction::*;
+    use serial_test::serial;
     use solana_program::{
         account_info::IntoAccountInfo, clock::Epoch, instruction::Instruction, program_error,
         sysvar::rent,
@@ -6406,6 +6454,7 @@ mod tests {
     }
 
     #[test]
+    #[serial]
     fn test_get_account_data_size() {
         // see integration tests for return-data validity
         let program_id = crate::id();
@@ -6416,7 +6465,7 @@ mod tests {
         let mint_key = Pubkey::new_unique();
         // fail if an invalid mint is passed in
         assert_eq!(
-            Err(ProgramError::UninitializedAccount),
+            Err(TokenError::InvalidMint.into()),
             do_process_instruction(
                 get_account_data_size(&program_id, &mint_key).unwrap(),
                 vec![&mut mint_account],
@@ -6488,4 +6537,179 @@ mod tests {
             )
         );
     }
+
+    #[test]
+    #[serial]
+    fn test_amount_to_ui_amount() {
+        let program_id = crate::id();
+        let owner_key = Pubkey::new_unique();
+        let mint_key = Pubkey::new_unique();
+        let mut mint_account =
+            SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id);
+        let mut rent_sysvar = rent_sysvar();
+
+        // fail if an invalid mint is passed in
+        assert_eq!(
+            Err(TokenError::InvalidMint.into()),
+            do_process_instruction(
+                amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+
+        // create mint
+        do_process_instruction(
+            initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(),
+            vec![&mut mint_account, &mut rent_sysvar],
+        )
+        .unwrap();
+
+        set_expected_data("0.23".as_bytes().to_vec());
+        do_process_instruction(
+            amount_to_ui_amount(&program_id, &mint_key, 23).unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data("1.1".as_bytes().to_vec());
+        do_process_instruction(
+            amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data("42".as_bytes().to_vec());
+        do_process_instruction(
+            amount_to_ui_amount(&program_id, &mint_key, 4200).unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data("0".as_bytes().to_vec());
+        do_process_instruction(
+            amount_to_ui_amount(&program_id, &mint_key, 0).unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+    }
+
+    #[test]
+    #[serial]
+    fn test_ui_amount_to_amount() {
+        let program_id = crate::id();
+        let owner_key = Pubkey::new_unique();
+        let mint_key = Pubkey::new_unique();
+        let mut mint_account =
+            SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id);
+        let mut rent_sysvar = rent_sysvar();
+
+        // fail if an invalid mint is passed in
+        assert_eq!(
+            Err(TokenError::InvalidMint.into()),
+            do_process_instruction(
+                ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+
+        // create mint
+        do_process_instruction(
+            initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(),
+            vec![&mut mint_account, &mut rent_sysvar],
+        )
+        .unwrap();
+
+        set_expected_data(23u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "0.23").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(20u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "0.20").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(20u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "0.2000").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(20u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, ".20").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(110u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(110u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "1.10").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(4200u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "42").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(4200u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "42.").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        set_expected_data(0u64.to_le_bytes().to_vec());
+        do_process_instruction(
+            ui_amount_to_amount(&program_id, &mint_key, "0").unwrap(),
+            vec![&mut mint_account],
+        )
+        .unwrap();
+
+        // fail if invalid ui_amount passed in
+        assert_eq!(
+            Err(ProgramError::InvalidArgument),
+            do_process_instruction(
+                ui_amount_to_amount(&program_id, &mint_key, "").unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+        assert_eq!(
+            Err(ProgramError::InvalidArgument),
+            do_process_instruction(
+                ui_amount_to_amount(&program_id, &mint_key, ".").unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+        assert_eq!(
+            Err(ProgramError::InvalidArgument),
+            do_process_instruction(
+                ui_amount_to_amount(&program_id, &mint_key, "0.111").unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+        assert_eq!(
+            Err(ProgramError::InvalidArgument),
+            do_process_instruction(
+                ui_amount_to_amount(&program_id, &mint_key, "0.t").unwrap(),
+                vec![&mut mint_account],
+            )
+        );
+    }
 }