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
This commit is contained in:
Tyera Eulberg 2022-02-17 16:25:57 -07:00 committed by GitHub
parent bb6a91c56f
commit 3d92f8f4dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 720 additions and 24 deletions

24
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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() {

View File

@ -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<Self, ProgramError> {
pub fn unpack(input: &'a [u8]) -> Result<Self, ProgramError> {
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::<ExtensionType>()) {
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<Instruction, ProgramError> {
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<Instruction, ProgramError> {
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);

View File

@ -1029,6 +1029,39 @@ impl Processor {
token_account.init_extension::<ImmutableOwner>().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::<Mint>::unpack(&mint_data)
.map_err(|_| Into::<ProgramError>::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::<Mint>::unpack(&mint_data)
.map_err(|_| Into::<ProgramError>::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();
@ -1204,6 +1237,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)
@ -1377,6 +1418,7 @@ mod tests {
crate::{
extension::transfer_fee::instruction::initialize_transfer_fee_config, instruction::*,
},
serial_test::serial,
solana_program::{
account_info::IntoAccountInfo,
clock::Epoch,
@ -6963,6 +7005,7 @@ mod tests {
}
#[test]
#[serial]
fn test_get_account_data_size() {
// see integration tests for return-data validity
let program_id = crate::id();
@ -7116,4 +7159,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],
)
);
}
}

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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

View File

@ -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],
)
);
}
}