diff --git a/Cargo.lock b/Cargo.lock index 5ecda8a007f..4d078bdeed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2628,6 +2628,28 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "serial_test" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.14", + "syn 1.0.84", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -3884,6 +3906,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", + "serial_test", "solana-program", "solana-program-test", "solana-sdk", @@ -3900,6 +3923,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", + "serial_test", "solana-program", "solana-program-test", "solana-sdk", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index 4b62a824db1..05d78ecbca8 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -25,6 +25,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" diff --git a/token/program-2022/src/extension/transfer_fee/instruction.rs b/token/program-2022/src/extension/transfer_fee/instruction.rs index 69df456f568..ae2c0e9656b 100644 --- a/token/program-2022/src/extension/transfer_fee/instruction.rs +++ b/token/program-2022/src/extension/transfer_fee/instruction.rs @@ -412,7 +412,7 @@ pub fn set_transfer_fee( mod test { use super::*; - const TRANSFER_FEE_PREFIX: u8 = 24; + const TRANSFER_FEE_PREFIX: u8 = 26; #[test] fn test_instruction_packing() { diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index 4394b31dad9..89527619a50 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -28,7 +28,7 @@ const U64_BYTES: usize = 8; /// 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. /// @@ -469,6 +469,32 @@ pub enum TokenInstruction { /// None /// InitializeImmutableOwner, + /// Convert an Amount of tokens to a UiAmount `string`, using the given mint. + /// + /// 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 convert. + amount: u64, + }, + /// Convert a UiAmount of tokens to a little-endian `u64` raw Amount, using the given mint. + /// + /// 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 convert. + ui_amount: &'a str, + }, /// Initialize the close account authority on a new mint. /// /// Fails if the mint has already been initialized, so must be called before @@ -541,9 +567,9 @@ pub enum TokenInstruction { /// CreateNativeMint, } -impl TokenInstruction { +impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html). - pub fn unpack(input: &[u8]) -> Result { + pub fn unpack(input: &'a [u8]) -> Result { use TokenError::InvalidInstruction; let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; @@ -642,24 +668,32 @@ impl TokenInstruction { } 22 => Self::InitializeImmutableOwner, 23 => { + let (amount, _rest) = Self::unpack_u64(rest)?; + Self::AmountToUiAmount { amount } + } + 24 => { + let ui_amount = std::str::from_utf8(rest).map_err(|_| InvalidInstruction)?; + Self::UiAmountToAmount { ui_amount } + } + 25 => { let (close_authority, _rest) = Self::unpack_pubkey_option(rest)?; Self::InitializeMintCloseAuthority { close_authority } } - 24 => { + 26 => { let (instruction, _rest) = TransferFeeInstruction::unpack(rest)?; Self::TransferFeeExtension(instruction) } - 25 => Self::ConfidentialTransferExtension, - 26 => Self::DefaultAccountStateExtension, - 27 => { + 27 => Self::ConfidentialTransferExtension, + 28 => Self::DefaultAccountStateExtension, + 29 => { let mut extension_types = vec![]; for chunk in rest.chunks(size_of::()) { extension_types.push(chunk.try_into()?); } Self::Reallocate { extension_types } } - 28 => Self::MemoTransferExtension, - 29 => Self::CreateNativeMint, + 30 => Self::MemoTransferExtension, + 31 => Self::CreateNativeMint, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -768,35 +802,43 @@ 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()); + } &Self::InitializeMintCloseAuthority { ref close_authority, } => { - buf.push(23); + buf.push(25); Self::pack_pubkey_option(close_authority, &mut buf); } &Self::TransferFeeExtension(ref instruction) => { - buf.push(24); + buf.push(26); TransferFeeInstruction::pack(instruction, &mut buf); } &Self::ConfidentialTransferExtension => { - buf.push(25); + buf.push(27); } &Self::DefaultAccountStateExtension => { - buf.push(26); + buf.push(28); } &Self::Reallocate { ref extension_types, } => { - buf.push(27); + buf.push(29); for extension_type in extension_types { buf.extend_from_slice(&<[u8; 2]>::from(*extension_type)); } } &Self::MemoTransferExtension => { - buf.push(28); + buf.push(30); } &Self::CreateNativeMint => { - buf.push(29); + buf.push(31); } }; buf @@ -1552,6 +1594,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 { + check_spl_token_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 { + check_spl_token_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(), + }) +} + /// Creates a `Reallocate` instruction pub fn reallocate( token_program_id: &Pubkey, @@ -1846,11 +1918,25 @@ mod test { 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); + let check = TokenInstruction::InitializeMintCloseAuthority { close_authority: COption::Some(Pubkey::new(&[10u8; 32])), }; let packed = check.pack(); - let mut expect = vec![23u8, 1]; + let mut expect = vec![25u8, 1]; expect.extend_from_slice(&[10u8; 32]); assert_eq!(packed, expect); let unpacked = TokenInstruction::unpack(&expect).unwrap(); @@ -1858,7 +1944,7 @@ mod test { let check = TokenInstruction::CreateNativeMint; let packed = check.pack(); - let expect = vec![29u8]; + let expect = vec![31u8]; assert_eq!(packed, expect); let unpacked = TokenInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index a4caa30c3e2..53a04ba5d1a 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -1027,6 +1027,39 @@ impl Processor { token_account.init_extension::().map(|_| ()) } + /// Processes an [AmountToUiAmount](enum.TokenInstruction.html) instruction + pub fn process_amount_to_ui_amount(accounts: &[AccountInfo], amount: u64) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + check_program_account(mint_info.owner)?; + + let mint_data = mint_info.data.borrow(); + let mint = StateWithExtensions::::unpack(&mint_data) + .map_err(|_| Into::::into(TokenError::InvalidMint))?; + // TODO: update this with interest-bearing token extension logic + let ui_amount = spl_token::amount_to_ui_amount_string_trimmed(amount, mint.base.decimals); + + set_return_data(&ui_amount.into_bytes()); + Ok(()) + } + + /// Processes an [AmountToUiAmount](enum.TokenInstruction.html) instruction + pub fn process_ui_amount_to_amount(accounts: &[AccountInfo], ui_amount: &str) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + check_program_account(mint_info.owner)?; + + let mint_data = mint_info.data.borrow(); + let mint = StateWithExtensions::::unpack(&mint_data) + .map_err(|_| Into::::into(TokenError::InvalidMint))?; + // TODO: update this with interest-bearing token extension logic + let amount = + spl_token::try_ui_amount_into_amount(ui_amount.to_string(), mint.base.decimals)?; + + set_return_data(&amount.to_le_bytes()); + Ok(()) + } + /// Processes a [CreateNativeMint](enum.TokenInstruction.html) instruction pub fn process_create_native_mint(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -1202,6 +1235,14 @@ impl Processor { msg!("Instruction: InitializeImmutableOwner"); Self::process_initialize_immutable_owner(accounts) } + TokenInstruction::AmountToUiAmount { amount } => { + msg!("Instruction: AmountToUiAmount"); + Self::process_amount_to_ui_amount(accounts, amount) + } + TokenInstruction::UiAmountToAmount { ui_amount } => { + msg!("Instruction: UiAmountToAmount"); + Self::process_ui_amount_to_amount(accounts, ui_amount) + } TokenInstruction::Reallocate { extension_types } => { msg!("Instruction: Reallocate"); reallocate::process_reallocate(program_id, accounts, extension_types) @@ -1375,6 +1416,7 @@ mod tests { crate::{ extension::transfer_fee::instruction::initialize_transfer_fee_config, instruction::*, }, + serial_test::serial, solana_program::{ account_info::IntoAccountInfo, clock::Epoch, @@ -6961,6 +7003,7 @@ mod tests { } #[test] + #[serial] fn test_get_account_data_size() { // see integration tests for return-data validity let program_id = crate::id(); @@ -7114,4 +7157,179 @@ mod tests { Err(ProgramError::IncorrectProgramId) ); } + + #[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], + ) + ); + } } diff --git a/token/program/Cargo.toml b/token/program/Cargo.toml index ed73a076016..2fe11c39cf8 100644 --- a/token/program/Cargo.toml +++ b/token/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" diff --git a/token/program/src/instruction.rs b/token/program/src/instruction.rs index d548a99c63c..f3b22f102a3 100644 --- a/token/program/src/instruction.rs +++ b/token/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 { + pub fn unpack(input: &'a [u8]) -> Result { 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 { + 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 { + 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); } } diff --git a/token/program/src/lib.rs b/token/program/src/lib.rs index 4dda68d3487..2252ac168de 100644 --- a/token/program/src/lib.rs +++ b/token/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 { + 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::() + .map_err(|_| ProgramError::InvalidArgument) +} + solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); /// Checks that the supplied program ID is the correct one for SPL-token diff --git a/token/program/src/processor.rs b/token/program/src/processor.rs index eeee4451bd0..4dded8672c7 100644 --- a/token/program/src/processor.rs +++ b/token/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::::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::::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::::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], + ) + ); + } }