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:
parent
bb6a91c56f
commit
3d92f8f4dc
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue